tero@440: import collections tero@440: import ipaddr tero@440: import logging; log = logging.getLogger('pvl.hosts.host') tero@457: import pvl.dns tero@440: tero@450: class HostError (Exception): tero@485: """ tero@485: An error associated with some specific Host. tero@485: """ tero@485: tero@485: def __init__(self, host, error): tero@485: """ tero@485: host : Host which caused error tero@485: error : Exception or str message tero@485: """ tero@485: self.host = host tero@440: self.error = error tero@440: tero@440: def __str__ (self): tero@485: return "{self.host}: {self.error}".format(self=self) tero@440: tero@450: def parse_bool(value): tero@450: """ tero@450: Normalize optional boolean value. tero@450: """ tero@450: tero@450: if value is None: tero@450: return None tero@450: elif value: tero@450: return True tero@450: else: tero@450: return False tero@450: tero@450: def parse_ip(value, type): tero@450: if value: tero@450: return type(value) tero@450: else: tero@450: return None tero@450: tero@450: def parse_list(value): tero@450: """ tero@450: Parse list of strings. tero@450: """ tero@450: tero@450: if value: tero@450: return value.split() tero@450: else: tero@450: return () tero@450: tero@470: def parse_location(location, domain): tero@450: """ tero@450: Parse location@domain. tero@450: """ tero@450: tero@470: if not location: tero@450: return None tero@450: tero@450: if '@' in location: tero@450: location, location_domain = location.split('@', 1) tero@450: else: tero@450: location_domain = domain tero@450: tero@450: return (location, location_domain) tero@450: tero@450: def parse_ethernet(value): tero@440: """ tero@440: Normalize ethernet str. tero@440: """ tero@440: tero@440: return ':'.join('%02x' % int(x, 16) for x in value.split(':')) tero@440: tero@689: def parse_dhcp_boot(boot): tero@689: """ tero@689: Parse the dhcp boot=... option tero@689: tero@689: >>> print parse_dhcp_boot(None) tero@696: {} tero@689: >>> print parse_dhcp_boot({'filename': '/foo'}) tero@689: {'filename': '/foo'} tero@689: >>> print parse_dhcp_boot({'filename': '/foo', 'next-server': 'bar'}) tero@689: {'next-server': 'bar', 'filename': '/foo'} tero@689: >>> print parse_dhcp_boot('/foo') tero@689: {'filename': '/foo'} tero@689: >>> print parse_dhcp_boot('bar:/foo') tero@689: {'next-server': 'bar', 'filename': '/foo'} tero@689: >>> print parse_dhcp_boot('bar:') tero@689: {'next-server': 'bar'} tero@689: >>> print parse_dhcp_boot('foo') tero@689: Traceback (most recent call last): tero@689: ... tero@689: ValueError: invalid boot=foo tero@689: """ tero@691: tero@691: # normalize to dict tero@689: if not boot: tero@691: boot = { } tero@691: elif not isinstance(boot, dict): tero@691: boot = { None: boot } tero@691: else: tero@691: boot = dict(boot) tero@691: tero@691: # support either an instanced dict or a plain str or a mixed instanced-with-plain-str tero@691: boot_str = boot.pop(None, None) tero@689: tero@691: if not (set(boot) <= set(('filename', 'next-server', None))): tero@691: raise ValueError("Invalid boot.*: {instances}".format(instances=' '.join(boot))) tero@689: tero@691: # any boot= given overrides boot.* fields tero@691: if not boot_str: tero@691: pass tero@691: elif boot_str.startswith('/'): tero@691: boot['filename'] = boot_str tero@689: tero@691: elif boot_str.endswith(':'): tero@691: boot['next-server'] = boot_str[:-1] tero@689: tero@691: elif ':' in boot_str: tero@691: boot['next-server'], boot['filename'] = boot_str.split(':', 1) tero@689: tero@689: else : tero@691: raise ValueError("invalid boot={boot}".format(boot=boot_str)) tero@691: tero@691: return boot tero@689: tero@450: def parse_str(value): tero@450: """ tero@450: Normalize optional string value. tero@450: """ tero@450: tero@450: if value is None: tero@450: # omit tero@450: return None tero@450: tero@450: elif value: tero@450: return str(value) tero@450: tero@450: else: tero@450: # empty value tero@450: return False tero@450: tero@450: def parse_dict(value, parse): tero@694: if not value: tero@694: return { } tero@694: tero@450: if isinstance(value, dict): tero@450: values = value tero@450: else: tero@450: values = {None: value} tero@450: tero@450: return { instance: parse(value) for instance, value in values.iteritems() } tero@450: tero@440: class Host (object) : tero@440: """ tero@440: A host is a network node that can have multiple ethernet interfaces, and multiple IP addresses in different domains. tero@440: """ tero@440: terom@734: EXTENSIONS = { } terom@734: terom@734: @classmethod terom@734: def build_extensions(cls, extensions): terom@734: for extension, value in extensions.iteritems(): terom@734: extension_cls = cls.EXTENSIONS.get(extension) terom@734: terom@734: if extension_cls: terom@734: yield extension, extension_cls.build(**value) terom@734: else: terom@734: log.warning("skip unknown extension: %s", extension) terom@734: tero@440: @classmethod tero@440: def build (cls, name, domain, tero@450: ip=None, ip6=None, tero@694: ethernet=None, tero@450: owner=None, tero@450: location=None, tero@440: alias=None, alias4=None, alias6=None, tero@440: forward=None, reverse=None, tero@440: down=None, tero@440: boot=None, tero@694: extensions={ }, tero@450: ) : tero@440: """ tero@440: Return a Host initialized from data attributes. tero@440: tero@440: This handles all string parsing to our data types. tero@440: """ tero@440: terom@733: ip4 = parse_dict(ip, ipaddr.IPv4Address) terom@733: ip6 = parse_dict(ip6, ipaddr.IPv6Address) terom@733: terom@733: ip = {label: (ip4.get(label), ip6.get(label)) for label in set(ip4) | set(ip6)} terom@733: terom@734: tero@440: return cls(name, tero@440: domain = domain, terom@733: ip4 = ip4.get(None), terom@733: ip6 = ip6.get(None), terom@733: ip = ip, tero@450: ethernet = parse_dict(ethernet, parse_ethernet), tero@440: owner = owner, tero@450: location = parse_location(location, domain), tero@450: alias = parse_list(alias), tero@450: alias4 = parse_list(alias4), tero@450: alias6 = parse_list(alias6), tero@450: forward = parse_str(forward), tero@450: reverse = parse_str(reverse), tero@450: down = parse_bool(down), tero@689: boot = parse_dhcp_boot(boot), terom@734: extensions = dict(cls.build_extensions(extensions)), tero@440: ) tero@440: tero@446: def __init__ (self, name, domain, terom@733: ip4=None, ip6=None, terom@733: ip={}, tero@440: ethernet={ }, tero@440: owner=None, tero@440: location=None, tero@450: alias=(), alias4=(), alias6=(), tero@440: forward=None, reverse=None, tero@440: down=None, tero@450: boot=None, tero@450: extensions={}, tero@493: ): tero@440: """ tero@446: name - str tero@440: domain - str terom@733: ip4 - primary ipaddr.IPv4Address terom@733: ip6 - primary ipaddr.IPv6Address terom@733: ip - secondary { index: (ip4, ip6) } interface addresses tero@440: ethernet - { index: ethernet } tero@468: alias - [ str ]: generate CNAMEs for given relative names tero@440: owner - str: LDAP uid tero@470: location - None or (name, domain) tero@468: alias4 - [ str ]: generate additional A records for given relative names tero@468: alias6 - [ str ]: generate additional AAAA records for given relative names tero@464: forward - None: generate forward zone A/AAAA records per ip/ip6 tero@467: False: omit A/AAAA records (and any alias= CNAMEs) tero@464: str: generate forward zone CNAME to given fqdn tero@464: reverse - None: generate reverse zone PTR records per ip/ip6 tero@464: False: omit PTR records for ip/ip6 tero@464: str: generate IPv4 reverse zone CNAME to given fqdn, and omit IPv6 PTR tero@464: down - mark as offline for polling tero@440: """ tero@440: tero@446: self.name = name tero@440: self.domain = domain terom@733: self.ip4 = ip4 terom@733: self.ip6 = ip6 tero@440: self.ip = ip tero@440: self.ethernet = ethernet tero@440: self.alias = alias tero@440: self.alias4 = alias4 tero@440: self.alias6 = alias6 tero@440: self.owner = owner tero@440: self.location = location tero@440: self.boot = boot tero@440: self.forward = forward tero@440: self.reverse = reverse tero@440: self.down = down tero@440: self.extensions = extensions tero@440: tero@440: def sort_key (self): tero@440: """ tero@440: Stable sort ordering tero@440: """ tero@440: tero@493: if self.ip: tero@441: return self.ip tero@493: else: tero@440: # sorts first tero@440: return ipaddr.IPAddress(0) tero@440: terom@734: def addresses (self): terom@734: """ terom@734: Yield (sublabel, ipaddr) records. terom@734: """ terom@734: terom@734: for sublabel, (ip4, ip6) in self.ip.iteritems(): terom@734: terom@734: if ip4: terom@734: yield sublabel, ip4 terom@734: terom@734: if ip6: terom@734: yield sublabel, ip6 terom@734: terom@734: for extension in self.extensions.itervalues(): terom@734: for sublabel, ip in extension.addresses(): terom@734: yield sublabel, ip terom@734: tero@493: def fqdn (self): tero@493: if self.domain: tero@457: return pvl.dns.fqdn(self.name, self.domain) tero@493: else: tero@457: return pvl.dns.fqdn(self.name) tero@440: tero@493: def __str__ (self): tero@503: return "{self.name}@{domain}".format(self=self, tero@503: domain = self.domain or '', tero@503: ) terom@734: terom@734: class HostExtension (object): terom@734: """ terom@734: Extension hooks terom@734: """ terom@734: terom@734: def addresses (self): terom@734: return () terom@734: terom@734: def extension (cls): terom@734: """ terom@734: Register an extension class terom@734: """ terom@734: terom@734: Host.EXTENSIONS[cls.EXTENSION] = cls terom@734: