Commit b6dd7535 authored by Anders Blomdell's avatar Anders Blomdell
Browse files

Version 2013-10-22 15:39

M  src/hostinfo.py
M  src/hostinfo/dhcpd.py
M  src/hostinfo/dhcpd_ipv6.py
M  src/hostinfo/ifconfig.py
M  src/hostinfo/util.py
parent 9344ec7a
......@@ -233,9 +233,9 @@ if __name__ == '__main__':
optParser.add_argument("--ifconfig",
action="store", metavar="DIR",
help="Generate DIR/ifcfg-eth*")
optParser.add_argument("--kickstart",
action="store", metavar="DIR",
help="kickstart files should be fetched from DIR")
optParser.add_argument("--kickstart",
action="store", metavar="PREFIX",
help="kickstart file PREFIX")
optParser.add_argument("--macosx_auto",
action="store", metavar="DIR",
help="Generate MacOSX autmount maps")
......@@ -288,13 +288,8 @@ if __name__ == '__main__':
file["%s/%s" % (options.automount, f)] = c
if options.dhcpd:
if options.kickstart:
kickstart = os.path.abspath(options.kickstart)
else:
kickstart = None
pass
file["%s/dhcpd.conf" % options.dhcpd] = hostinfo.dhcpd.generate(
tree, host, kickstart, next_server=options.next_server)
tree, options)
pass
if options.dhcpd6:
......
......@@ -3,19 +3,120 @@ import os
from hostinfo.util import ntoa, aton
import hostinfo.util as util
CLASS_PXECLIENT = """
class "pxeclient" {
match if substring (option vendor-class-identifier, 0, 9) = "PXEClient";
next-server %(next_server)s;
"""
ddns-update-style none;
authoritative;
option space PXE;
option PXE.mtftp-ip code 1 = ip-address;
option PXE.mtftp-cport code 2 = unsigned integer 16;
option PXE.mtftp-sport code 3 = unsigned integer 16;
option PXE.mtftp-tmout code 4 = unsigned integer 8;
option PXE.mtftp-delay code 5 = unsigned integer 8;
option arch code 93 = unsigned integer 16; # RFC4578
shared-network "MAC(02:64:01:00:00:02)" {
subnet 192.168.73.0 netmask 255.255.255.0 {
option subnet-mask 255.255.255.0;
option broadcast-address 192.168.73.255;
option routers 192.168.73.1;
option domain-name "i.control.lth.se";
option domain-name-servers 192.168.73.1;
default-lease-time 14400; # 4 hours
max-lease-time 86400; # 1 day
get-lease-hostnames true;
use-host-decl-names on;
class "pxeclient" {
match if substring (option vendor-class-identifier, 0, 9) = "PXEClient";
next-server dist-01.i.control.lth.se;
if option arch = 00:06 {
filename "pxelinux/bootia32.efi";
filename "bootia32.efi";
} else if option arch = 00:07 {
filename "pxelinux/bootx64.efi";
filename "bootx64.efi";
} else {
filename "pxelinux.0";
}
}
class "anaconda" {
match if substring (option vendor-class-identifier, 0, 14) = "anaconda-Linux";
match hardware;
next-server dist-01.i.control.lth.se;
}
pool {
allow members of "pxeclient";
#allow members of "anaconda";
range 192.168.73.100 192.168.73.119;
}
pool {
allow members of "anaconda";
range 192.168.73.200 192.168.73.219;
}
host test.control.lth.se {
hardware ethernet 02:13:10:10:18:51;
option host-name "test";
#filename "/bootserver/kickstart/Fedora-19-qemu-dist";
}
subclass "anaconda" 1:02:13:10:10:18:51 {
# filename "/bootserver/kickstart/Fedora-19-qemu-dist";
}
}
}
"""
PXE_ALLOW="""
option space PXE;
option PXE.mtftp-ip code 1 = ip-address;
option PXE.mtftp-cport code 2 = unsigned integer 16;
option PXE.mtftp-sport code 3 = unsigned integer 16;
option PXE.mtftp-tmout code 4 = unsigned integer 8;
option PXE.mtftp-delay code 5 = unsigned integer 8;
option arch code 93 = unsigned integer 16; # RFC4578
class "pxeclient_allow" {
match if substring (option vendor-class-identifier, 0, 9) = "PXEClient";
if option arch = 00:06 {
filename "bootia32.efi";
} else if option arch = 00:07 {
filename "bootx64.efi";
} else {
filename "pxelinux.0";
}
}
"""
PXE_DENY="""
class "pxeclient_deny" {
match if substring (option vendor-class-identifier, 0, 9) = "PXEClient";
ignore booting;
}
"""
CLASS_PXECLIENT = """
class "pxeclient" {
match if substring (option vendor-class-identifier, 0, 9) = "PXEClient";
next-server %(next_server)s;
if option arch = 00:06 {
filename "bootia32.efi";
} else if option arch = 00:07 {
filename "bootx64.efi";
} else {
filename "pxelinux.0";
}
}
}
class "noboot" {
match if substring (option vendor-class-identifier, 0, 9) != "PXEClient";
ignore booting;
}
"""
def MacOS_NETBOOT(dhcphost):
if not os.path.exists('/local/macos'):
......@@ -61,248 +162,196 @@ class "AppleNBI-ppc" {
""" % (dhcphost, dhcphost)
def generate(tree, dhcphost, kickstart, next_server=None):
#
# A. Get interfaces to serve
#
interface = []
for ds in tree._host_._interface_._dhcpserver_:
if dhcphost in ds.name[0::1]:
# Host is dhcpserver for this interface
dhcpip = ds._parent._ip_[0].address[0]
interface.append(ds._parent)
pass
pass
if not interface:
raise Exception("%s is not a dhcpserver" % dhcphost)
def generate(tree, options):
result = util.StringArray()
dhcp = set(filter(lambda d: options.host in d.name[0::1],
tree._host_._interface_._ip_._dhcpserver_))
assert dhcp, '%s is not a dhcp server' % options.host
#
# B. Emit header
#
result = "ddns-update-style none;\n"
result += "authoritative;\n"
result += MacOS_NETBOOT(dhcpip)
result.append_lines("""
|ddns-update-style none;
|authoritative;
|get-lease-hostnames true;
|use-host-decl-names on;
""")
if get_pxeboot(dhcp) == 'only':
result += PXE_ALLOW
pass
else:
result += PXE_DENY
pass
#result += MacOS_NETBOOT(dhcpip)
#
# C. Emit subnet declarations for each interface
#
interface = set(map(lambda d: d._parent._parent, dhcp))
for i in interface:
address = []
vlan = []
for ip in i._ip_:
if ip.alias[0] and ip.address[0]:
# Alias addresses
address.append(ip.address[0])
pass
elif ip.vlan[0] and ip.address[0]:
vlan.append(ip.address[0])
pass
elif ip.address[0]:
address.append(ip.address[0])
pass
pass
result += 'shared-network "MAC(%s)" {\n\n' % i.ethernet[0]
result += emit_network(tree, dhcphost, address, kickstart,
interface=i, next_server=next_server)
result += "}\n"
for v in vlan:
result += emit_network(tree, dhcphost, [ v ], kickstart,
next_server=next_server)
pass
result += emit_interface(tree, options, i)
pass
return result
def emit_network(tree, dhcphost, addr, kickstart,
interface=None, next_server=None):
result = ""
subnet = {}
for s in filter(util.network, tree._subnet_):
n = util.network(s)
for a in addr:
if util.address(a) in n:
subnet[n] = s
pass
pass
pass
for n in sorted(subnet):
for l in emit_subnet(tree, subnet[n], dhcphost, kickstart,
interface=interface,
next_server=next_server).split("\n"):
result += " %s\n" % l
pass
served_networks = map(lambda d: util.network(get_subnet(tree, d._parent)),
dhcp)
result += emit_hosts(tree, options, served_networks)
return str(result)
def emit_interface(tree, options, interface):
result = util.StringArray()
subnet = dict(map(lambda d: (get_subnet(tree, d._parent), d),
interface._ip_._dhcpserver_))
result += 'shared-network "MAC(%s)" {' % interface.ethernet[0]
for sn,dhcp in map(lambda n: (n, subnet[n]), sorted(subnet)):
result += emit_network(tree, options, sn, dhcp).indent()
pass
result += "}"
return result
def emit_subnet(tree, subnet, dhcphost, kickstart,
interface=None, next_server=None):
result = ""
def emit_network(tree, options, subnet, dhcp):
result = util.StringArray()
net = util.network(subnet)
if next_server == None:
next_server = dhcphost
pass
static = {}
dynamic = set()
never = []
for ip in tree._host_._interface_._ip_:
# Find all hosts that belong to this network
a = util.address(ip)
if a:
a = util.address(ip)
if a in net:
if ip.dynamic[0] == dhcphost:
# Dynamic address served by this host
dynamic.add(a)
pass
else:
static[ip.name[0:]] = ip
pass
pass
pxeboot = get_pxeboot(dhcp)
result.append_lines("""
|subnet %(network)s netmask %(netmask)s {
| option subnet-mask %(netmask)s;
| option broadcast-address %(broadcast)s;
| server-name "%(host)s";
| default-lease-time 14400; # 4 hours
| max-lease-time 86400; # 1 day
""" % dict(network=net.network,
netmask=net.netmask,
broadcast=net.broadcast,
host=options.host))
if dhcp.first[0] and dhcp.last[0]:
first = util.address(dhcp.first[0])
last = util.address(dhcp.last[0])
assert first in net, '%s not part of %s' % (first, net)
assert last in net, '%s not part of %s' % (last, net)
if pxeboot == 'no':
result += " range %s %s;" % (first, last)
pass
if ip.never[0]:
a = util.address(ip.never[0])
if a in net:
never.append(ip.ethernet[0:])
pass
elif pxeboot == 'only':
result.append_lines("""
| pool {
| allow members of "pxeclient";
| range %(first)s %(last)s;
| }""" % dict(first=first, last=last))
pass
pass
result += "subnet %s netmask %s {\n" % (net.network, net.netmask)
result += " option subnet-mask %s;\n" % net.netmask
result += " option broadcast-address %s;\n" % net.broadcast
result += emit_subnet_info(subnet).indent()
result += '}'
return result
def emit_subnet_info(subnet):
result = util.StringArray()
if subnet.gateway[0]:
result += " option routers %s;\n" % subnet.gateway[0]
result += "option routers %s;" % subnet.gateway[0]
pass
if subnet.domain[0]:
result += " option domain-name \"%s\";\n" % subnet.domain[0]
result += "option domain-name \"%s\";" % subnet.domain[0]
pass
if subnet.name_servers[0]:
result += " option domain-name-servers %s;\n" % subnet.name_servers[0]
result += "option domain-name-servers %s;" % subnet.name_servers[0]
pass
if subnet.ntp_servers[0]:
result += " option ntp-servers %s;\n" % (subnet.ntp_servers[0])
result += "option ntp-servers %s;" % (subnet.ntp_servers[0])
pass
#
# Emit dynamic hosts
#
result += " default-lease-time 14400; # 4 hours\n"
result += " max-lease-time 86400; # 1 day\n"
for s in interface_subnets_matching(interface, net):
if s.first[0] and s.last[0]:
result += " range %s %s;\n" % (util.address(s.first[0]),
util.address(s.last[0]))
result += ''
return result
def emit_hosts(tree, options, networks):
result = util.StringArray()
def match(a):
return filter(lambda n: a in n, networks)
static = {}
never = {}
for ip in tree._host_._interface_._ip_:
# Find all hosts that associated with this network
ethernet = ip.ethernet[0:]
if not ethernet:
continue
if ethernet.lower() != ethernet:
raise util.HostinfoException('%s not lower-case' % ethernet)
if ip.never[0]:
if match(util.address(ip.never[0])):
if not ethernet in never:
never[ethernet] = []
pass
never[ethernet].append(ip)
pass
continue
if match(util.address(ip)):
if not ethernet in static:
static[ethernet] = []
pass
static[ethernet].append(ip)
pass
pass
# FIXME: Old style dynamic...
min = 0
max = 0
for d in sorted(dynamic):
if d == max + 1:
max = d
def by_name(ether_ip_dict):
result = {}
for e in ether_ip_dict:
name = set(map(lambda n: n.name[0:], ether_ip_dict[e]))
if len(name) != 1:
raise util.HostinfoException('Multiple names %s' % name,
where=ether_ip_dict[e])
name = name.pop()
result[name] = (e, ether_ip_dict[e])
pass
else:
if min:
result += " range %s %s;\n" % (min, max)
pass
min = d
max = d
for name in sorted(result):
ether, ip = result[name]
yield name, ether, ip
pass
pass
if min:
result += " range %s %s;\n" % (min, max)
pass
result += " get-lease-hostnames true;\n"
result += " use-host-decl-names on;\n"
#
# Emit static hosts
#
result += "\n group {\n"
result += " default-lease-time 315360000; # 10 year\n"
result += " max-lease-time 315360000; # 10 years\n\n"
sk = static.keys()
sk.sort()
for i in [static[x] for x in sk]:
ether = i._parent.ethernet[0]
ip = i.address[0]
if ether:
assert ether.lower() == ether, "%s is not lower-case" % ether
result += " host %s.%s {\n"% (i.name[0:], subnet.domain[0])
result += " hardware ethernet %s;\n" % ether
result += " fixed-address %s;\n" % ip
result += " option host-name \"%s\";\n" % i.name[0:]
if i._parent._kickstart_:
if not kickstart:
raise Exception("--kickstart needed for %s" % i.name[0:])
kf = i._parent._kickstart_[0].file[0]
result += " server-name \"%s\";\n" % dhcphost
result += " next-server %s;\n" % next_server
result += " if substring(option vendor-class-identifier, "
result += "0, 20) = \n"
result += " \"PXEClient:Arch:00000\" {\n"
result += " filename \"pxelinux.0\";\n"
result += " } else {\n"
result += " filename \"%s/%s\";\n" % (kickstart, kf)
result += " }\n"
pass
for d in i._dhcp_:
result += " %s\n" % d.value[0]
pass
result += " }\n"
if static:
result.append_lines("""
|group { # Static hosts
| default-lease-time 315360000; # 10 year
| max-lease-time 315360000; # 10 years
|""")
for name, ether, ip in by_name(static):
if ether in never:
never.pop(ether)
result.append_lines("""
| host %(id)s {
| hardware ethernet %(ethernet)s;
| fixed-address %(address)s;
| option host-name "%(name)s";
| }""" % dict(id='%s_%s' % (name, ether.replace(':','')),
name=name,
ethernet=ether,
address=', '.join(map(str,
map(util.address, ip)))))
pass
result += "}"
pass
for e in never:
result += " host never_%s {\n" % e.replace(':', '')
result += " hardware ethernet %s;\n" % e
result += " ignore booting;\n"
result += " }\n"
pass
# result += CLASS_PXECLIENT % { 'next_server': next_server }
for s in interface_subnets_matching(interface, net):
if s.kickstart[0] != 'any':
continue
else:
continue
for i in filter(lambda i: i.ethernet[0] and i._kickstart_,
tree._host_._interface_):
e = i.ethernet[0]
assert e.lower() == e, "%s not lower-case" % e
result += " host %s {\n"% (i.name[0:])
result += " hardware ethernet %s;\n" % e
result += " option host-name \"%s\";\n" % i.name[0:]
if not kickstart:
raise Exception("--kickstart needed for %s" % i.name[0:])
kf = i._kickstart_[0].file[0]
result += " server-name \"%s\";\n" % dhcphost
result += " next-server %s;\n" % next_server
result += " if substring(option vendor-class-identifier, "
result += "0, 20) = \n"
result += " \"PXEClient:Arch:00000\" {\n"
result += " filename \"pxelinux.0\";\n"
result += " } else {\n"
result += " filename \"%s/%s\";\n" % (kickstart, kf)
result += " }\n"
result += " }\n"
if never:
result += 'group { # Disallowed hosts'
for name, ether, ip in by_name(never):
result.append_lines("""
| host %(name)s {
| hardware ethernet %(ethernet)s;
| ignore booting;
| }""" % dict(name='%s_never_%s' % (name, ether.replace(':','')),
ethernet=ether))
pass
result += " if substring(option vendor-class-identifier, "
result += "0, 20) = \n"
result += " \"PXEClient:Arch:00000\" {\n"
result += " filename \"pxelinux.0\";\n"
result += " } else {\n"
result += " filename \"%s/default\";\n" % (kickstart)
result += " }\n"
break
result += " }\n"
result += "}\n"
result += "}"
pass
return result
def interface_subnets_matching(interface, net):
if not interface:
return
for s in filter(lambda s: util.network(s) == net, interface._subnet_):
yield s
def get_subnet(tree, ip):
a = util.address(ip)
def match(subnet):
s = util.network(subnet)
return s and a in s
subnet = filter(match, tree._subnet_)
assert len(subnet) == 1, '%s matches multiple networks %s' % (a, subnet)
return subnet[0]
def get_pxeboot(dhcp):
try:
p = set(map(lambda d: d.pxeboot[0] or 'no', dhcp))
assert len(p) == 1, "Mixed pxeboot not allowed (%s)" % ','.join(p)
result = p.pop()
pass
pass
except TypeError:
result = dhcp.pxeboot[0] or 'no'
pass
allowed = [ 'no', 'only' ]
assert result in allowed,"pxeboot ('%s') shold be in %s" % (result, allowed)
return result
......@@ -2,88 +2,9 @@ import sys
import os
import re
import ipaddr
import hostinfo.util as util
class AttributeDict(dict):
__getattr__ = dict.__getitem__
__setattr__ = dict.__setitem__
class StringArray(list):
def append_lines(self, lines, sep='|'):
for l in map(lambda s: s.strip(), lines.split('\n')):
if not l:
continue
if not l.startswith(sep):
raise Exception('%s do not start with "%s"' % (l, pattern))
self.append(l[len(sep):])
pass
def indent(self):
for l in self:
yield ' %s' % l
pass
pass
pass
def generate(tree, options):
result = StringArray()
interface = dict([ (h.name[0], [ i for i in h._interface_ ])
for h in tree._host_ if h._interface_ ])
host = [ h for h in tree._host_ if h.name[0] == options.host ][0]
with_dhcp = list([ i for i in host._interface_ if i._dhcpserver_ ])
if not with_dhcp:
raise Exception("%s is not a dhcpserver" % options.host)
with_ipv6 = [ i for i in with_dhcp if i._ipv6_ ]
if not with_ipv6:
return ""
result.append_lines("""
|ddns-update-style none;
|authoritative;
|allow leasequery;
""")
for i in with_ipv6:
result.append('shared-network "MAC(%s)" {' % i.ethernet[0])