dhcpd.py 11.1 KB
Newer Older
Anders Blomdell's avatar
Anders Blomdell committed
1
import sys
Anders Blomdell's avatar
Anders Blomdell committed
2
import os
Anders Blomdell's avatar
Anders Blomdell committed
3
from hostinfo.util import ntoa, aton
Anders Blomdell's avatar
Anders Blomdell committed
4
import hostinfo.util as util
Anders Blomdell's avatar
Anders Blomdell committed
5

Anders Blomdell's avatar
Anders Blomdell committed
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
"""
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;
Anders Blomdell's avatar
Anders Blomdell committed
34
      if option arch = 00:06 {
Anders Blomdell's avatar
Anders Blomdell committed
35
        filename "bootia32.efi";
Anders Blomdell's avatar
Anders Blomdell committed
36
      } else if option arch = 00:07 {
Anders Blomdell's avatar
Anders Blomdell committed
37
        filename "bootx64.efi";
Anders Blomdell's avatar
Anders Blomdell committed
38
39
40
41
      } else {
        filename "pxelinux.0";
      }
    }
Anders Blomdell's avatar
Anders Blomdell committed
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82

    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

Anders Blomdell's avatar
Anders Blomdell committed
83
class "pxeclient" {
Anders Blomdell's avatar
Anders Blomdell committed
84
  match if substring (option vendor-class-identifier, 0, 9) = "PXEClient";
Anders Blomdell's avatar
Anders Blomdell committed
85
  next-server %(next_server)s;
Anders Blomdell's avatar
Anders Blomdell committed
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
  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" {
  match if substring (option vendor-class-identifier, 0, 9) = "PXEClient";
  ignore booting;
}
Anders Blomdell's avatar
Anders Blomdell committed
101
"""
Anders Blomdell's avatar
Anders Blomdell committed
102

Anders Blomdell's avatar
Anders Blomdell committed
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
def MacOS_NETBOOT(dhcphost):
    if not os.path.exists('/local/macos'):
        return ''
    else:
        return """
class "AppleNBI-i386" {
    match if substring (option vendor-class-identifier, 0, 14) =
             "AAPLBSDPC/i386";
    option dhcp-parameter-request-list 1,3,17,43,60;
    if (option dhcp-message-type = 1) {
    	option vendor-class-identifier "AAPLBSDPC/i386";
    }
    if (option dhcp-message-type = 1) {
    	option vendor-encapsulated-options 08:04:81:00:00:67;
    }
    # The Apple Boot Loader binary image. This file will in turn TFTP
    # the kernel image and extension cache.
    filename "i386/boot.efi";
 
    option root-path "nfs:%s:/local/macos:NetBoot.dmg";
}
 
class "AppleNBI-ppc" {
    match if substring (option vendor-class-identifier, 0, 13) =
             "AAPLBSDPC/ppc";
    option dhcp-parameter-request-list 1,3,6,12,15,17,43,53,54,60;
    # The Apple Boot Loader binary image. This file will in turn TFTP
    # the kernel image and extension cache.
    filename "ppc/bootx.bootinfo";
    option vendor-class-identifier "AAPLBSDPC";
 
    if (option dhcp-message-type = 1) {
    	option vendor-encapsulated-options 08:04:81:00:00:09;
    } elsif (option dhcp-message-type = 8) {
    	option vendor-encapsulated-options 01:01:02:08:04:81:00:00:09;
    } else {
    	option vendor-encapsulated-options 00:01:02:03:04:05:06:07;
    }
    
    option root-path "nfs:%s:/local/macos:NetBoot.dmg";
}

""" % (dhcphost, dhcphost)
    
Anders Blomdell's avatar
Anders Blomdell committed
147
148
149
150
151
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
Anders Blomdell's avatar
Anders Blomdell committed
152

Anders Blomdell's avatar
Anders Blomdell committed
153
154
155
156
157
158
159
    result.append_lines("""
      |ddns-update-style none;
      |authoritative;
      |get-lease-hostnames true;
      |use-host-decl-names on;
    """)
    if get_pxeboot(dhcp) == 'only':
Anders Blomdell's avatar
Anders Blomdell committed
160
161
162
163
        if not options.next_server:
            raise util.HostinfoException('next_server not defined',
                                   where=dhcp)
        result += PXE_ALLOW % dict(next_server=options.next_server)
Anders Blomdell's avatar
Anders Blomdell committed
164
165
166
167
168
        pass
    else:
        result += PXE_DENY
        pass
    #result += MacOS_NETBOOT(dhcpip)
Anders Blomdell's avatar
Anders Blomdell committed
169

Anders Blomdell's avatar
Anders Blomdell committed
170
    interface = set(map(lambda d: d._parent._parent, dhcp))
Anders Blomdell's avatar
Anders Blomdell committed
171
    for i in interface:
Anders Blomdell's avatar
Anders Blomdell committed
172
        result += emit_interface(tree, options, i)
Anders Blomdell's avatar
Anders Blomdell committed
173
        pass
Anders Blomdell's avatar
Anders Blomdell committed
174
175
176
177
178
179
180
    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()
Anders Blomdell's avatar
Anders Blomdell committed
181
182
    subnet = dict(map(lambda d: (get_subnet(tree, d._parent), 
                                 map(None, d._parent._dhcpserver_)),
Anders Blomdell's avatar
Anders Blomdell committed
183
184
185
186
                      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()
Anders Blomdell's avatar
Anders Blomdell committed
187
        pass
Anders Blomdell's avatar
Anders Blomdell committed
188
    result += "}"
Anders Blomdell's avatar
Anders Blomdell committed
189
190
    return result

Anders Blomdell's avatar
Anders Blomdell committed
191
192
def emit_network(tree, options, subnet, dhcp):
    result = util.StringArray()
Anders Blomdell's avatar
Anders Blomdell committed
193
    net = util.network(subnet)
Anders Blomdell's avatar
Anders Blomdell committed
194
195
196
197
198
199
200
201
202
203
204
205
    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))
Anders Blomdell's avatar
Anders Blomdell committed
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
    for d in dhcp:
        if d.first[0] and d.last[0]:
            first = util.address(d.first[0])
            last = util.address(d.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
            elif pxeboot == 'only':
                result.append_lines("""
                  |  pool {
                  |    allow members of "pxeclient";
                  |    range %(first)s %(last)s;
                  |  }""" % dict(first=first, last=last))
                pass
Anders Blomdell's avatar
Anders Blomdell committed
222
223
            pass
        pass
Anders Blomdell's avatar
Anders Blomdell committed
224
225
226
227
228
229
    result += emit_subnet_info(subnet).indent()
    result += '}'
    return result

def emit_subnet_info(subnet):
    result = util.StringArray()
Anders Blomdell's avatar
Anders Blomdell committed
230
    if subnet.gateway[0]:
Anders Blomdell's avatar
Anders Blomdell committed
231
        result += "option routers %s;" % subnet.gateway[0]
Anders Blomdell's avatar
Anders Blomdell committed
232
        pass
Anders Blomdell's avatar
Anders Blomdell committed
233
    if subnet.domain[0]:
Anders Blomdell's avatar
Anders Blomdell committed
234
        result += "option domain-name \"%s\";" % subnet.domain[0]
Anders Blomdell's avatar
Anders Blomdell committed
235
        pass
Anders Blomdell's avatar
Anders Blomdell committed
236
    if subnet.name_servers[0]:
Anders Blomdell's avatar
Anders Blomdell committed
237
        result += "option domain-name-servers %s;" % subnet.name_servers[0]
Anders Blomdell's avatar
Anders Blomdell committed
238
        pass
Anders Blomdell's avatar
Anders Blomdell committed
239
    if subnet.ntp_servers[0]:
Anders Blomdell's avatar
Anders Blomdell committed
240
        result += "option ntp-servers %s;" % (subnet.ntp_servers[0])
Anders Blomdell's avatar
Anders Blomdell committed
241
        pass
Anders Blomdell's avatar
Anders Blomdell committed
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
    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)
Anders Blomdell's avatar
Anders Blomdell committed
271
272
            pass
        pass
Anders Blomdell's avatar
Anders Blomdell committed
273
274
275
276
277
278
279
280
281
    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])
Anders Blomdell's avatar
Anders Blomdell committed
282
            pass
Anders Blomdell's avatar
Anders Blomdell committed
283
284
285
        for name in sorted(result):
            ether, ip = result[name]
            yield name, ether, ip
Anders Blomdell's avatar
Anders Blomdell committed
286
287
            pass
        pass
Anders Blomdell's avatar
Anders Blomdell committed
288

Anders Blomdell's avatar
Anders Blomdell committed
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
    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)))))
Anders Blomdell's avatar
Anders Blomdell committed
308
            pass
Anders Blomdell's avatar
Anders Blomdell committed
309
        result += "}"
Anders Blomdell's avatar
Anders Blomdell committed
310
        pass
Anders Blomdell's avatar
Anders Blomdell committed
311
312
313
314
315
316
317
318
319
    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))
Anders Blomdell's avatar
Anders Blomdell committed
320
            pass
Anders Blomdell's avatar
Anders Blomdell committed
321
322
        result += "}"
        pass
Anders Blomdell's avatar
Anders Blomdell committed
323
    return result
Anders Blomdell's avatar
Anders Blomdell committed
324

Anders Blomdell's avatar
Anders Blomdell committed
325
326
327
328
329
330
331
332
333
334
335
336
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))
Anders Blomdell's avatar
Anders Blomdell committed
337
338
339
340
        if len(p) != 1:
            raise util.HostinfoException("Mixed pxeboot not allowed (%s)" % 
                                         ','.join(p),
                                         where=dhcp)
Anders Blomdell's avatar
Anders Blomdell committed
341
        result = p.pop()
Anders Blomdell's avatar
Anders Blomdell committed
342
        pass
Anders Blomdell's avatar
Anders Blomdell committed
343
344
345
346
347
348
    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