Commit c965227f authored by Thomas Gambier's avatar Thomas Gambier 🚴🏼

powerdns: target Python 3 and upgrade to 4.2.1

This MR includes the following steps:
1. Target Python 3
2. Upgrade powerdns from version 3.3.1 to version 4.2.1 --- which also means...
3. Rework the configuration to support the [new GeoIP backend config format](https://doc.powerdns.com/authoritative/backends/geoip.html)

See the commit messages for more detail.

Note: EDNS Client Subnet extension is enabled.

See merge request !764
parents 53331088 32df49b3
Pipeline #10364 failed with stage
...@@ -2,33 +2,42 @@ ...@@ -2,33 +2,42 @@
extends = extends =
../autoconf/buildout.cfg ../autoconf/buildout.cfg
../automake/buildout.cfg ../automake/buildout.cfg
../bison/buildout.cfg
../flex/buildout.cfg
../git/buildout.cfg
../boost-lib/buildout.cfg ../boost-lib/buildout.cfg
../libtool/buildout.cfg ../libtool/buildout.cfg
../make/buildout.cfg ../make/buildout.cfg
../mariadb/buildout.cfg ../openssl/buildout.cfg
../pkgconfig/buildout.cfg ../pkgconfig/buildout.cfg
../ragel/buildout.cfg
../zlib/buildout.cfg ../zlib/buildout.cfg
# For geoip backend
# https://doc.powerdns.com/authoritative/backends/geoip.html
../geoip2/buildout.cfg
../yaml-cpp/buildout.cfg
parts = parts =
powerdns powerdns
[powerdns] [powerdns]
recipe = slapos.recipe.cmmi recipe = slapos.recipe.cmmi
url = http://downloads.powerdns.com/releases/pdns-3.3.1.tar.gz url = http://downloads.powerdns.com/releases/pdns-4.2.1.tar.bz2
md5sum = 074e2ff211fd12ecad25b5c1cc190dd4 md5sum = b5f3998a3bc438b905c72c0473408839
configure-options = configure-options =
--prefix=${buildout:parts-directory}/${:_buildout_section_name_} --prefix=${buildout:parts-directory}/${:_buildout_section_name_}
--with-boost=${boost-lib:location} --with-boost=${boost-lib:location}
--with-modules="geo" --with-libcrypto=${openssl:location}
--with-modules="geoip"
--with-dynmodules="" --with-dynmodules=""
--without-lua --without-lua
--disable-lua-records
pkg_config_depends = ${yaml-cpp:location}/lib/pkgconfig
environment = environment =
PATH=${make:location}/bin:${libtool:location}/bin:${pkgconfig:location}/bin:${bison:location}/bin:${flex:location}/bin:${git:location}/bin:${ragel:location}/bin:%(PATH)s PATH=${autoconf:location}/bin:${automake:location}/bin:${libmaxminddb:location}/bin:${libtool:location}/bin:${make:location}/bin:${pkgconfig:location}/bin:%(PATH)s
LDFLAGS=-L${boost-lib:location}/lib -Wl,-rpath=${boost-lib:location}/lib -L${zlib:location}/lib -Wl,-rpath -Wl,${zlib:location}/lib -lz LDFLAGS=-L${boost-lib:location}/lib -Wl,-rpath=${boost-lib:location}/lib -L${libmaxminddb:location}/lib -Wl,-rpath=${libmaxminddb:location}/lib -L${openssl:location}/lib -Wl,-rpath=${openssl:location}/lib -L${yaml-cpp:location}/lib -Wl,-rpath=${yaml-cpp:location}/lib -L${zlib:location}/lib -Wl,-rpath=${zlib:location}/lib
CPPFLAGS=-I${boost-lib:location}/include CPPFLAGS=-I${boost-lib:location}/include -I${libmaxminddb:location}/include -I${yaml-cpp:location}/include
PKG_CONFIG_PATH=${:pkg_config_depends}
# XXX: Override the default value "-Llib -lyaml-cpp"; "-Llib" is a problem
YAML_LIBS = -lyaml-cpp
make-options =
LIBTOOL=libtool
make-target = make-target =
install install
[buildout]
extends =
../cmake/buildout.cfg
parts =
yaml-cpp
[yaml-cpp]
recipe = slapos.recipe.cmmi
shared = true
url = https://github.com/jbeder/yaml-cpp/archive/yaml-cpp-0.6.3.tar.gz
md5sum = b45bf1089a382e81f6b661062c10d0c2
location = @@LOCATION@@
configure-command =
mkdir build && cd build && \
${cmake:location}/bin/cmake \
-DYAML_BUILD_SHARED_LIBS=ON \
-DCMAKE_INSTALL_PREFIX=${:location} \
..
make-options = -C build
...@@ -14,24 +14,24 @@ ...@@ -14,24 +14,24 @@
# not need these here). # not need these here).
[template] [template]
filename = instance.cfg filename = instance.cfg
md5sum = da8be58db4255c07750f7a7583eab3ca md5sum = fddea033e1aa9d6147a1a47bd7cc4b62
[template-powerdns] [template-powerdns]
filename = instance-powerdns.cfg filename = instance-powerdns.cfg
md5sum = 681cd9564e491d1f7b7ccb810f8ca7df md5sum = 2adb91323d60fc350f52910a3257d4a7
[template-pdns-configuration] [template-pdns-configuration]
_update_hash_filename_ = template/pdns.conf.jinja2 _update_hash_filename_ = template/pdns.conf.jinja2
md5sum = 7934b7037344678eff3031e1e73e0bb2 md5sum = e45d72de87b4adb89c195ba463be4077
[template-dns-replicate] [template-dns-replicate]
_update_hash_filename_ = instance-powerdns-replicate.cfg.jinja2 _update_hash_filename_ = instance-powerdns-replicate.cfg.jinja2
md5sum = 46acd4ed071df8d7139dcd0434be42eb md5sum = a23e241a236f90ae1afbb5bd5ba0b32d
[iso-list] [iso-list]
_update_hash_filename_ = template/zz.countries.nexedi.dk.rbldnsd _update_hash_filename_ = template/zz.countries.nexedi.dk.rbldnsd
md5sum = c4dc8c141d81b92d92cdb82ca67a13ee md5sum = c4dc8c141d81b92d92cdb82ca67a13ee
[template-cdn-conf] [template-zones-file]
_update_hash_filename_ = template/cdn.conf.in _update_hash_filename_ = template/zones-file.yml.jinja2
md5sum = 29c29f93b3b0bd2f71f86f7b337e4543 md5sum = c820a4f53c3e7706f51a5e0be3a8cf74
...@@ -60,7 +60,7 @@ context = ...@@ -60,7 +60,7 @@ context =
<= replicate <= replicate
name = {{dns_name}} name = {{dns_name}}
{% set state_key = "-dns-%s-state" % i %} {% set state_key = "-dns-%s-state" % i %}
{% if slapparameter_dict.has_key(state_key) %} {% if state_key in slapparameter_dict %}
state = {{ slapparameter_dict.pop(state_key) }} state = {{ slapparameter_dict.pop(state_key) }}
{% endif%} {% endif%}
config-zone = {{ zone }} config-zone = {{ zone }}
...@@ -95,7 +95,7 @@ monitor-url-list += ...@@ -95,7 +95,7 @@ monitor-url-list +=
<= slap-connection <= slap-connection
recipe = slapos.cookbook:requestoptional recipe = slapos.cookbook:requestoptional
{% set dns_software_url_key = "-dns-software-release-url" %} {% set dns_software_url_key = "-dns-software-release-url" %}
{% if slapparameter_dict.has_key(dns_software_url_key) %} {% if dns_software_url_key in slapparameter_dict %}
software-url = {{ slapparameter_dict.pop(dns_software_url_key) }} software-url = {{ slapparameter_dict.pop(dns_software_url_key) }}
{% else %} {% else %}
software-url = ${slap-connection:software-release-url} software-url = ${slap-connection:software-release-url}
...@@ -104,7 +104,7 @@ software-type = {{dns_type}} ...@@ -104,7 +104,7 @@ software-type = {{dns_type}}
return = private-ipv4 public-ipv4 slave-instance-information-list monitor-base-url return = private-ipv4 public-ipv4 slave-instance-information-list monitor-base-url
config-server-admin = {{ server_admin }} config-server-admin = {{ server_admin }}
config-ns-record = {{ ns_record }} config-ns-record = {{ ns_record }}
{% for parameter, value in slapparameter_dict.iteritems() -%} {% for parameter, value in slapparameter_dict.items() -%}
config-{{parameter}} = {{ value }} config-{{parameter}} = {{ value }}
{% endfor -%} {% endfor -%}
config-{{ slave_list_name }} = {{ json_module.dumps(slave_instance_list) }} config-{{ slave_list_name }} = {{ json_module.dumps(slave_instance_list) }}
...@@ -151,7 +151,7 @@ key_file = ${slap-connection:key-file} ...@@ -151,7 +151,7 @@ key_file = ${slap-connection:key-file}
cert_file = ${slap-connection:cert-file} cert_file = ${slap-connection:cert-file}
[slap-parameter] [slap-parameter]
slave_instance_list = {% for k, v in slapparameter_dict.items() -%}
-dns-quantity = 1 {{ k }} = {{ v }}
-dns-type = single-default {% endfor -%}
{%- endif %} {%- endif %}
{% if slap_software_type in software_type -%} {% if slap_software_type in software_type -%}
{% set part_list = [] %}
# Create all needed directories # Create all needed directories
[directory] [directory]
...@@ -63,13 +62,12 @@ socket-directory = $${pdns-directory:socket} ...@@ -63,13 +62,12 @@ socket-directory = $${pdns-directory:socket}
webserver-port = 8088 webserver-port = 8088
[geo] [geo]
ip-map-zonefile = ${iso-list:target} zones-file = $${zones-file-template:rendered}
geo-maps = $${pdns-directory:geo-maps} database = ${geolite2-country:location}/GeoLite2-Country.mmdb
[pdns-directory] [pdns-directory]
recipe = slapos.cookbook:mkdirectory recipe = slapos.cookbook:mkdirectory
configuration = $${directory:etc}/pdns configuration = $${directory:etc}/pdns
geo-maps = $${:configuration}/geo-maps
socket = $${directory:run}/pdns-socket socket = $${directory:run}/pdns-socket
[pdns-configuration-template] [pdns-configuration-template]
...@@ -80,6 +78,39 @@ extra-context = ...@@ -80,6 +78,39 @@ extra-context =
section pdns pdns section pdns pdns
section geo geo section geo geo
[asia]
japan = jp
hong-kong = hk
china-telecom = cn-t
china-unicom = cn-u
china-mobile = cn-m
west-asia = ae af am az bh cc cy ge il iq ir jo kg kw kz lb om pk qa ru sa sy tj tm tr uz ye
east-asia = bn bt cx id in io kh kp kr la lk mm mn mo mv my np ph sg th to tw vn
[china]
recipe = slapos.recipe.build
iso-list = ${iso-list:target}
init =
import re
ip_split = []
match = re.compile(r"(.*) :.*:(cn-\w)\n").fullmatch
with open(options["iso-list"]) as f:
for line in f:
m = match(line)
if m is None:
continue
ip_split.append(m.groups())
options["ip-split"] = ip_split
[zones-file-template]
< = jinja2-template-base
template = ${template-zones-file:target}
extensions = jinja2.ext.do
rendered = $${pdns-directory:configuration}/zones-file.yml
extra-context =
section asia asia
key china china:ip-split
# Executables # Executables
[pdns-server] [pdns-server]
recipe = slapos.cookbook:wrapper recipe = slapos.cookbook:wrapper
...@@ -117,29 +148,6 @@ monitor-url = $${monitor-publish-parameters:monitor-url} ...@@ -117,29 +148,6 @@ monitor-url = $${monitor-publish-parameters:monitor-url}
monitor-user = $${monitor-publish-parameters:monitor-user} monitor-user = $${monitor-publish-parameters:monitor-user}
monitor-password = $${monitor-publish-parameters:monitor-password} monitor-password = $${monitor-publish-parameters:monitor-password}
#####################
# Power DNS Slave configuration
#
{% set slave_instance_list = json_module.loads(slapparameter_dict.get('extra_slave_instance_list', '[]')) %}
# Iter through slave list to prepare configuration
{% for slave in slave_instance_list %}
{% if 'record' in slave and 'origin' in slave and 'default' in slave %}
{% set slave_reference = slave.get('slave_reference') %}
{% set slave_section_name = 'map-configuration-%s' % slave_reference %}
{% do part_list.append(slave_section_name) %}
[{{ slave_section_name }}]
< = jinja2-template-base
template = ${template-cdn-conf:target}
rendered = $${geo:geo-maps}/{{ slave_reference }}
configuration = {{ json_module.dumps(slave) }}
extra-context =
key json_cdn :configuration
{% endif %}
{% endfor %}
####################
[buildout] [buildout]
parts = parts =
pdns-configuration-template pdns-configuration-template
...@@ -149,9 +157,6 @@ parts = ...@@ -149,9 +157,6 @@ parts =
pdns-promise-listen-port pdns-promise-listen-port
monitor-base monitor-base
publish-connection-informations publish-connection-informations
{% for part in part_list %}
{{ ' %s' % part }}
{% endfor %}
extends = ${monitor-template:output} extends = ${monitor-template:output}
......
...@@ -8,10 +8,12 @@ offline = true ...@@ -8,10 +8,12 @@ offline = true
[switch-softwaretype] [switch-softwaretype]
recipe = slapos.cookbook:softwaretype recipe = slapos.cookbook:switch-softwaretype
default = $${dynamic-powerdns-replicate:rendered} default = dynamic-powerdns-replicate:rendered
single-default = $${dynamic-template-powerdns:rendered} single-default = dynamic-template-powerdns:rendered
# BBB
RootSoftwareInstance = $${:default}
[jinja2-template-base] [jinja2-template-base]
recipe = slapos.recipe.template:jinja2 recipe = slapos.recipe.template:jinja2
...@@ -21,9 +23,9 @@ context = ...@@ -21,9 +23,9 @@ context =
import json_module json import json_module json
key eggs_directory buildout:eggs-directory key eggs_directory buildout:eggs-directory
key develop_eggs_directory buildout:develop-eggs-directory key develop_eggs_directory buildout:develop-eggs-directory
key slap_software_type slap-parameters:slap-software-type key slap_software_type slap-configuration:slap-software-type
key slapparameter_dict slap-parameters:configuration key slapparameter_dict slap-configuration:configuration
key slave_instance_list slap-parameters:slave-instance-list key slave_instance_list slap-configuration:slave-instance-list
$${:extra-context} $${:extra-context}
[dynamic-template-powerdns] [dynamic-template-powerdns]
...@@ -38,14 +40,14 @@ extra-context = ...@@ -38,14 +40,14 @@ extra-context =
[dynamic-powerdns-replicate] [dynamic-powerdns-replicate]
< = jinja2-template-base < = jinja2-template-base
template = ${template-dns-replicate:target} template = ${template-dns-replicate:target}
filename = instance-apache-replicate.cfg filename = instance-powerdns-replicate.cfg
extensions = jinja2.ext.do extensions = jinja2.ext.do
extra-context = extra-context =
# Must match the key id in [switch-softwaretype] which uses this section. # Must match the key id in [switch-softwaretype] which uses this section.
raw software_type RootSoftwareInstance-default raw software_type RootSoftwareInstance-default
raw template_monitor ${monitor2-template:rendered} raw template_monitor ${monitor2-template:rendered}
[slap-parameters] [slap-configuration]
recipe = slapos.cookbook:slapconfiguration recipe = slapos.cookbook:slapconfiguration
computer = $${slap-connection:computer-id} computer = $${slap-connection:computer-id}
partition = $${slap-connection:partition-id} partition = $${slap-connection:partition-id}
......
...@@ -10,12 +10,12 @@ ...@@ -10,12 +10,12 @@
}, },
"origin": { "origin": {
"title": "Origin", "title": "Origin",
"description": "Used to qualify RR in the configuration. i.e.: if your origin is a.example.com and the RR for Europe is 'eu' the european clients will use eu.a.exmple.com", "description": "Used to qualify RR in the configuration. i.e.: if your origin is a.example.com and the RR for Europe is 'eu' the european clients will use eu.a.example.com",
"type": "string" "type": "string"
}, },
"default": { "default": {
"title": "Default RR", "title": "Default RR",
"description": "Defautl record to use when the ip is not regognized", "description": "Default record to use when the ip is not recognized",
"type": "string" "type": "string"
}, },
"europe": { "europe": {
......
...@@ -10,10 +10,8 @@ parts = ...@@ -10,10 +10,8 @@ parts =
slapos-cookbook slapos-cookbook
eggs eggs
[gcc] [python]
# For old version of PowerDNS and Ragel. part = python3
part = gcc-5.5
max_version = 6
[eggs] [eggs]
recipe = zc.recipe.egg recipe = zc.recipe.egg
...@@ -47,7 +45,7 @@ recipe = slapos.recipe.build:download ...@@ -47,7 +45,7 @@ recipe = slapos.recipe.build:download
url = ${:_profile_base_location_}/${:_update_hash_filename_} url = ${:_profile_base_location_}/${:_update_hash_filename_}
mode = 0644 mode = 0644
[template-cdn-conf] [template-zones-file]
recipe = slapos.recipe.build:download recipe = slapos.recipe.build:download
url = ${:_profile_base_location_}/${:_update_hash_filename_} url = ${:_profile_base_location_}/${:_update_hash_filename_}
mode = 0644 mode = 0644
......
This diff is collapsed.
...@@ -31,7 +31,8 @@ cache-ttl=0 ...@@ -31,7 +31,8 @@ cache-ttl=0
# things are working. :) # things are working. :)
log-dns-details=yes log-dns-details=yes
log-dns-queries=yes log-dns-queries=yes
log-failed-updates=yes # https://github.com/PowerDNS/pdns/commit/df9d980
# log-failed-updates=yes
loglevel=4 loglevel=4
# This disables wildcards which is more efficient. geobackend doesn't use # This disables wildcards which is more efficient. geobackend doesn't use
...@@ -40,52 +41,11 @@ loglevel=4 ...@@ -40,52 +41,11 @@ loglevel=4
# wildcards=no # wildcards=no
# The geobackend # The geobackend
launch=geo launch=geoip
# The zone that your geo-balanced RR is inside of. The whole zone has to be edns-subnet-processing=yes
# delegated to the PowerDNS backend, so you will generally want to make up some
# subzone of your main zone. We chose "geo.blitzed.org".
#
geo-zone={{ slapparameter_dict.get('zone', 'example.com') }}
# The only parts of the SOA for "geo.blitzed.org" that apply here are the
# master server name and the contact address.
geo-soa-values={{ slapparameter_dict.get('soa', 'ns0.example.com,admin@example.com') }}
# List of NS records of the PowerDNS servers that are authoritative for your
# GLB zone.
geo-ns-records={{ slapparameter_dict.get('ns-record', 'ns0.example.com,ns1.example.com') }}
# The TTL of the CNAME records that geobackend will return. Since the same
# resolver will always get the same CNAME (apart from if the director-map
# changes) it is safe to return a reasonable TTL, so if you leave this
# commented then a sane default will be chosen.
#geo-ttl=3600
# The TTL of the NS records that will be returned. Leave this commented if you
# don't understand.
#geo-ns-ttl=86400
# This is the real guts of the data that drives this backend. This is a DNS
# zone file for RBLDNSD, a nameserver specialised for running large DNS zones
# typical of DNSBLs and such. We choose it for our data because it is easier
# to parse than the BIND-format one.
#
# Anyway, it comes from http://countries.nerd.dk/more.html - there are details
# there for how to rsync your own copy. You'll want to do that regularly,
# every couple of days maybe. We believe the nerd.dk guys take the netblock
# info from Regional Internet Registries (RIRs) like RIPE, ARIN, APNIC. From
# that they build a big zonefile of IP/prefixlen -> ISO-country-code mappings.
geo-ip-map-zonefile={{ geo.get('ip-map-zonefile') }}
# And finally this last directive tells the geobackend where to find the map geoip-database-files={{ geo['database'] }}
# files that say a) which RR to answer for, and b) what actual resource record geoip-zones-file={{ geo['zones-file'] }}
# to return for each ISO country code. The setting here is a comma-separated
# list of paths, each of which may either be a single map file or a directory
# that will contain map files. If you are only ever going to serve one RR then
# a single file is probably better, but if you're going to serve many then a
# directory would probably be better. The rest of this documentation will
# assume you chose a directory.
geo-maps={{ geo.get('geo-maps') }}
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
\ No newline at end of file
# See https://doc.powerdns.com/authoritative/backends/geoip.html
{%- set slave_instance_list = json_module.loads(slapparameter_dict.get('extra_slave_instance_list', '[]')) %}
{%- set zone = slapparameter_dict.get('zone', 'example.com') %}
{%- macro disambiguate_domain_name(a, b) %}
{#- See http://www.dns-sd.org/trailingdotsindomainnames.html #}
{%- if a.endswith('.') %}
{{- a[:-1] }}
{%- else %}
{{- a }}.{{ b }}
{%- endif %}
{%- endmacro %}
domains:
- domain: {{ zone }}
# TODO: what value for ttl?
ttl: 300
# Note: For each domain, one record of the domain name MUST exist with a soa record.
records:
{{ zone }}:
- soa: {{ slapparameter_dict.get('soa', 'ns0.example.com,admin@example.com').replace(',', ' ') }}
{%- for ns in slapparameter_dict.get('ns-record', 'ns0.example.com,ns1.example.com').split(',') %}
- ns: {{ ns }}
{%- endfor %}
{#- Split the world the way we prefer. 'GeoLite2-Country.mmdb' divides the
world into 7 continents: Oceania, Asia, Europe, South America,
North America, Africa and Antarctica. However, we also want a more
fine-grained division for Asia, i.e. West/East, China, Hong-Kong and
Japan. #}
{%- set world_split = {
"continent": {
"eu": "europe",
"af": "africa",
"sa": "south-america",
"na": "north-america",
"oc": "oceania",
},
"country": {}} %}
{%- for region, codes in asia.items() %}
{%- for country_code in codes.split() %}
{%- do world_split["country"].__setitem__(country_code, region) %}
{%- endfor %}
{%- endfor %}
{%- for slave in slave_instance_list %}
{#- Set the RR to use for each region, as described in
'slave-instance-powerdns-input-schema.json' #}
{%- set rr_dict = {} %}
{%- for region, default_rr in {"europe": "eu",
"africa": "af",
"south-america": "sa",
"north-america": "na",
"china-telecom": "cn-t",
"china-unicom": "cn-u",
"china-mobile": "cn-m",
"japan": "jp",
"hong-kong": "hk",
"east-asia": "as",
"west-asia": "eu",
"oceania": "oc"}.items() %}
{%- do rr_dict.__setitem__(region, slave.get(region, default_rr)) %}
{%- endfor %}
{#- 'code2region' maps an ISO3166 country/continent code (i.e. the client
origin) to a geographical region. The latter is then used to find the
right RR (thanks to 'rr_dict') #}
{%- for placeholder, code2region in world_split.items() %}
{%- for code, region in code2region.items() %}
{%- set origin = slave['origin'] %}
{{ code }}.{{ placeholder }}.{{ origin }}:
- cname: {{ disambiguate_domain_name(rr_dict[region], origin) }}
{%- endfor %}
{%- endfor %}
{%- endfor %}
services:
{%- for slave in slave_instance_list %}
{%- set origin = slave['origin'] %}
{{ disambiguate_domain_name(slave['record'], zone) }}:
{#- Note: Placeholders (i.e. "country." and "continent.") are used to avoid
possible name collisions, e.g.:
- %cc for American Samoa is 'as'
- %cn for Asia is also 'as' #}
default: ['%cc.country.{{ origin }}', '%cn.continent.{{ origin }}', '{{ disambiguate_domain_name(slave['default'], origin) }}']
# Split China's ip addresses according to ISP
{%- for ip_range, country_code in china %}
{{ ip_range }}: {{ country_code }}.country.{{ origin }}
{%- endfor %}
{%- endfor %}
...@@ -13,3 +13,4 @@ eggs -= ...@@ -13,3 +13,4 @@ eggs -=
[template] [template]
extra = extra =
${slapos.test.monitor-setup:setup} ${slapos.test.monitor-setup:setup}
${slapos.test.powerdns-setup:setup}
...@@ -249,7 +249,6 @@ extra = ...@@ -249,7 +249,6 @@ extra =
${slapos.test.htmlvalidatorserver-setup:setup} ${slapos.test.htmlvalidatorserver-setup:setup}
${slapos.test.slapos-master-setup:setup} ${slapos.test.slapos-master-setup:setup}
${slapos.test.plantuml-setup:setup} ${slapos.test.plantuml-setup:setup}
${slapos.test.powerdns-setup:setup}
${slapos.test.proftpd-setup:setup} ${slapos.test.proftpd-setup:setup}
${slapos.test.re6stnet-setup:setup} ${slapos.test.re6stnet-setup:setup}
${slapos.test.seleniumserver-setup:setup} ${slapos.test.seleniumserver-setup:setup}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment