commit
7fe5ca09fe
@ -0,0 +1,671 @@
|
|||||||
|
#! /usr/bin/env python
|
||||||
|
|
||||||
|
import re
|
||||||
|
import string
|
||||||
|
import jinja2
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from ipaddr import get_mixed_type_key, Bytes, \
|
||||||
|
IPv4Address, IPv6Address, IPAddress, IPNetwork
|
||||||
|
from .revzones import reverse_name
|
||||||
|
|
||||||
|
from . import log
|
||||||
|
from . import yaml_loader
|
||||||
|
|
||||||
|
|
||||||
|
class DBClass(type):
|
||||||
|
|
||||||
|
"""Metaclass for all objects from the configuration database.
|
||||||
|
|
||||||
|
This metaclass provides a dictionary collecting all instances
|
||||||
|
of each concrete database object class.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __new__(mcs, name, bases, dict):
|
||||||
|
cls = super(DBClass, mcs).__new__(mcs, name, bases, dict)
|
||||||
|
|
||||||
|
# DBObject is derived from object, and serves as an abstract base
|
||||||
|
# class: it has no instances dict. Each class directly derived from
|
||||||
|
# DBObject has one. Further derived classes share their parent's
|
||||||
|
# instances dict (e.g. RevZone shares the Zone instance dict).
|
||||||
|
|
||||||
|
if len(bases) > 0 and bases[0] != object:
|
||||||
|
instances_dict = getattr(bases[0], '_instances', {})
|
||||||
|
setattr(cls, name.lower() + 's', instances_dict)
|
||||||
|
setattr(cls, '_instances', instances_dict)
|
||||||
|
return cls
|
||||||
|
|
||||||
|
|
||||||
|
class DBObject(object, metaclass=DBClass):
|
||||||
|
|
||||||
|
"""Root (abstract) class for all objects from the configuration
|
||||||
|
database.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def add_attr(self, k, v):
|
||||||
|
if k == 'flags' and not isinstance(v, list):
|
||||||
|
v = v.split(',')
|
||||||
|
|
||||||
|
if isinstance(self.attrs.get(k), list):
|
||||||
|
if isinstance(v, list):
|
||||||
|
self.attrs[k].extend(v)
|
||||||
|
else:
|
||||||
|
self.attrs[k].append(v)
|
||||||
|
else:
|
||||||
|
self.attrs[k] = v
|
||||||
|
|
||||||
|
def add_flag(self, f):
|
||||||
|
self.add_attr('flags', f)
|
||||||
|
|
||||||
|
def has_flag(self, f):
|
||||||
|
return f in self.attrs.get('flags', [])
|
||||||
|
|
||||||
|
def instance_key(self):
|
||||||
|
'''Unique key to identify instances'''
|
||||||
|
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def __init__(self, name, **kwargs):
|
||||||
|
|
||||||
|
self.name = name
|
||||||
|
if 'attrs' not in self.__dict__:
|
||||||
|
self.attrs = {}
|
||||||
|
self.attrs['flags'] = []
|
||||||
|
|
||||||
|
for k in list(kwargs.keys()):
|
||||||
|
v = kwargs[k]
|
||||||
|
if not isinstance(v, list):
|
||||||
|
v = [v]
|
||||||
|
for vv in v:
|
||||||
|
self.add_attr(k, vv)
|
||||||
|
|
||||||
|
if isinstance(name, dict):
|
||||||
|
# Singleton
|
||||||
|
instance_name = None
|
||||||
|
else:
|
||||||
|
instance_name = self.instance_key()
|
||||||
|
|
||||||
|
if instance_name in type(self)._instances:
|
||||||
|
raise Exception("Duplicate %s: %s" % (type(self), instance_name))
|
||||||
|
|
||||||
|
type(self)._instances[instance_name] = self
|
||||||
|
|
||||||
|
|
||||||
|
class Vars(DBObject):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Group(DBObject):
|
||||||
|
|
||||||
|
def __init__(self, name, **kwargs):
|
||||||
|
self.hosts = []
|
||||||
|
# List of Host objects
|
||||||
|
|
||||||
|
super(Group, self).__init__(name, **kwargs)
|
||||||
|
|
||||||
|
def get_attr(self, key):
|
||||||
|
return self.attrs.get(key, '')
|
||||||
|
|
||||||
|
def add_host(self, host):
|
||||||
|
self.hosts.append(host)
|
||||||
|
|
||||||
|
def del_host(self, host):
|
||||||
|
self.hosts.remove(host)
|
||||||
|
|
||||||
|
|
||||||
|
class OrgUnit(DBObject):
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def strip_view(s):
|
||||||
|
"""Remove view part from a <domain>_<view> specification.
|
||||||
|
"""
|
||||||
|
|
||||||
|
i = s.find('_')
|
||||||
|
if i == -1:
|
||||||
|
i = len(s)
|
||||||
|
return s[:i]
|
||||||
|
|
||||||
|
|
||||||
|
class Zone(DBObject):
|
||||||
|
|
||||||
|
def __init__(self, name, **kwargs):
|
||||||
|
self.rrs = {}
|
||||||
|
self.has_root_cname = 0
|
||||||
|
self.attrs = {'mx': []}
|
||||||
|
super(Zone, self).__init__(name, **kwargs)
|
||||||
|
|
||||||
|
def add_rr(self, key, type, value):
|
||||||
|
m = re.match("(?i)((.*)\.|)" + strip_view(self.name), key)
|
||||||
|
if m:
|
||||||
|
key = m.group(1)
|
||||||
|
if key == "":
|
||||||
|
key = "@"
|
||||||
|
else:
|
||||||
|
# Strip dot
|
||||||
|
key = key[:-1]
|
||||||
|
|
||||||
|
if key not in self.rrs:
|
||||||
|
self.rrs[key] = []
|
||||||
|
new_rr = {'type': type, 'value': value}
|
||||||
|
if self.rrs[key].count(new_rr) == 0:
|
||||||
|
self.rrs[key].append(new_rr)
|
||||||
|
return key
|
||||||
|
|
||||||
|
def add_a(self, key, value):
|
||||||
|
if isinstance(value, IPv6Address):
|
||||||
|
self.add_rr(key, 'AAAA', value)
|
||||||
|
else:
|
||||||
|
self.add_rr(key, 'A', value)
|
||||||
|
|
||||||
|
def add_mxes(self, fqdn):
|
||||||
|
for mx in self.attrs['mx']:
|
||||||
|
self.add_rr(fqdn, 'MX', mx)
|
||||||
|
|
||||||
|
def add_ptr(self, inaddr, fqdn):
|
||||||
|
if fqdn[-1] != '.':
|
||||||
|
fqdn = fqdn + '.'
|
||||||
|
self.add_rr(inaddr, 'PTR', fqdn)
|
||||||
|
|
||||||
|
def add_cname(self, alias, name):
|
||||||
|
# Name may be an absolute name, or a name relative
|
||||||
|
# to the root of this zone.
|
||||||
|
|
||||||
|
self_suffix = "." + strip_view(self.name) + "."
|
||||||
|
if name.endswith(self_suffix):
|
||||||
|
name = name[:-len(self_suffix)]
|
||||||
|
alias_key = self.add_rr(alias, 'CNAME', name)
|
||||||
|
if alias_key == "@":
|
||||||
|
self.has_root_cname = 1
|
||||||
|
|
||||||
|
|
||||||
|
class RevZone(Zone):
|
||||||
|
|
||||||
|
def __init__(self, name, **kwargs):
|
||||||
|
self.prefix = IPNetwork(name)
|
||||||
|
super(RevZone, self).__init__(reverse_name(self.prefix), **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class LDAPTemplate(DBObject):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def ipvfilter(a, v):
|
||||||
|
if isinstance(a, list):
|
||||||
|
return [_f for _f in map(LDAPTemplate.ipvfilter, a, [v] * len(a)) if _f]
|
||||||
|
|
||||||
|
elif a.version == v:
|
||||||
|
return str(a)
|
||||||
|
|
||||||
|
def __init__(self, name, **kwargs):
|
||||||
|
self._tmpl = {}
|
||||||
|
self._vars = {}
|
||||||
|
self.env = jinja2.Environment()
|
||||||
|
self.env.filters['ipv'] = LDAPTemplate.ipvfilter
|
||||||
|
self.attrs = {'requires': []}
|
||||||
|
super(LDAPTemplate, self).__init__(name, **kwargs)
|
||||||
|
|
||||||
|
def add_attr(self, key, value):
|
||||||
|
if key == 'template':
|
||||||
|
value = self.env.from_string(value)
|
||||||
|
super(LDAPTemplate, self).add_attr(key, value)
|
||||||
|
|
||||||
|
def render(self, tmpl_key, **kwargs):
|
||||||
|
for required in self.attrs['requires']:
|
||||||
|
if not kwargs.get(required):
|
||||||
|
log.vv("No LDAP %s entry for %s (missing %s)" %
|
||||||
|
(tmpl_key, kwargs['name'], required))
|
||||||
|
return
|
||||||
|
return self.attrs['template'].render(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def ldap_render(ldif, obj=None, **kwargs):
|
||||||
|
res = []
|
||||||
|
obj_vars = dict(Vars.varss[None].attrs)
|
||||||
|
if obj is not None:
|
||||||
|
obj_vars.update(obj.__dict__)
|
||||||
|
obj_vars.update(obj_vars.pop('attrs'))
|
||||||
|
obj_vars.update(kwargs)
|
||||||
|
if 'template' in kwargs:
|
||||||
|
tmpl_key = kwargs.pop('template')
|
||||||
|
else:
|
||||||
|
tmpl_key = next(k for (k, Cl) in class_map if isinstance(obj, Cl))
|
||||||
|
|
||||||
|
tmpl = LDAPTemplate.ldaptemplates.get(tmpl_key)
|
||||||
|
if tmpl is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
res = tmpl.render(tmpl_key, **obj_vars)
|
||||||
|
if not res:
|
||||||
|
return False
|
||||||
|
if res[-1] != '\n':
|
||||||
|
res = res + '\n'
|
||||||
|
ldif.append(res)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# Find the zone(s) in which to output name d for the given view.
|
||||||
|
# If a view is requested, and views are specified for the domain,
|
||||||
|
# then only that view is returned.
|
||||||
|
# If no view is specified for the domain, return the (only) zone for the
|
||||||
|
# domain.
|
||||||
|
# If no view is requested, and views are specified for the domain,
|
||||||
|
# returns ALL views.
|
||||||
|
|
||||||
|
def find_zones_for_domain(d, default_view=None, orig_d=None):
|
||||||
|
if orig_d is None:
|
||||||
|
orig_d = d
|
||||||
|
|
||||||
|
# Strip trailing dot in FQDN
|
||||||
|
|
||||||
|
if d[-1] == ".":
|
||||||
|
d = d[:-1]
|
||||||
|
d = d.lower()
|
||||||
|
|
||||||
|
# If a view is specified, first try to find a specific zone for the view
|
||||||
|
|
||||||
|
if default_view is not None:
|
||||||
|
dv = d + '_' + default_view
|
||||||
|
if dv in Zone.zones:
|
||||||
|
return [Zone.zones[dv]]
|
||||||
|
|
||||||
|
# No specific view, look for a view-less zone
|
||||||
|
|
||||||
|
if d in Zone.zones:
|
||||||
|
return [Zone.zones[d]]
|
||||||
|
|
||||||
|
# No view specified: look for zones with view for domain
|
||||||
|
|
||||||
|
if default_view is None:
|
||||||
|
view_prefix = d + '_'
|
||||||
|
views = [Zone.zones[z]
|
||||||
|
for z in Zone.zones if z.startswith(view_prefix)]
|
||||||
|
if len(views) > 0:
|
||||||
|
return views
|
||||||
|
|
||||||
|
# No zone found so far, climb up to parent domain
|
||||||
|
|
||||||
|
dot = d.find(".")
|
||||||
|
if dot < 0:
|
||||||
|
return []
|
||||||
|
|
||||||
|
return find_zones_for_domain(d[dot + 1:], default_view, orig_d)
|
||||||
|
|
||||||
|
|
||||||
|
def find_reverse_zone(ipa):
|
||||||
|
'''Return the reverse zone for ipa'''
|
||||||
|
|
||||||
|
matchlen = 0
|
||||||
|
match = None
|
||||||
|
for z in list(Zone.zones.values()):
|
||||||
|
if isinstance(z, RevZone) \
|
||||||
|
and ipa in z.prefix \
|
||||||
|
and z.prefix.prefixlen > matchlen:
|
||||||
|
matchlen = z.prefix.prefixlen
|
||||||
|
match = z
|
||||||
|
return match
|
||||||
|
|
||||||
|
|
||||||
|
class IPnet:
|
||||||
|
ipnets = []
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.prefix)
|
||||||
|
|
||||||
|
def __init__(self, prefix):
|
||||||
|
self.prefix = IPNetwork(prefix)
|
||||||
|
self.hosts_lines = {}
|
||||||
|
self.html_hosts_lines = {}
|
||||||
|
self._hostmask = int(self.prefix.hostmask)
|
||||||
|
if self.prefix.version == 4:
|
||||||
|
self.freelist = [self.host_id(h) for h in self.prefix]
|
||||||
|
|
||||||
|
def key(self):
|
||||||
|
"""Key function for comparisons and sorting
|
||||||
|
"""
|
||||||
|
|
||||||
|
return get_mixed_type_key(self.prefix)
|
||||||
|
|
||||||
|
def mark_used(self, addr):
|
||||||
|
if self.prefix.version == 4:
|
||||||
|
try:
|
||||||
|
self.freelist.remove(self.host_id(addr))
|
||||||
|
except Exception:
|
||||||
|
print("Warning: %s already used in %s" % (addr, str(self)))
|
||||||
|
|
||||||
|
def host_id(self, ipa):
|
||||||
|
return int(ipa) & int(self._hostmask)
|
||||||
|
|
||||||
|
def set_hosts_line(self, addr, line):
|
||||||
|
hid = self.host_id(addr)
|
||||||
|
if hid in self.hosts_lines:
|
||||||
|
self.hosts_lines[hid] += ' %s' % line
|
||||||
|
else:
|
||||||
|
self.hosts_lines[hid] = '%s\t%s' % (str(addr), line)
|
||||||
|
|
||||||
|
def set_html_hosts_line(self, addr, line):
|
||||||
|
hid = self.host_id(addr)
|
||||||
|
if hid not in self.html_hosts_lines:
|
||||||
|
self.html_hosts_lines[hid] = ''
|
||||||
|
self.html_hosts_lines[hid] += line
|
||||||
|
|
||||||
|
def list_hosts(self):
|
||||||
|
if self.prefix.version == 4:
|
||||||
|
hosts_list = []
|
||||||
|
for h in self.prefix:
|
||||||
|
try:
|
||||||
|
hosts_list.append(self.hosts_lines[self.host_id(h)] + '\n')
|
||||||
|
except KeyError:
|
||||||
|
hosts_list.append("#%s unused\n" % (h))
|
||||||
|
return hosts_list
|
||||||
|
else:
|
||||||
|
return [l + '\n' for l in list(self.hosts_lines.values())]
|
||||||
|
|
||||||
|
def html_list_hosts(self):
|
||||||
|
""" Same as list_hosts, but return an HTML table, and collapse the
|
||||||
|
unused entries. """
|
||||||
|
|
||||||
|
if self.prefix.version != 4:
|
||||||
|
print("%s is-a %s" % (self.prefix, self.__class__))
|
||||||
|
return list(self.html_host_lines.items())
|
||||||
|
|
||||||
|
hosts_list = []
|
||||||
|
in_unused_range = False
|
||||||
|
unused_first = ""
|
||||||
|
unused_last = ""
|
||||||
|
|
||||||
|
for h in self.prefix:
|
||||||
|
hid = self.host_id(h)
|
||||||
|
line = self.html_hosts_lines.get(hid, None)
|
||||||
|
if line is None:
|
||||||
|
if in_unused_range:
|
||||||
|
unused_last = h
|
||||||
|
else:
|
||||||
|
unused_first = h
|
||||||
|
unused_last = unused_first
|
||||||
|
in_unused_range = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
if in_unused_range:
|
||||||
|
if unused_first == unused_last:
|
||||||
|
label = str(unused_first)
|
||||||
|
else:
|
||||||
|
label = ("%s<br> <small>to</small><br>%s"
|
||||||
|
% (unused_first, unused_last))
|
||||||
|
|
||||||
|
hosts_list.append(
|
||||||
|
'<tr class="unused"><td>' +
|
||||||
|
label +
|
||||||
|
'</td><td>unused</td><td> </td></tr>\n')
|
||||||
|
|
||||||
|
in_unused_range = False
|
||||||
|
hosts_list.append(line)
|
||||||
|
|
||||||
|
return hosts_list
|
||||||
|
|
||||||
|
|
||||||
|
class Net(DBObject):
|
||||||
|
|
||||||
|
def __init__(self, name, **kwargs):
|
||||||
|
self.dhcp_fixed = []
|
||||||
|
self.ipnets = []
|
||||||
|
# List of (IPnet, dict) tuples
|
||||||
|
self.binat = []
|
||||||
|
super(Net, self).__init__(name, **kwargs)
|
||||||
|
|
||||||
|
def prefixes(self, v=None, len=None):
|
||||||
|
'''Return prefixes of self.
|
||||||
|
If v or len are specified, only return prefixes with
|
||||||
|
the corresponding version or length.'''
|
||||||
|
|
||||||
|
return [ipn.prefix for (ipn, _) in self.ipnets
|
||||||
|
if (v is None or ipn.prefix.version == v)
|
||||||
|
and (len is None or ipn.prefix.prefixlen == len)]
|
||||||
|
|
||||||
|
def add_attr(self, key, value):
|
||||||
|
if key == "addr":
|
||||||
|
if isinstance(value, dict):
|
||||||
|
self.ipnets.extend([(IPnet(k), value[k]) for k in value])
|
||||||
|
else:
|
||||||
|
self.ipnets.append((IPnet(value), {}))
|
||||||
|
elif key == "binat":
|
||||||
|
self.binat.append(IPNetwork(value))
|
||||||
|
else:
|
||||||
|
super(Net, self).add_attr(key, value)
|
||||||
|
|
||||||
|
|
||||||
|
class Host(DBObject):
|
||||||
|
|
||||||
|
def instance_key(self):
|
||||||
|
"""Provide unique names across entire database
|
||||||
|
|
||||||
|
Two hosts in different organizational units can have the same
|
||||||
|
name, so differentiate them using their internal object ids."""
|
||||||
|
|
||||||
|
return "%s.%d" % (self.name, id(self))
|
||||||
|
|
||||||
|
def __init__(self, name, **kwargs):
|
||||||
|
self.aliases = []
|
||||||
|
self.addrs = []
|
||||||
|
self.mac_addrs = []
|
||||||
|
self.groups = []
|
||||||
|
self.attrs = {'interface': [], 'dhcp': []}
|
||||||
|
|
||||||
|
super(Host, self).__init__(name, **kwargs)
|
||||||
|
|
||||||
|
def add_mac_addr(self, type, value):
|
||||||
|
self.mac_addrs.append([type, value])
|
||||||
|
|
||||||
|
def add_alias(self, alias, kind):
|
||||||
|
self.aliases.append([alias, kind])
|
||||||
|
|
||||||
|
def add_addr(self, addr):
|
||||||
|
a = IPAddress(addr)
|
||||||
|
if a not in self.addrs:
|
||||||
|
self.addrs.append(a)
|
||||||
|
|
||||||
|
def add_attr(self, key, value):
|
||||||
|
if 'decommissioned' in self.attrs:
|
||||||
|
return
|
||||||
|
|
||||||
|
if key == "alias":
|
||||||
|
self.add_alias(value, "cname")
|
||||||
|
elif key == "valias":
|
||||||
|
self.add_alias(value, "a")
|
||||||
|
self.add_alias(value, "mx")
|
||||||
|
elif key == "aalias":
|
||||||
|
self.add_alias(value, "a")
|
||||||
|
elif key == "mxalias":
|
||||||
|
self.add_alias(value, "mx")
|
||||||
|
elif key == "addr":
|
||||||
|
self.add_addr(value)
|
||||||
|
elif key == "ether":
|
||||||
|
self.add_mac_addr("ethernet", value)
|
||||||
|
|
||||||
|
elif key == "groups":
|
||||||
|
# Parse the list of groups
|
||||||
|
r = re.compile("([^,]+),?")
|
||||||
|
for g in r.findall(value):
|
||||||
|
group = g.strip()
|
||||||
|
self.groups.append(group)
|
||||||
|
|
||||||
|
if group in Group.groups:
|
||||||
|
Group.groups[group].add_host(self)
|
||||||
|
else:
|
||||||
|
print("Warning, host %s is assigned to unknown group %s" \
|
||||||
|
% (self.name, group))
|
||||||
|
|
||||||
|
elif key == "decommissioned":
|
||||||
|
# Remove entry from hosts table, and also from any group that
|
||||||
|
# references it.
|
||||||
|
|
||||||
|
self.attrs['decommissioned'] = value
|
||||||
|
del Host.hosts[self.instance_key()]
|
||||||
|
for g in self.groups:
|
||||||
|
if g in Group.groups:
|
||||||
|
Group.groups[g].del_host(self)
|
||||||
|
|
||||||
|
else:
|
||||||
|
super(Host, self).add_attr(key, value)
|
||||||
|
|
||||||
|
def get_attr(self, key):
|
||||||
|
if key in self.attrs:
|
||||||
|
return self.attrs[key]
|
||||||
|
else:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def has_address_in(self, prefix):
|
||||||
|
for a in self.addrs:
|
||||||
|
if a in prefix:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def html(self, addr):
|
||||||
|
return """<tr><td>%s</td>
|
||||||
|
<td><a href="machines/%s.html">%s</a></td><td>%s</td></tr>
|
||||||
|
""" % (addr, self.name, self.name, self.get_attr("purpose") or " ")
|
||||||
|
|
||||||
|
|
||||||
|
def find_nets(addr):
|
||||||
|
'''Return all nets that match addr and have the longest prefix length'''
|
||||||
|
ipa = IPAddress(addr)
|
||||||
|
matchlen = 0
|
||||||
|
matches = []
|
||||||
|
for n in list(Net.nets.values()):
|
||||||
|
for ipn, ipnprops in n.ipnets:
|
||||||
|
if ipa in ipn.prefix:
|
||||||
|
if ipn.prefix.prefixlen > matchlen:
|
||||||
|
matches = []
|
||||||
|
matchlen = ipn.prefix.prefixlen
|
||||||
|
if ipn.prefix.prefixlen == matchlen:
|
||||||
|
matches.append((n, ipn, ipnprops))
|
||||||
|
return matches
|
||||||
|
|
||||||
|
|
||||||
|
def nat_addr(ipnet, ipa):
|
||||||
|
m = ipnet.hostmask
|
||||||
|
return IPv4Address(int(ipnet) | (int(ipa) & int(m)))
|
||||||
|
|
||||||
|
|
||||||
|
def find_nat(addr):
|
||||||
|
ipa = IPAddress(addr)
|
||||||
|
if not isinstance(ipa, IPv4Address):
|
||||||
|
return None
|
||||||
|
for n in list(Net.nets.values()):
|
||||||
|
ipv4_prefixes = n.prefixes(v=4)
|
||||||
|
for b in n.binat:
|
||||||
|
if ipa in b:
|
||||||
|
return nat_addr(ipv4_prefixes[0], ipa)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def autoconfig_address(p, e):
|
||||||
|
if p.version != 6 or p.prefixlen != 64:
|
||||||
|
raise Exception("Invalid prefix for SLAAC: %s" % (p,))
|
||||||
|
|
||||||
|
eui48 = [int(b, 16) for b in e.split(':')]
|
||||||
|
eui48[0] = eui48[0] & ~3
|
||||||
|
eui64 = [eui48[0] | 2] + eui48[1:3] + [0xff, 0xfe] + eui48[3:]
|
||||||
|
return IPv6Address(Bytes(p.packed[:8] + b''.join(map(chr, eui64))))
|
||||||
|
|
||||||
|
|
||||||
|
def is_slac(ipa):
|
||||||
|
return isinstance(ipa, IPv6Address) and ipa.packed[11:13] == '\xff\xfe'
|
||||||
|
|
||||||
|
|
||||||
|
def get_fqdn(name, addr, nprops):
|
||||||
|
default_domain = nprops['domain']
|
||||||
|
if addr.version == 6:
|
||||||
|
suffix = nprops.get('suffix6', '')
|
||||||
|
else:
|
||||||
|
suffix = ''
|
||||||
|
|
||||||
|
if name[-1] == ".":
|
||||||
|
name_comps = name[:-1].split('.')
|
||||||
|
name_comps[0] = name_comps[0] + suffix
|
||||||
|
return '.'.join(name_comps)
|
||||||
|
else:
|
||||||
|
return name + suffix + "." + default_domain
|
||||||
|
|
||||||
|
# For a generate block, expand a template of the form:
|
||||||
|
# $
|
||||||
|
# or with optional modifiers:
|
||||||
|
# ${offset[,width[,base]]}
|
||||||
|
#
|
||||||
|
# Modifiers change the offset from the iterator, field width and base.
|
||||||
|
# Modifiers are introduced by a { immediately following the $ as
|
||||||
|
# ${offset[,width[,base]]}. e.g. ${-20,3,d} which subtracts 20 from the current
|
||||||
|
# value, prints the result as a decimal in a zero padded field of with 3.
|
||||||
|
# Available output forms are decimal (d), octal (o) and hexadecimal (x or X
|
||||||
|
# for uppercase). The default modifier is ${0,0,d}.
|
||||||
|
# (consistent with BIND's $GENERATE directive).
|
||||||
|
|
||||||
|
|
||||||
|
generator_pattern = re.compile('\$(\{([0-9-]*)(,([0-9]*)(,([doxX]))?)?\})?')
|
||||||
|
|
||||||
|
|
||||||
|
def expand_index(m, index):
|
||||||
|
offset = 0
|
||||||
|
width = 0
|
||||||
|
base = 'd'
|
||||||
|
|
||||||
|
if m.group(2):
|
||||||
|
offset = int(m.group(2))
|
||||||
|
if m.group(4):
|
||||||
|
width = int(m.group(4))
|
||||||
|
if m.group(6):
|
||||||
|
base = m.group(6)
|
||||||
|
|
||||||
|
template = "%%0%d%c" % (width, base)
|
||||||
|
return template % (index + offset)
|
||||||
|
|
||||||
|
|
||||||
|
def expand_index_refs(obj, index):
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
return dict([(k_v[0], expand_index_refs(k_v[1], index)) for k_v in list(obj.items())])
|
||||||
|
|
||||||
|
elif isinstance(obj, list):
|
||||||
|
return [expand_index_refs(v, index) for v in obj]
|
||||||
|
|
||||||
|
else:
|
||||||
|
return re.sub(generator_pattern,
|
||||||
|
lambda m: expand_index(m, index),
|
||||||
|
obj)
|
||||||
|
|
||||||
|
|
||||||
|
def enter_object(obj):
|
||||||
|
db_obj = None
|
||||||
|
for k, Cl in class_map:
|
||||||
|
if k in obj:
|
||||||
|
db_obj = Cl(obj.pop(k), **obj)
|
||||||
|
break
|
||||||
|
if db_obj is None:
|
||||||
|
log.err("Invalid object:\n%s" % obj)
|
||||||
|
|
||||||
|
|
||||||
|
# Note: order is significant:
|
||||||
|
# 'ldap' object has 'host' and 'group' attributes
|
||||||
|
# 'host' objects may have a 'net' attribute
|
||||||
|
|
||||||
|
class_map = [('ldap', LDAPTemplate),
|
||||||
|
('org-unit', OrgUnit),
|
||||||
|
('host', Host),
|
||||||
|
('net', Net),
|
||||||
|
('group', Group),
|
||||||
|
('zone', Zone),
|
||||||
|
('rev-zone', RevZone),
|
||||||
|
('vars', Vars)]
|
||||||
|
|
||||||
|
|
||||||
|
def load_db(f):
|
||||||
|
for obj in yaml.load(f, Loader=yaml_loader.UniqueKeyLoader):
|
||||||
|
if 'generate' in obj:
|
||||||
|
g = obj['generate']
|
||||||
|
index, hi = list(map(int, g.pop('range').split('-')))
|
||||||
|
|
||||||
|
while index <= hi:
|
||||||
|
enter_object(expand_index_refs(g, index))
|
||||||
|
index = index + 1
|
||||||
|
else:
|
||||||
|
enter_object(obj)
|
@ -0,0 +1,311 @@
|
|||||||
|
#! /usr/bin/env python
|
||||||
|
|
||||||
|
# ldapdip
|
||||||
|
# LDAP Diff and Patch
|
||||||
|
# $Id: ldapdip.py 253716 2017-01-18 14:47:26Z quinot $
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import ldap
|
||||||
|
import ldap.modlist
|
||||||
|
import ldapurl
|
||||||
|
import ldif
|
||||||
|
import os.path
|
||||||
|
import sys
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
conf = {}
|
||||||
|
success = True
|
||||||
|
|
||||||
|
# Always exclude internal attributes
|
||||||
|
# (useful when comparing slapcat dumps)
|
||||||
|
|
||||||
|
exclude = set(map(str.lower, [
|
||||||
|
'structuralObjectClass',
|
||||||
|
'entryUUID',
|
||||||
|
'creatorsName',
|
||||||
|
'createTimestamp',
|
||||||
|
'entryCSN',
|
||||||
|
'modifiersName',
|
||||||
|
'modifyTimestamp',
|
||||||
|
'contextCSN'
|
||||||
|
]))
|
||||||
|
|
||||||
|
# If non-empty, consider only these attributes
|
||||||
|
|
||||||
|
include = set()
|
||||||
|
|
||||||
|
|
||||||
|
def vv(str):
|
||||||
|
"""Output str if verbose mode is specified."""
|
||||||
|
if conf.get('verbose'):
|
||||||
|
print(str, file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
# Editors provide functions to be called for each operation required to
|
||||||
|
# make the OLD tree identifcal to the NEW tree.
|
||||||
|
|
||||||
|
class LDIFEditor:
|
||||||
|
|
||||||
|
"""The LDIF Editor produces an LDIF change record stream."""
|
||||||
|
|
||||||
|
def __init__(self, outf):
|
||||||
|
self._ldw = ldif.LDIFWriter(outf)
|
||||||
|
|
||||||
|
def output_entry(self, dn, arg):
|
||||||
|
self._ldw.unparse(dn, arg)
|
||||||
|
|
||||||
|
def add_entry(self, dn, modlist):
|
||||||
|
self.output_entry(dn, modlist)
|
||||||
|
|
||||||
|
def mod_entry(self, dn, modlist):
|
||||||
|
self.output_entry(dn, modlist)
|
||||||
|
|
||||||
|
def del_entry(self, dn):
|
||||||
|
self.output_entry(dn, {'changetype': ['delete']})
|
||||||
|
|
||||||
|
|
||||||
|
class LDAPEditor:
|
||||||
|
|
||||||
|
"""The LDAP Editor applies the modifications to an LDAP server."""
|
||||||
|
|
||||||
|
def __init__(self, lo):
|
||||||
|
self._lo = lo
|
||||||
|
|
||||||
|
def add_entry(self, dn, modlist):
|
||||||
|
self._lo.add_s(dn, modlist)
|
||||||
|
|
||||||
|
def mod_entry(self, dn, modlist):
|
||||||
|
self._lo.modify_s(dn, modlist)
|
||||||
|
|
||||||
|
def del_entry(self, dn):
|
||||||
|
self._lo.delete_s(dn)
|
||||||
|
|
||||||
|
|
||||||
|
def open_ldap(uri, update):
|
||||||
|
"""Open LDAP server URI. If update is True, also return an LDAPEditor
|
||||||
|
based on this connection."""
|
||||||
|
|
||||||
|
vv('Querying LDAP server: %s' % uri)
|
||||||
|
lo = ldap.initialize(uri)
|
||||||
|
lo.simple_bind_s(conf.get('binddn', ''), conf.get('bindpw', ''))
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
|
||||||
|
lf = conf.get('filter', None)
|
||||||
|
if lf is not None:
|
||||||
|
args['filterstr'] = lf
|
||||||
|
|
||||||
|
entries = {}
|
||||||
|
|
||||||
|
base = conf.get('base', '')
|
||||||
|
if not isinstance(base, list):
|
||||||
|
base = [base]
|
||||||
|
|
||||||
|
for b in base:
|
||||||
|
lr = lo.search_s(b, ldap.SCOPE_SUBTREE, **args)
|
||||||
|
entries.update(dict(lr))
|
||||||
|
|
||||||
|
return entries, (LDAPEditor(lo) if update else None)
|
||||||
|
|
||||||
|
|
||||||
|
class LDIFDictLoader(ldif.LDIFParser):
|
||||||
|
|
||||||
|
"""LDIF parser that populates a dict of entries indexed by DN"""
|
||||||
|
|
||||||
|
def __init__(self, *args):
|
||||||
|
ldif.LDIFParser.__init__(self, *args)
|
||||||
|
self.entries = {}
|
||||||
|
|
||||||
|
def handle(self, dn, entry):
|
||||||
|
self.entries[dn] = entry
|
||||||
|
|
||||||
|
|
||||||
|
def open_ldif(uri, update):
|
||||||
|
"""Open an LDIF file. update must be False (an LDIF file cannot be
|
||||||
|
edited), and no editor is ever returned."""
|
||||||
|
|
||||||
|
if update:
|
||||||
|
raise Exception('cannot update %s' % uri)
|
||||||
|
vv('Loading LDIF file: %s' % uri)
|
||||||
|
parser = LDIFDictLoader(open(uri))
|
||||||
|
parser.parse()
|
||||||
|
return parser.entries, None
|
||||||
|
|
||||||
|
|
||||||
|
def open_tree(tree, update):
|
||||||
|
'''Open an LDAP URI or LDIF file, or return a tree object unchanged.'''
|
||||||
|
|
||||||
|
if isinstance(tree, str):
|
||||||
|
if ldapurl.isLDAPUrl(tree):
|
||||||
|
return open_ldap(tree, update)
|
||||||
|
else:
|
||||||
|
return open_ldif(tree, update)
|
||||||
|
|
||||||
|
else:
|
||||||
|
return tree, None
|
||||||
|
|
||||||
|
|
||||||
|
def handle_exception(dn, op, e):
|
||||||
|
"""Output an exception message and set global variable success to False"""
|
||||||
|
|
||||||
|
global success
|
||||||
|
|
||||||
|
print("Exception raised trying to %s %s:\n%s" % (op, dn, e), file=sys.stderr)
|
||||||
|
success = False
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup(e):
|
||||||
|
"""Remove from entry e all attributes that are to be ignored"""
|
||||||
|
|
||||||
|
for ea in list(e.keys()):
|
||||||
|
eal = ea.lower()
|
||||||
|
if (len(include) > 0 and eal not in include) \
|
||||||
|
or (eal in exclude):
|
||||||
|
e.pop(ea)
|
||||||
|
|
||||||
|
|
||||||
|
def dn_key(dn):
|
||||||
|
"""Key function used for sorting DNs by rightmost component first."""
|
||||||
|
return list(reversed(ldap.dn.explode_dn(dn)))
|
||||||
|
|
||||||
|
def diff_entries(old_tree, new_tree, update, out, editor=None):
|
||||||
|
"""Recursively compare the given trees and compute modification operations
|
||||||
|
that must be applied to the old tree to make it identical to the new one.
|
||||||
|
|
||||||
|
If out is not None, send modification operations there;
|
||||||
|
if out is None, and update is True, apply modifications
|
||||||
|
to old_tree directly.
|
||||||
|
If editor is not None, changes are also sent to that editor."""
|
||||||
|
|
||||||
|
old_entries, ed = open_tree(old_tree, update and out is None)
|
||||||
|
new_entries, _ = open_tree(new_tree, False)
|
||||||
|
|
||||||
|
if out is not None:
|
||||||
|
ed = LDIFEditor(out)
|
||||||
|
|
||||||
|
editors = []
|
||||||
|
if ed is not None:
|
||||||
|
editors.append(ed)
|
||||||
|
if editor is not None:
|
||||||
|
if isinstance(editor, list):
|
||||||
|
editors.extend(editor)
|
||||||
|
else:
|
||||||
|
editors.append(editor)
|
||||||
|
|
||||||
|
old_dns = set(old_entries.keys())
|
||||||
|
new_dns = set(new_entries.keys())
|
||||||
|
|
||||||
|
# Note: deletions need to be processed in reverse order because you
|
||||||
|
# can only delete a leaf object (i.e. one that has no children).
|
||||||
|
|
||||||
|
for dn in sorted(old_dns - new_dns, key=dn_key, reverse=True):
|
||||||
|
for ed in editors:
|
||||||
|
try:
|
||||||
|
ed.del_entry(dn)
|
||||||
|
except Exception as e:
|
||||||
|
handle_exception(dn, 'delete', e)
|
||||||
|
|
||||||
|
for dn in sorted(old_dns & new_dns, key=dn_key):
|
||||||
|
oe = old_entries[dn]
|
||||||
|
cleanup(oe)
|
||||||
|
ne = new_entries[dn]
|
||||||
|
cleanup(ne)
|
||||||
|
|
||||||
|
# Compute differences between oe and ne as a list of
|
||||||
|
# LDAP modification operations that transform oe into ne.
|
||||||
|
# Note that oe and ne might be identical in LDAP sense
|
||||||
|
# (i.e. yield an empty mod list) even if oe != ne because
|
||||||
|
# some differences, such as the order of values for
|
||||||
|
# multi-valued attributes, are irrelevant for LDAP.
|
||||||
|
|
||||||
|
mod = ldap.modlist.modifyModlist(oe, ne)
|
||||||
|
if len(mod) > 0:
|
||||||
|
for ed in editors:
|
||||||
|
ed.mod_entry(dn, mod)
|
||||||
|
|
||||||
|
for dn in sorted(new_dns - old_dns, key=dn_key):
|
||||||
|
for ed in editors:
|
||||||
|
try:
|
||||||
|
ed.add_entry(dn, ldap.modlist.addModlist(new_entries[dn]))
|
||||||
|
except Exception as e:
|
||||||
|
handle_exception(dn, 'add', e)
|
||||||
|
|
||||||
|
|
||||||
|
def update_incl_excl(ie, args):
|
||||||
|
"""Add all attributes in comma-separated list args to set ie"""
|
||||||
|
|
||||||
|
for arg in args:
|
||||||
|
ie.update(arg.lower().split(','))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
global conf
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Compute differences between two LDAP directories')
|
||||||
|
parser.add_argument('old', help='old directory URI')
|
||||||
|
parser.add_argument('new', help='new directory URI')
|
||||||
|
|
||||||
|
parser.add_argument('--update', '-u', action='store_true', default=False,
|
||||||
|
help='update to OLD to sync it with NEW')
|
||||||
|
parser.add_argument('--verbose', '-v', action='store_true', default=False,
|
||||||
|
help='write verbose messages to stderr')
|
||||||
|
parser.add_argument('--output', '-o', action='store', default='-',
|
||||||
|
help='output LDIF file (default stdout)')
|
||||||
|
parser.add_argument('--conf', '-c', action='append',
|
||||||
|
default=['ldapdip.conf'],
|
||||||
|
help='configuration file')
|
||||||
|
parser.add_argument('--option', '-O', action='append', default=[],
|
||||||
|
help='override configuration option')
|
||||||
|
parser.add_argument('--include', '-i', action='append', default=[],
|
||||||
|
help='include only these attributes in comparison')
|
||||||
|
parser.add_argument('--exclude', '-x', action='append', default=[],
|
||||||
|
help='exclude these attributes from comparison')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
conf = {}
|
||||||
|
|
||||||
|
# Load default config file (ldapdip.conf), as well as any additional
|
||||||
|
# one specified on the command line.
|
||||||
|
|
||||||
|
for conf_file in args.conf:
|
||||||
|
if os.path.exists(conf_file):
|
||||||
|
conf.update(yaml.load(open(conf_file, 'r').read()))
|
||||||
|
|
||||||
|
# Configuration overrides from command line
|
||||||
|
|
||||||
|
if args.verbose:
|
||||||
|
conf['verbose'] = True
|
||||||
|
|
||||||
|
for confopt in args.option:
|
||||||
|
optname = confopt[:confopt.index('=')]
|
||||||
|
optval = confopt[confopt.index('=') + 1:]
|
||||||
|
conf[optname] = optval
|
||||||
|
|
||||||
|
# Inclusion/exclusion processing
|
||||||
|
# Should also allow setting these from conf???
|
||||||
|
|
||||||
|
update_incl_excl(include, args.include)
|
||||||
|
update_incl_excl(exclude, args.exclude)
|
||||||
|
|
||||||
|
# Prepare output
|
||||||
|
|
||||||
|
if args.update:
|
||||||
|
# Updating directory in place, no LDIF output
|
||||||
|
out = None
|
||||||
|
|
||||||
|
else:
|
||||||
|
if args.output == '-':
|
||||||
|
out = sys.stdout
|
||||||
|
else:
|
||||||
|
out = open(args.output, 'w')
|
||||||
|
|
||||||
|
# Work!
|
||||||
|
|
||||||
|
diff_entries(args.old, args.new, args.update, out)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
if not success:
|
||||||
|
sys.exit(1)
|
@ -0,0 +1,12 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
verbose = False
|
||||||
|
|
||||||
|
|
||||||
|
def err(msg):
|
||||||
|
print(msg, file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def vv(msg):
|
||||||
|
if verbose:
|
||||||
|
err(msg)
|
@ -0,0 +1,767 @@
|
|||||||
|
#! /usr/bin/env python
|
||||||
|
|
||||||
|
# hostdb
|
||||||
|
# Hosts database processing
|
||||||
|
# $Id: main.py 265777 2019-01-15 11:33:46Z quinot $
|
||||||
|
|
||||||
|
|
||||||
|
from . import db as db
|
||||||
|
from . import log as log
|
||||||
|
|
||||||
|
from .db import OrgUnit, Host, Group, Net, Zone
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
from ipaddr import IPv4Address, IPv6Address, IPAddress
|
||||||
|
from .revzones import reverse_name
|
||||||
|
|
||||||
|
#reload(sys)
|
||||||
|
#sys.setdefaultencoding('utf-8')
|
||||||
|
|
||||||
|
autogen_marker = ";;; Automatically generated -- do not edit!\n"
|
||||||
|
dhcpd_conf_template = 'dhcpd.conf.in'
|
||||||
|
|
||||||
|
html_subdir = 'html'
|
||||||
|
csv_subdir = 'csv'
|
||||||
|
csv_sep = ','
|
||||||
|
html_machines_subdir = os.path.join(html_subdir, 'machines')
|
||||||
|
|
||||||
|
|
||||||
|
def html_header(title, subdir=0):
|
||||||
|
"""Return the html header for a page.
|
||||||
|
|
||||||
|
:param str title: Title of the page
|
||||||
|
:param int subdir: Depth of subdirectory containing the page
|
||||||
|
(relative to that containing hosts.css).
|
||||||
|
"""
|
||||||
|
|
||||||
|
css_path = os.path.join(*([".."] * subdir + ["hosts.css"]))
|
||||||
|
|
||||||
|
return """<html><head><meta charset="UTF-8"><title>%s</title>
|
||||||
|
<link href="%s" rel="stylesheet" type="text/css">
|
||||||
|
</head><body>""" % (title, css_path)
|
||||||
|
|
||||||
|
|
||||||
|
newserial = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_newserial():
|
||||||
|
global newserial
|
||||||
|
if newserial is not None:
|
||||||
|
return newserial
|
||||||
|
|
||||||
|
today = int(time.strftime("%Y%m%d00", time.localtime(time.time())))
|
||||||
|
try:
|
||||||
|
serial_file = open("SERIAL", "r+")
|
||||||
|
oldserial = int(serial_file.readline())
|
||||||
|
except IOError:
|
||||||
|
# Case where serial file does not exist yet: create one, and use today
|
||||||
|
# as the new serial.
|
||||||
|
serial_file = open("SERIAL", "w")
|
||||||
|
oldserial = today - 1
|
||||||
|
|
||||||
|
if (today - oldserial) > 0:
|
||||||
|
newserial = str(today)
|
||||||
|
else:
|
||||||
|
newserial = str(oldserial + 1)
|
||||||
|
serial_file.seek(0)
|
||||||
|
serial_file.truncate()
|
||||||
|
serial_file.write(str(newserial + '\n'))
|
||||||
|
serial_file.close()
|
||||||
|
|
||||||
|
return newserial
|
||||||
|
|
||||||
|
|
||||||
|
def gen_file(name, lines):
|
||||||
|
open(name + '.new', 'w').writelines(lines)
|
||||||
|
try:
|
||||||
|
os.rename(name, name + '~')
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
os.rename(name + '.new', name)
|
||||||
|
os.chmod(name, 0o444)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# 0. Command line
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser('Process the hosts database')
|
||||||
|
parser.add_argument('-f', '--force', action='store_true', default=False,
|
||||||
|
help='force bumping serial number')
|
||||||
|
parser.add_argument('-v', '--verbose', action='store_true', default=False,
|
||||||
|
help='verbose output')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
log.verbose = args.verbose
|
||||||
|
|
||||||
|
# 1. Parse input
|
||||||
|
|
||||||
|
db.load_db(sys.stdin)
|
||||||
|
|
||||||
|
# 2. Populate zones and hostfile
|
||||||
|
|
||||||
|
hostfile = [
|
||||||
|
"### Automatically generated, do not edit!\n",
|
||||||
|
"\n",
|
||||||
|
"::1\tlocalhost localhost.my.domain\n",
|
||||||
|
"127.0.0.1\tlocalhost localhost.my.domain\n"]
|
||||||
|
|
||||||
|
dhcpfile = ["### Automatically generated, do not edit!\n", "\n"] \
|
||||||
|
+ open(dhcpd_conf_template, 'r').readlines()
|
||||||
|
|
||||||
|
ldif = ["### Automatically generated, do not edit!\n", "\n"]
|
||||||
|
|
||||||
|
# Generate any required entries for base OUs
|
||||||
|
|
||||||
|
for ou in list(OrgUnit.orgunits.values()):
|
||||||
|
db.ldap_render(ldif, ou)
|
||||||
|
|
||||||
|
# Set default domain for networks based on their OU
|
||||||
|
|
||||||
|
for n in list(Net.nets.values()):
|
||||||
|
if 'domain' not in n.attrs \
|
||||||
|
and 'ou' in n.attrs \
|
||||||
|
and 'domain' in OrgUnit.orgunits[n.attrs['ou']].attrs:
|
||||||
|
n.add_attr('domain', OrgUnit.orgunits[n.attrs['ou']].attrs['domain'])
|
||||||
|
|
||||||
|
hostfile_html = [html_header("Hosts")]
|
||||||
|
|
||||||
|
# Generate additional hosts for interfaces
|
||||||
|
|
||||||
|
for h in list(Host.hosts.values()):
|
||||||
|
for i in h.get_attr('interface'):
|
||||||
|
# Prefix name and aliases with host name if they start with an hyphen
|
||||||
|
|
||||||
|
for i_attr in i:
|
||||||
|
if (i_attr == 'name' or i_attr.endswith('alias')) \
|
||||||
|
and i[i_attr][0] == '-':
|
||||||
|
i[i_attr] = '%s%s' % (h.name, i[i_attr])
|
||||||
|
|
||||||
|
# Add new host
|
||||||
|
|
||||||
|
Host(i.pop('name'), **i)
|
||||||
|
|
||||||
|
# Group processing pass 1 (before host processing)
|
||||||
|
# * optionally, generate aliases of the form "<groupname>-<hostid>"
|
||||||
|
|
||||||
|
for g in list(Group.groups.values()):
|
||||||
|
if g.has_flag("aliases"):
|
||||||
|
last = 0
|
||||||
|
for h in g.hosts:
|
||||||
|
h.add_alias("%s-%d" % (g.name, last), "cname")
|
||||||
|
last = last + 1
|
||||||
|
|
||||||
|
# Host processing main loop
|
||||||
|
|
||||||
|
for h in list(Host.hosts.values()):
|
||||||
|
|
||||||
|
no_ldap = h.has_flag('noldap')
|
||||||
|
|
||||||
|
# Add implicit NAT addresses
|
||||||
|
|
||||||
|
real_addrs = h.addrs[:]
|
||||||
|
if not h.has_flag("nonat"):
|
||||||
|
for a in real_addrs:
|
||||||
|
nat_a = db.find_nat(a)
|
||||||
|
if nat_a:
|
||||||
|
h.add_attr('addr', nat_a)
|
||||||
|
|
||||||
|
# If no address is specified, try to fetch external ones
|
||||||
|
|
||||||
|
if len(h.addrs) == 0 and h.name[-1] == '.':
|
||||||
|
for ai in socket.getaddrinfo(h.name, None):
|
||||||
|
# If ai's address family is unknown (e.g. case of Python built
|
||||||
|
# without IPv6 support) then ai[4][0] is the address family (int),
|
||||||
|
# not the canonical address representation (str).
|
||||||
|
if isinstance(ai[4][0], str):
|
||||||
|
h.add_addr(ai[4][0])
|
||||||
|
|
||||||
|
dhcp_addresses = {}
|
||||||
|
primary_net = None
|
||||||
|
for a in h.addrs:
|
||||||
|
# print "%s -> %s" % (h.name, a)
|
||||||
|
|
||||||
|
# Identify what network this address belongs to.
|
||||||
|
# This is normally just the unique network with the longest
|
||||||
|
# address match. However some IP prefixes appear in more than
|
||||||
|
# one network (e.g. in the case of several distinct logical
|
||||||
|
# subnets that share the same broadcast domain, and therefore
|
||||||
|
# the same IPv6 SLAAC prefix). In this case, the ambiguity
|
||||||
|
# can be resolved if another address of the same host uniquely
|
||||||
|
# identifies one of these networks ("primary" network).
|
||||||
|
# Note that interfaces have already been expanded to distinct
|
||||||
|
# Host objects, so the primary network for a base Host is
|
||||||
|
# (correctly) not taken into account for its dependent interfaces.
|
||||||
|
|
||||||
|
n, ipnet, ipnprops = None, None, None
|
||||||
|
nets = db.find_nets(a)
|
||||||
|
ambiguous = (len(nets) > 1)
|
||||||
|
for n, ipnet, ipnprops in nets:
|
||||||
|
# The primary net is the first one that is unambiguously
|
||||||
|
# identified for this host.
|
||||||
|
|
||||||
|
if primary_net is None and len(nets) == 1:
|
||||||
|
primary_net = n
|
||||||
|
|
||||||
|
# If one or more nets were found, and the primary one
|
||||||
|
# is among them, return it.
|
||||||
|
|
||||||
|
if primary_net == n:
|
||||||
|
ambiguous = False
|
||||||
|
break
|
||||||
|
|
||||||
|
# If more that one net was found, but none matches the
|
||||||
|
# primary one, we have an unresolvable ambiguity.
|
||||||
|
|
||||||
|
if ambiguous:
|
||||||
|
n = None
|
||||||
|
|
||||||
|
if n is None:
|
||||||
|
try:
|
||||||
|
n = Net.nets[h.attrs['net']]
|
||||||
|
except KeyError:
|
||||||
|
log.err("Can't find network for %s (%s)" % (h.name, a))
|
||||||
|
n, ipnet = None, None
|
||||||
|
|
||||||
|
ipa = IPAddress(a)
|
||||||
|
if ipnet is not None:
|
||||||
|
ipnet.mark_used(ipa)
|
||||||
|
|
||||||
|
host_nicks = []
|
||||||
|
if n is None:
|
||||||
|
fqdn = h.name
|
||||||
|
|
||||||
|
# If network is unknown, assume any name is for external view
|
||||||
|
|
||||||
|
nprops = {'view': 'ext'}
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Set organizational unit from network
|
||||||
|
|
||||||
|
if 'ou' in n.attrs:
|
||||||
|
n_ou = n.attrs['ou']
|
||||||
|
h_ou = h.attrs.setdefault('ou', n_ou)
|
||||||
|
if h_ou != n_ou:
|
||||||
|
print("Host %s OU %s inconsistent with network OU %s" \
|
||||||
|
% (h.name, h_ou, n_ou))
|
||||||
|
|
||||||
|
if h.name[-1] != '.':
|
||||||
|
host_nicks.extend([h.name, '%s.%s' % (h.name, n.name)])
|
||||||
|
|
||||||
|
# Get network attributes, but allow per-IPnet overrides
|
||||||
|
|
||||||
|
nprops = dict(n.attrs)
|
||||||
|
if ipnprops is not None:
|
||||||
|
nprops.update(ipnprops)
|
||||||
|
|
||||||
|
fqdn = db.get_fqdn(h.name, ipa, nprops)
|
||||||
|
|
||||||
|
if n.has_flag("dhcp") and not db.is_slac(ipa):
|
||||||
|
if n not in dhcp_addresses:
|
||||||
|
dhcp_addresses[n] = []
|
||||||
|
dhcp_addresses[n].append(a)
|
||||||
|
|
||||||
|
if h.has_flag("64ac") and n.has_flag("64ac"):
|
||||||
|
for p in n.prefixes(v=6, len=64):
|
||||||
|
if isinstance(ipa, IPv4Address):
|
||||||
|
h.addrs.append(IPv6Address(str(p.ip) + str(a)))
|
||||||
|
|
||||||
|
elif n.has_flag('slaac') and not h.has_flag('noslaac'):
|
||||||
|
for p in n.prefixes(v=6, len=64):
|
||||||
|
if not h.has_address_in(p):
|
||||||
|
for (htype, haddr) in h.mac_addrs:
|
||||||
|
if htype == 'ethernet':
|
||||||
|
h.addrs.append(db.autoconfig_address(p, haddr))
|
||||||
|
|
||||||
|
host_nicks = [fqdn] + host_nicks
|
||||||
|
|
||||||
|
# DNS direct records
|
||||||
|
|
||||||
|
view = nprops.get('view', None)
|
||||||
|
if not h.has_flag("nodirect"):
|
||||||
|
for dz in db.find_zones_for_domain(fqdn, view):
|
||||||
|
dz.add_a(fqdn, ipa)
|
||||||
|
if h.has_flag("acme-cname") and "acme-cname" in dz.attrs:
|
||||||
|
dz.add_cname("_acme-challenge." + fqdn,
|
||||||
|
fqdn + "." + dz.attrs["acme-cname"])
|
||||||
|
if not h.has_flag("nomx"):
|
||||||
|
dz.add_mxes(fqdn)
|
||||||
|
|
||||||
|
for [alias, kind] in h.aliases:
|
||||||
|
if n is not None:
|
||||||
|
if alias[-1] != '.':
|
||||||
|
host_nicks.extend([alias, '%s.%s' % (alias, n.name)])
|
||||||
|
alias = db.get_fqdn(alias, ipa, nprops)
|
||||||
|
|
||||||
|
for alias_dz in db.find_zones_for_domain(alias, view):
|
||||||
|
if kind == "mx":
|
||||||
|
alias_dz.add_mxes(alias)
|
||||||
|
else:
|
||||||
|
# "a" or "cname"
|
||||||
|
|
||||||
|
host_nicks.append(alias)
|
||||||
|
if kind == "a":
|
||||||
|
alias_dz.add_a(alias, ipa)
|
||||||
|
else:
|
||||||
|
alias_dz.add_cname(alias, fqdn + ".")
|
||||||
|
if h.has_flag("acme-cname"):
|
||||||
|
alias_dz.add_cname("_acme-challenge." + alias,
|
||||||
|
alias + dz.attrs["acme-cname"])
|
||||||
|
|
||||||
|
# DNS reverse records
|
||||||
|
|
||||||
|
if (ipnet is not None) and not (h.has_flag('noreverse') or
|
||||||
|
n.has_flag('noreverse')):
|
||||||
|
rz = db.find_reverse_zone(ipnet.prefix)
|
||||||
|
if rz is not None:
|
||||||
|
rz.add_ptr(reverse_name(rz.prefix, ipa), fqdn)
|
||||||
|
|
||||||
|
hostline = ''
|
||||||
|
for nick in host_nicks:
|
||||||
|
if len(hostline) > 0:
|
||||||
|
hostline += ' '
|
||||||
|
if nick[-1] == '.':
|
||||||
|
nick = nick[:-1]
|
||||||
|
hostline = hostline + re.sub('@\.', '', nick)
|
||||||
|
if ipnet is not None:
|
||||||
|
ipnet.set_html_hosts_line(ipa, h.html(a))
|
||||||
|
ipnet.set_hosts_line(ipa, hostline)
|
||||||
|
else:
|
||||||
|
hostfile.append('%s\t%s\n' % (ipa, hostline))
|
||||||
|
|
||||||
|
# DHCP
|
||||||
|
|
||||||
|
alias_index = 0
|
||||||
|
for (mac_type, mac_value) in h.mac_addrs:
|
||||||
|
for (n, d_addresses) in list(dhcp_addresses.items()):
|
||||||
|
n.dhcp_fixed.append(
|
||||||
|
'host %s-%u {\n option host-name "%s";\n hardware %s %s;\n'
|
||||||
|
% (h.name, alias_index, h.name, mac_type, mac_value))
|
||||||
|
|
||||||
|
# Pending debugging, IPv6 addresses are commented out
|
||||||
|
for (cl, prefix, suffix) in \
|
||||||
|
[(IPv4Address, '', ''), (IPv6Address, '#', '6')]:
|
||||||
|
cl_addresses = [str(a)
|
||||||
|
for a in d_addresses if isinstance(a, cl)]
|
||||||
|
if len(cl_addresses) > 0:
|
||||||
|
n.dhcp_fixed.append(
|
||||||
|
"%s fixed-address%s %s;\n" %
|
||||||
|
(prefix, suffix, ', '.join(cl_addresses)))
|
||||||
|
|
||||||
|
for o in h.attrs.get('dhcp'):
|
||||||
|
n.dhcp_fixed.append(' ' + o + ';\n')
|
||||||
|
n.dhcp_fixed.append('}\n\n')
|
||||||
|
alias_index = alias_index + 1
|
||||||
|
|
||||||
|
# Generate LDAP entry
|
||||||
|
|
||||||
|
if not (no_ldap or 'ou' not in h.attrs):
|
||||||
|
if db.ldap_render(ldif, h):
|
||||||
|
for alias, kind in h.aliases:
|
||||||
|
if kind != 'mx':
|
||||||
|
db.ldap_render(
|
||||||
|
ldif, h, template='alias', alias=alias, kind=kind)
|
||||||
|
else:
|
||||||
|
# Host did not render: set noldap flag to prevent it from
|
||||||
|
# being referenced in a host group.
|
||||||
|
|
||||||
|
# print "%s failed to render for LDAP" % h.name
|
||||||
|
h.add_flag('noldap')
|
||||||
|
|
||||||
|
for z in list(Zone.zones.values()):
|
||||||
|
if not z.has_root_cname:
|
||||||
|
z.add_mxes("@")
|
||||||
|
|
||||||
|
# Group processing pass 2 (after host processing):
|
||||||
|
# * generate LDAP entry
|
||||||
|
# * generate Ansible inventory
|
||||||
|
# * remove hosts from "ungrouped" set
|
||||||
|
|
||||||
|
ungrouped_hosts = [h for h in list(Host.hosts.values())
|
||||||
|
if not h.has_flag('noldap')]
|
||||||
|
ansible_inventory = []
|
||||||
|
|
||||||
|
for g in list(Group.groups.values()):
|
||||||
|
ansible_inventory.extend(["", "[%s]" % (g.name)])
|
||||||
|
ldap_hosts = {}
|
||||||
|
# Dict indexed by OU, elements are lists of Host objects
|
||||||
|
|
||||||
|
for h in g.hosts:
|
||||||
|
if h in ungrouped_hosts:
|
||||||
|
ungrouped_hosts.remove(h)
|
||||||
|
|
||||||
|
if not (h.has_flag('noldap') or 'ou' not in h.attrs):
|
||||||
|
ldap_hosts.setdefault(h.attrs['ou'], []).append(h)
|
||||||
|
ansible_inventory.append(h.instance_key())
|
||||||
|
|
||||||
|
for g.ou, g.ldap_hosts in list(ldap_hosts.items()):
|
||||||
|
db.ldap_render(ldif, g)
|
||||||
|
|
||||||
|
|
||||||
|
open('ansible_inventory', 'w').write(
|
||||||
|
'\n'.join([h.instance_key() for h in ungrouped_hosts] + ansible_inventory))
|
||||||
|
|
||||||
|
# 3. Output files
|
||||||
|
|
||||||
|
open('hostdb.ldif', 'w').write('\n'.join(ldif))
|
||||||
|
for z in list(Zone.zones.values()):
|
||||||
|
|
||||||
|
# Zone head file
|
||||||
|
hfn = "hd." + z.name
|
||||||
|
|
||||||
|
# Generated zone file
|
||||||
|
zfn = "db." + z.name
|
||||||
|
|
||||||
|
# Read zone head, identify serial placeholder
|
||||||
|
|
||||||
|
nserial_seen = 0
|
||||||
|
nzone = [autogen_marker, "\n"]
|
||||||
|
ln = len(nzone)
|
||||||
|
for l in open(hfn, "r").readlines():
|
||||||
|
nzone.append(l)
|
||||||
|
if re.match("\s+SERIALNO\s+", l):
|
||||||
|
nserial_seen = 1
|
||||||
|
serial_ln = ln
|
||||||
|
serial_placeholder = l
|
||||||
|
ln = ln + 1
|
||||||
|
if not nserial_seen:
|
||||||
|
print("Could not find serial number placeholder in %s!" % (hfn))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Try to load old zone file, and replace serial with placeholder (used to
|
||||||
|
# detect if this zone has changed)
|
||||||
|
|
||||||
|
try:
|
||||||
|
ozone_file = open(zfn, "r")
|
||||||
|
ozone = ozone_file.readlines()
|
||||||
|
ozone_file.close()
|
||||||
|
for ln in range(0, len(ozone)):
|
||||||
|
if re.match("\s+[0-9]+\s+;\s+serial", ozone[ln]):
|
||||||
|
ozone[ln] = serial_placeholder
|
||||||
|
except IOError:
|
||||||
|
print("No existing zone info for " + zfn)
|
||||||
|
ozone = []
|
||||||
|
|
||||||
|
nzone.append("\n")
|
||||||
|
nzone.append(";;; Body of zone " + z.name + "\n")
|
||||||
|
|
||||||
|
keys = list(z.rrs.keys())
|
||||||
|
keys.sort()
|
||||||
|
for k in keys:
|
||||||
|
first = 1
|
||||||
|
for rr in z.rrs[k]:
|
||||||
|
if first:
|
||||||
|
nzone.append('\n')
|
||||||
|
key = k
|
||||||
|
first = 0
|
||||||
|
else:
|
||||||
|
key = "\t" * (len(k) / 8)
|
||||||
|
nzone.append("%s\tIN\t" % (key) + "%(type)s\t%(value)s\n" % rr)
|
||||||
|
|
||||||
|
if nzone != ozone or args.force:
|
||||||
|
# Record forced indication
|
||||||
|
|
||||||
|
forced = " (serial bump forced)" if nzone == ozone else ""
|
||||||
|
|
||||||
|
# Now substitute serial in new zone
|
||||||
|
|
||||||
|
nzone[serial_ln] = re.sub(
|
||||||
|
"SERIALNO", get_newserial(), nzone[serial_ln])
|
||||||
|
nzone_file = open(zfn + ".new", "w")
|
||||||
|
nzone_file.writelines(nzone)
|
||||||
|
nzone_file.close()
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.rename(zfn, zfn + "~")
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
os.rename(zfn + ".new", zfn)
|
||||||
|
os.chmod(zfn, 0o444)
|
||||||
|
|
||||||
|
zstatus = "updated%s." % forced
|
||||||
|
else:
|
||||||
|
zstatus = "unchanged."
|
||||||
|
|
||||||
|
log.vv("%s %s" % (zfn, zstatus))
|
||||||
|
|
||||||
|
xlat_slash_underscore = str.maketrans("/", "_")
|
||||||
|
|
||||||
|
# Hosts header
|
||||||
|
|
||||||
|
hostfile_html.append("<p><ul>")
|
||||||
|
|
||||||
|
for net in list(Net.nets.values()):
|
||||||
|
info = []
|
||||||
|
if 'purpose' in net.attrs:
|
||||||
|
info.append(net.attrs['purpose'])
|
||||||
|
if 'addr' in net.__dict__:
|
||||||
|
info.append(str(net.addr))
|
||||||
|
|
||||||
|
hostfile_html.append("""<li><a href="#%s">%s</a> (%s)</li>
|
||||||
|
""" % (net.name, net.name, ", ".join(info)))
|
||||||
|
|
||||||
|
hostfile_html.append('</ul><small>')
|
||||||
|
|
||||||
|
for net in list(Net.nets.values()):
|
||||||
|
addr_usage = ''
|
||||||
|
ipn4 = []
|
||||||
|
|
||||||
|
total_addresses = 0
|
||||||
|
free_addresses = 0
|
||||||
|
for ipn, _ in sorted(net.ipnets,
|
||||||
|
key=lambda ipn__: ipn__[0].key()):
|
||||||
|
hostfile.append('\n# %s - %s\n\n' % (net.name, ipn.prefix))
|
||||||
|
hostfile.extend(ipn.list_hosts())
|
||||||
|
|
||||||
|
if ipn.prefix.version == 4:
|
||||||
|
total_addresses += ipn.prefix.numhosts
|
||||||
|
free_addresses += len(ipn.freelist)
|
||||||
|
ipn4.append(ipn)
|
||||||
|
|
||||||
|
used_addresses = total_addresses - free_addresses
|
||||||
|
if total_addresses > 0:
|
||||||
|
addr_usage = "%d/%d available addresses" % \
|
||||||
|
(free_addresses, total_addresses)
|
||||||
|
addr = " (%s)" % ', '.join(map(str, ipn4))
|
||||||
|
hostfile_html.append("""
|
||||||
|
<td><a name="%s"></a><h3>%s%s</h3>
|
||||||
|
%s
|
||||||
|
<table class="hosts">
|
||||||
|
""" % (net.name, net.name, addr, addr_usage))
|
||||||
|
if len(ipn4) > 0:
|
||||||
|
for n in ipn4:
|
||||||
|
hostfile_html.extend(n.html_list_hosts())
|
||||||
|
else:
|
||||||
|
for h in [h for h in list(Host.hosts.values()) if 'net' in h.attrs and h.attrs['net'] == net.name]:
|
||||||
|
for a in h.addrs:
|
||||||
|
hostfile_html.append(h.html(a))
|
||||||
|
|
||||||
|
hostfile_html.append("\n</table></td>")
|
||||||
|
|
||||||
|
for net in list(Net.nets.values()):
|
||||||
|
if len(net.dhcp_fixed) > 0:
|
||||||
|
try:
|
||||||
|
fixed_index = dhcpfile.index(
|
||||||
|
"# FIXED ASSIGNMENTS FOR %s\n" % (net.name))
|
||||||
|
dhcpfile[fixed_index + 1:fixed_index + 1] = net.dhcp_fixed
|
||||||
|
except ValueError:
|
||||||
|
print("Could not find where to generate fixed DHCP assignments" \
|
||||||
|
+ " for %s\n" \
|
||||||
|
% (net.name))
|
||||||
|
raise
|
||||||
|
|
||||||
|
gen_file('hosts', hostfile)
|
||||||
|
gen_file('dhcpd.conf', dhcpfile)
|
||||||
|
|
||||||
|
# 4. Generate HTML view of the machines
|
||||||
|
|
||||||
|
# Create the HTML subdirectories if they do not exist
|
||||||
|
|
||||||
|
for path in [html_subdir, html_machines_subdir]:
|
||||||
|
if not os.path.isdir(path):
|
||||||
|
os.mkdir(path)
|
||||||
|
|
||||||
|
|
||||||
|
def html_line_for_host(host):
|
||||||
|
""" Return a html table line or group of lines for host.
|
||||||
|
"""
|
||||||
|
purpose = host.get_attr("purpose")
|
||||||
|
if purpose == "":
|
||||||
|
purpose = "(no description)"
|
||||||
|
|
||||||
|
return """<tr><td><a href="machines/%s.html">%s</a></td>
|
||||||
|
<td>%s</td>
|
||||||
|
</tr>
|
||||||
|
""" % (host.name, host.name, purpose)
|
||||||
|
|
||||||
|
|
||||||
|
# Generate one file for each group
|
||||||
|
for g in list(Group.groups.values()):
|
||||||
|
groupdata = html_header(g.name) + """<h2>Group '%s'</h2> <h3>%s</h3>
|
||||||
|
""" % (g.name, g.get_attr("purpose"))
|
||||||
|
|
||||||
|
# Write the hosts table
|
||||||
|
groupdata += '<table class="hosts">'
|
||||||
|
|
||||||
|
g.hosts.sort()
|
||||||
|
for h in g.hosts:
|
||||||
|
groupdata += html_line_for_host(h)
|
||||||
|
|
||||||
|
groupdata += '</table>'
|
||||||
|
|
||||||
|
gen_file(html_subdir + os.sep + g.name + ".html", groupdata)
|
||||||
|
|
||||||
|
# Generate one file for all the machines that do not belong to any group
|
||||||
|
|
||||||
|
groupdata = html_header("Ungrouped")
|
||||||
|
groupdata += """<h2>Devices not associated to any group</h2>"""
|
||||||
|
groupdata += '<table class="hosts">'
|
||||||
|
|
||||||
|
for h in list(Host.hosts.values()):
|
||||||
|
if h.groups == []:
|
||||||
|
groupdata += html_line_for_host(h)
|
||||||
|
|
||||||
|
groupdata += '</table>'
|
||||||
|
gen_file(html_subdir + os.sep + "ungrouped.html", groupdata)
|
||||||
|
|
||||||
|
# Generate one file for each host, with all the details
|
||||||
|
|
||||||
|
for h in list(Host.hosts.values()):
|
||||||
|
hostdata = html_header(h.name, subdir=1) + \
|
||||||
|
"<h2>%s</h2>""" % h.name
|
||||||
|
|
||||||
|
purpose = h.get_attr("purpose")
|
||||||
|
if purpose != "":
|
||||||
|
hostdata += """ Purpose:<ul><li>%s</li></ul>
|
||||||
|
""" % purpose
|
||||||
|
|
||||||
|
if h.groups != []:
|
||||||
|
hostdata += """ Groups: <ul>%s</ul>
|
||||||
|
""" % ("".join(["<li><a href=../%s.html>%s</a></li>"
|
||||||
|
% (x, x) for x in h.groups]))
|
||||||
|
|
||||||
|
if h.aliases != []:
|
||||||
|
hostdata += """ Aliases: <ul>%s</ul>
|
||||||
|
""" % ("".join(['<li>' + x[0] + ' (' + x[1] + ')</li>' for x in h.aliases]))
|
||||||
|
|
||||||
|
if h.addrs != []:
|
||||||
|
hostdata += """ Addresses: <ul>%s</ul>
|
||||||
|
""" % ("".join(["<li>%s</li>" % (a) for a in h.addrs]))
|
||||||
|
|
||||||
|
if h.mac_addrs != []:
|
||||||
|
hostdata += """ MAC addresses: <ul>%s</ul>
|
||||||
|
""" % ("<li> ".join(['<li><b>' + x[0] + '</b>: ' + x[1] + '</li>' for x in h.mac_addrs]))
|
||||||
|
|
||||||
|
if h.attrs != {}:
|
||||||
|
hostdata += """ Attributes: <ul> """
|
||||||
|
for j in h.attrs:
|
||||||
|
hostdata += "<li><b>%s</b>: %s</li>" % (j, h.attrs[j])
|
||||||
|
hostdata += "</ul>"
|
||||||
|
|
||||||
|
gen_file(html_machines_subdir + os.sep + h.name + ".html", hostdata)
|
||||||
|
|
||||||
|
# Generate one table of all machines, ordered by IP address
|
||||||
|
|
||||||
|
gen_file(html_subdir + os.sep + "hosts.html", hostfile_html)
|
||||||
|
|
||||||
|
# Generate a table ordered by commission date
|
||||||
|
|
||||||
|
dated = [] # contains tuples ('date', 'hostname')
|
||||||
|
undated = [] # contains list of hostnames
|
||||||
|
for h in list(Host.hosts.values()):
|
||||||
|
if 'commissioned' in h.attrs:
|
||||||
|
dated.append((h.attrs['commissioned'], h.name))
|
||||||
|
else:
|
||||||
|
if not ('role' in h.groups):
|
||||||
|
undated.append(h.name)
|
||||||
|
|
||||||
|
dated.sort()
|
||||||
|
|
||||||
|
html = html_header("Hosts by date of commission") + """
|
||||||
|
<h2>Dated hosts</h2>
|
||||||
|
<table class="hosts">
|
||||||
|
"""
|
||||||
|
|
||||||
|
for h in dated:
|
||||||
|
html += """<tr>
|
||||||
|
<td>%s</td><td><a href="machines/%s.html">%s</a></td>
|
||||||
|
</tr>""" % (h[0], h[1], h[1])
|
||||||
|
|
||||||
|
html += """</table><h2>Non-dated hosts</h2><table class="hosts">"""
|
||||||
|
|
||||||
|
for h in undated:
|
||||||
|
html += '<tr><td><a href="machines/%s.html">%s</a></td></tr>' % (h, h)
|
||||||
|
|
||||||
|
gen_file(html_subdir + os.sep + "by_date.html", html)
|
||||||
|
|
||||||
|
# Generate an index file
|
||||||
|
|
||||||
|
index = html_header("Hosts") + """
|
||||||
|
<h1>Machine inventory</h2>
|
||||||
|
|
||||||
|
<h2>View by IP address</h2>
|
||||||
|
<ul>
|
||||||
|
<li> <a href="hosts.html">Hosts</a> </li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>View by group</h2>
|
||||||
|
<ul>
|
||||||
|
<li> <a href="ungrouped.html">ungrouped</a>: not belonging to any group
|
||||||
|
</li>
|
||||||
|
|
||||||
|
"""
|
||||||
|
group_purpose = [(g.name, g.get_attr("purpose"))
|
||||||
|
for g in list(Group.groups.values())]
|
||||||
|
group_purpose.sort()
|
||||||
|
|
||||||
|
for g_name, g_purpose in group_purpose:
|
||||||
|
index += '<li><a href="%s.html">%s</a>: %s</li>' % (
|
||||||
|
g_name, g_name, g_purpose)
|
||||||
|
|
||||||
|
index += """</ul>
|
||||||
|
<h2>View by date of commission</h2>
|
||||||
|
<ul>
|
||||||
|
<li> <a href="by_date.html">Hosts by date</a> </li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Download CSVs</h2>
|
||||||
|
<ul>
|
||||||
|
<li> <a href="inventory.csv">Inventory</a> </li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
gen_file(html_subdir + os.sep + "index.html", index)
|
||||||
|
|
||||||
|
# Generate a .csv file
|
||||||
|
|
||||||
|
csv = ""
|
||||||
|
|
||||||
|
# ... csv table headers
|
||||||
|
|
||||||
|
csv += csv_sep.join(
|
||||||
|
["name", "commissioned"] +
|
||||||
|
[k for k in Group.groups]) + '\n'
|
||||||
|
|
||||||
|
# ... csv table body
|
||||||
|
|
||||||
|
# Create a sublist of hosts relevant for listing in the inventory
|
||||||
|
|
||||||
|
hosts = [h for h in list(Host.hosts.values())
|
||||||
|
if 'role' not in h.groups and
|
||||||
|
'virtual' not in h.groups and
|
||||||
|
'commissioned' in h.attrs]
|
||||||
|
|
||||||
|
for h in hosts:
|
||||||
|
try:
|
||||||
|
csv += h.name + csv_sep
|
||||||
|
|
||||||
|
if 'commissioned' in h.attrs:
|
||||||
|
csv += "%s%s" % (h.attrs['commissioned'], csv_sep)
|
||||||
|
else:
|
||||||
|
csv += csv_sep
|
||||||
|
|
||||||
|
for g in Group.groups:
|
||||||
|
if g in h.groups:
|
||||||
|
csv += '1' + csv_sep
|
||||||
|
else:
|
||||||
|
csv += '0' + csv_sep
|
||||||
|
except Exception:
|
||||||
|
log.err("Failed to generate CSV for " +
|
||||||
|
h.name + str(h.attrs))
|
||||||
|
|
||||||
|
csv += '\n'
|
||||||
|
|
||||||
|
if not os.path.isdir(csv_subdir):
|
||||||
|
os.mkdir(csv_subdir)
|
||||||
|
|
||||||
|
gen_file(csv_subdir + os.sep + "inventory.csv", csv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
@ -0,0 +1,63 @@
|
|||||||
|
# revzones
|
||||||
|
# Compute reverse names for IP networks and hosts
|
||||||
|
|
||||||
|
from ipaddr import IPv4Network, IPv6Network, IPAddress
|
||||||
|
|
||||||
|
|
||||||
|
def reverse_name(ipnet, ipa=None):
|
||||||
|
|
||||||
|
def _get_element(packed, index):
|
||||||
|
val = packed[index // 8]
|
||||||
|
shift = 8 - (index % 8 + step)
|
||||||
|
val = (val >> shift) & ((1 << step) - 1)
|
||||||
|
return val
|
||||||
|
|
||||||
|
if ipnet.version == 6:
|
||||||
|
revn = "ip6.arpa"
|
||||||
|
step = 4
|
||||||
|
base = 'x'
|
||||||
|
addrlen = 128
|
||||||
|
else:
|
||||||
|
revn = "in-addr.arpa"
|
||||||
|
step = 8
|
||||||
|
base = 'd'
|
||||||
|
addrlen = 32
|
||||||
|
prefixlen = ipnet.prefixlen
|
||||||
|
index = 0
|
||||||
|
|
||||||
|
# Generate network part
|
||||||
|
|
||||||
|
while index < prefixlen:
|
||||||
|
val = _get_element(ipnet.packed, index)
|
||||||
|
if prefixlen - index < step:
|
||||||
|
suffix = "-" + str(prefixlen)
|
||||||
|
else:
|
||||||
|
suffix = ""
|
||||||
|
revn = format(val, base) + suffix + "." + revn
|
||||||
|
index = index + step
|
||||||
|
|
||||||
|
# Generate host part
|
||||||
|
|
||||||
|
if ipa:
|
||||||
|
index = prefixlen - prefixlen % step
|
||||||
|
while index < addrlen:
|
||||||
|
val = _get_element(ipa.packed, index)
|
||||||
|
revn = format(val, base) + "." + revn
|
||||||
|
index = index + step
|
||||||
|
|
||||||
|
return revn
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
for (ipnet, ipa) in [
|
||||||
|
(IPv4Network('192.168.12.128/26'), '192.168.12.154'),
|
||||||
|
(IPv4Network('192.168.12.128/24'), '192.168.12.154'),
|
||||||
|
(IPv4Network('192.168.12.128/18'), '192.168.12.154'),
|
||||||
|
(IPv4Network('192.168.12.128/16'), '192.168.12.154'),
|
||||||
|
(IPv6Network('2001:470:1f0b:1b0c::/64'),
|
||||||
|
'2001:470:1f0b:1b0c::1234:5678'),
|
||||||
|
(IPv6Network('2001:470:1f0b:1b0c::/62'),
|
||||||
|
'2001:470:1f0b:1b0c::1234:5678'),
|
||||||
|
(IPv6Network('2001:470:1f0b:1b0c::/48'),
|
||||||
|
'2001:470:1f0b:1b0c::1234:5678')]:
|
||||||
|
print("Net %s -> %s" % (str(ipnet), reverse_name(ipnet, None)))
|
||||||
|
print("Host %s -> %s" % (ipa, reverse_name(ipnet, IPAddress(ipa))))
|
@ -0,0 +1,36 @@
|
|||||||
|
# YaML loader that rejects duplicate keys in objects
|
||||||
|
|
||||||
|
from yaml.constructor import ConstructorError
|
||||||
|
from yaml.nodes import MappingNode
|
||||||
|
|
||||||
|
try:
|
||||||
|
from yaml import CLoader as Loader
|
||||||
|
except ImportError:
|
||||||
|
from yaml import Loader
|
||||||
|
|
||||||
|
|
||||||
|
class UniqueKeyLoader(Loader):
|
||||||
|
|
||||||
|
def construct_mapping(self, node, deep=False):
|
||||||
|
if not isinstance(node, MappingNode):
|
||||||
|
raise ConstructorError(
|
||||||
|
None, None,
|
||||||
|
"expected a mapping node, but found %s" % node.id,
|
||||||
|
node.start_mark)
|
||||||
|
mapping = {}
|
||||||
|
for key_node, value_node in node.value:
|
||||||
|
key = self.construct_object(key_node, deep=deep)
|
||||||
|
try:
|
||||||
|
hash(key)
|
||||||
|
except TypeError as exc:
|
||||||
|
raise ConstructorError(
|
||||||
|
"while constructing a mapping", node.start_mark,
|
||||||
|
"found unacceptable key (%s)" % exc, key_node.start_mark)
|
||||||
|
# check for duplicate keys
|
||||||
|
if key in mapping:
|
||||||
|
raise ConstructorError(
|
||||||
|
"while constructing a mapping", node.start_mark,
|
||||||
|
"found duplicate key", key_node.start_mark)
|
||||||
|
value = self.construct_object(value_node, deep=deep)
|
||||||
|
mapping[key] = value
|
||||||
|
return mapping
|
@ -0,0 +1,11 @@
|
|||||||
|
[project]
|
||||||
|
name = "hostdb"
|
||||||
|
version = "0.0.1"
|
||||||
|
dependencies = [
|
||||||
|
"ipaddr",
|
||||||
|
"jinja2",
|
||||||
|
"pyyaml",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
hostdb = "hostdb.main:main"
|
Loading…
Reference in New Issue