Alex Monk has uploaded a new change for review. https://gerrit.wikimedia.org/r/304146
Change subject: [WIP] dnsrecursor: Rewrite code setting up lua hooks ...................................................................... [WIP] dnsrecursor: Rewrite code setting up lua hooks TODO: Basic idea has been tested, but this puppetisation needs testig To: * Not use separate files for our labs hooks, as PowerDNS only reads one file - preventing a rather nasty gotcha where one lua hook file would define a function and then get it overridden by the next. * Handle NXDOMAINs/SOAs for metal names properly. * Have all these files in the role module for labs dnsrecursors, rather than the dnsrecursor module itself. Bug: T139438 Change-Id: I1e4fae6ab33da9b229bc27868136783b8aedc010 --- R hieradata/common/role/labs/dnsrecursor/lua_hooks.yaml D modules/dnsrecursor/files/labs-ip-alias-dump.py M modules/dnsrecursor/manifests/init.pp D modules/dnsrecursor/manifests/labsaliaser.pp D modules/dnsrecursor/manifests/metalresolver.pp D modules/dnsrecursor/templates/metaldns.lua.erb D modules/dnsrecursor/templates/recursorhooks.lua.erb A modules/role/files/labs/dnsrecursor-hooks-builder.py M modules/role/manifests/labs/dnsrecursor.pp 9 files changed, 200 insertions(+), 278 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/operations/puppet refs/changes/46/304146/1 diff --git a/hieradata/common/dnsrecursor/labsaliaser.yaml b/hieradata/common/role/labs/dnsrecursor/lua_hooks.yaml similarity index 100% rename from hieradata/common/dnsrecursor/labsaliaser.yaml rename to hieradata/common/role/labs/dnsrecursor/lua_hooks.yaml diff --git a/modules/dnsrecursor/files/labs-ip-alias-dump.py b/modules/dnsrecursor/files/labs-ip-alias-dump.py deleted file mode 100644 index 3f5a8f2..0000000 --- a/modules/dnsrecursor/files/labs-ip-alias-dump.py +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/python -import os -import sys -import yaml -import argparse -import itertools - -from keystoneclient.session import Session as KeystoneSession -from keystoneclient.auth.identity.v2 import Password as KeystonePassword -from keystoneclient.client import Client as KeystoneClient - -from novaclient import client as novaclient - -argparser = argparse.ArgumentParser() -argparser.add_argument( - '--config-file', - help='Path to config file', - default='/etc/labs-dns-alias.yaml', - type=argparse.FileType('r') -) -argparser.add_argument( - '--check-changes-only', - help='Exit with 0 if there are no changes and 1 if there are changes. Do not write to file', - action='store_true' -) - -LUA_LINE_TEMPLATE = '{table}["{key}"] = "{value}" -- {comment}\n' - -args = argparser.parse_args() -config = yaml.safe_load(args.config_file) - -auth = KeystonePassword( - auth_url=config['nova_api_url'], - username=config['username'], - password=config['password'], - tenant_name=config['admin_project_name'] -) -keystoneClient = KeystoneClient( - session=KeystoneSession(auth=auth), endpoint=config['nova_api_url']) - -projects = [] -for tenant in keystoneClient.tenants.list(): - projects.append(tenant.name) - -aliases = {} -for project in projects: - client = novaclient.Client( - "1.1", - config['username'], - config['password'], - project, - config['nova_api_url'] - ) - - for server in client.servers.list(): - serverAddresses = {} - try: - private = [ - str(ip['addr']) for ip in server.addresses['public'] - if ip['OS-EXT-IPS:type'] == 'fixed' - ] - public = [ - str(ip['addr']) for ip in server.addresses['public'] - if ip['OS-EXT-IPS:type'] == 'floating' - ] - if public: - # Match all possible public IPs to all possible private ones - # Technically there can be more than one floating IP and more than one private IP - # Although this is never practically the case... - aliases[server.name] = list(itertools.product(public, private)) - except KeyError: - # This can happen if a server doesn't (yet) have any addresses, while it's being - # constructed. In which case we simply harmlessly ignore it. - pass - -output = 'aliasmapping = {}\n' -# Sort to prevent flapping around due to random ordering -for name in sorted(aliases.keys()): - ips = aliases[name] - for public, private in ips: - output += LUA_LINE_TEMPLATE.format( - table='aliasmapping', - key=public, - value=private, - comment=name - ) - -output += """ -function postresolve (remoteip, domain, qtype, records, origrcode) - for key,val in ipairs(records) - do - if (aliasmapping[val.content] and val.qtype == pdns.A) then - val.content = aliasmapping[val.content] - setvariable() - end - end - return origrcode, records -end - -""" - -if 'extra_records' in config: - output += 'extra_records = {}\n' - extra_records = config['extra_records'] - - for q in sorted(extra_records.keys()): - output += LUA_LINE_TEMPLATE.format( - table='extra_records', - key=q, - value=extra_records[q], - comment=q - ) - - output += """ -function preresolve(remoteip, domain, qtype) - if extra_records[domain] - then - return 0, { - {qtype=pdns.A, content=extra_records[domain], ttl=300, place="1"}, - } - end - return -1, {} -end -""" - -if os.path.exists(config['output_path']): - with open(config['output_path']) as f: - current_contents = f.read() -else: - current_contents = "" - -if output == current_contents: - # Do nothing! - if args.check_changes_only: - sys.exit(0) -else: - if args.check_changes_only: - sys.exit(1) - with open(config['output_path'], 'w') as f: - f.write(output) diff --git a/modules/dnsrecursor/manifests/init.pp b/modules/dnsrecursor/manifests/init.pp index 43ef452..23ee7ae 100644 --- a/modules/dnsrecursor/manifests/init.pp +++ b/modules/dnsrecursor/manifests/init.pp @@ -5,6 +5,9 @@ # # [*allow_from] # Prefixes from which to allow recursive DNS queries +# +# [*lua_hooks] +# Boolean. If true, will load lua_hooks found at /etc/powerdns/recursorhooks.lua class dnsrecursor( $listen_addresses = [$::ipaddress], @@ -41,18 +44,6 @@ mode => '0444', notify => Service['pdns-recursor'], content => template('dnsrecursor/recursor.conf.erb'), - } - - if $lua_hooks { - file { '/etc/powerdns/recursorhooks.lua': - ensure => 'present', - require => Package['pdns-recursor'], - owner => 'root', - group => 'root', - mode => '0444', - notify => Service['pdns-recursor'], - content => template('dnsrecursor/recursorhooks.lua.erb'), - } } service { 'pdns-recursor': diff --git a/modules/dnsrecursor/manifests/labsaliaser.pp b/modules/dnsrecursor/manifests/labsaliaser.pp deleted file mode 100644 index 24a2d9d..0000000 --- a/modules/dnsrecursor/manifests/labsaliaser.pp +++ /dev/null @@ -1,47 +0,0 @@ -class dnsrecursor::labsaliaser( - $username, - $password, - $nova_api_url, - $extra_records, - $alias_file, - $admin_project_name, -) { - - require_package(['python-novaclient', 'python-keystoneclient']) - - $config = { - 'username' => $username, - 'password' => $password, - 'output_path' => $alias_file, - 'nova_api_url' => $nova_api_url, - 'extra_records' => $extra_records, - 'admin_project_name' => $admin_project_name, - } - - file { '/etc/labs-dns-alias.yaml': - ensure => present, - owner => 'root', - group => 'root', - mode => '0440', - content => ordered_yaml($config), - } - - file { '/usr/local/bin/labs-ip-alias-dump.py': - ensure => present, - owner => 'root', - group => 'root', - mode => '0550', - source => 'puppet:///modules/dnsrecursor/labs-ip-alias-dump.py', - } - - exec { '/usr/local/bin/labs-ip-alias-dump.py': - user => 'root', - group => 'root', - notify => Service['pdns-recursor'], - require => File[ - '/usr/local/bin/labs-ip-alias-dump.py', - '/etc/labs-dns-alias.yaml' - ], - unless => '/usr/local/bin/labs-ip-alias-dump.py --check-changes-only', - } -} diff --git a/modules/dnsrecursor/manifests/metalresolver.pp b/modules/dnsrecursor/manifests/metalresolver.pp deleted file mode 100644 index 0db5eba..0000000 --- a/modules/dnsrecursor/manifests/metalresolver.pp +++ /dev/null @@ -1,16 +0,0 @@ -class dnsrecursor::metalresolver( - $metal_resolver, - $tld, -) { - $labs_metal = hiera('labs_metal',[]) - - file { $metal_resolver: - ensure => present, - require => Package['pdns-recursor'], - owner => 'root', - group => 'root', - mode => '0444', - notify => Service['pdns-recursor'], - content => template('dnsrecursor/metaldns.lua.erb'), - } -} diff --git a/modules/dnsrecursor/templates/metaldns.lua.erb b/modules/dnsrecursor/templates/metaldns.lua.erb deleted file mode 100644 index 459c703..0000000 --- a/modules/dnsrecursor/templates/metaldns.lua.erb +++ /dev/null @@ -1,34 +0,0 @@ --- This script comes from puppet: modules/dnsrecursor/templates/metaldns.lua.erb. --- --- It inserts a few select entries for labs metal DNS resolution. --- --- This is handled here rather than in designate because it's easier to puppetize --- this file than to insert things into designate from puppet, and currently --- puppet/hiera contains the canonical representation of bare metal hosts and names. - -ARecords = {} -PTRRecords = {} - -<% @labs_metal.sort.map do |k,v| -%> -ARecords["<%= k %>.<%= @site %>.<%= @tld %>."] = "<%= v['IPv4'] %>" -ARecords["<%= k %>.<%= v['project'] %>.<%= @site %>.<%= @tld %>."] = "<%= v['IPv4'] %>" -PTRRecords["<%= v['IPv4'].split('.').reverse().join('.') %>.in-addr.arpa."] = "<%= k %>.<%= v['project'] %>.<%= @site %>.<%= @tld %>." -<% end -%> - -function nxdomain (remoteip, domain, qtype) - if ((qtype == pdns.PTR or qtype == pdns.ANY) and PTRRecords[domain]) then - return 0, {{qtype=pdns.PTR, content=(PTRRecords[domain]), ttl=300}} - end - - if ((qtype == pdns.A or qtype == pdns.ANY) and ARecords[domain]) then - return 0, {{qtype=pdns.A, content=(ARecords[domain]), ttl=300}} - end - - -- Prevent NXDOMAIN if the domain exists, we just don't have a record of the matching type. - if (ARecords[domain] or PTRRecords[domain]) then - -- There isn't really a way to provide a proper serial here, so I chose '1'. - return 0, {{qtype=pdns.SOA, content="labs-ns0.wikimedia.org. root.wmflabs.org. 1 3600 600 86400 3600", ttl=60}} - end - - return -1, {} -end diff --git a/modules/dnsrecursor/templates/recursorhooks.lua.erb b/modules/dnsrecursor/templates/recursorhooks.lua.erb deleted file mode 100644 index fb158f2..0000000 --- a/modules/dnsrecursor/templates/recursorhooks.lua.erb +++ /dev/null @@ -1,7 +0,0 @@ --- This file is managed by puppet. --- --- The Powerdns recursor only supports a single .lua file. This is that file; it includes --- other files via 'dofile'. -<% @lua_hooks.sort.map do |hook| -%> -dofile("<%= hook %>") -<% end -%> diff --git a/modules/role/files/labs/dnsrecursor-hooks-builder.py b/modules/role/files/labs/dnsrecursor-hooks-builder.py new file mode 100644 index 0000000..53d11df --- /dev/null +++ b/modules/role/files/labs/dnsrecursor-hooks-builder.py @@ -0,0 +1,188 @@ +#!/usr/bin/python +import os +import sys +import yaml +import argparse +import itertools + +from keystoneclient.session import Session as KeystoneSession +from keystoneclient.auth.identity.v2 import Password as KeystonePassword +from keystoneclient.client import Client as KeystoneClient + +from novaclient import client as novaclient + +argparser = argparse.ArgumentParser() +argparser.add_argument( + '--config-file', + help='Path to config file', + default='/etc/labs-dnsrecursor-hooks-builder-config.yaml', + type=argparse.FileType('r') +) +argparser.add_argument( + '--check-changes-only', + help='Exit with 0 if there are no changes and 1 if there are changes. Do not write to file', + action='store_true' +) + +LUA_LINE_TEMPLATE = '{table}["{key}"] = "{value}"\n' +LUA_LINE_TEMPLATE_COMMENT = '{table}["{key}"] = "{value}" -- {comment}\n' +INSTANCE_DNS_SUFFIX = '.' + config['site'] + '.' + config['tld'] + '.' + +args = argparser.parse_args() +config = yaml.safe_load(args.config_file) + +auth = KeystonePassword( + auth_url=config['nova_api_url'], + username=config['username'], + password=config['password'], + tenant_name=config['admin_project_name'] +) +keystoneClient = KeystoneClient( + session=KeystoneSession(auth=auth), endpoint=config['nova_api_url']) + +projects = [] +for tenant in keystoneClient.tenants.list(): + projects.append(tenant.name) + +aliases = {} +for project in projects: + client = novaclient.Client( + "1.1", + config['username'], + config['password'], + project, + config['nova_api_url'] + ) + + for server in client.servers.list(): + serverAddresses = {} + try: + private = [ + str(ip['addr']) for ip in server.addresses['public'] + if ip['OS-EXT-IPS:type'] == 'fixed' + ] + public = [ + str(ip['addr']) for ip in server.addresses['public'] + if ip['OS-EXT-IPS:type'] == 'floating' + ] + if public: + # Match all possible public IPs to all possible private ones + # Technically there can be more than one floating IP and more than one private IP + # Although this is never practically the case... + aliases[server.name] = list(itertools.product(public, private)) + except KeyError: + # This can happen if a server doesn't (yet) have any addresses, while it's being + # constructed. In which case we simply harmlessly ignore it. + pass + +output = """-- THIS FILE IS GENERATED BY dnsrecursor-hooks-builder.py, RUN BY PUPPET +-- Queries hitting this code (i.e., the recursors) are from internal labs machines. +-- +-- The metal entries are handled here rather than in designate because it's easier to puppetize +-- this file than to insert things into designate from puppet, and currently puppet/hiera contains +-- the canonical representation of bare metal hosts and names. +-- +-- The IP aliasing is handled here because it is dynamic, we need to return different records +-- to the outside world than to internal machines. The problem this solves is that labs instances +-- without public IPs cannot connect to labs public IPs, only private IPs. + +IPAliases = {} +MetalARecords = {} +MetalPTRRecords = {} +""" +# Sort to prevent flapping around due to random ordering +for name in sorted(aliases.keys()): + ips = aliases[name] + for public, private in ips: + output += LUA_LINE_TEMPLATE_COMMENT.format( + table='IPAliases', + key=public, + value=private, + comment=name + ) + +for instance_name in sorted(config['metal']): + instance_details = config['metal'][instance_name] + IPv4 = instance_details['IPv4'] + project = instance_details['project'] + output += LUA_LINE_TEMPLATE.format( + table='MetalARecords', + key=instance_name + '.' + INSTANCE_DNS_SUFFIX, + value=IPv4 + ) + LUA_LINE_TEMPLATE.format( + table='MetalARecords', + key=instance_name + '.' + project + INSTANCE_DNS_SUFFIX, + value=IPv4 + ) + LUA_LINE_TEMPLATE.format( + table='MetalPTRRecords', + key='.'.join(list(reversed(IPv4.split('.')))) + '.in-addr.arpa.', + value=instance_name + '.' + project + INSTANCE_DNS_SUFFIX + ) + '\n' + +output += """ +function postresolve (remoteip, domain, qtype, records, origrcode) + if ((qtype == pdns.PTR or qtype == pdns.ANY) and MetalPTRRecords[domain]) then + return 0, {{qtype=pdns.PTR, content=(MetalPTRRecords[domain]), ttl=300}} + end + + if ((qtype == pdns.A or qtype == pdns.ANY) and MetalARecords[domain]) then + return 0, {{qtype=pdns.A, content=(MetalARecords[domain]), ttl=300}} + end + + -- Prevent NXDOMAIN if the domain exists, we just don't have a record of the matching type. + if (MetalARecords[domain] or MetalPTRRecords[domain]) then + return 0, records + end + + for key, val in ipairs(records) + do + if (IPAliases[val.content] and val.qtype == pdns.A) then + val.content = IPAliases[val.content] + setvariable() + end + end + + return origrcode, records +end + +""" + +if 'extra_records' in config: + output += 'extra_records = {}\n' + extra_records = config['extra_records'] + + for q in sorted(extra_records.keys()): + output += LUA_LINE_TEMPLATE.format( + table='extra_records', + key=q, + value=extra_records[q], + comment=q + ) + + output += """ +function preresolve(remoteip, domain, qtype) + if extra_records[domain] + then + return 0, { + {qtype=pdns.A, content=extra_records[domain], ttl=300, place="1"}, + } + end + return -1, {} +end +""" + +if os.path.exists(config['output_path']): + with open(config['output_path']) as f: + current_contents = f.read() +else: + current_contents = "" + +if output == current_contents: + # Do nothing! + if args.check_changes_only: + sys.exit(0) +else: + if args.check_changes_only: + sys.exit(1) + with open(config['output_path'], 'w') as f: + f.write(output) diff --git a/modules/role/manifests/labs/dnsrecursor.pp b/modules/role/manifests/labs/dnsrecursor.pp index af84f4b..f258ca5 100644 --- a/modules/role/manifests/labs/dnsrecursor.pp +++ b/modules/role/manifests/labs/dnsrecursor.pp @@ -37,9 +37,6 @@ address => $recursor_ip } - # We need to alias some public IPs to their corresponding private IPs. - $wikitech_nova_ldap_user_pass = $keystoneconfig['ldap_user_pass'] - $wikitech_nova_admin_project_name = $keystoneconfig['admin_project_name'] $nova_controller_hostname = hiera('labs_nova_controller') $listen_addresses = $::realm ? { @@ -48,36 +45,26 @@ } $labs_auth_dns = ipresolve($dnsconfig['host'],4) - - $alias_file = '/etc/powerdns/labs-ip-alias.lua' - $metal_resolver = '/etc/powerdns/metaldns.lua' - $lua_hooks = [$alias_file, $metal_resolver] - - $tld = hiera('labs_tld') $private_reverse = hiera('labs_private_ips_reverse_dns') + + # Purge old files + # TODO: Remove + file { ['/etc/labs-dns-alias.yaml', '/usr/local/bin/labs-ip-alias-dump.py']: + ensure => absent + } + + include dnsrecursor::lua_hooks class { '::dnsrecursor': listen_addresses => $listen_addresses, allow_from => $all_networks, additional_forward_zones => "${tld}=${labs_auth_dns}, ${private_reverse}=${labs_auth_dns}", auth_zones => 'labsdb=/var/zones/labsdb', - lua_hooks => $lua_hooks, + lua_hooks => true, max_negative_ttl => 900, max_tcp_per_client => 10, max_cache_entries => 3000000, client_tcp_timeout => 1, - } - - class { '::dnsrecursor::labsaliaser': - username => 'novaadmin', - password => $wikitech_nova_ldap_user_pass, - nova_api_url => "http://${nova_controller_hostname}:35357/v2.0", - alias_file => $alias_file, - admin_project_name => $wikitech_nova_admin_project_name - } - class { '::dnsrecursor::metalresolver': - metal_resolver => $metal_resolver, - tld => $tld } # There are three replica servers (c1, c2, c3). The mapping of -- To view, visit https://gerrit.wikimedia.org/r/304146 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I1e4fae6ab33da9b229bc27868136783b8aedc010 Gerrit-PatchSet: 1 Gerrit-Project: operations/puppet Gerrit-Branch: production Gerrit-Owner: Alex Monk <a...@wikimedia.org> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits