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