Source code for cobbler.modules.managers.bind

"""
This is some of the code behind 'cobbler sync'.
"""

import re
import socket
import time
from builtins import object
from builtins import range
from builtins import str

import cobbler.clogger as clogger
import cobbler.templar as templar
import cobbler.utils as utils
from cobbler.cexceptions import CX
from cobbler.utils import _


[docs]def register(): """ The mandatory cobbler module registration hook. """ return "manage"
[docs]class BindManager(object):
[docs] def what(self): return "bind"
def __init__(self, collection_mgr, logger): """ Constructor """ self.logger = logger if self.logger is None: self.logger = clogger.Logger() self.collection_mgr = collection_mgr self.api = collection_mgr.api self.distros = collection_mgr.distros() self.profiles = collection_mgr.profiles() self.systems = collection_mgr.systems() self.settings = collection_mgr.settings() self.repos = collection_mgr.repos() self.templar = templar.Templar(collection_mgr) self.settings_file = utils.namedconf_location(self.api) self.zonefile_base = utils.zonefile_base(self.api)
[docs] def regen_hosts(self): pass # not used
def __expand_IPv6(self, address): """ Expands an IPv6 address to long format i.e. xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx """ # Function by Chris Miller, approved for GLP use, taken verbatim from: # http://forrst.com/posts/Python_Expand_Abbreviated_IPv6_Addresses-1kQ fullAddress = "" # All groups expandedAddress = "" # Each group padded with leading zeroes validGroupCount = 8 validGroupSize = 4 if "::" not in address: # All groups are already present fullAddress = address else: # Consecutive groups of zeroes have been collapsed with "::" sides = address.split("::") groupsPresent = 0 for side in sides: if len(side) > 0: groupsPresent += len(side.split(":")) if len(sides[0]) > 0: fullAddress += sides[0] + ":" for i in range(0, validGroupCount - groupsPresent): fullAddress += "0000:" if len(sides[1]) > 0: fullAddress += sides[1] if fullAddress[-1] == ":": fullAddress = fullAddress[:-1] groups = fullAddress.split(":") for group in groups: while (len(group) < validGroupSize): group = "0" + group expandedAddress += group + ":" if expandedAddress[-1] == ":": expandedAddress = expandedAddress[:-1] return expandedAddress def __forward_zones(self): """ Returns a map of zones and the records that belong in them """ zones = {} forward_zones = self.settings.manage_forward_zones if not isinstance(forward_zones, list): # gracefully handle when user inputs only a single zone # as a string instead of a list with only a single item forward_zones = [forward_zones] for zone in forward_zones: zones[zone] = {} for system in self.systems: for (name, interface) in list(system.interfaces.items()): host = interface["dns_name"] ip = interface["ip_address"] ipv6 = interface["ipv6_address"] ipv6_sec_addrs = interface["ipv6_secondaries"] if not system.is_management_supported(cidr_ok=False): continue if not host: # gotsta have some dns_name and ip or else! continue if host.find(".") == -1: continue # match the longest zone! # e.g. if you have a host a.b.c.d.e # if manage_forward_zones has: # - c.d.e # - b.c.d.e # then a.b.c.d.e should go in b.c.d.e best_match = '' for zone in list(zones.keys()): if re.search(r'\.%s$' % zone, host) and len(zone) > len(best_match): best_match = zone if best_match == '': # no match continue # strip the zone off the dns_name host = re.sub(r'\.%s$' % best_match, '', host) # if we are to manage ipmi hosts, add that too if (self.settings.bind_manage_ipmi): if (system.power_address != ""): power_address_is_ip = False # see if the power address is an IP try: socket.inet_aton(system.power_address) power_address_is_ip = True except socket.error: power_address_is_ip = False # if the power address is an IP, then add it to the DNS # with the host suffix of "-ipmi" # TODO: Perhpas the suffix can be configurable through settings? if (power_address_is_ip): ipmi_host = host + "-ipmi" ipmi_ips = [] ipmi_ips.append(system.power_address) try: zones[best_match][ipmi_host] = ipmi_ips + zones[best_match][ipmi_host] except KeyError: zones[best_match][ipmi_host] = ipmi_ips # Create a list of IP addresses for this host ips = [] if ip: ips.append(ip) if ipv6: ips.append(ipv6) if ipv6_sec_addrs: ips += ipv6_sec_addrs if ips: try: zones[best_match][host] = ips + zones[best_match][host] except KeyError: zones[best_match][host] = ips return zones def __reverse_zones(self): """ Returns a map of zones and the records that belong in them """ zones = {} reverse_zones = self.settings.manage_reverse_zones if not isinstance(reverse_zones, list): # gracefully handle when user inputs only a single zone # as a string instead of a list with only a single item reverse_zones = [reverse_zones] for zone in reverse_zones: # expand and IPv6 zones if ":" in zone: zone = (self.__expand_IPv6(zone + '::1'))[:19] zones[zone] = {} for system in self.systems: for (name, interface) in list(system.interfaces.items()): host = interface["dns_name"] ip = interface["ip_address"] ipv6 = interface["ipv6_address"] ipv6_sec_addrs = interface["ipv6_secondaries"] if not system.is_management_supported(cidr_ok=False): continue if not host or ((not ip) and (not ipv6)): # gotsta have some dns_name and ip or else! continue if ip: # match the longest zone! # e.g. if you have an ip 1.2.3.4 # if manage_reverse_zones has: # - 1.2 # - 1.2.3 # then 1.2.3.4 should go in 1.2.3 best_match = '' for zone in list(zones.keys()): if re.search(r'^%s\.' % zone, ip) and len(zone) > len(best_match): best_match = zone if best_match != '': # strip the zone off the front of the ip # reverse the rest of the octets # append the remainder + dns_name ip = ip.replace(best_match, '', 1) if ip[0] == '.': # strip leading '.' if it's there ip = ip[1:] tokens = ip.split('.') tokens.reverse() ip = '.'.join(tokens) zones[best_match][ip] = host + '.' if ipv6 or ipv6_sec_addrs: ip6s = [] if ipv6: ip6s.append(ipv6) for each_ipv6 in ip6s + ipv6_sec_addrs: # convert the IPv6 address to long format long_ipv6 = self.__expand_IPv6(each_ipv6) # All IPv6 zones are forced to have the format # xxxx:xxxx:xxxx:xxxx zone = long_ipv6[:19] ipv6_host_part = long_ipv6[20:] tokens = list(re.sub(':', '', ipv6_host_part)) tokens.reverse() ip = '.'.join(tokens) zones[zone][ip] = host + '.' return zones def __write_named_conf(self): """ Write out the named.conf main config file from the template. """ settings_file = self.settings.bind_chroot_path + self.settings_file template_file = "/etc/cobbler/named.template" # forward_zones = self.settings.manage_forward_zones # reverse_zones = self.settings.manage_reverse_zones metadata = {'forward_zones': list(self.__forward_zones().keys()), 'reverse_zones': [], 'zone_include': ''} for zone in metadata['forward_zones']: txt = """ zone "%(zone)s." { type master; file "%(zone)s"; }; """ % {'zone': zone} metadata['zone_include'] = metadata['zone_include'] + txt for zone in list(self.__reverse_zones().keys()): # IPv6 zones are : delimited if ":" in zone: # if IPv6, assume xxxx:xxxx:xxxx:xxxx # 0123456789012345678 long_zone = (self.__expand_IPv6(zone + '::1'))[:19] tokens = list(re.sub(':', '', long_zone)) tokens.reverse() arpa = '.'.join(tokens) + '.ip6.arpa' else: # IPv4 address split by '.' tokens = zone.split('.') tokens.reverse() arpa = '.'.join(tokens) + '.in-addr.arpa' # metadata['reverse_zones'].append((zone, arpa)) txt = """ zone "%(arpa)s." { type master; file "%(zone)s"; }; """ % {'arpa': arpa, 'zone': zone} metadata['zone_include'] = metadata['zone_include'] + txt try: f2 = open(template_file, "r") except: raise CX(_("error reading template from file: %s") % template_file) template_data = "" template_data = f2.read() f2.close() if self.logger is not None: self.logger.info("generating %s" % settings_file) self.templar.render(template_data, metadata, settings_file, None) def __write_secondary_conf(self): """ Write out the secondary.conf secondary config file from the template. """ settings_file = self.settings.bind_chroot_path + '/etc/secondary.conf' template_file = "/etc/cobbler/secondary.template" # forward_zones = self.settings.manage_forward_zones # reverse_zones = self.settings.manage_reverse_zones metadata = {'forward_zones': list(self.__forward_zones().keys()), 'reverse_zones': [], 'zone_include': ''} for zone in metadata['forward_zones']: txt = """ zone "%(zone)s." { type slave; masters { %(master)s; }; file "data/%(zone)s"; }; """ % {'zone': zone, 'master': self.settings.bind_master} metadata['zone_include'] = metadata['zone_include'] + txt for zone in list(self.__reverse_zones().keys()): # IPv6 zones are : delimited if ":" in zone: # if IPv6, assume xxxx:xxxx:xxxx:xxxx for the zone # 0123456789012345678 long_zone = (self.__expand_IPv6(zone + '::1'))[:19] tokens = list(re.sub(':', '', long_zone)) tokens.reverse() arpa = '.'.join(tokens) + '.ip6.arpa' else: # IPv4 zones split by '.' tokens = zone.split('.') tokens.reverse() arpa = '.'.join(tokens) + '.in-addr.arpa' # metadata['reverse_zones'].append((zone, arpa)) txt = """ zone "%(arpa)s." { type slave; masters { %(master)s; }; file "data/%(zone)s"; }; """ % {'arpa': arpa, 'zone': zone, 'master': self.settings.bind_master} metadata['zone_include'] = metadata['zone_include'] + txt metadata['bind_master'] = self.settings.bind_master try: f2 = open(template_file, "r") except: raise CX(_("error reading template from file: %s") % template_file) template_data = "" template_data = f2.read() f2.close() if self.logger is not None: self.logger.info("generating %s" % settings_file) self.templar.render(template_data, metadata, settings_file, None) def __ip_sort(self, ips): """ Sorts IP addresses (or partial addresses) in a numerical fashion per-octet or quartet """ quartets = [] octets = [] for each_ip in ips: # IPv6 addresses are ':' delimited if ":" in each_ip: # IPv6 # strings to integer quartet chunks so we can sort numerically quartets.append([int(i, 16) for i in each_ip.split(':')]) else: # IPv4 # strings to integer octet chunks so we can sort numerically octets.append([int(i) for i in each_ip.split('.')]) quartets.sort() # integers back to four character hex strings quartets = [[format(i, '04x') for i in x] for x in quartets] # octets.sort() # integers back to strings octets = [[str(i) for i in x] for x in octets] # return ['.'.join(i) for i in octets] + [':'.join(i) for i in quartets] def __pretty_print_host_records(self, hosts, rectype='A', rclass='IN'): """ Format host records by order and with consistent indentation """ # Warns on hosts without dns_name, need to iterate over system to name the # particular system for system in self.systems: for (name, interface) in list(system.interfaces.items()): if interface["dns_name"] == "": self.logger.info( ("Warning: dns_name unspecified in the system: %s, while writing host records") % system.name) names = [k for k, v in list(hosts.items())] if not names: return '' # zones with no hosts if rectype == 'PTR': names = self.__ip_sort(names) else: names.sort() max_name = max([len(i) for i in names]) s = "" for name in names: spacing = " " * (max_name - len(name)) my_name = "%s%s" % (name, spacing) my_host_record = hosts[name] my_host_list = [] if isinstance(my_host_record, str): my_host_list = [my_host_record] else: my_host_list = my_host_record for my_host in my_host_list: my_rectype = rectype[:] if rectype == 'A': if ":" in my_host: my_rectype = 'AAAA' else: my_rectype = 'A ' s += "%s %s %s %s;\n" % (my_name, rclass, my_rectype, my_host) return s def __pretty_print_cname_records(self, hosts, rectype='CNAME'): """ Format CNAMEs and with consistent indentation """ s = "" # This loop warns and skips the host without dns_name instead of outright exiting # Which results in empty records without any warning to the users for system in self.systems: for (name, interface) in list(system.interfaces.items()): cnames = interface.get("cnames", []) try: if interface.get("dns_name", "") != "": dnsname = interface["dns_name"].split('.')[0] for cname in cnames: s += "%s %s %s;\n" % (cname.split('.')[0], rectype, dnsname) else: self.logger.info(("Warning: dns_name unspecified in the system: %s, Skipped!, while writing cname records") % system.name) continue except: pass return s def __write_zone_files(self): """ Write out the forward and reverse zone files for all configured zones """ default_template_file = "/etc/cobbler/zone.template" cobbler_server = self.settings.server # this could be a config option too serial_filename = "/var/lib/cobbler/bind_serial" # need a counter for new bind format serial = time.strftime("%Y%m%d00") try: serialfd = open(serial_filename, "r") old_serial = serialfd.readline() # same date if serial[0:8] == old_serial[0:8]: if int(old_serial[8:10]) < 99: serial = "%s%.2i" % (serial[0:8], int(old_serial[8:10]) + 1) else: pass serialfd.close() except: pass serialfd = open(serial_filename, "w") serialfd.write(serial) serialfd.close() forward = self.__forward_zones() reverse = self.__reverse_zones() try: f2 = open(default_template_file, "r") except: raise CX(_("error reading template from file: %s") % default_template_file) default_template_data = "" default_template_data = f2.read() f2.close() zonefileprefix = self.settings.bind_chroot_path + self.zonefile_base for (zone, hosts) in list(forward.items()): metadata = { 'cobbler_server': cobbler_server, 'serial': serial, 'zonename': zone, 'zonetype': 'forward', 'cname_record': '', 'host_record': '' } if ":" in zone: long_zone = (self.__expand_IPv6(zone + '::1'))[:19] tokens = list(re.sub(':', '', long_zone)) tokens.reverse() zone_origin = '.'.join(tokens) + '.ip6.arpa.' else: zone_origin = '' # grab zone-specific template if it exists try: fd = open('/etc/cobbler/zone_templates/%s' % zone) # If this is an IPv6 zone, set the origin to the zone for this # template if zone_origin: template_data = r"\$ORIGIN " + zone_origin + "\n" + fd.read() else: template_data = fd.read() fd.close() except: # If this is an IPv6 zone, set the origin to the zone for this # template if zone_origin: template_data = r"\$ORIGIN " + zone_origin + "\n" + default_template_data else: template_data = default_template_data metadata['cname_record'] = self.__pretty_print_cname_records(hosts) metadata['host_record'] = self.__pretty_print_host_records(hosts) zonefilename = zonefileprefix + zone if self.logger is not None: self.logger.info("generating (forward) %s" % zonefilename) self.templar.render(template_data, metadata, zonefilename, None) for (zone, hosts) in list(reverse.items()): metadata = { 'cobbler_server': cobbler_server, 'serial': serial, 'zonename': zone, 'zonetype': 'reverse', 'cname_record': '', 'host_record': '' } # grab zone-specific template if it exists try: fd = open('/etc/cobbler/zone_templates/%s' % zone) template_data = fd.read() fd.close() except: template_data = default_template_data metadata['cname_record'] = self.__pretty_print_cname_records(hosts) metadata['host_record'] = self.__pretty_print_host_records(hosts, rectype='PTR') zonefilename = zonefileprefix + zone if self.logger is not None: self.logger.info("generating (reverse) %s" % zonefilename) self.templar.render(template_data, metadata, zonefilename, None)
[docs] def write_dns_files(self): """ BIND files are written when manage_dns is set in /var/lib/cobbler/settings. """ self.__write_named_conf() self.__write_secondary_conf() self.__write_zone_files()
[docs]def get_manager(collection_mgr, logger): return BindManager(collection_mgr, logger)