Setting tag pyplnet-4.3-20
[pyplnet.git] / plnet.py
1 #!/usr/bin/python /usr/bin/plcsh
2
3 import os
4 import socket
5 import time
6 import tempfile
7 import errno
8 import struct
9 import re
10
11 import sioc
12 import modprobe
13
14 global version
15 version = 4.3
16
17 def ovs_check(logger):
18     """ Return True if openvswitch is running, False otherwise. Try restarting
19         it once.
20     """
21     rc = os.system("service openvswitch status")
22     if rc == 0:
23         return True
24     logger.log("net: restarting openvswitch")
25     rc = os.system("service openvswitch restart")
26     rc = os.system("service openvswitch status")
27     if rc == 0:
28         return True
29     logger.log("net: failed to restart openvswitch")
30     return False
31
32 def InitInterfaces(logger, plc, data, root="", files_only=False, program="NodeManager"):
33     global version
34
35     sysconfig = "{}/etc/sysconfig/network-scripts".format(root)
36     try:
37         os.makedirs(sysconfig)
38     except OSError, e:
39         if e.errno != errno.EEXIST:
40             raise e
41
42     # query running network interfaces
43     devs = sioc.gifconf()
44     ips = dict(zip(devs.values(), devs.keys()))
45     macs = {}
46     for dev in devs:
47         macs[sioc.gifhwaddr(dev).lower()] = dev
48
49     devices_map = {}
50     device_id = 1
51     hostname = data.get('hostname',socket.gethostname())
52     gateway = None
53     # assume data['interfaces'] contains this node's Interfaces
54     # can cope with 4.3 ('networks') or 5.0 ('interfaces')
55     try:
56         interfaces = data['interfaces']
57     except:
58         interfaces = data['networks']
59     failedToGetSettings = False
60
61     # NOTE: GetInterfaces/NodeNetworks does not necessarily order the interfaces
62     # returned.  Because 'interface' is decremented as each interface is processed,
63     # by the time is_primary=True (primary) interface is reached, the device
64     # "eth<interface>" is not eth0.  But, something like eth-4, or eth-12.
65     # This code sorts the interfaces, placing is_primary=True interfaces first.  
66     # There is a lot of room for improvement to how this
67     # script handles interfaces and how it chooses the primary interface.
68     def compare_by (fieldname):
69         def compare_two_dicts (a, b):
70             return cmp(a[fieldname], b[fieldname])
71         return compare_two_dicts
72
73     # NOTE: by sorting on 'is_primary' and then reversing (since False is sorted
74     # before True) all 'is_primary' interfaces are at the beginning of the list.
75     interfaces.sort( compare_by('is_primary') )
76     interfaces.reverse()
77
78     # The names of the bridge devices
79     bridgeDevices = []
80
81     for interface in interfaces:
82         logger.verbose('net:InitInterfaces interface {}: {}'.format(device_id, interface))
83         logger.verbose('net:InitInterfaces macs = {}'.format(macs))
84         logger.verbose('net:InitInterfaces ips = {}'.format(ips))
85         # Get interface name preferably from MAC address, falling back
86         # on IP address.
87         hwaddr=interface['mac']
88         if hwaddr <> None: hwaddr=hwaddr.lower()
89         if hwaddr in macs:
90             orig_ifname = macs[hwaddr]
91         elif interface['ip'] in ips:
92             orig_ifname = ips[interface['ip']]
93         else:
94             orig_ifname = None
95
96         if orig_ifname:
97             logger.verbose('net:InitInterfaces orig_ifname = {}'.format(orig_ifname))
98
99         details = prepDetails(interface, hostname)
100
101         if interface['is_primary']:
102             gateway = interface['gateway']
103
104         if 'interface_tag_ids' in interface:
105             version = 4.3
106             interface_tag_ids = "interface_tag_ids"
107             interface_tag_id = "interface_tag_id"
108             name_key = "tagname"
109         else:
110             version = 4.2
111             interface_tag_ids = "nodenetwork_setting_ids"
112             interface_tag_id = "nodenetwork_setting_id"
113             name_key = "name"
114
115         if len(interface[interface_tag_ids]) > 0:
116             try:
117                 filter = { interface_tag_id : interface[interface_tag_ids] }
118                 if version == 4.3:
119                     settings = plc.GetInterfaceTags(filter)
120                 else:
121                     settings = plc.GetNodeNetworkSettings(filter)
122             except:
123                 logger.log("net:InitInterfaces FATAL: failed call GetInterfaceTags({})"
124                            .format(filter))
125                 failedToGetSettings = True
126                 continue # on to the next interface
127
128             for setting in settings:
129                 settingname = setting[name_key].upper()
130                 if ((settingname in ('IFNAME','ALIAS','CFGOPTIONS','DRIVER','VLAN','TYPE','DEVICETYPE')) or \
131                     (re.search('^IPADDR[0-9]+$|^NETMASK[0-9]+$', settingname))):
132                     # TD: Added match for secondary IPv4 configuration.
133                     details[settingname]=setting['value']
134                 # IPv6 support on IPv4 interface
135                 elif settingname in ('IPV6ADDR','IPV6_DEFAULTGW','IPV6ADDR_SECONDARIES', 'IPV6_AUTOCONF'):
136                     # TD: Added IPV6_AUTOCONF.
137                     details[settingname]=setting['value']
138                     details['IPV6INIT']='yes'
139                 # wireless settings
140                 elif settingname in \
141                         [  "MODE", "ESSID", "NW", "FREQ", "CHANNEL", "SENS", "RATE",
142                            "KEY", "KEY1", "KEY2", "KEY3", "KEY4", "SECURITYMODE", 
143                            "IWCONFIG", "IWPRIV" ] :
144                     details [settingname] = setting['value']
145                     details ['TYPE']='Wireless'
146                 # Bridge setting
147                 elif settingname in [ 'BRIDGE' ]:
148                     details['BRIDGE'] = setting['value']
149                 elif settingname in [ 'OVS_BRIDGE' ]:
150                     # If openvswitch isn't running, then we'll lose network
151                     # connectivity when we reconfigure eth0.
152                     if ovs_check(logger):
153                         details['OVS_BRIDGE'] = setting['value']
154                         details['TYPE'] = "OVSPort"
155                         details['DEVICETYPE'] = "ovs"
156                     else:
157                         logger.log("net:InitInterfaces ERROR: OVS_BRIDGE specified, yet ovs is not running")
158                 else:
159                     logger.log("net:InitInterfaces WARNING: ignored setting named {}"
160                                .format(setting[name_key]))
161
162         # support aliases to interfaces either by name or HWADDR
163         if 'ALIAS' in details:
164             if 'HWADDR' in details:
165                 hwaddr = details['HWADDR'].lower()
166                 del details['HWADDR']
167                 if hwaddr in macs:
168                     hwifname = macs[hwaddr]
169                     if ('IFNAME' in details) and details['IFNAME'] <> hwifname:
170                         logger.log("net:InitInterfaces WARNING: alias ifname ({}) and hwaddr ifname ({}) do not match"
171                                    .format(details['IFNAME'], hwifname))
172                         details['IFNAME'] = hwifname
173                 else:
174                     logger.log('net:InitInterfaces WARNING: mac addr {} for alias not found'.format(hwaddr))
175
176             if 'IFNAME' in details:
177                 # stupid RH /etc/sysconfig/network-scripts/ifup-aliases:new_interface()
178                 # checks if the "$DEVNUM" only consists of '^[0-9A-Za-z_]*$'. Need to make
179                 # our aliases compliant.
180                 parts = details['ALIAS'].split('_')
181                 isValid=True
182                 for part in parts:
183                     isValid=isValid and part.isalnum()
184
185                 if isValid:
186                     devices_map["{}:{}".format(details['IFNAME'], details['ALIAS'])] = details 
187                 else:
188                     logger.log("net:InitInterfaces WARNING: interface alias ({}) not a valid string for RH ifup-aliases"
189                                .format(details['ALIAS']))
190             else:
191                 logger.log("net:InitInterfaces WARNING: interface alias ({}) not matched to an interface"
192                            .format(details['ALIAS']))
193             device_id -= 1
194         elif ('BRIDGE' in details or 'OVS_BRIDGE' in details) and 'IFNAME' in details:
195             # The bridge inherits the mac of the first attached interface.
196             ifname = details['IFNAME']
197             device_id -= 1
198             if 'BRIDGE' in details:
199                 bridgeName = details['BRIDGE']
200                 bridgeType = 'Bridge'
201             else:
202                 bridgeName = details['OVS_BRIDGE']
203                 bridgeType = 'OVSBridge'
204
205             logger.log('net:InitInterfaces: {} detected. Adding {} to devices_map'
206                        .format(bridgeType, ifname))
207             devices_map[ifname] = removeBridgedIfaceDetails(details)
208
209             logger.log('net:InitInterfaces: Adding {} {}'.format(bridgeType, bridgeName))
210             bridgeDetails = prepDetails(interface)
211             
212             # TD: Add configuration for secondary IPv4 and IPv6 addresses to the bridge.
213             if len(interface[interface_tag_ids]) > 0:
214                 filter = { interface_tag_id : interface[interface_tag_ids] }
215                 try:
216                     if version == 4.3:
217                         settings = plc.GetInterfaceTags(filter)
218                     else:
219                         settings = plc.GetNodeNetworkSettings(filter)
220                 except:
221                     logger.log("net:InitInterfaces FATAL: failed call GetInterfaceTags({})"
222                                .format(filter))
223                     failedToGetSettings = True
224                     continue # on to the next interface
225
226                 for setting in settings:
227                     settingname = setting[name_key].upper()
228                     if (re.search('^IPADDR[0-9]+$|^NETMASK[0-9]+$', settingname)):
229                         # TD: Added match for secondary IPv4 configuration.
230                         bridgeDetails[settingname]=setting['value']
231                     # IPv6 support on IPv4 interface
232                     elif settingname in ('IPV6ADDR','IPV6_DEFAULTGW','IPV6ADDR_SECONDARIES', 'IPV6_AUTOCONF'):
233                         # TD: Added IPV6_AUTOCONF.
234                         bridgeDetails[settingname]=setting['value']
235                         bridgeDetails['IPV6INIT']='yes'
236
237             bridgeDevices.append(bridgeName)
238             bridgeDetails['TYPE'] = bridgeType
239             if bridgeType == 'OVSBridge':
240                 bridgeDetails['DEVICETYPE'] = 'ovs'
241                 if bridgeDetails['BOOTPROTO'] == 'dhcp':
242                     del bridgeDetails['BOOTPROTO']
243                     bridgeDetails['OVSBOOTPROTO'] = 'dhcp'
244                     bridgeDetails['OVSDHCPINTERFACES'] = ifname
245             devices_map[bridgeName] = bridgeDetails
246         else:
247             if 'IFNAME' in details:
248                 ifname = details['IFNAME']
249                 device_id -= 1
250             elif orig_ifname:
251                 ifname = orig_ifname
252                 device_id -= 1
253             else:
254                 while True:
255                     ifname="eth{}".format(device_id - 1)
256                     if ifname not in devices_map:
257                         break
258                     device_id += 1
259                 if os.path.exists("{}/ifcfg-{}".format(sysconfig, ifname)):
260                     logger.log("net:InitInterfaces WARNING: possibly blowing away {} configuration"
261                                .format(ifname))
262             devices_map[ifname] = details
263         device_id += 1 
264     logger.log('net:InitInterfaces: Device map: {}'.format(devices_map))
265     m = modprobe.Modprobe()
266     try:
267         m.input("{}/etc/modprobe.conf".format(root))
268     except:
269         pass
270     for (dev, details) in devices_map.iteritems():
271         # get the driver string "moduleName option1=a option2=b"
272         driver=details.get('DRIVER','')
273         if driver <> '':
274             driver=driver.split()
275             kernelmodule=driver[0]
276             m.aliasset(dev,kernelmodule)
277             options=" ".join(driver[1:])
278             if options <> '':
279                 m.optionsset(dev,options)
280     m.output("{}/etc/modprobe.conf".format(root), program)
281
282     # clean up after any ifcfg-$dev script that's no longer listed as
283     # part of the Interfaces associated with this node
284
285     # list all network-scripts
286     files = os.listdir(sysconfig)
287
288     # filter out the ifcfg-* files
289     ifcfgs=[]
290     for f in files:
291         if f.find("ifcfg-") == 0:
292             ifcfgs.append(f)
293
294     # remove loopback (lo) from ifcfgs list
295     lo = "ifcfg-lo"
296     if lo in ifcfgs: ifcfgs.remove(lo)
297
298     # remove known devices from ifcfgs list
299     for (dev, details) in devices_map.iteritems():
300         ifcfg = 'ifcfg-'+dev
301         if ifcfg in ifcfgs: ifcfgs.remove(ifcfg)
302
303     # delete the remaining ifcfgs from 
304     deletedSomething = False
305
306     if not failedToGetSettings:
307         for ifcfg in ifcfgs:
308             dev = ifcfg[len('ifcfg-'):]
309             path = "{}/ifcfg-{}".format(sysconfig, dev)
310             if not files_only:
311                 logger.verbose("net:InitInterfaces removing {} {}".format(dev, path))
312                 os.system("/sbin/ifdown {}".format(dev))
313             deletedSomething=True
314             os.unlink(path)
315
316     # wait a bit for the one or more ifdowns to have taken effect
317     if deletedSomething:
318         time.sleep(2)
319
320     # Write network configuration file
321     with open("{}/etc/sysconfig/network".format(root), "w") as networkconf:
322         networkconf.write("NETWORKING=yes\nHOSTNAME={}\n".format(hostname))
323         if gateway is not None:
324             networkconf.write("GATEWAY={}\n".format(gateway))
325
326     # Process ifcfg-$dev changes / additions
327     newdevs = []
328     table = 10
329     for (dev, details) in devices_map.iteritems():
330         (fd, tmpnam) = tempfile.mkstemp(dir=sysconfig)
331         f = os.fdopen(fd, "w")
332         f.write("# Autogenerated by pyplnet... do not edit!\n")
333         if 'DRIVER' in details:
334             f.write("# using {} driver for device {}\n".format(details['DRIVER'], dev))
335         f.write('DEVICE={}\n'.format(dev))
336         
337         # print the configuration values
338         for (key, val) in details.iteritems():
339             if key not in ('IFNAME','ALIAS','CFGOPTIONS','DRIVER','GATEWAY'):
340                 f.write('{}="{}"\n'.format(key, val))
341
342         # print the configuration specific option values (if any)
343         if 'CFGOPTIONS' in details:
344             cfgoptions = details['CFGOPTIONS']
345             f.write('#CFGOPTIONS are {}\n'.format(cfgoptions))
346             for cfgoption in cfgoptions.split():
347                 key,val = cfgoption.split('=')
348                 key=key.strip()
349                 key=key.upper()
350                 val=val.strip()
351                 f.write('{}="{}"\n'.format(key, val))
352         f.close()
353
354         # compare whether two files are the same
355         def comparefiles(a,b):
356             try:
357                 logger.verbose("net:InitInterfaces comparing {} with {}".format(a, b))
358                 if not os.path.exists(a) or not os.path.exists(b):
359                     return False
360                 with open(a) as fb:
361                     buf_a = fb.read()
362
363                 with open(b) as fb:
364                     buf_b = fb.read()
365
366                 return buf_a == buf_b
367             except IOError, e:
368                 return False
369
370         src_route_changed = False
371         if ('PRIMARY' not in details and 'GATEWAY' in details and
372             details['GATEWAY'] != ''):
373             table += 1
374             (fd, rule_tmpnam) = tempfile.mkstemp(dir=sysconfig)
375             os.write(fd, "from {} lookup {}\n".format(details['IPADDR'], table))
376             os.close(fd)
377             rule_dest = "{}/rule-{}".format(sysconfig, dev)
378             if not comparefiles(rule_tmpnam, rule_dest):
379                 os.rename(rule_tmpnam, rule_dest)
380                 os.chmod(rule_dest, 0644)
381                 src_route_changed = True
382             else:
383                 os.unlink(rule_tmpnam)
384             (fd, route_tmpnam) = tempfile.mkstemp(dir=sysconfig)
385             netmask = struct.unpack("I", socket.inet_aton(details['NETMASK']))[0]
386             ip = struct.unpack("I", socket.inet_aton(details['IPADDR']))[0]
387             network = socket.inet_ntoa(struct.pack("I", (ip & netmask)))
388             netmask = socket.ntohl(netmask)
389             i = 0
390             while (netmask & (1 << i)) == 0:
391                 i += 1
392             prefix = 32 - i
393             os.write(fd, "{}/{} dev {} table {}\n".format(network, prefix, dev, table))
394             os.write(fd, "default via {} dev {} table {}\n".format(details['GATEWAY'], dev, table))
395             os.close(fd)
396             route_dest = "{}/route-{}".format(sysconfig, dev)
397             if not comparefiles(route_tmpnam, route_dest):
398                 os.rename(route_tmpnam, route_dest)
399                 os.chmod(route_dest, 0644)
400                 src_route_changed = True
401             else:
402                 os.unlink(route_tmpnam)
403
404         path = "{}/ifcfg-{}".format(sysconfig,dev)
405         if not os.path.exists(path):
406             logger.verbose('net:InitInterfaces adding configuration for {}'.format(dev))
407             # add ifcfg-$dev configuration file
408             os.rename(tmpnam,path)
409             os.chmod(path,0644)
410             newdevs.append(dev)
411             
412         elif not comparefiles(tmpnam,path) or src_route_changed:
413             logger.verbose('net:InitInterfaces Configuration change for {}'.format(dev))
414             if not files_only:
415                 logger.verbose('net:InitInterfaces ifdown {}'.format(dev))
416                 # invoke ifdown for the old configuration
417                 os.system("/sbin/ifdown {}".format(dev))
418                 # wait a few secs for ifdown to complete
419                 time.sleep(2)
420
421             logger.log('replacing configuration for {}'.format(dev))
422             # replace ifcfg-$dev configuration file
423             os.rename(tmpnam,path)
424             os.chmod(path,0644)
425             newdevs.append(dev)
426         else:
427             # tmpnam & path are identical
428             os.unlink(tmpnam)
429
430     for dev in newdevs:
431         cfgvariables = {}
432         with file("{}/ifcfg-{}".format(sysconfig, dev), "r") as fb:
433             for line in fb.readlines():
434                 parts = line.split()
435                 if parts[0][0]=="#":continue
436                 if parts[0].find('='):
437                     name,value = parts[0].split('=')
438                     # clean up name & value
439                     name = name.strip()
440                     value = value.strip()
441                     value = value.strip("'")
442                     value = value.strip('"')
443                     cfgvariables[name]=value
444
445         def getvar(name):
446             if name in cfgvariables:
447                 value=cfgvariables[name]
448                 value = value.lower()
449                 return value
450             return ''
451
452         # skip over device configs with ONBOOT=no
453         if getvar("ONBOOT") == 'no': continue
454
455         # don't bring up slave devices, the network scripts will
456         # handle those correctly
457         if getvar("SLAVE") == 'yes': continue
458
459         # Delay bringing up any bridge devices
460         if dev in bridgeDevices: continue
461
462         if not files_only:
463             logger.verbose('net:InitInterfaces bringing up {}'.format(dev))
464             os.system("/sbin/ifup {}".format(dev))
465
466     # Bring up the bridge devices
467     for bridge in bridgeDevices:
468         if not files_only and bridge in newdevs:
469             logger.verbose('net:InitInterfaces bringing up bridge {}'.format(bridge))
470             os.system("/sbin/ifup {}".format(bridge))
471
472 ##
473 # Prepare the interface details.
474 #
475 def prepDetails(interface, hostname=''):
476     details = {}
477     details['ONBOOT']  = 'yes'
478     details['USERCTL'] = 'no'
479     # starting with f27, it's OK to use NetworkManager
480     # attempt to work around issues seen starting with f23
481     # details['NM_CONTROLLED'] = 'no'
482     if interface['mac']:
483         details['HWADDR'] = interface['mac']
484     if interface['is_primary']:
485         details['PRIMARY'] = 'yes'
486
487     if interface['method'] == "static":
488         details['BOOTPROTO'] = "static"
489         details['IPADDR']    = interface['ip']
490         details['NETMASK']   = interface['netmask']
491         details['GATEWAY']   = interface['gateway']
492         if interface['is_primary']:
493             if interface['dns1']:
494                 details['DNS1'] = interface['dns1']
495             if interface['dns2']:
496                 details['DNS2'] = interface['dns2']
497
498     elif interface['method'] == "dhcp":
499         details['BOOTPROTO'] = "dhcp"
500         details['PERSISTENT_DHCLIENT'] = "yes"
501         if interface['hostname']:
502             details['DHCP_HOSTNAME'] = interface['hostname']
503         else:
504             details['DHCP_HOSTNAME'] = hostname
505         if not interface['is_primary']:
506             details['DHCLIENTARGS'] = "-R subnet-mask"
507
508     return details
509
510 ##
511 # Remove duplicate entry from the bridged interface's configuration file.
512 #
513 def removeBridgedIfaceDetails(details):
514     # TD: Also added secondary IPv4 keys and IPv6 keys to the keys to be removed.
515     allKeys = [ 'PRIMARY', 'PERSISTENT_DHCLIENT', 'DHCLIENTARGS', 'DHCP_HOSTNAME',
516                 'BOOTPROTO', 'IPADDR', 'NETMASK', 'GATEWAY', 'DNS1', 'DNS2',
517                 'IPV6ADDR', 'IPV6_DEFAULTGW', 'IPV6ADDR_SECONDARIES',
518                 'IPV6_AUTOCONF', 'IPV6INIT' ]
519     for i in range(1, 256):
520        allKeys.append('IPADDR' + str(i))
521        allKeys.append('NETMASK' + str(i))
522
523     for key in allKeys:
524         if key in details:
525             del details[key]
526
527     # TD: Also turn off IPv6
528     details['IPV6INIT']      = 'no'
529     details['IPV6_AUTOCONF'] = 'no'
530     
531     return details
532
533 if __name__ == "__main__":
534     import optparse
535     import sys
536
537     parser = optparse.OptionParser(usage="plnet [-v] [-f] [-p <program>] -r root node_id")
538     parser.add_option("-v", "--verbose", action="store_true", dest="verbose")
539     parser.add_option("-r", "--root", action="store", type="string",
540                       dest="root", default=None)
541     parser.add_option("-f", "--files-only", action="store_true",
542                       dest="files_only")
543     parser.add_option("-p", "--program", action="store", type="string",
544                       dest="program", default="plnet")
545     (options, args) = parser.parse_args()
546     if len(args) != 1 or options.root is None:
547         print sys.argv
548         print >>sys.stderr, "Missing root or node_id"
549         parser.print_help()
550         sys.exit(1)
551
552     node = shell.GetNodes({'node_id': [int(args[0])]})
553     try:
554         interfaces = shell.GetInterfaces({'interface_id': node[0]['interface_ids']})
555     except AttributeError:
556         interfaces = shell.GetNodeNetworks({'nodenetwork_id':node[0]['nodenetwork_ids']})
557         version = 4.2
558
559
560     data = {'hostname': node[0]['hostname'], 'interfaces': interfaces}
561     class logger:
562         def __init__(self, verbose):
563             self.verbosity = verbose
564         def log(self, msg, loglevel=2):
565             if self.verbosity:
566                 print msg
567         def verbose(self, msg):
568             self.log(msg, 1)
569     l = logger(options.verbose)
570     InitInterfaces(l, shell, data, options.root, options.files_only)