terom@277: """ terom@277: Host definitions. terom@277: """ terom@277: terom@277: import pvl.args terom@277: import pvl.dns.zone terom@277: terom@277: import configobj terom@277: import ipaddr terom@277: import optparse terom@287: import os.path terom@277: terom@331: import logging; log = logging.getLogger('pvl.hosts') terom@331: terom@277: def optparser (parser) : terom@277: hosts = optparse.OptionGroup(parser, "Hosts input") terom@277: hosts.add_option('--hosts-charset', metavar='CHARSET', default='utf-8', terom@277: help="Encoding used for host files") terom@277: terom@277: hosts.add_option('--hosts-domain', metavar='DOMAIN', terom@277: help="Default domain for hosts") terom@334: terom@334: hosts.add_option('--hosts-include', metavar='PATH', terom@334: help="Optional path for hosts includes, beyond host config dir") terom@277: terom@277: return hosts terom@277: terom@334: class HostError (Exception) : terom@334: pass terom@334: terom@334: class HostConfigError (HostError) : terom@334: pass terom@334: terom@277: class Host (object) : terom@308: # the label used for alias4/6 hosts terom@308: ALIAS4_FMT = '{host}-ipv4' terom@308: ALIAS6_FMT = '{host}-ipv6' terom@308: terom@277: @classmethod terom@331: def expand (cls, options, range, host, domain, ip, **opts) : terom@277: host = pvl.dns.zone.parse_generate_field(host) terom@277: ip = pvl.dns.zone.parse_generate_field(ip) terom@277: terom@277: for i in range : terom@331: yield cls.build(options, host(i), domain, terom@277: ip = ip(i), terom@277: **opts terom@277: ) terom@277: terom@277: @classmethod terom@331: def config (cls, options, host, section=None, domain=None, ip=None, **extra) : terom@277: """ terom@277: Yield Hosts from a config section's scalars. terom@331: terom@331: options - pvl.args Options terom@331: host - the name of the section (or file) containing this host item. terom@331: section - the parent section containing this host item, or None terom@331: used for domain terom@277: """ terom@277: terom@331: if domain : terom@331: log.debug("%s: explicit domain: %s", host, domain) terom@331: elif options.hosts_domain : terom@331: log.debug("%s: default domain to --hots-domain: %s", host, options.hosts_domain) terom@331: domain = options.hosts_domain terom@331: elif section : terom@331: log.debug("%s: default domain to section: %s", host, section) terom@331: domain = section terom@331: elif '.' in host : terom@331: log.debug("%s: using as fqdn without domain", host) terom@331: domain = None terom@331: else : terom@331: raise ValueError("%s: no domain given" % (host, )) terom@331: terom@277: if '{' in host : terom@277: pre, host = host.split('{', 1) terom@277: range, post = host.rsplit('}', 1) terom@277: terom@277: range = pvl.dns.zone.parse_generate_range(range) terom@277: host = pre + "$" + post terom@277: terom@331: for host in cls.expand(options, range, host, domain, ip, **extra) : terom@277: yield host terom@277: else : terom@331: yield cls.build(options, host, domain, ip=ip, **extra) terom@277: terom@277: @classmethod terom@331: def build (cls, options, host, domain, terom@331: ip=None, ip6=None, owner=None, boot=None, alias=None, alias4=None, alias6=None, forward=None, reverse=None, terom@331: **extra) : terom@277: """ terom@277: Return a Host from a config section's scalars. terom@331: terom@277: """ terom@277: terom@305: if alias : terom@305: alias = alias.split() terom@305: else : terom@305: alias = () terom@308: terom@308: if alias4 : terom@308: alias4 = alias4.split() terom@308: else : terom@308: alias4 = () terom@308: terom@308: if alias6 : terom@308: alias6 = alias6.split() terom@308: else : terom@308: alias6 = () terom@305: terom@305: ethernet = { } terom@277: terom@305: for field, value in extra.iteritems() : terom@305: if '.' in field : terom@305: field, instance = field.split('.') terom@305: else : terom@305: instance = None terom@305: terom@305: if field == 'ethernet' : terom@305: if instance : terom@305: ethernet[instance] = value terom@305: else : terom@305: for eth in value.split() : terom@305: ethernet[len(ethernet)] = eth terom@277: else : terom@306: raise ValueError("%s: Unknown host field: %s=%s" % (host, field, value)) terom@277: terom@331: terom@331: if forward is None : terom@331: # normal zone terom@331: pass terom@331: elif forward : terom@331: # alias to external zone terom@331: pass terom@331: else : terom@331: # omit terom@331: forward = False terom@277: terom@331: if reverse is None : terom@331: # normal zone terom@331: pass terom@331: elif reverse : terom@331: # alias to external zone terom@331: pass terom@331: else : terom@331: # omit terom@331: reverse = False terom@331: terom@277: return cls(host, terom@277: domain = domain, terom@282: ip = ipaddr.IPv4Address(ip) if ip else None, terom@282: ip6 = ipaddr.IPv6Address(ip6) if ip6 else None, terom@277: ethernet = ethernet, terom@277: alias = alias, terom@308: alias4 = alias4, terom@308: alias6 = alias6, terom@277: owner = owner, terom@282: boot = boot, terom@331: forward = forward, terom@331: reverse = reverse, terom@277: ) terom@277: terom@331: def __init__ (self, host, terom@331: domain=None, terom@331: ip=None, ip6=None, terom@331: ethernet={ }, terom@331: alias=(), terom@331: owner=None, terom@331: boot=None, terom@331: alias4=None, alias6=None, terom@331: forward=None, reverse=None, terom@331: ) : terom@277: """ terom@277: host - str terom@277: domain - str terom@282: ip - ipaddr.IPv4Address terom@282: ip6 - ipaddr.IPv6Address terom@305: ethernet - { index: ethernet } terom@277: alias - list terom@277: owner - str: LDAP uid terom@308: alias4 - list (CNAME -> A) terom@308: alias6 - list (CNAME -> AAAA) terom@331: forward - generate forward records, or CNAME into given zone terom@331: reverse - generate reverse records, or CNAME into given zone terom@277: """ terom@331: terom@277: self.host = host terom@277: self.domain = domain terom@277: self.ip = ip terom@282: self.ip6 = ip6 terom@277: self.ethernet = ethernet terom@277: self.alias = alias terom@308: self.alias4 = alias4 terom@308: self.alias6 = alias6 terom@277: self.owner = owner terom@282: self.boot = boot terom@331: self.forward = forward terom@331: self.reverse = reverse terom@277: terom@299: def fqdn (self) : terom@299: if '.' in self.host : terom@299: return self.host + '.' terom@299: elif self.domain : terom@299: return pvl.dns.zone.fqdn(self.host, self.domain) terom@299: else : terom@299: raise ValueError("%s: have no fqdn/domain" % (self, )) terom@299: terom@277: def __str__ (self) : terom@277: return str(self.host) terom@277: terom@333: def apply_host_includes (options, config_path, include) : terom@333: """ terom@333: Yield files from a given config's include=... value terom@333: """ terom@334: terom@334: if options.hosts_include : terom@334: include_paths = (os.path.dirname(config_path), options.hosts_include) terom@334: else : terom@334: include_paths = (os.path.dirname(config_path), ) terom@333: terom@333: for include in include.split() : terom@334: for include_path in include_paths : terom@334: path = os.path.join(include_path, include) terom@334: if os.path.exists(path) : terom@334: break terom@334: else : terom@334: raise HostConfigError(config_path, "Unable to find include %s in include path: %s" % (include, ' '.join(include_paths))) terom@333: terom@333: if include.endswith('/') : terom@333: for name in os.listdir(path) : terom@333: file_path = os.path.join(path, name) terom@333: terom@333: if name.startswith('.') : terom@333: pass terom@333: elif not os.path.exists(file_path) : terom@333: log.warn("%s: skip nonexistant include: %s", config_path, file_path) terom@333: else : terom@333: log.info("%s: include: %s", config_path, file_path) terom@333: yield open(file_path) terom@333: terom@333: else : terom@333: log.info("%s: include: %s", config_path, path) terom@333: yield open(path) terom@333: terom@333: def apply_hosts_config (options, path, name, config, defaults={}, parent=None) : terom@287: """ terom@287: Load hosts from a ConfigObj section. terom@287: """ terom@287: terom@287: scalars = dict((scalar, config[scalar]) for scalar in config.scalars) terom@287: terom@287: if config.sections : terom@333: # includes? terom@333: for file in apply_host_includes(options, path, scalars.pop('include', '')) : terom@333: # within our domain context terom@333: for host in apply_hosts_file(options, file, parent=name) : terom@333: yield host terom@333: terom@287: # recurse; this is a domain meta-section terom@331: params = dict(defaults) terom@307: params.update(**scalars) # override terom@333: terom@333: log.debug("%s: %s -> %s", path, parent, name) terom@287: terom@287: for section in config.sections : terom@333: for host in apply_hosts_config(options, path, section, config[section], params, name) : terom@287: yield host terom@287: terom@287: elif name : terom@290: params = dict(defaults, **scalars) terom@290: terom@333: log.debug("%s: %s: %s", path, parent, name) terom@333: terom@287: # this is a host section terom@331: for host in Host.config(options, name, parent, **params) : terom@287: yield host terom@287: terom@287: else : terom@287: raise ValueError("No sections in config") terom@287: terom@287: terom@333: def apply_hosts_file (options, file, parent=None) : terom@277: """ terom@277: Load Hosts from a file. terom@287: terom@277: """ terom@287: terom@277: config = configobj.ConfigObj(file, terom@277: encoding = options.hosts_charset, terom@277: ) terom@287: terom@287: # use file basename as default terom@333: path = file.name terom@333: name = os.path.basename(path) terom@287: terom@333: return apply_hosts_config(options, path, name, config, parent=parent) terom@277: terom@277: def apply_hosts (options, files) : terom@277: """ terom@277: Load Hosts from files. terom@277: """ terom@277: terom@277: for file in files : terom@277: for host in apply_hosts_file(options, file) : terom@277: yield host terom@277: terom@277: def sort_hosts (options, hosts) : terom@277: """ terom@277: Yields hosts with a sorting key. terom@277: """ terom@277: terom@277: for host in hosts : terom@277: if host.ip : terom@277: sort = host.ip terom@277: else : terom@277: # sorts first terom@277: sort = ipaddr.IPAddress(0) terom@277: terom@277: yield sort, host terom@277: terom@277: def apply (options, args) : terom@277: """ terom@277: Load Hosts from arguments. terom@277: """ terom@277: terom@277: # without unicode terom@277: files = pvl.args.apply_files(args, 'r') terom@277: terom@277: # load configs terom@277: hosts = apply_hosts(options, files) terom@277: terom@277: # sort terom@277: hosts = list(sort_hosts(options, hosts)) terom@277: hosts.sort() terom@277: hosts = [host for sort, host in hosts] terom@277: terom@277: return hosts