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@440: def __init__(self, name, error): tero@440: self.name = name tero@440: self.error = error tero@440: tero@440: def __str__ (self): tero@440: return "{self.name}: {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@450: def parse_location(value, domain): tero@450: """ tero@450: Parse location@domain. tero@450: """ tero@450: tero@450: if not value: 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@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@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: tero@440: # the label used for alias4/6 hosts tero@440: ALIAS4_FMT = '{host}-ipv4' tero@440: ALIAS6_FMT = '{host}-ipv6' tero@440: tero@440: @classmethod tero@440: def build (cls, name, domain, tero@450: ip=None, ip6=None, tero@450: ethernet={ }, 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@450: **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: tero@440: return cls(name, tero@440: domain = domain, tero@450: ip = parse_ip(ip, ipaddr.IPv4Address), tero@450: ip6 = parse_ip(ip6, ipaddr.IPv6Address), 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@440: boot = boot, tero@450: extensions = extensions tero@440: ) tero@440: tero@446: def __init__ (self, name, domain, tero@440: ip=None, ip6=None, 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@440: ) : tero@440: """ tero@446: name - str tero@440: domain - str tero@440: ip - ipaddr.IPv4Address tero@440: ip6 - ipaddr.IPv6Address tero@440: ethernet - { index: ethernet } tero@440: alias - list tero@440: owner - str: LDAP uid tero@450: location - location (name, domain) or None tero@440: alias4 - list (CNAME -> A) tero@440: alias6 - list (CNAME -> AAAA) 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 tero@440: self.ip = ip tero@440: self.ip6 = ip6 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@440: if self.ip : tero@441: return self.ip tero@440: else : tero@440: # sorts first tero@440: return ipaddr.IPAddress(0) tero@440: tero@440: def fqdn (self) : tero@457: if self.domain : tero@457: return pvl.dns.fqdn(self.name, self.domain) tero@440: else : tero@457: return pvl.dns.fqdn(self.name) tero@440: tero@440: def __str__ (self) : tero@446: return "{self.name}@{self.domain}".format(self=self)