diff --git a/.gitignore b/.gitignore index 2d22306..d1f7e7b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .secrets.cfg +__pycache__ diff --git a/bundles/apt/items.py b/bundles/apt/items.py index 02cb8fb..1e2c416 100644 --- a/bundles/apt/items.py +++ b/bundles/apt/items.py @@ -67,10 +67,12 @@ pkg_apt = { 'arping': {}, 'at': {}, + 'build-essential': {}, 'bzip2': {}, 'curl': {}, 'diffutils': {}, 'dnsutils': {}, + 'git': {}, 'grep': {}, 'gzip': {}, 'htop': {}, @@ -88,6 +90,7 @@ pkg_apt = { 'netcat': {}, 'nmap': {}, 'python3': {}, + 'python3-dev': {}, 'python3-pip': {}, 'python3-virtualenv': {}, 'tar': {}, diff --git a/bundles/bind/files/keys.conf b/bundles/bind/files/keys.conf deleted file mode 100644 index faf4ce4..0000000 --- a/bundles/bind/files/keys.conf +++ /dev/null @@ -1,6 +0,0 @@ -% for key in keys: -key ${key['name']} { - algorithm ${key['algorithm']}; - secret "${key['secret']}"; -}; -% endfor diff --git a/bundles/bind/files/named.conf.local b/bundles/bind/files/named.conf.local deleted file mode 100644 index 5f5e826..0000000 --- a/bundles/bind/files/named.conf.local +++ /dev/null @@ -1,30 +0,0 @@ -include "/etc/bind/keys.conf"; - -% for zone in sorted(primary_zones): -zone "${zone}" IN { - type master; - file "/var/lib/bind/primary/${zone}"; -}; -% endfor - - -zone "10.in-addr.arpa" { type master; file "/etc/bind/db.empty"; }; - -zone "16.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; }; -zone "17.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; }; -zone "18.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; }; -zone "19.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; }; -zone "20.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; }; -zone "21.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; }; -zone "22.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; }; -zone "23.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; }; -zone "24.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; }; -zone "25.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; }; -zone "26.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; }; -zone "27.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; }; -zone "28.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; }; -zone "29.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; }; -zone "30.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; }; -zone "31.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; }; - -zone "168.192.in-addr.arpa" { type master; file "/etc/bind/db.empty"; }; diff --git a/bundles/bind/files/named.conf.options b/bundles/bind/files/named.conf.options deleted file mode 100644 index 1e9db6e..0000000 --- a/bundles/bind/files/named.conf.options +++ /dev/null @@ -1,3 +0,0 @@ -% for o in node.metadata.get('bind', {}).get('options', []): -<%include file="options/${o}"/> -% endfor diff --git a/bundles/bind/items.py b/bundles/bind/items.py deleted file mode 100644 index 958fffc..0000000 --- a/bundles/bind/items.py +++ /dev/null @@ -1,146 +0,0 @@ -from os import listdir -from os.path import isfile, join -from datetime import datetime -from subprocess import check_output - -ZONE_HEADER = """ -; _ ____ _ _ _____ _ _ _ _ ____ -; / \\ / ___| | | |_ _| | | | \\ | |/ ___| -; / _ \\| | | |_| | | | | | | | \\| | | _ -; / ___ \\ |___| _ | | | | |_| | |\\ | |_| | -; /_/ \\_\\____|_| |_| |_| \\___/|_| \\_|\\____| -; -; --> Diese Datei wird von BundleWrap verwaltet! <-- - -$TTL 60 -@ IN SOA ns-1.kunbox.net. hostmaster.kunbox.net. ( - {serial} - 3600 - 3600 - 86400 - 300 - ) -@ IN NS bind01.gce.kunbox.net. - IN NS b.ns14.net. - IN NS c.ns14.net. - IN NS d.ns14.net. -""" - -svc_systemd = { - 'bind9': { - 'needs': { - 'pkg_apt:bind9', - }, - }, -} - -pkg_apt = { - 'bind9': {}, -} - -directories = { - "/var/lib/bind/primary": { - 'group': 'bind', - 'needs': { - 'pkg_apt:bind9', - }, - 'owner': 'bind', - 'purge': True, - }, - "/var/log/named": { - 'group': 'bind', - 'needs': { - 'pkg_apt:bind9', - }, - 'owner': 'bind', - }, -} - -files = { - "/etc/bind/keys.conf": { - 'content_type': 'mako', - 'group': 'bind', - 'mode': '0440', - 'context': { - 'keys': node.metadata.get('bind', {}).get('keys', []), - }, - 'triggers': { - 'svc_systemd:bind9:reload', - }, - 'needs': { - 'pkg_apt:bind9', - }, - }, - "/etc/bind/named.conf.options": { - 'content_type': 'mako', - 'group': 'bind', - 'mode': '0440', - 'triggers': { - 'svc_systemd:bind9:reload', - }, - 'needs': { - 'pkg_apt:bind9', - }, - }, -} - -if node.metadata.get('bind', {}).get('rndc', ''): - files['/etc/bind/rndc.conf'] = { - 'mode': '0440', - 'source': 'rndc/{}'.format(node.metadata['bind']['rndc']), - 'content_type': 'mako', - 'triggers': { - 'svc_systemd:bind9:reload', - }, - } - -# this looks for zones either directly at data/bind/zones/ or in a subdirectory if so configured -zone_path = join( - repo.path, - 'data', 'bind', 'files', 'zones', - node.metadata.get('bind', {}).get('zone_path', ""), -) - -primary_zones = set() - -for zone in listdir(zone_path): - if not isfile(join(zone_path, zone)) or zone.startswith(".") or zone.startswith("_"): - continue - - output = check_output(['git', 'log', '-1', '--pretty=%ci', join(zone_path, zone)]).decode('utf-8').strip() - serial = datetime.strptime(output, '%Y-%m-%d %H:%M:%S %z').strftime('%y%m%d%H%M') - - primary_zones.add(zone) - - files["/var/lib/bind/primary/{}".format(zone)] = { - 'content_type': 'mako', - 'context': { - 'header': ZONE_HEADER.format(serial=serial), - 'metadata_records': node.metadata.get('bind', {}).get('zones_primary', {}).get(zone, {}).get('records', []), - }, - 'mode': '0444', - 'owner': 'bind', - 'source': 'zones/{}'.format(join(node.metadata.get('bind', {}).get('zone_path', ""), zone)), - 'triggers': { - 'svc_systemd:bind9:reload', - }, - 'needs': { - 'pkg_apt:bind9' - }, - } - -primary_zones.union(set(node.metadata.get('bind', {}).get('zones_primary', {}).keys())) - -files['/etc/bind/named.conf.local'] = { - 'content_type': 'mako', - 'context': { - 'primary_zones': list(primary_zones), - }, - 'group': 'bind', - 'triggers': { - 'svc_systemd:bind9:reload', - }, - 'needs': { - 'pkg_apt:bind9', - }, -} diff --git a/bundles/bind/metadata.py b/bundles/bind/metadata.py deleted file mode 100644 index a99c341..0000000 --- a/bundles/bind/metadata.py +++ /dev/null @@ -1,72 +0,0 @@ -from bundlewrap.metadata import atomic - - -defaults = { - 'icinga2_api': { - 'bind': { - 'services': { - 'BIND PROCESS': { - 'command_on_monitored_host': '/usr/lib/nagios/plugins/check_procs -C named -c 1:1', - }, - }, - }, - }, -} - -@metadata_reactor -def port_checks(metadata): - services = {} - - for interface in metadata.get('bind/listen', set()): - services[f'BIND PORT {interface}'] = { - 'check_command': 'tcp', - 'vars.tcp_address': metadata.get(f'interfaces/{interface}/ip_addresses')[0], - 'vars.tcp_port': 53, - } - - return { - 'icinga2_api': { - 'bind': { - 'services': services, - }, - }, - } - -@metadata_reactor -def generate_dns_entries_for_nodes(metadata): - results = set() - - for rnode in repo.nodes: - node_name_split = rnode.name.split('.') - node_name_split.reverse() - dns_name = '.'.join(node_name_split) - ip4 = None - ip6 = None - - # We only need this for GCE, because machines over there don't - # have a public ipv4 address. - if rnode.metadata.get('external_ipv4', None): - ip4 = rnode.metadata.get('external_ipv4') - - for iface, config in sorted(rnode.metadata.get('interfaces', {}).items()): - if not ip4 and 'ipv4' in config: - ip4 = sorted(config['ipv4'])[0] - - if not ip6 and 'ipv6' in config: - ip6 = sorted(config['ipv6'])[0] - - if ip4: - results.add('{} IN A {}'.format(dns_name, ip4)) - - if ip6: - results.add('{} IN AAAA {}'.format(dns_name, ip6)) - - return { - 'bind': { - 'zones_primary': { - 'kunbox.net': { - 'records': results, - }, - }, - }, - } diff --git a/bundles/postgresql/items.py b/bundles/postgresql/items.py index ff24ccd..7bbf141 100644 --- a/bundles/postgresql/items.py +++ b/bundles/postgresql/items.py @@ -1,6 +1,7 @@ pkg_apt = { 'postgresql-11': {}, 'postgresql-client-11': {}, + 'postgresql-server-dev-11': {}, } if node.has_bundle('zfs'): diff --git a/bundles/powerdns/files/bind.conf b/bundles/powerdns/files/bind.conf new file mode 100644 index 0000000..01775c1 --- /dev/null +++ b/bundles/powerdns/files/bind.conf @@ -0,0 +1,2 @@ +launch+=bind +bind-config=/etc/powerdns/named.conf diff --git a/bundles/powerdns/files/named.conf b/bundles/powerdns/files/named.conf new file mode 100644 index 0000000..925e314 --- /dev/null +++ b/bundles/powerdns/files/named.conf @@ -0,0 +1,6 @@ +% for zone in sorted(zones): +zone "${zone}" { + file "/var/lib/powerdns/zones/${zone}"; + type native; +}; +% endfor diff --git a/bundles/powerdns/files/pdns.conf b/bundles/powerdns/files/pdns.conf new file mode 100644 index 0000000..3bdffcf --- /dev/null +++ b/bundles/powerdns/files/pdns.conf @@ -0,0 +1,28 @@ +launch= +include-dir=/etc/powerdns/pdns.d + +api=yes +api-key=${api_key} +webserver=yes + +disable-syslog=yes +log-timestamp=no + +max-tcp-connections=500 +max-tcp-connections-per-client=10 + +security-poll-suffix= + +server-id=${my_hostname} + +default-ttl=60 + +% if is_secondary: +# Primary servers: ${', '.join(sorted(my_primary_servers['nodes']))} +slave=yes +superslave=yes +allow-notify-from=${','.join(sorted(my_primary_servers['ips']))} +% else: +allow-notify-from= +master=yes +% endif diff --git a/bundles/powerdns/files/pgsql.conf b/bundles/powerdns/files/pgsql.conf new file mode 100644 index 0000000..293d286 --- /dev/null +++ b/bundles/powerdns/files/pgsql.conf @@ -0,0 +1,6 @@ +launch+=gpgsql +gpgsql-host=localhost +gpgsql-port=5432 +gpgsql-dbname=powerdns +gpgsql-user=powerdns +gpgsql-password=${password} diff --git a/bundles/powerdns/files/schema.pgsql.sql b/bundles/powerdns/files/schema.pgsql.sql new file mode 100644 index 0000000..9635168 --- /dev/null +++ b/bundles/powerdns/files/schema.pgsql.sql @@ -0,0 +1,105 @@ +-- 4.3 schema, https://doc.powerdns.com/authoritative/backends/generic-postgresql.html + +CREATE TABLE domains ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + master VARCHAR(128) DEFAULT NULL, + last_check INT DEFAULT NULL, + type VARCHAR(6) NOT NULL, + notified_serial BIGINT DEFAULT NULL, + account VARCHAR(40) DEFAULT NULL, + CONSTRAINT c_lowercase_name CHECK (((name)::TEXT = LOWER((name)::TEXT))) +); + +CREATE UNIQUE INDEX name_index ON domains(name); + +ALTER TABLE domains OWNER TO ${user}; + +CREATE TABLE records ( + id BIGSERIAL PRIMARY KEY, + domain_id INT DEFAULT NULL, + name VARCHAR(255) DEFAULT NULL, + type VARCHAR(10) DEFAULT NULL, + content VARCHAR(65535) DEFAULT NULL, + ttl INT DEFAULT NULL, + prio INT DEFAULT NULL, + change_date INT DEFAULT NULL, + disabled BOOL DEFAULT 'f', + ordername VARCHAR(255), + auth BOOL DEFAULT 't', + CONSTRAINT domain_exists + FOREIGN KEY(domain_id) REFERENCES domains(id) + ON DELETE CASCADE, + CONSTRAINT c_lowercase_name CHECK (((name)::TEXT = LOWER((name)::TEXT))) +); + +CREATE INDEX rec_name_index ON records(name); +CREATE INDEX nametype_index ON records(name,type); +CREATE INDEX domain_id ON records(domain_id); +CREATE INDEX recordorder ON records (domain_id, ordername text_pattern_ops); + +ALTER TABLE records OWNER TO ${user}; + +CREATE TABLE supermasters ( + ip INET NOT NULL, + nameserver VARCHAR(255) NOT NULL, + account VARCHAR(40) NOT NULL, + PRIMARY KEY(ip, nameserver) +); + +ALTER TABLE supermasters OWNER TO ${user}; + +CREATE TABLE comments ( + id SERIAL PRIMARY KEY, + domain_id INT NOT NULL, + name VARCHAR(255) NOT NULL, + type VARCHAR(10) NOT NULL, + modified_at INT NOT NULL, + account VARCHAR(40) DEFAULT NULL, + comment VARCHAR(65535) NOT NULL, + CONSTRAINT domain_exists + FOREIGN KEY(domain_id) REFERENCES domains(id) + ON DELETE CASCADE, + CONSTRAINT c_lowercase_name CHECK (((name)::TEXT = LOWER((name)::TEXT))) +); + +CREATE INDEX comments_domain_id_idx ON comments (domain_id); +CREATE INDEX comments_name_type_idx ON comments (name, type); +CREATE INDEX comments_order_idx ON comments (domain_id, modified_at); + +ALTER TABLE comments OWNER TO ${user}; + +CREATE TABLE domainmetadata ( + id SERIAL PRIMARY KEY, + domain_id INT REFERENCES domains(id) ON DELETE CASCADE, + kind VARCHAR(32), + content TEXT +); + +CREATE INDEX domainidmetaindex ON domainmetadata(domain_id); + +ALTER TABLE domainmetadata OWNER TO ${user}; + +CREATE TABLE cryptokeys ( + id SERIAL PRIMARY KEY, + domain_id INT REFERENCES domains(id) ON DELETE CASCADE, + flags INT NOT NULL, + active BOOL, + content TEXT +); + +CREATE INDEX domainidindex ON cryptokeys(domain_id); +ALTER TABLE cryptokeys OWNER TO ${user}; + + +CREATE TABLE tsigkeys ( + id SERIAL PRIMARY KEY, + name VARCHAR(255), + algorithm VARCHAR(50), + secret VARCHAR(255), + CONSTRAINT c_lowercase_name CHECK (((name)::TEXT = LOWER((name)::TEXT))) +); + +CREATE UNIQUE INDEX namealgoindex ON tsigkeys(name, algorithm); + +ALTER TABLE tsigkeys OWNER TO ${user}; diff --git a/bundles/powerdns/items.py b/bundles/powerdns/items.py new file mode 100644 index 0000000..fef1d6e --- /dev/null +++ b/bundles/powerdns/items.py @@ -0,0 +1,163 @@ +from datetime import datetime +from os import listdir +from os.path import isfile, join +from subprocess import check_output + +zone_path = join(repo.path, 'data', 'powerdns', 'files', 'bind-zones') + +ZONE_HEADER = """ +; _ ____ _ _ _____ _ _ _ _ ____ +; / \\ / ___| | | |_ _| | | | \\ | |/ ___| +; / _ \\| | | |_| | | | | | | | \\| | | _ +; / ___ \\ |___| _ | | | | |_| | |\\ | |_| | +; /_/ \\_\\____|_| |_| |_| \\___/|_| \\_|\\____| +; +; --> Diese Datei wird von BundleWrap verwaltet! <-- + +$TTL 60 +@ IN SOA ns-1.kunbox.net. hostmaster.kunbox.net. ( + {serial} + 3600 + 3600 + 86400 + 300 + ) +""" +for rnode in sorted(repo.nodes_in_group('dns')): + ZONE_HEADER += '@ IN NS {}.\n'.format(rnode.metadata.get('powerdns', {}).get('my_hostname', rnode.hostname)) + +directories = { + '/etc/powerdns/pdns.d': { + 'purge': True, + 'needs': { + 'pkg_apt:pdns-server', + 'pkg_apt:pdns-backend-bind', + 'pkg_apt:pdns-backend-pgsql', + }, + 'triggers': { + 'svc_systemd:pdns:restart', + }, + }, + '/var/lib/powerdns/zones': { + 'purge': True, + 'needs': { + 'pkg_apt:pdns-backend-bind', + }, + } +} + +files = { + '/etc/powerdns/pdns.conf': { + 'content_type': 'mako', + 'context': { + 'api_key': node.metadata['powerdns']['api_key'], + 'my_hostname': node.metadata['powerdns'].get('my_hostname', node.name), + 'is_secondary': node.metadata['powerdns'].get('is_secondary', False), + 'my_primary_servers': node.metadata['powerdns'].get('my_primary_servers', {}), + }, + 'needs': { + 'pkg_apt:pdns-server', + }, + 'triggers': { + 'svc_systemd:pdns:restart', + }, + }, +} + +svc_systemd = { + 'pdns': { + 'needs': { + 'directory:', + 'file:', + 'pkg_apt:pdns-server', + 'pkg_apt:pdns-backend-bind', + 'pkg_apt:pdns-backend-pgsql', + }, + }, +} + +actions = { + 'powerdns_reload_zones': { + 'triggered': True, + 'command': 'pdns_control rediscover; pdns_control reload', + 'needs': { + 'svc_systemd:pdns', + }, + }, +} + +if node.metadata['powerdns'].get('features', {}).get('bind', False): + primary_zones = set() + for zone in listdir(zone_path): + if not isfile(join(zone_path, zone)) or zone.startswith(".") or zone.startswith("_"): + continue + + try: + output = check_output(['git', 'log', '-1', '--pretty=%ci', join(zone_path, zone)]).decode('utf-8').strip() + serial = datetime.strptime(output, '%Y-%m-%d %H:%M:%S %z').strftime('%y%m%d%H%M') + except: + serial = datetime.now().strftime('%y%m%d0000') + + primary_zones.add(zone) + + files["/var/lib/powerdns/zones/{}".format(zone)] = { + 'content_type': 'mako', + 'context': { + 'header': ZONE_HEADER.format(serial=serial), + 'metadata_records': node.metadata.get('powerdns', {}).get('bind-zones', {}).get(zone, {}).get('records', []), + }, + 'source': 'bind-zones/{}'.format(zone), + 'triggers': { + 'action:powerdns_reload_zones', + }, + } + + files['/etc/powerdns/pdns.d/bind.conf'] = { + 'needs': { + 'pkg_apt:pdns-backend-bind', + }, + 'triggers': { + 'action:powerdns_reload_zones', + }, + } + + files['/etc/powerdns/named.conf'] = { + 'content_type': 'mako', + 'context': { + 'zones': primary_zones, + }, + 'needs': { + 'pkg_apt:pdns-backend-bind', + }, + 'triggers': { + 'action:powerdns_reload_zones', + }, + } + +if node.metadata['powerdns'].get('features', {}).get('pgsql', False): + files['/etc/powerdns/pdns.d/pgsql.conf'] = { + 'content_type': 'mako', + 'context': { + 'password': node.metadata['postgresql']['users']['powerdns']['password'], + }, + 'needs': { + 'pkg_apt:pdns-backend-pgsql', + }, + 'triggers': { + 'action:powerdns_reload_zones', + }, + } + + files['/etc/powerdns/schema.pgsql.sql'] = {} + + actions['powerdns_load_pgsql_schema'] = { + 'command': node.metadata['postgresql']['users']['powerdns']['password'].format_into('PGPASSWORD={} psql -h 127.0.0.1 -d powerdns -U powerdns -w < /etc/powerdns/schema.pgsql.sql'), + 'unless': 'sudo -u postgres psql -d powerdns -c "\dt" | grep domains 2>&1 >/dev/null', + 'needs': { + 'bundle:postgresql', + 'file:/etc/powerdns/schema.pgsql.sql', + }, + 'needed_by': { + 'svc_systemd:pdns', + }, + } diff --git a/bundles/powerdns/metadata.py b/bundles/powerdns/metadata.py new file mode 100644 index 0000000..1161c37 --- /dev/null +++ b/bundles/powerdns/metadata.py @@ -0,0 +1,91 @@ +from bundlewrap.exceptions import NoSuchGroup + +defaults = { + 'apt': { + 'packages': { + 'pdns-server': {}, + 'pdns-tools': {}, + 'pdns-backend-bind': {}, + 'pdns-backend-pgsql': {}, + }, + }, + 'powerdns': { + 'api_key': repo.vault.password_for('{} powerdns api'.format(node.name)), + }, + 'postgresql': { + 'users': { + 'powerdns': { + 'password': repo.vault.password_for('{} postgresql powerdns'.format(node.name)), + }, + }, + 'databases': { + 'powerdns': { + 'owner': 'powerdns', + }, + }, + }, +} + + +@metadata_reactor +def get_ips_of_primary_nameservers(metadata): + if not metadata.get('powerdns/is_secondary', False): + return {} + + ips = set() + nodes = set() + for rnode in repo.nodes_in_group('dns'): + if not rnode.metadata.get('powerdns/is_secondary', False): + ips.update({ + str(ip) for ip in repo.libs.tools.resolve_identifier(repo, rnode.name) + }) + nodes.add(rnode.name) + + return { + 'powerdns': { + 'my_primary_servers': { + 'ips': ips, + 'nodes': nodes, + }, + }, + } + + +@metadata_reactor +def generate_dns_entries_for_nodes(metadata): + results = set() + + for rnode in repo.nodes: + node_name_split = rnode.name.split('.') + node_name_split.reverse() + dns_name = '.'.join(node_name_split) + ip4 = None + ip6 = None + + # We only need this for GCE, because machines over there don't + # have a public ipv4 address. + if rnode.metadata.get('external_ipv4', None): + ip4 = rnode.metadata.get('external_ipv4') + + for iface, config in sorted(rnode.metadata.get('interfaces', {}).items()): + if not ip4 and 'ipv4' in config: + ip4 = sorted(config['ipv4'])[0] + + if not ip6 and 'ipv6' in config: + ip6 = sorted(config['ipv6'])[0] + + if ip4: + results.add('{} IN A {}'.format(dns_name, ip4)) + + if ip6: + results.add('{} IN AAAA {}'.format(dns_name, ip6)) + + return { + 'powerdns': { + 'bind-zones': { + 'kunbox.net': { + 'records': results, + }, + }, + }, + } diff --git a/bundles/powerdnsadmin/files/config.py b/bundles/powerdnsadmin/files/config.py new file mode 100644 index 0000000..53a70bb --- /dev/null +++ b/bundles/powerdnsadmin/files/config.py @@ -0,0 +1,14 @@ +SALT = '${repo.vault.decrypt('encrypt$gAAAAABfidFVqVEgWvlXgP-GSQUgVtcTxzoZx2G8VYWHaGKRpgaLDchlTRcKwqgvfG5orNpXt7aDd5i2aehi6cvIlxYNdL87twfVhDLBDho8j-Uz5Vga8-9cEzEZULl5pFCIcRlYUCKyEIOcdXSaLCM3p8pGjrh-O8_g49rbADKmLFoJx2vVTVs=')}' +SECRET_KEY = '${repo.vault.password_for('{} powerdnsadmin secret_key'.format(node.name))}' +BIND_ADDRESS = '127.0.0.1' +PORT = 9191 +OFFLINE_MODE = True + +SQLA_DB_USER = 'powerdnsadmin' +SQLA_DB_PASSWORD = '${node.metadata['postgresql']['users']['powerdnsadmin']['password']}' +SQLA_DB_HOST = '127.0.0.1' +SQLA_DB_NAME = 'powerdnsadmin' +SQLALCHEMY_TRACK_MODIFICATIONS = True +SQLALCHEMY_DATABASE_URI = 'postgresql://' + SQLA_DB_USER + ':' + SQLA_DB_PASSWORD + '@' + SQLA_DB_HOST + '/' + SQLA_DB_NAME + +SAML_ENABLED = False diff --git a/bundles/powerdnsadmin/files/powerdnsadmin.service b/bundles/powerdnsadmin/files/powerdnsadmin.service new file mode 100644 index 0000000..3f7eb31 --- /dev/null +++ b/bundles/powerdnsadmin/files/powerdnsadmin.service @@ -0,0 +1,14 @@ +[Unit] +Description=PowerDNS-Admin +After=network.target postgresql.service + +[Service] +User=powerdnsadmin +Group=powerdnsadmin +Environment=FLASK_CONF=/opt/powerdnsadmin/config.py +WorkingDirectory=/opt/powerdnsadmin/src +ExecStartPre=-/bin/chown powerdnsadmin:powerdnsadmin /opt/powerdnsadmin/src/powerdnsadmin/static +ExecStart=/opt/powerdnsadmin/venv/bin/gunicorn 'powerdnsadmin:create_app()' + +[Install] +WantedBy=multi-user.target diff --git a/bundles/powerdnsadmin/items.py b/bundles/powerdnsadmin/items.py new file mode 100644 index 0000000..2d1802b --- /dev/null +++ b/bundles/powerdnsadmin/items.py @@ -0,0 +1,87 @@ +assert node.has_bundle('nodejs') +assert node.has_bundle('postgresql') + +directories = { + '/opt/powerdnsadmin/src': {}, +} + +git_deploy = { + '/opt/powerdnsadmin/src': { + 'repo': 'https://github.com/ngoduykhanh/PowerDNS-Admin.git', + 'rev': 'master', + 'triggers': { + 'action:powerdnsadmin_install_deps', + 'action:powerdnsadmin_upgrade_database', + 'action:powerdnsadmin_compile_assets', + 'svc_systemd:powerdnsadmin:restart', + }, + }, +} + +files = { + '/opt/powerdnsadmin/config.py': { + 'content_type': 'mako', + }, + '/etc/systemd/system/powerdnsadmin.service': { + 'triggers': { + 'action:systemd-reload', + }, + }, +} + +actions = { + 'powerdnsadmin_create_virtualenv': { + 'command': '/usr/bin/python3 -m virtualenv -p python3 /opt/powerdnsadmin/venv/', + 'unless': 'test -d /opt/powerdnsadmin/venv/', + 'needs': { + 'directory:/opt/powerdnsadmin', # provided by bundle:users + }, + }, + 'powerdnsadmin_install_deps': { + 'triggered': True, + 'command': '/opt/powerdnsadmin/venv/bin/pip install -r /opt/powerdnsadmin/src/requirements.txt', + 'needs': { + 'action:powerdnsadmin_create_virtualenv', + 'pkg_apt:', + }, + }, + 'powerdnsadmin_install_deps': { + 'triggered': True, + 'command': '/opt/powerdnsadmin/venv/bin/pip install -r /opt/powerdnsadmin/src/requirements.txt', + 'needs': { + 'action:powerdnsadmin_create_virtualenv', + 'pkg_apt:', + }, + }, + 'powerdnsadmin_upgrade_database': { + 'triggered': True, + 'command': 'FLASK_CONF=/opt/powerdnsadmin/config.py FLASK_APP=/opt/powerdnsadmin/src/powerdnsadmin/__init__.py /opt/powerdnsadmin/venv/bin/flask db upgrade', + # TODO unless + 'needs': { + 'action:powerdnsadmin_install_deps', + 'bundle:postgresql', + 'pkg_apt:', + }, + }, + 'powerdnsadmin_compile_assets': { + 'triggered': True, + 'command': 'cd /opt/powerdnsadmin/src && yarn install --pure-lockfile && FLASK_APP=/opt/powerdnsadmin/src/powerdnsadmin/__init__.py /opt/powerdnsadmin/venv/bin/flask assets build', + 'needs': { + 'action:powerdnsadmin_install_deps', + 'pkg_apt:', + }, + }, +} + +svc_systemd = { + 'powerdnsadmin': { + 'needs': { + 'file:/opt/powerdnsadmin/config.py', + 'file:/etc/systemd/system/powerdnsadmin.service', + 'git_deploy:/opt/powerdnsadmin/src', + 'action:powerdnsadmin_install_deps', + 'action:powerdnsadmin_upgrade_database', + 'action:powerdnsadmin_compile_assets', + }, + }, +} diff --git a/bundles/powerdnsadmin/metadata.py b/bundles/powerdnsadmin/metadata.py new file mode 100644 index 0000000..aeb3562 --- /dev/null +++ b/bundles/powerdnsadmin/metadata.py @@ -0,0 +1,35 @@ +defaults = { + 'apt': { + 'packages': { + 'default-libmysqlclient-dev': {}, + 'libffi-dev': {}, + 'libldap2-dev': {}, + 'libsasl2-dev': {}, + 'libssl-dev': {}, + 'libxml2-dev': {}, + 'libxmlsec1-dev': {}, + 'libxslt1-dev': {}, + 'pkg-config': {}, + 'python3-psycopg2': {}, + 'python3-wheel': {}, + }, + }, + 'users': { + 'powerdnsadmin': { + 'home': '/opt/powerdnsadmin', + }, + }, + 'postgresql': { + 'users': { + 'powerdnsadmin': { + 'password': repo.vault.password_for('{} postgresql powerdnsadmin'.format(node.name)), + }, + }, + 'databases': { + 'powerdnsadmin': { + 'owner': 'powerdnsadmin', + }, + }, + }, + +} diff --git a/data/bind/files/zones/franzi.business b/data/bind/files/zones/franzi.business deleted file mode 100644 index ce9b927..0000000 --- a/data/bind/files/zones/franzi.business +++ /dev/null @@ -1,28 +0,0 @@ -${header} - -$ORIGIN franzi.business. - -@ IN A 94.130.52.224 - IN AAAA 2a01:4f8:10b:2a5f::2 - IN MX 10 mx0.kunbox.net. - IN TXT v=spf1 mx ~all - -* IN A 94.130.52.224 - IN AAAA 2a01:4f8:10b:2a5f::2 - -grafana IN A 165.232.105.69 - IN AAAA 2a03:b0c0:1:e0::627:8001 - -icinga IN A 165.232.42.173 - IN AAAA 2a03:b0c0:1:e0::665:8001 - -sewfile IN A 116.203.205.248 - IN AAAA 2a01:4f8:c0c:c71b::1 - IN TXT v=spf1 a mx ~all - - -_matrix._tcp IN SRV 10 10 8448 matrix - -2019._domainkey IN TXT v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwkg6UAcu3V98hal1UVf6yB0WT1CKDS0AK83CUlSP8bUwraPxkxK1nkQOUsmjbQs6a3FhdsKprMi32GeUaTVvZg81JIybPk3jNugfNWfSjs2TXPomYu+XD2pmmbR3cZlzC5NGR2nmBFt/P/S2ihPHj35KziiBIwK1TdvOi1M2+upCjK33Icco0ByCm0gJpD2O0cbqcBcUKqd6X440vYhNXH1ygp0e91P0iRnvS9sg6yD0xjD8kD6j/8GfxBY+9bpU3EvDoBgyJSbjw5b6PUVJbKMXzw1NIRNj0SXKs5BakjS8+7u62vR11IPCYRwy+yr0rDT0tNegM7gStIIgoTpOoQIDAQAB -_dmarc IN TXT v=DMARC1; p=none; rua=mailto:postmaster@kunsmann.eu; ruf=mailto:postmaster@kunsmann.eu; fo=0:d:s; adkim=r; aspf=r -_token._dnswl IN TXT gg3mbwjx9bbuo5osvhq7oz6bc881wcmc diff --git a/data/bind/files/zones/kunbox.net b/data/bind/files/zones/kunbox.net deleted file mode 100644 index 94fa4e5..0000000 --- a/data/bind/files/zones/kunbox.net +++ /dev/null @@ -1,27 +0,0 @@ -${header} - -$ORIGIN kunbox.net. - -@ IN A 94.130.52.224 - IN AAAA 2a01:4f8:10b:2a5f::2 - -; Needs to have a working Mail address, otherwise Telekom goes mimimi - IN MX 10 mx0 - IN TXT v=spf1 mx ~all - -; Mail servers -mx0 IN A 94.130.52.224 - IN AAAA 2a01:4f8:10b:2a5f::2 -*.mx0 IN CNAME mx0 - -% for record in sorted(metadata_records): -${record} -% endfor - -2019._domainkey IN TXT v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwkg6UAcu3V98hal1UVf6yB0WT1CKDS0AK83CUlSP8bUwraPxkxK1nkQOUsmjbQs6a3FhdsKprMi32GeUaTVvZg81JIybPk3jNugfNWfSjs2TXPomYu+XD2pmmbR3cZlzC5NGR2nmBFt/P/S2ihPHj35KziiBIwK1TdvOi1M2+upCjK33Icco0ByCm0gJpD2O0cbqcBcUKqd6X440vYhNXH1ygp0e91P0iRnvS9sg6yD0xjD8kD6j/8GfxBY+9bpU3EvDoBgyJSbjw5b6PUVJbKMXzw1NIRNj0SXKs5BakjS8+7u62vR11IPCYRwy+yr0rDT0tNegM7gStIIgoTpOoQIDAQAB -_dmarc IN TXT v=DMARC1; p=none; rua=mailto:postmaster@kunsmann.eu; ruf=mailto:postmaster@kunsmann.eu; fo=0:d:s; adkim=r; aspf=r -_token._dnswl IN TXT 6akc10htbgmg56e072w0w2n0wql4oezu - -f2k1.de._report._dmarc IN TXT v=DMARC1 -franzi.business._report._dmarc IN TXT v=DMARC1 -kunsmann.eu._report._dmarc IN TXT v=DMARC1 diff --git a/data/bind/files/zones/kunsmann.eu b/data/bind/files/zones/kunsmann.eu deleted file mode 100644 index 009e53b..0000000 --- a/data/bind/files/zones/kunsmann.eu +++ /dev/null @@ -1,15 +0,0 @@ -${header} - -$ORIGIN kunsmann.eu. - -@ IN A 94.130.52.224 - IN AAAA 2a01:4f8:10b:2a5f::2 - IN MX 10 mx0.kunbox.net. - IN TXT v=spf1 a mx ~all - -* IN A 94.130.52.224 - IN AAAA 2a01:4f8:10b:2a5f::2 - -2019._domainkey IN TXT v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwkg6UAcu3V98hal1UVf6yB0WT1CKDS0AK83CUlSP8bUwraPxkxK1nkQOUsmjbQs6a3FhdsKprMi32GeUaTVvZg81JIybPk3jNugfNWfSjs2TXPomYu+XD2pmmbR3cZlzC5NGR2nmBFt/P/S2ihPHj35KziiBIwK1TdvOi1M2+upCjK33Icco0ByCm0gJpD2O0cbqcBcUKqd6X440vYhNXH1ygp0e91P0iRnvS9sg6yD0xjD8kD6j/8GfxBY+9bpU3EvDoBgyJSbjw5b6PUVJbKMXzw1NIRNj0SXKs5BakjS8+7u62vR11IPCYRwy+yr0rDT0tNegM7gStIIgoTpOoQIDAQAB -_dmarc IN TXT v=DMARC1; p=none; rua=mailto:postmaster@kunsmann.eu; ruf=mailto:postmaster@kunsmann.eu; fo=0:d:s; adkim=r; aspf=r -_token._dnswl IN TXT 5mx0rv9ru8s1zz4tf4xlt48osh09czmg diff --git a/data/bind/files/zones/felix-kunsmann.de b/data/powerdns/files/bind-zones/felix-kunsmann.de similarity index 100% rename from data/bind/files/zones/felix-kunsmann.de rename to data/powerdns/files/bind-zones/felix-kunsmann.de diff --git a/data/powerdns/files/bind-zones/franzi.business b/data/powerdns/files/bind-zones/franzi.business new file mode 100644 index 0000000..33ddf88 --- /dev/null +++ b/data/powerdns/files/bind-zones/franzi.business @@ -0,0 +1,28 @@ +${header} + +$ORIGIN franzi.business. + +@ IN A 94.130.52.224 + IN AAAA 2a01:4f8:10b:2a5f::2 + IN MX 10 mx0.kunbox.net. + IN TXT "v=spf1 mx ~all" + +* IN A 94.130.52.224 + IN AAAA 2a01:4f8:10b:2a5f::2 + +grafana IN A 165.232.105.69 + IN AAAA 2a03:b0c0:1:e0::627:8001 + +icinga IN A 165.232.42.173 + IN AAAA 2a03:b0c0:1:e0::665:8001 + +sewfile IN A 116.203.205.248 + IN AAAA 2a01:4f8:c0c:c71b::1 + IN TXT "v=spf1 a mx ~all" + + +_matrix._tcp IN SRV 10 10 8448 matrix + +2019._domainkey IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwkg6UAcu3V98hal1UVf6yB0WT1CKDS0AK83CUlSP8bUwraPxkxK1nkQOUsmjbQs6a3FhdsKprMi32GeUaTVvZg81JIybPk3jNugfNWfSjs2TXPomYu+XD2pmmbR3cZlzC5NGR2nmBFt/P/S2ihPHj35KziiBIwK1TdvOi1M2+upCjK33Icco0ByCm0gJpD2O0cbqcBcUKqd6X440vYhNXH1ygp0e91P0iRnvS9sg6yD0xjD8kD6j/8GfxBY+9bpU3EvDoBgyJSbjw5b6PUVJbKMXzw1NIRNj0SXKs5BakjS8+7u62vR11IPCYRwy+yr0rDT0tNegM7gStIIgoTpOoQIDAQAB" +_dmarc IN TXT "v=DMARC1; p=none; rua=mailto:postmaster@kunsmann.eu; ruf=mailto:postmaster@kunsmann.eu; fo=0:d:s; adkim=r; aspf=r" +_token._dnswl IN TXT "gg3mbwjx9bbuo5osvhq7oz6bc881wcmc" diff --git a/data/powerdns/files/bind-zones/kunbox.net b/data/powerdns/files/bind-zones/kunbox.net new file mode 100644 index 0000000..bae3e61 --- /dev/null +++ b/data/powerdns/files/bind-zones/kunbox.net @@ -0,0 +1,30 @@ +${header} + +$ORIGIN kunbox.net. + +@ IN A 94.130.52.224 + IN AAAA 2a01:4f8:10b:2a5f::2 + +; Needs to have a working Mail address, otherwise Telekom goes mimimi + IN MX 10 mx0 + IN TXT "v=spf1 mx ~all" + +; Mail servers +mx0 IN A 94.130.52.224 + IN AAAA 2a01:4f8:10b:2a5f::2 +*.mx0 IN CNAME mx0 + +; Nameservers +ns-1 IN A 34.89.208.78 + +% for record in sorted(metadata_records): +${record} +% endfor + +2019._domainkey IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwkg6UAcu3V98hal1UVf6yB0WT1CKDS0AK83CUlSP8bUwraPxkxK1nkQOUsmjbQs6a3FhdsKprMi32GeUaTVvZg81JIybPk3jNugfNWfSjs2TXPomYu+XD2pmmbR3cZlzC5NGR2nmBFt/P/S2ihPHj35KziiBIwK1TdvOi1M2+upCjK33Icco0ByCm0gJpD2O0cbqcBcUKqd6X440vYhNXH1ygp0e91P0iRnvS9sg6yD0xjD8kD6j/8GfxBY+9bpU3EvDoBgyJSbjw5b6PUVJbKMXzw1NIRNj0SXKs5BakjS8+7u62vR11IPCYRwy+yr0rDT0tNegM7gStIIgoTpOoQIDAQAB" +_dmarc IN TXT "v=DMARC1; p=none; rua=mailto:postmaster@kunsmann.eu; ruf=mailto:postmaster@kunsmann.eu; fo=0:d:s; adkim=r; aspf=r" +_token._dnswl IN TXT "6akc10htbgmg56e072w0w2n0wql4oezu" + +f2k1.de._report._dmarc IN TXT "v=DMARC1" +franzi.business._report._dmarc IN TXT "v=DMARC1" +kunsmann.eu._report._dmarc IN TXT "v=DMARC1" diff --git a/data/powerdns/files/bind-zones/kunsmann.eu b/data/powerdns/files/bind-zones/kunsmann.eu new file mode 100644 index 0000000..2e58da4 --- /dev/null +++ b/data/powerdns/files/bind-zones/kunsmann.eu @@ -0,0 +1,15 @@ +${header} + +$ORIGIN kunsmann.eu. + +@ IN A 94.130.52.224 + IN AAAA 2a01:4f8:10b:2a5f::2 + IN MX 10 mx0.kunbox.net. + IN TXT "v=spf1 a mx ~all" + +* IN A 94.130.52.224 + IN AAAA 2a01:4f8:10b:2a5f::2 + +2019._domainkey IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwkg6UAcu3V98hal1UVf6yB0WT1CKDS0AK83CUlSP8bUwraPxkxK1nkQOUsmjbQs6a3FhdsKprMi32GeUaTVvZg81JIybPk3jNugfNWfSjs2TXPomYu+XD2pmmbR3cZlzC5NGR2nmBFt/P/S2ihPHj35KziiBIwK1TdvOi1M2+upCjK33Icco0ByCm0gJpD2O0cbqcBcUKqd6X440vYhNXH1ygp0e91P0iRnvS9sg6yD0xjD8kD6j/8GfxBY+9bpU3EvDoBgyJSbjw5b6PUVJbKMXzw1NIRNj0SXKs5BakjS8+7u62vR11IPCYRwy+yr0rDT0tNegM7gStIIgoTpOoQIDAQAB" +_dmarc IN TXT "v=DMARC1; p=none; rua=mailto:postmaster@kunsmann.eu; ruf=mailto:postmaster@kunsmann.eu; fo=0:d:s; adkim=r; aspf=r" +_token._dnswl IN TXT "5mx0rv9ru8s1zz4tf4xlt48osh09czmg" diff --git a/data/bind/files/zones/trans-agenda.de b/data/powerdns/files/bind-zones/trans-agenda.de similarity index 100% rename from data/bind/files/zones/trans-agenda.de rename to data/powerdns/files/bind-zones/trans-agenda.de diff --git a/data/bind/files/zones/trans-agenda.eu b/data/powerdns/files/bind-zones/trans-agenda.eu similarity index 78% rename from data/bind/files/zones/trans-agenda.eu rename to data/powerdns/files/bind-zones/trans-agenda.eu index 0da91a1..29c3787 100644 --- a/data/bind/files/zones/trans-agenda.eu +++ b/data/powerdns/files/bind-zones/trans-agenda.eu @@ -3,7 +3,7 @@ ${header} $ORIGIN trans-agenda.eu. @ IN MX 10 mx0.kunbox.net. - IN TXT v=spf1 a mx ~all + IN TXT "v=spf1 a mx ~all" part.of.the IN A 94.130.52.224 part.of.the IN AAAA 2a01:4f8:10b:2a5f::2 diff --git a/groups/features.py b/groups/features.py index c7c0c25..4605270 100644 --- a/groups/features.py +++ b/groups/features.py @@ -7,6 +7,17 @@ groups['webserver'] = { groups['dns'] = { 'bundles': { - 'bind', + 'postgresql', + 'powerdns', + }, + 'metadata': { + 'powerdns': { + 'features': { + 'bind': True, + 'pgsql': True, + }, + # Overridden in node metadata for primary server + 'is_secondary': True, + }, }, } diff --git a/libs/tools.py b/libs/tools.py new file mode 100644 index 0000000..373f2f3 --- /dev/null +++ b/libs/tools.py @@ -0,0 +1,31 @@ +from bundlewrap.exceptions import NoSuchGroup, NoSuchNode +from ipaddress import ip_address + +def resolve_identifier(repo, identifier): + """ + Try to resolve an identifier (group or node). Return a set of ip + addresses valid for this identifier. + """ + try: + nodes = {repo.get_node(identifier)} + except NoSuchNode: + try: + nodes = repo.nodes_in_group(identifier) + except NoSuchGroup: + try: + return {ip_address(identifier)} + except: + return set() + + found_ips = set() + for node in nodes: + for interface, config in node.metadata.get('interfaces', {}).items(): + for ip in config.get('ipv4', set()): + found_ips.add(ip_address(ip)) + for ip in config.get('ipv4', set()): + found_ips.add(ip_address(ip)) + + if node.metadata.get('external_ipv4'): + found_ips.add(ip_address(node.metadata.get('external_ipv4'))) + + return found_ips diff --git a/nodes/a.ns14.net.py b/nodes/a.ns14.net.py new file mode 100644 index 0000000..db4d7c8 --- /dev/null +++ b/nodes/a.ns14.net.py @@ -0,0 +1,11 @@ +# This node is not actually part of this repository, it's a DNS server +# managed by AutoDNS. It needs a node file, because we're using that to +# auto-generate DNS configs. + +nodes['a.ns14.net'] = { + 'hostname': 'a.ns14.net', + 'dummy': True, + 'groups': { + 'dns', + }, +} diff --git a/nodes/b.ns14.net.py b/nodes/b.ns14.net.py new file mode 100644 index 0000000..c9ea427 --- /dev/null +++ b/nodes/b.ns14.net.py @@ -0,0 +1,11 @@ +# This node is not actually part of this repository, it's a DNS server +# managed by AutoDNS. It needs a node file, because we're using that to +# auto-generate DNS configs. + +nodes['b.ns14.net'] = { + 'hostname': 'b.ns14.net', + 'dummy': True, + 'groups': { + 'dns', + }, +} diff --git a/nodes/c.ns14.net.py b/nodes/c.ns14.net.py new file mode 100644 index 0000000..58b36c9 --- /dev/null +++ b/nodes/c.ns14.net.py @@ -0,0 +1,11 @@ +# This node is not actually part of this repository, it's a DNS server +# managed by AutoDNS. It needs a node file, because we're using that to +# auto-generate DNS configs. + +nodes['c.ns14.net'] = { + 'hostname': 'c.ns14.net', + 'dummy': True, + 'groups': { + 'dns', + }, +} diff --git a/nodes/d.ns14.net.py b/nodes/d.ns14.net.py new file mode 100644 index 0000000..728c644 --- /dev/null +++ b/nodes/d.ns14.net.py @@ -0,0 +1,11 @@ +# This node is not actually part of this repository, it's a DNS server +# managed by AutoDNS. It needs a node file, because we're using that to +# auto-generate DNS configs. + +nodes['d.ns14.net'] = { + 'hostname': 'd.ns14.net', + 'dummy': True, + 'groups': { + 'dns', + }, +} diff --git a/nodes/gce/bind01.py b/nodes/gce/bind01.py index abe38f3..9c0230c 100644 --- a/nodes/gce/bind01.py +++ b/nodes/gce/bind01.py @@ -1,9 +1,14 @@ -# ns-3.kunbox.net +# ns-1.kunbox.net # Frankfurt, Germany nodes['gce.bind01'] = { + 'bundles': { + 'nodejs', + 'powerdnsadmin', + }, 'groups': { 'dns', + 'webserver', }, 'metadata': { 'interfaces': { @@ -15,6 +20,22 @@ nodes['gce.bind01'] = { }, }, 'external_ipv4': '34.89.208.78', + 'nginx': { + 'vhosts': { + 'ns-1.kunbox.net': { + 'proxy': { + '/': { + 'target': 'http://127.0.0.1:8000/', + }, + }, + }, + }, + }, + 'powerdns': { + 'is_secondary': False, + 'secondary_nameservers': 'dns', + 'my_hostname': 'ns-1.kunbox.net', + }, 'vm': { 'cpu': 1, 'ram': 1,