pvl.hosts: update boot= to support split boot.next-server= boot.filename=, which enables inheriting defaults
import collections
import ipaddr
import logging; log = logging.getLogger('pvl.hosts.host')
import pvl.dns
class HostError (Exception):
"""
An error associated with some specific Host.
"""
def __init__(self, host, error):
"""
host : Host which caused error
error : Exception or str message
"""
self.host = host
self.error = error
def __str__ (self):
return "{self.host}: {self.error}".format(self=self)
def parse_bool(value):
"""
Normalize optional boolean value.
"""
if value is None:
return None
elif value:
return True
else:
return False
def parse_ip(value, type):
if value:
return type(value)
else:
return None
def parse_list(value):
"""
Parse list of strings.
"""
if value:
return value.split()
else:
return ()
def parse_location(location, domain):
"""
Parse location@domain.
"""
if not location:
return None
if '@' in location:
location, location_domain = location.split('@', 1)
else:
location_domain = domain
return (location, location_domain)
def parse_ethernet(value):
"""
Normalize ethernet str.
"""
return ':'.join('%02x' % int(x, 16) for x in value.split(':'))
def parse_dhcp_boot(boot):
"""
Parse the dhcp boot=... option
>>> print parse_dhcp_boot(None)
{}
>>> print parse_dhcp_boot({'filename': '/foo'})
{'filename': '/foo'}
>>> print parse_dhcp_boot({'filename': '/foo', 'next-server': 'bar'})
{'next-server': 'bar', 'filename': '/foo'}
>>> print parse_dhcp_boot('/foo')
{'filename': '/foo'}
>>> print parse_dhcp_boot('bar:/foo')
{'next-server': 'bar', 'filename': '/foo'}
>>> print parse_dhcp_boot('bar:')
{'next-server': 'bar'}
>>> print parse_dhcp_boot('foo')
Traceback (most recent call last):
...
ValueError: invalid boot=foo
"""
if not boot:
return { }
elif isinstance(boot, dict):
if set(boot) <= set(('filename', 'next-server')):
return boot
else:
raise ValueError("invalid boot={boot}".format(boot=boot))
elif boot.startswith('/'):
return {'filename': boot}
elif boot.endswith(':'):
return {'next-server': boot[:-1]}
elif ':' in boot :
next_server, filename = boot.split(':', 1)
return {'next-server': next_server, 'filename': filename}
else :
raise ValueError("invalid boot={boot}".format(boot=boot))
def parse_str(value):
"""
Normalize optional string value.
"""
if value is None:
# omit
return None
elif value:
return str(value)
else:
# empty value
return False
def parse_dict(value, parse):
if isinstance(value, dict):
values = value
else:
values = {None: value}
return { instance: parse(value) for instance, value in values.iteritems() }
class Host (object) :
"""
A host is a network node that can have multiple ethernet interfaces, and multiple IP addresses in different domains.
"""
# the label used for alias4/6 hosts
ALIAS4_FMT = '{host}-ipv4'
ALIAS6_FMT = '{host}-ipv6'
@classmethod
def build (cls, name, domain,
ip=None, ip6=None,
ethernet={ },
owner=None,
location=None,
alias=None, alias4=None, alias6=None,
forward=None, reverse=None,
down=None,
boot=None,
extensions=None,
) :
"""
Return a Host initialized from data attributes.
This handles all string parsing to our data types.
"""
return cls(name,
domain = domain,
ip = parse_ip(ip, ipaddr.IPv4Address),
ip6 = parse_ip(ip6, ipaddr.IPv6Address),
ethernet = parse_dict(ethernet, parse_ethernet),
owner = owner,
location = parse_location(location, domain),
alias = parse_list(alias),
alias4 = parse_list(alias4),
alias6 = parse_list(alias6),
forward = parse_str(forward),
reverse = parse_str(reverse),
down = parse_bool(down),
boot = parse_dhcp_boot(boot),
extensions = extensions,
)
def __init__ (self, name, domain,
ip=None, ip6=None,
ethernet={ },
owner=None,
location=None,
alias=(), alias4=(), alias6=(),
forward=None, reverse=None,
down=None,
boot=None,
extensions={},
):
"""
name - str
domain - str
ip - ipaddr.IPv4Address
ip6 - ipaddr.IPv6Address
ethernet - { index: ethernet }
alias - [ str ]: generate CNAMEs for given relative names
owner - str: LDAP uid
location - None or (name, domain)
alias4 - [ str ]: generate additional A records for given relative names
alias6 - [ str ]: generate additional AAAA records for given relative names
forward - None: generate forward zone A/AAAA records per ip/ip6
False: omit A/AAAA records (and any alias= CNAMEs)
str: generate forward zone CNAME to given fqdn
reverse - None: generate reverse zone PTR records per ip/ip6
False: omit PTR records for ip/ip6
str: generate IPv4 reverse zone CNAME to given fqdn, and omit IPv6 PTR
down - mark as offline for polling
"""
self.name = name
self.domain = domain
self.ip = ip
self.ip6 = ip6
self.ethernet = ethernet
self.alias = alias
self.alias4 = alias4
self.alias6 = alias6
self.owner = owner
self.location = location
self.boot = boot
self.forward = forward
self.reverse = reverse
self.down = down
self.extensions = extensions
def sort_key (self):
"""
Stable sort ordering
"""
if self.ip:
return self.ip
else:
# sorts first
return ipaddr.IPAddress(0)
def fqdn (self):
if self.domain:
return pvl.dns.fqdn(self.name, self.domain)
else:
return pvl.dns.fqdn(self.name)
def __str__ (self):
return "{self.name}@{domain}".format(self=self,
domain = self.domain or '',
)