#!/usr/bin/env python # # Copyright (c) 2008,2009 Citrix Systems, Inc. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published # by the Free Software Foundation; version 2.1 only. with the special # exception on linking described in file LICENSE. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # """Usage: %(command-name)s up %(command-name)s down %(command-name)s rewrite %(command-name)s --force up %(command-name)s --force down %(command-name)s --force rewrite --device= --mac= where is one of: --session --pif --pif-uuid and is one of: --mode=dhcp --mode=static --ip= --netmask= [--gateway=] Options: --session A session reference to use to access the xapi DB --pif A PIF reference within the session. --pif-uuid The UUID of a PIF. --force An interface name. --root-prefix=DIR Use DIR as alternate root directory (for testing). --no-syslog Write log messages to stderr instead of system log. """ # Notes: # 1. Every pif belongs to exactly one network # 2. Every network has zero or one pifs # 3. A network may have an associated bridge, allowing vifs to be attached # 4. A network may be bridgeless (there's no point having a bridge over a storage pif) from InterfaceReconfigure import * import os, sys, getopt import syslog import traceback import re import random import syslog management_pif = None dbcache_file = "/var/xapi/network.dbcache" # # Logging. # def log_pif_action(action, pif): pifrec = db().get_pif_record(pif) rec = {} rec['uuid'] = pifrec['uuid'] rec['ip_configuration_mode'] = pifrec['ip_configuration_mode'] rec['action'] = action rec['pif_netdev_name'] = pif_netdev_name(pif) rec['message'] = "Bring %(action)s PIF %(uuid)s" % rec log("%(message)s: %(pif_netdev_name)s configured as %(ip_configuration_mode)s" % rec) # # Exceptions. # class Usage(Exception): def __init__(self, msg): Exception.__init__(self) self.msg = msg # # Boot from Network filesystem or device. # def check_allowed(pif): """Determine whether interface-reconfigure should be manipulating this PIF. Used to prevent system PIFs (such as network root disk) from being interfered with. """ pifrec = db().get_pif_record(pif) try: f = open(root_prefix() + "/proc/ardence") macline = filter(lambda x: x.startswith("HWaddr:"), f.readlines()) f.close() if len(macline) == 1: p = re.compile(".*\s%(MAC)s\s.*" % pifrec, re.IGNORECASE) if p.match(macline[0]): log("Skipping PVS device %(device)s (%(MAC)s)" % pifrec) return False except IOError: pass return True # # Bare Network Devices -- network devices without IP configuration # def netdev_remap_name(pif, already_renamed=[]): """Check whether 'pif' exists and has the correct MAC. If not, try to find a device with the correct MAC and rename it. 'already_renamed' is used to avoid infinite recursion. """ def read1(name): file = None try: file = open(name, 'r') return file.readline().rstrip('\n') finally: if file != None: file.close() def get_netdev_mac(device): try: return read1("%s/sys/class/net/%s/address" % (root_prefix(), device)) except: # Probably no such device. return None def get_netdev_tx_queue_len(device): try: return int(read1("%s/sys/class/net/%s/tx_queue_len" % (root_prefix(), device))) except: # Probably no such device. return None def get_netdev_by_mac(mac): for device in os.listdir(root_prefix() + "/sys/class/net"): dev_mac = get_netdev_mac(device) if (dev_mac and mac.lower() == dev_mac.lower() and get_netdev_tx_queue_len(device)): return device return None def rename_netdev(old_name, new_name): raise Error("Trying to rename %s to %s - This functionality has been removed" % (old_name, new_name)) # log("Changing the name of %s to %s" % (old_name, new_name)) # run_command(['/sbin/ifconfig', old_name, 'down']) # if not run_command(['/sbin/ip', 'link', 'set', old_name, 'name', new_name]): # raise Error("Could not rename %s to %s" % (old_name, new_name)) pifrec = db().get_pif_record(pif) device = pifrec['device'] mac = pifrec['MAC'] # Is there a network device named 'device' at all? device_exists = netdev_exists(device) if device_exists: # Yes. Does it have MAC 'mac'? found_mac = get_netdev_mac(device) if found_mac and mac.lower() == found_mac.lower(): # Yes, everything checks out the way we want. Nothing to do. return else: log("No network device %s" % device) # What device has MAC 'mac'? cur_device = get_netdev_by_mac(mac) if not cur_device: log("No network device has MAC %s" % mac) return # First rename 'device', if it exists, to get it out of the way # for 'cur_device' to replace it. if device_exists: rename_netdev(device, "dev%d" % random.getrandbits(24)) # Rename 'cur_device' to 'device'. rename_netdev(cur_device, device) # # IP Network Devices -- network devices with IP configuration # def ifdown(netdev): """Bring down a network interface""" if not netdev_exists(netdev): log("ifdown: device %s does not exist, ignoring" % netdev) return if not os.path.exists("%s/etc/sysconfig/network-scripts/ifcfg-%s" % (root_prefix(), netdev)): log("ifdown: device %s exists but ifcfg-%s does not" % (netdev,netdev)) run_command(["/sbin/ifconfig", netdev, 'down']) return run_command(["/sbin/ifdown", netdev]) def ifup(netdev): """Bring up a network interface""" if not os.path.exists(root_prefix() + "/etc/sysconfig/network-scripts/ifcfg-%s" % netdev): raise Error("ifup: device %s exists but ifcfg-%s does not" % (netdev,netdev)) d = os.getenv("DHCLIENTARGS","") if os.path.exists("/etc/firstboot.d/data/firstboot_in_progress"): os.putenv("DHCLIENTARGS", d + " -T 240 " ) run_command(["/sbin/ifup", netdev]) os.putenv("DHCLIENTARGS", d ) # # # def pif_rename_physical_devices(pif): if pif_is_tunnel(pif): return if pif_is_vlan(pif): pif = pif_get_vlan_slave(pif) if pif_is_bond(pif): pifs = pif_get_bond_slaves(pif) else: pifs = [pif] for pif in pifs: netdev_remap_name(pif) # # IP device configuration # def ipdev_configure_static_routes(interface, oc, f): """Open a route- file for static routes. Opens the static routes configuration file for interface and writes one line for each route specified in the network's other config "static-routes" value. E.g. if interface ( RO): xenbr1 other-config (MRW): static-routes: 172.16.0.0/15/192.168.0.3,172.18.0.0/16/192.168.0.4;... Then route-xenbr1 should be 172.16.0.0/15 via 192.168.0.3 dev xenbr1 172.18.0.0/16 via 192.168.0.4 dev xenbr1 """ if 'static-routes' in oc: # The key is present - extract comma separates entries lines = oc['static-routes'].split(',') else: # The key is not present, i.e. there are no static routes lines = [] child = ConfigurationFile("%s/etc/sysconfig/network-scripts/route-%s" % (root_prefix(), interface)) child.write("# DO NOT EDIT: This file (%s) was autogenerated by %s\n" % \ (os.path.basename(child.path()), os.path.basename(sys.argv[0]))) try: for l in lines: network, masklen, gateway = l.split('/') child.write("%s/%s via %s dev %s\n" % (network, masklen, gateway, interface)) f.attach_child(child) child.close() except ValueError as e: log("Error in other-config['static-routes'] format for network %s: %s" % (interface, e)) def ipdev_open_ifcfg(pif): ipdev = pif_ipdev_name(pif) log("Writing network configuration for %s" % ipdev) f = ConfigurationFile("%s/etc/sysconfig/network-scripts/ifcfg-%s" % (root_prefix(), ipdev)) f.write("# DO NOT EDIT: This file (%s) was autogenerated by %s\n" % \ (os.path.basename(f.path()), os.path.basename(sys.argv[0]))) f.write("XEMANAGED=yes\n") f.write("DEVICE=%s\n" % ipdev) f.write("ONBOOT=no\n") f.write("NOZEROCONF=yes\n") return f def ipdev_configure_network(pif, dp): """Write the configuration file for a network. Writes configuration derived from the network object into the relevant ifcfg file. The configuration file is passed in, but if the network is bridgeless it will be ifcfg-, otherwise it will be ifcfg-. This routine may also write ifcfg files of the networks corresponding to other PIFs in order to maintain consistency. params: pif: Opaque_ref of pif dp: Datapath object """ pifrec = db().get_pif_record(pif) nw = pifrec['network'] nwrec = db().get_network_record(nw) ipdev = pif_ipdev_name(pif) f = ipdev_open_ifcfg(pif) mode = pifrec['ip_configuration_mode'] log("Configuring %s using %s configuration" % (ipdev, mode)) oc = None if 'other_config' in pifrec: oc = pifrec['other_config'] dp.configure_ipdev(f) if pifrec['ip_configuration_mode'] == "DHCP": f.write("BOOTPROTO=dhcp\n") f.write("PERSISTENT_DHCLIENT=yes\n") elif pifrec['ip_configuration_mode'] == "Static": f.write("BOOTPROTO=none\n") f.write("NETMASK=%(netmask)s\n" % pifrec) f.write("IPADDR=%(IP)s\n" % pifrec) f.write("GATEWAY=%(gateway)s\n" % pifrec) elif pifrec['ip_configuration_mode'] == "None": f.write("BOOTPROTO=none\n") else: raise Error("Unknown ip-configuration-mode %s" % pifrec['ip_configuration_mode']) if 'other_config' in nwrec: settings,offload = ethtool_settings(nwrec['other_config']) if len(settings): f.write("ETHTOOL_OPTS=\"%s\"\n" % str.join(" ", settings)) if len(offload): f.write("ETHTOOL_OFFLOAD_OPTS=\"%s\"\n" % str.join(" ", offload)) ipdev_configure_static_routes(ipdev, nwrec['other_config'], f) mtu = mtu_setting(nw, "Network", nwrec['other_config']) if mtu: f.write("MTU=%s\n" % mtu) if 'DNS' in pifrec and pifrec['DNS'] != "": ServerList = pifrec['DNS'].split(",") for i in range(len(ServerList)): f.write("DNS%d=%s\n" % (i+1, ServerList[i])) if oc and 'domain' in oc: f.write("DOMAIN='%s'\n" % oc['domain'].replace(',', ' ')) # There can be only one DNSDEV and one GATEWAYDEV in /etc/sysconfig/network. # # The peerdns pif will be the one with # pif::other-config:peerdns=true, or the mgmt pif if none have # this set. # # The gateway pif will be the one with # pif::other-config:defaultroute=true, or the mgmt pif if none # have this set. # Work out which pif on this host should be the DNSDEV and which # should be the GATEWAYDEV # # Note: we prune out the bond master pif (if it exists). This is # because when we are called to bring up an interface with a bond # master, it is implicit that we should bring down that master. pifs_on_host = [p for p in db().get_all_pifs() if not p in pif_get_bond_masters(pif)] # now prune out bond slaves as they are not connected to the IP # stack and so cannot be used as gateway or DNS devices. pifs_on_host = [ p for p in pifs_on_host if len(pif_get_bond_masters(p)) == 0] # loop through all the pifs on this host looking for one with # other-config:peerdns = true, and one with # other-config:default-route=true peerdns_pif = None defaultroute_pif = None for __pif in pifs_on_host: __pifrec = db().get_pif_record(__pif) __oc = __pifrec['other_config'] if 'peerdns' in __oc and __oc['peerdns'] == 'true': if peerdns_pif == None: peerdns_pif = __pif else: log('Warning: multiple pifs with "peerdns=true" - choosing %s and ignoring %s' % \ (db().get_pif_record(peerdns_pif)['device'], __pifrec['device'])) if 'defaultroute' in __oc and __oc['defaultroute'] == 'true': if defaultroute_pif == None: defaultroute_pif = __pif else: log('Warning: multiple pifs with "defaultroute=true" - choosing %s and ignoring %s' % \ (db().get_pif_record(defaultroute_pif)['device'], __pifrec['device'])) # If no pif is explicitly specified then use the mgmt pif for # peerdns/defaultroute. if peerdns_pif == None: peerdns_pif = management_pif if defaultroute_pif == None: defaultroute_pif = management_pif is_dnsdev = peerdns_pif == pif is_gatewaydev = defaultroute_pif == pif if is_dnsdev or is_gatewaydev: fnetwork = ConfigurationFile(root_prefix() + "/etc/sysconfig/network") for line in fnetwork.readlines(): if is_dnsdev and line.lstrip().startswith('DNSDEV='): fnetwork.write('DNSDEV=%s\n' % ipdev) is_dnsdev = False elif is_gatewaydev and line.lstrip().startswith('GATEWAYDEV='): fnetwork.write('GATEWAYDEV=%s\n' % ipdev) is_gatewaydev = False else: fnetwork.write(line) if is_dnsdev: fnetwork.write('DNSDEV=%s\n' % ipdev) if is_gatewaydev: fnetwork.write('GATEWAYDEV=%s\n' % ipdev) fnetwork.close() f.attach_child(fnetwork) return f # # Toplevel actions # def action_up(pif, force): pifrec = db().get_pif_record(pif) ipdev = pif_ipdev_name(pif) dp = DatapathFactory()(pif) log("action_up: %s" % ipdev) f = ipdev_configure_network(pif, dp) dp.preconfigure(f) f.close() pif_rename_physical_devices(pif) # if we are not forcing the interface up then attempt to tear down # any existing devices which might interfere with brinign this one # up. if not force: ifdown(ipdev) dp.bring_down_existing() try: f.apply() dp.configure() ifup(ipdev) dp.post() # Update /etc/issue (which contains the IP address of the management interface) os.system(root_prefix() + "/sbin/update-issue") f.commit() except Error as e: log("failed to apply changes: %s" % e.msg) f.revert() raise def action_down(pif): ipdev = pif_ipdev_name(pif) dp = DatapathFactory()(pif) log("action_down: %s" % ipdev) ifdown(ipdev) dp.bring_down() def action_rewrite(): DatapathFactory().rewrite() # This is useful for reconfiguring the mgmt interface after having lost connectivity to the pool master def action_force_rewrite(bridge, config): def getUUID(): import subprocess uuid,_ = subprocess.Popen(['uuidgen'], stdout = subprocess.PIPE).communicate() return uuid.strip() # Notes: # 1. that this assumes the interface is bridged # 2. If --gateway is given it will make that the default gateway for the host # extract the configuration try: mode = config['mode'] mac = config['mac'] interface = config['device'] except: raise Usage("Please supply --mode, --mac and --device") if mode == 'static': try: netmask = config['netmask'] ip = config['ip'] except: raise Usage("Please supply --netmask and --ip") try: gateway = config['gateway'] except: gateway = None elif mode != 'dhcp': raise Usage("--mode must be either static or dhcp") if 'vlan' in config: is_vlan = True vlan_slave, vlan_vid = config['vlan'].split('.') else: is_vlan = False if is_vlan: raise Error("Force rewrite of VLAN not implemented") log("Configuring %s using %s configuration" % (bridge, mode)) f = ConfigurationFile(root_prefix() + dbcache_file) pif_uuid = getUUID() network_uuid = getUUID() f.write('\n') f.write('\n') f.write('\t\n' % pif_uuid) f.write('\t\tOpaqueRef:%s\n' % network_uuid) f.write('\t\tTrue\n') f.write('\t\t%sPif\n' % interface) f.write('\t\tOpaqueRef:NULL\n') f.write('\t\t\n') f.write('\t\t\n') f.write('\t\tOpaqueRef:NULL\n') f.write('\t\t-1\n') f.write('\t\t\n') f.write('\t\t\n') f.write('\t\t%s\n' % interface) f.write('\t\t%s\n' % mac) f.write('\t\t\n') if mode == 'dhcp': f.write('\t\tDHCP\n') f.write('\t\t\n') f.write('\t\t\n') f.write('\t\t\n') f.write('\t\t\n') elif mode == 'static': f.write('\t\tStatic\n') f.write('\t\t%s\n' % ip) f.write('\t\t%s\n' % netmask) if gateway is not None: f.write('\t\t%s\n' % gateway) f.write('\t\t\n') else: raise Error("Unknown mode %s" % mode) f.write('\t\n') f.write('\t\n' % network_uuid) f.write('\t\tInitialManagementNetwork\n') f.write('\t\t\n') f.write('\t\t\tOpaqueRef:%s\n' % pif_uuid) f.write('\t\t\n') f.write('\t\t%s\n' % bridge) f.write('\t\t\n') f.write('\t\n') f.write('\n') f.close() try: f.apply() f.commit() except Error as e: log("failed to apply changes: %s" % e.msg) f.revert() raise def main(argv=None): global management_pif session = None pif_uuid = None pif = None force_interface = None force_management = False if argv is None: argv = sys.argv try: try: shortops = "h" longops = [ "pif=", "pif-uuid=", "session=", "force=", "force-interface=", "management", "mac=", "device=", "mode=", "ip=", "netmask=", "gateway=", "root-prefix=", "no-syslog", "help" ] arglist, args = getopt.gnu_getopt(argv[1:], shortops, longops) except getopt.GetoptError as msg: raise Usage(msg) force_rewrite_config = {} for o,a in arglist: if o == "--pif": pif = a elif o == "--pif-uuid": pif_uuid = a elif o == "--session": session = a elif o == "--force-interface" or o == "--force": force_interface = a elif o == "--management": force_management = True elif o in ["--mac", "--device", "--mode", "--ip", "--netmask", "--gateway"]: force_rewrite_config[o[2:]] = a elif o == "--root-prefix": set_root_prefix(a) elif o == "--no-syslog": set_log_destination("stderr") elif o == "-h" or o == "--help": print(__doc__ % {'command-name': os.path.basename(argv[0])}) return 0 if get_log_destination() == "syslog": syslog.openlog(os.path.basename(argv[0])) log("Called as " + str.join(" ", argv)) if len(args) < 1: raise Usage("Required option not present") if len(args) > 1: raise Usage("Too many arguments") action = args[0] if not action in ["up", "down", "rewrite", "rewrite-configuration"]: raise Usage("Unknown action \"%s\"" % action) # backwards compatibility if action == "rewrite-configuration": action = "rewrite" if ( session or pif ) and pif_uuid: raise Usage("--session/--pif and --pif-uuid are mutually exclusive.") if ( session and not pif ) or ( not session and pif ): raise Usage("--session and --pif must be used together.") if force_interface and ( session or pif or pif_uuid ): raise Usage("--force is mutually exclusive with --session, --pif and --pif-uuid") if len(force_rewrite_config) and not (force_interface and action == "rewrite"): raise Usage("\"--force rewrite\" needed for --device, --mode, --ip, --netmask, and --gateway") if (action == "rewrite") and (pif or pif_uuid ): raise Usage("rewrite action does not take --pif or --pif-uuid") global db if force_interface: log("Force interface %s %s" % (force_interface, action)) if action == "rewrite": action_force_rewrite(force_interface, force_rewrite_config) elif action in ["up", "down"]: db_init_from_cache(dbcache_file) pif = db().get_pif_by_bridge(force_interface) management_pif = db().get_management_pif() if action == "up": action_up(pif, True) elif action == "down": action_down(pif) else: raise Error("Unknown action %s" % action) else: db_init_from_xenapi(session) if pif_uuid: pif = db().get_pif_by_uuid(pif_uuid) if action == "rewrite": action_rewrite() else: if not pif: raise Usage("No PIF given") if force_management: # pif is going to be the management pif management_pif = pif else: # pif is not going to be the management pif. # Search DB cache for pif on same host with management=true pifrec = db().get_pif_record(pif) management_pif = db().get_management_pif() log_pif_action(action, pif) if not check_allowed(pif): return 0 if action == "up": action_up(pif, False) elif action == "down": action_down(pif) else: raise Error("Unknown action %s" % action) # Save cache. db().save(dbcache_file) except Usage as err: sys.stderr.write(err.msg + "\n") sys.stderr.write("For help use --help.\n") sys.stderr.flush() return 2 except Error as err: log(err.msg) return 1 return 0 if __name__ == "__main__": rc = 1 try: rc = main() except: ex = sys.exc_info() err = traceback.format_exception(*ex) for exline in err: log(exline) syslog.closelog() sys.exit(rc)