#!/usr/bin/python # Copyright (c) 2015 Monty Taylor # Copyright (c) 2015 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import argparse import contextlib import copy import errno import json import logging import os import subprocess import sys import time from glean import systemlock from glean import utils from glean._vendor import distro try: import configparser except ImportError: import ConfigParser as configparser try: from StringIO import StringIO except ImportError: from io import StringIO log = logging.getLogger("glean") # Type value for permanent mac addrs as defined by the linux kernel. PERMANENT_ADDR_TYPE = '0' # Global flag for selinux restore. SELINUX_RESTORECON = '/usr/sbin/restorecon' HAVE_SELINUX = os.path.exists(SELINUX_RESTORECON) # Wrap open calls in this to make sure that any created or modified # files retain their selinux context. @contextlib.contextmanager def safe_open(*args, **kwargs): f = open(*args, **kwargs) yield f f.flush() os.fsync(f.fileno()) f.close() path = os.path.abspath(f.name) if HAVE_SELINUX: logging.debug("Restoring selinux context for %s" % path) subprocess.call([SELINUX_RESTORECON, path]) def _exists_rh_interface(name, distro): file_to_check = _network_files(distro)['ifcfg'] + '-{name}'.format( name=name ) return os.path.exists(file_to_check) def _is_suse(distro): # 'distro could be any of suse, opensuse, # opensuse-leap, opensuse-tumbleweed, sles return 'suse' in distro or 'sles' in distro def _network_files(distro): network_files = {} if _is_suse(distro): network_files = { "ifcfg": "/etc/sysconfig/network/ifcfg", "route": "/etc/sysconfig/network/ifroute", } else: network_files = { "ifcfg": "/etc/sysconfig/network-scripts/ifcfg", "route": "/etc/sysconfig/network-scripts/route", } return network_files def _network_config(args): distro = args.distro network_config = {} if _is_suse(distro): header = "\n".join(["# Automatically generated, do not edit", "BOOTPROTO={bootproto}", "LLADDR={hwaddr}"]) footer = "STARTMODE=auto" + "\n" network_config = { "static": "\n".join([header, "IPADDR={ip_address}", "NETMASK={netmask}", footer]) } else: header = "\n".join(["# Automatically generated, do not edit", "DEVICE={name}", "BOOTPROTO={bootproto}", "HWADDR={hwaddr}"]) footer = "\n".join(["ONBOOT=yes", "NM_CONTROLLED=%s" % ("yes" if args.use_nm else "no"), "TYPE=Ethernet"]) + "\n" network_config = { # RedHat does not use TYPE=Ethernet in the static configurations "static": "\n".join([header, "IPADDR={ip_address}", "NETMASK={netmask}", footer.replace("TYPE=Ethernet\n", "")]) } # RedHat does not use TYPE=Ethernet in the dhcp configurations network_config["dhcp"] = "\n".join([header, footer]) network_config["none"] = "\n".join([header, footer]) return network_config def _set_rh_bonding(name, interface, distro, results): if not any(bond in ['bond_slaves', 'bond_master'] for bond in interface): return results # Careful, we are operating on the live 'results' variable # so we need to always append our data if _is_suse(distro): # SUSE configures the slave interfaces on the master ifcfg file. # The master interface contains a 'bond_slaves' key containing a list # of the slave interfaces if 'bond_slaves' in interface: results += "BONDING_MASTER=yes\n" slave_cnt = 0 for slave in interface['bond_slaves']: results += "BONDING_SLAVE_{id}={name}\n".format( id=slave_cnt, name=slave) slave_cnt += 1 else: # Slave interfaces do not know they are part of a bonded # interface. All we need to do is to set the STARTMODE # to hotplug results = results.replace("=auto", "=hotplug") else: # RedHat does not add any specific configuration to the master # interface. All configuration is done in the slave ifcfg files. if 'bond_slaves' in interface: return results results += "SLAVE=yes\n" results += "MASTER={0}\n".format(interface['bond_master']) return results def _set_rh_vlan(name, interface, distro): results = "" if 'vlan_id' not in interface: return results if _is_suse(distro): results += "VLAN_ID={vlan_id}\n".format(vlan_id=interface['vlan_id']) results += "ETHERDEVICE={etherdevice}\n".format( etherdevice=name.split('.')[0]) else: results += "VLAN=yes\n" return results def _write_rh_interface(name, interface, args): distro = args.distro files_to_write = dict() results = _network_config(args)["static"].format( bootproto="static", name=name, hwaddr=interface['mac_address'], ip_address=interface['ip_address'], netmask=interface['netmask'], ) results += _set_rh_vlan(name, interface, distro) # set_rh_bonding takes results as argument so we need to assign # the return value, not append it results = _set_rh_bonding(name, interface, distro, results) routes = [] for route in interface.get('routes', ()): if route['network'] == '0.0.0.0' and route['netmask'] == '0.0.0.0': if not _is_suse(distro): results += "DEFROUTE=yes\n" results += "GATEWAY={gw}\n".format(gw=route['gateway']) else: # Special notation for default route on SUSE/wicked routes.append(dict( net='default', mask='', gw=route['gateway'])) else: routes.append(dict( net=route['network'], mask=route['netmask'], gw=route['gateway'])) if routes: route_content = "" for x in range(0, len(routes)): if not _is_suse(distro): route_content += "ADDRESS{x}={net}\n".format(x=x, **routes[x]) route_content += "NETMASK{x}={mask}\n".format(x=x, **routes[x]) route_content += "GATEWAY{x}={gw}\n".format(x=x, **routes[x]) else: # Avoid the extra trailing whitespace for the default route # because mask is empty in that case. route_content += "{net} {gw} {mask}\n".format( **routes[x]).replace(' \n', '\n') files_to_write[_network_files(distro)["route"] + '-{name}' .format(name=name)] = route_content files_to_write[_network_files(distro)["ifcfg"] + '-{name}'.format( name=name)] = results return files_to_write def _write_rh_dhcp(name, interface, args): distro = args.distro filename = _network_files(distro)["ifcfg"] + '-{name}'.format(name=name) results = _network_config(args)["dhcp"].format( bootproto="dhcp", name=name, hwaddr=interface['mac_address']) results += _set_rh_vlan(name, interface, distro) # set_rh_bonding takes results as argument so we need to assign # the return value, not append it results = _set_rh_bonding(name, interface, distro, results) return {filename: results} def _write_rh_manual(name, interface, args): distro = args.distro filename = _network_files(distro)["ifcfg"] + '-{name}'.format(name=name) results = _network_config(args)["none"].format( bootproto="none", name=name, hwaddr=interface['mac_address']) results += _set_rh_vlan(name, interface, distro) # set_rh_bonding takes results as argument so we need to assign # the return value, not append it results = _set_rh_bonding(name, interface, distro, results) return {filename: results} def write_redhat_interfaces(interfaces, sys_interfaces, args): files_to_write = dict() # Sort the interfaces by id so that we'll have consistent output order for iname, interface in sorted( interfaces.items(), key=lambda x: x[1]['id']): if interface['type'] == 'ipv6': continue # sys_interfaces is pruned by --interface; if one of the # raw_macs (or, *the* MAC for single interfaces) does not # match as one of the interfaces we want configured, skip raw_macs = interface.get('raw_macs', [interface['mac_address']]) if not set(sys_interfaces).intersection(set(raw_macs)): continue if 'vlan_id' in interface: # raw_macs will have a single entry if the vlan device is a # phsical device and >1 when it is a bond device. if len(raw_macs) == 1: vlan_raw_device = sys_interfaces.get(raw_macs[0]) else: vlan_raw_device = interface['vlan_link'] interface_name = "{0}.{1}".format( vlan_raw_device, interface['vlan_id']) elif 'bond_mode' in interface: # It is possible our interface does not have a link, so fall back # to iname which is the link id. interface_name = interface.get('link', iname) else: interface_name = sys_interfaces[interface['mac_address']] if 'bond_links' in interface: # We need to keep track of the slave interfaces because # SUSE configures the slaves on the master ifcfg file bond_slaves = [] for phy in interface['raw_macs']: bond_slaves.append(sys_interfaces[phy]) interface['bond_slaves'] = bond_slaves # Remove the 'bond_links' key interface.pop('bond_links') if interface['type'] == 'ipv4': files_to_write.update( _write_rh_interface(interface_name, interface, args)) if interface['type'] == 'ipv4_dhcp': files_to_write.update( _write_rh_dhcp(interface_name, interface, args)) if interface['type'] == 'manual': files_to_write.update( _write_rh_manual(interface_name, interface, args)) for mac, iname in sorted( sys_interfaces.items(), key=lambda x: x[1]): if _exists_rh_interface(iname, args.distro): # This interface already has a config file, move on log.debug("%s already has config file, skipping" % iname) continue inter_macs = [intf['mac_address'] for intf in interfaces.values()] link_macs = [intf.get('link_mac') for intf in interfaces.values() if 'vlan_id' in intf] if mac in inter_macs or mac in link_macs: # We have a config drive config, move on log.debug("%s configured via config-drive" % mac) continue files_to_write.update(_write_rh_dhcp(iname, {'mac_address': mac}, args)) return files_to_write def _write_networkd_interface(name, interfaces, args, files_struct=dict()): vlans = [] for interface in interfaces: iname = name # if vlan set interface name to vlan format if 'vlan_id' in interface: iname = name + '-vlan' + str(interface['vlan_id']) vlans.append(iname) network_file = '/etc/systemd/network/{name}.network'.format(name=iname) if network_file not in files_struct: files_struct[network_file] = dict() if '[Match]' not in files_struct[network_file]: files_struct[network_file]['[Match]'] = list() files_struct[network_file]['[Match]'].append( 'MACAddress={mac_address}'.format( mac_address=interface['mac_address'] ) ) files_struct[network_file]['[Match]'].append( 'Name={name}'.format(name=iname) ) # define network if needed (basically always) if ((interface['type'] in ['ipv4_dhcp', 'ipv6_slaac', 'ipv6_dhcpv6_stateful', 'manual', 'ipv4', 'ipv6']) or ('vlan_id' in interface) or ('bond_mode' in interface)): if '[Network]' not in files_struct[network_file]: files_struct[network_file]['[Network]'] = list() if 'services' in interface: for service in interface['services']: if service['type'] == 'dns': if not args.skip_dns: files_struct[network_file]['[Network]'].append( 'DNS={address}'.format( address=service['address'] ) ) # dhcp network, set to yes if both dhcp6 and dhcp4 are set if interface['type'] == 'ipv4_dhcp': if 'DHCP=ipv6' in files_struct[network_file]['[Network]']: files_struct[network_file]['[Network]'].append('DHCP=yes') else: files_struct[network_file]['[Network]'].append('DHCP=ipv4') if interface['type'] == 'ipv6_dhcpv6_stateful': if 'DHCP=ipv4' in files_struct[network_file]['[Network]']: files_struct[network_file]['[Network]'].append('DHCP=yes') else: files_struct[network_file]['[Network]'].append('DHCP=ipv6') # slaac can start dhcp6 if the associated RA option is sent to server if (interface['type'] == 'ipv6_slaac' or interface['type'] == 'ipv6_dhcpv6-stateless'): # we are accepting slaac now, remove the disabling of slaac if 'IPv6AcceptRA=no' in files_struct[network_file]['[Network]']: files_struct[network_file]['[Network]'].remove( 'IPv6AcceptRA=no' ) files_struct[network_file]['[Network]'].append('IPv6AcceptRA=yes') else: # only disbale slaac if slac is not already enabled if 'IPv6AcceptRA=yes' not in \ files_struct[network_file]['[Network]']: files_struct[network_file]['[Network]'].append( 'IPv6AcceptRA=no' ) # vlan network # static network if interface['type'] in ['ipv4', 'ipv6']: if 'addresses' not in files_struct[network_file]: files_struct[network_file]['addresses'] = list() if interface['type'] == 'ipv4': files_struct[network_file]['addresses'].append( 'Address={address}/{cidr}'.format( address=interface['ip_address'], cidr=utils.ipv4_netmask_length(interface['netmask']) ) ) if interface['type'] == 'ipv6': files_struct[network_file]['addresses'].append( 'Address={address}/{cidr}'.format( address=interface['ip_address'], cidr=utils.ipv6_netmask_length(interface['netmask']) ) ) # routes if 'routes' in interface: if 'routes' not in files_struct[network_file]: files_struct[network_file]['routes'] = list() for route in interface['routes']: route_destination = None route_gateway = None if 'network' in route: if 'v6' in interface['type']: cidr = utils.ipv6_netmask_length(route['netmask']) else: cidr = utils.ipv4_netmask_length(route['netmask']) route_destination = 'Destination={network}/{cidr}'.format( network=route['network'], cidr=cidr ) if 'gateway' in route: route_gateway = 'Gateway={gateway}'.format( gateway=route['gateway'] ) # add route as a dictionary to the routes list files_struct[network_file]['routes'].append({ 'route': route_destination, 'gw': route_gateway }) # create netdev files if 'bond_mode' or 'vlan_id' in interface: netdev_file = \ '/etc/systemd/network/{name}.netdev'.format(name=iname) if netdev_file not in files_struct: files_struct[netdev_file] = dict() if '[NetDev]' not in files_struct[netdev_file]: files_struct[netdev_file]['[NetDev]'] = list() files_struct[netdev_file]['[NetDev]'].append( 'Name={name}'.format(name=iname) ) if 'mac_address' in interface: files_struct[netdev_file]['[NetDev]'].append( 'MACAddress={mac_address}'.format( mac_address=interface['mac_address'] ) ) if 'vlan_id' in interface: files_struct[netdev_file]['[NetDev]'].append('Kind=vlan') files_struct[netdev_file]['[VLAN]'] = list() files_struct[netdev_file]['[VLAN]'].append( 'Id={id}'.format(id=interface['vlan_id']) ) if 'bond_mode' in interface: files_struct[netdev_file]['[NetDev]'].append('Kind=bond') files_struct[netdev_file]['[Bond]'] = list() files_struct[netdev_file]['[Bond]'].append( 'Mode={bond_mode}'.format(bond_mode=interface['bond_mode']) ) files_struct[netdev_file]['[Bond]'].append( 'LACPTransmitRate=fast' ) if 'slaves' in interface: for slave in interface['slaves']: slave_net_file = \ '/etc/systemd/network/{name}.network'.format( name=slave ) if slave_net_file not in files_struct: files_struct[slave_net_file] = dict() if '[Network]' not in files_struct[slave_net_file]: files_struct[slave_net_file]['[Network]'] = list() files_struct[slave_net_file]['[Network]'].append( 'Bond={name}'.format(name=iname) ) if 'bond_xmit_hash_policy' in interface: files_struct[netdev_file]['[Bond]'].append( 'TransmitHashPolicy={bond_xmit_hash_policy}'.format( bond_xmit_hash_policy=interface[ 'bond_xmit_hash_policy' ] ) ) if 'bond_miimon' in interface: files_struct[netdev_file]['[Bond]'].append( 'MIIMonitorSec={milliseconds}'.format( milliseconds=interface['bond_miimon'] ) ) # vlan mapping sucks (forward and reverse) if vlans: netdev = vlans[0].split('-')[0] vlan_master_file = \ '/etc/systemd/network/{name}.network'.format(name=netdev) if vlan_master_file not in files_struct: files_struct[vlan_master_file] = dict() if '[Network]' not in files_struct[vlan_master_file]: files_struct[vlan_master_file]['[Network]'] = list() for vlan in vlans: files_struct[vlan_master_file]['[Network]'].append('VLAN=' + vlan) vlan_file = '/etc/systemd/network/{name}.network'.format(name=vlan) if vlan_file not in files_struct: files_struct[vlan_file] = dict() if '[Network]' not in files_struct[vlan_file]: files_struct[vlan_file]['[Network]'] = list() files_struct[vlan_file]['[Network]'].append('VLAN=' + vlan) return files_struct def write_networkd_interfaces(interfaces, sys_interfaces, args): files_to_write = dict() gen_intfs = {} files_struct = dict() # Sort the interfaces by id so that we'll have consistent output order for iname, interface in sorted( interfaces.items(), key=lambda x: x[1]['id']): # sys_interfaces is pruned by --interface; if one of the # raw_macs (or, *the* MAC for single interfaces) does not # match as one of the interfaces we want configured, skip raw_macs = interface.get('raw_macs', [interface['mac_address']]) if not set(sys_interfaces).intersection(set(raw_macs)): continue if 'bond_mode' in interface: interface['slaves'] = [ sys_interfaces[mac] for mac in interface['raw_macs']] if 'raw_macs' in interface: key = tuple(interface['raw_macs']) if key not in gen_intfs: gen_intfs[key] = [] gen_intfs[key].append(interface) else: key = (interface['mac_address'],) if key not in gen_intfs: gen_intfs[key] = [] gen_intfs[key].append(interface) for raw_macs, interfs in gen_intfs.items(): if len(raw_macs) == 1: interface_name = sys_interfaces[raw_macs[0]] else: # It is possible our interface does not have a link, so # fall back to interface id. interface_name = next( intf.get('link', intf['id']) for intf in interfs if 'bond_mode' in intf) files_struct = _write_networkd_interface( interface_name, interfs, args, files_struct) for mac, iname in sorted( sys_interfaces.items(), key=lambda x: x[1]): if _exists_networkd_interface(iname): # This interface already has a config file, move on log.debug("%s already has config file, skipping" % iname) continue if (mac,) in gen_intfs: # We have a config drive config, move on log.debug("%s configured via config-drive" % mac) continue interface = {'type': 'ipv4_dhcp', 'mac_address': mac} files_struct = _write_networkd_interface( iname, [interface], args, files_struct) for networkd_file in files_struct: file_contents = '# Automatically generated, do not edit\n' if '[Match]' in files_struct[networkd_file]: file_contents += '[Match]\n' for line in sorted(set(files_struct[networkd_file]['[Match]'])): file_contents += line file_contents += '\n' file_contents += '\n' if '[Network]' in files_struct[networkd_file]: file_contents += '[Network]\n' for line in sorted(set(files_struct[networkd_file]['[Network]'])): file_contents += line file_contents += '\n' file_contents += '\n' if 'addresses' in files_struct[networkd_file]: for address in files_struct[networkd_file]['addresses']: file_contents += '[Address]\n' file_contents += address + '\n\n' if 'routes' in files_struct[networkd_file]: for route in files_struct[networkd_file]['routes']: file_contents += '[Route]\n' if route['route'] is not None: file_contents += route['route'] + '\n' if route['gw'] is not None: file_contents += route['gw'] + '\n' file_contents += '\n' if '[NetDev]' in files_struct[networkd_file]: file_contents += '[NetDev]\n' for line in sorted(set(files_struct[networkd_file]['[NetDev]'])): file_contents += line file_contents += '\n' file_contents += '\n' if '[VLAN]' in files_struct[networkd_file]: file_contents += '[VLAN]\n' for line in sorted(set(files_struct[networkd_file]['[VLAN]'])): file_contents += line file_contents += '\n' file_contents += '\n' if '[Bond]' in files_struct[networkd_file]: file_contents += '[Bond]\n' for line in sorted(set(files_struct[networkd_file]['[Bond]'])): file_contents += line file_contents += '\n' file_contents += '\n' files_to_write['{path}'.format(path=networkd_file)] = file_contents return files_to_write def _exists_networkd_interface(name): network_file = '/etc/systemd/network/{name}.network'.format(name=name) netdev_file = '/etc/systemd/network/{name}.netdev'.format(name=name) return (os.path.exists(network_file) or os.path.exists(netdev_file)) def _exists_gentoo_interface(name): file_to_check = '/etc/conf.d/net.{name}'.format(name=name) return os.path.exists(file_to_check) def _enable_gentoo_interface(name): log.debug('rc-update add {name} default'.format(name=name)) subprocess.call(['rc-update', 'add', 'net.{name}'.format(name=name), 'default']) def _write_gentoo_interface(name, interfaces): files_to_write = dict() results = "" vlans = [] for interface in interfaces: iname = name if 'vlan_id' in interface: vlans.append(interface['vlan_id']) iname = "%s_%s" % (iname, interface['vlan_id']) if interface['type'] == 'ipv4': results += """config_{name}="{ip_address} netmask {netmask}" mac_{name}="{hwaddr}\"\n""".format( name=iname, ip_address=interface['ip_address'], netmask=interface['netmask'], hwaddr=interface['mac_address'] ) routes = list() for route in interface.get('routes', ()): if (route['network'] == '0.0.0.0' and route['netmask'] == '0.0.0.0'): # add default route if it exists routes.append('default via {gw}'.format( name=name, gw=route['gateway'] )) else: # add remaining static routes routes.append('{net} netmask {mask} via {gw}'.format( net=route['network'], mask=route['netmask'], gw=route['gateway'] )) if routes: routes_string = '\n'.join(route for route in routes) results += 'routes_{name}="{routes}"'.format( name=name, routes=routes_string # routes='\n'.join(str(route) for route in routes) ) results += '\n' elif interface['type'] == 'manual': results += """config_{name}="null" mac_{name}="{hwaddr}" """.format(name=iname, hwaddr=interface['mac_address']) _enable_gentoo_interface(iname) else: results += """config_{name}="dhcp" mac_{name}="{hwaddr}" """.format(name=iname, hwaddr=interface['mac_address']) _enable_gentoo_interface(iname) if 'bond_mode' in interface: slaves = ' '.join(interface['slaves']) results += """slaves_{name}="{slaves}" mode_{name}="{mode}" """.format(name=iname, slaves=slaves, mode=interface['bond_mode']) full_results = "# Automatically generated, do not edit\n" if vlans: full_results += 'vlans_{name}="{vlans}"\n'.format( name=name, vlans=' '.join(str(vlan) for vlan in vlans)) full_results += results files_to_write['/etc/conf.d/net.{name}'.format(name=name)] = full_results return files_to_write def _setup_gentoo_network_init(sys_interface, interfaces): for interface in interfaces: interface_name = '{name}'.format(name=sys_interface) if 'vlan_id' in interface: interface_name += ".{vlan}".format( vlan=interface['vlan_id']) log.debug('vlan {vlan} found, interface named {name}'. format(vlan=interface['vlan_id'], name=interface_name)) if 'bond_master' in interface: continue _create_gentoo_net_symlink_and_enable(interface_name) if not interfaces: _create_gentoo_net_symlink_and_enable(sys_interface) def _create_gentoo_net_symlink_and_enable(interface_name): file_path = '/etc/init.d/net.{name}'.format(name=interface_name) if not os.path.islink(file_path): log.debug('ln -s /etc/init.d/net.lo {file_path}'. format(file_path=file_path)) os.symlink('/etc/init.d/net.lo', '{file_path}'.format(file_path=file_path)) _enable_gentoo_interface(interface_name) def write_gentoo_interfaces(interfaces, sys_interfaces): files_to_write = dict() gen_intfs = {} # Sort the interfaces by id so that we'll have consistent output order for iname, interface in sorted( interfaces.items(), key=lambda x: x[1]['id']): if interface['type'] == 'ipv6': continue # sys_interfaces is pruned by --interface; if one of the # raw_macs (or, *the* MAC for single interfaces) does not # match as one of the interfaces we want configured, skip raw_macs = interface.get('raw_macs', [interface['mac_address']]) if not set(sys_interfaces).intersection(set(raw_macs)): continue if 'bond_mode' in interface: interface['slaves'] = [ sys_interfaces[mac] for mac in interface['raw_macs']] if 'raw_macs' in interface: key = tuple(interface['raw_macs']) if key not in gen_intfs: gen_intfs[key] = [] gen_intfs[key].append(interface) else: key = (interface['mac_address'],) if key not in gen_intfs: gen_intfs[key] = [] gen_intfs[key].append(interface) for raw_macs, interfs in gen_intfs.items(): if len(raw_macs) == 1: interface_name = sys_interfaces[raw_macs[0]] else: # It is possible our interface does not have a link, so # fall back to interface id. interface_name = next( intf.get('link', intf['id']) for intf in interfs if 'bond_mode' in intf) files_to_write.update( _write_gentoo_interface(interface_name, interfs)) _setup_gentoo_network_init(interface_name, interfs) for mac, iname in sorted( sys_interfaces.items(), key=lambda x: x[1]): if _exists_gentoo_interface(iname): # This interface already has a config file, move on log.debug("%s already has config file, skipping" % iname) continue if (mac,) in gen_intfs: # We have a config drive config, move on log.debug("%s configured via config-drive" % mac) continue interface = {'type': 'ipv4_dhcp', 'mac_address': mac} files_to_write.update(_write_gentoo_interface(iname, [interface])) _setup_gentoo_network_init(iname, []) return files_to_write def _write_debian_bond_conf(interface_name, interface, sys_interfaces): result = "" if interface['mac_address']: result += " hwaddress {0}\n".format( interface['mac_address']) result += " bond-mode {0}\n".format(interface['bond_mode']) result += " bond-miimon {0}\n".format( interface.get('bond_miimon', 0)) result += " bond-lacp-rate {0}\n".format( interface.get('bond_lacp_rate', 'slow')) result += " bond-xmit_hash_policy {0}\n".format( interface.get('bond_xmit_hash_policy', 'layer2')) slave_devices = [sys_interfaces[mac] for mac in interface['raw_macs']] slaves = ' '.join(slave_devices) result += " bond-slaves none\n" result += " post-up ifenslave {0} {1}\n".format(interface_name, slaves) result += " pre-down ifenslave -d {0} {1}\n".format( interface_name, slaves) return result def write_debian_interfaces(interfaces, sys_interfaces): eni_path = '/etc/network/interfaces' eni_d_path = eni_path + '.d' files_to_write = dict() files_to_write[eni_path] = "auto lo\niface lo inet loopback\n" files_to_write[eni_path] += "source /etc/network/interfaces.d/*.cfg\n" # Sort the interfaces by id so that we'll have consistent output order for iname, interface in sorted( interfaces.items(), key=lambda x: x[1]['id']): # sys_interfaces is pruned by --interface; if one of the # raw_macs (or, *the* MAC for single interfaces) does not # match as one of the interfaces we want configured, skip raw_macs = interface.get('raw_macs', [interface['mac_address']]) if not set(sys_interfaces).intersection(set(raw_macs)): continue # Determine the debian interface name and skip configuration for # this interface if config already exists for it. vlan_raw_device = None if 'vlan_id' in interface: # raw_macs will have a single entry if the vlan device is a # phsical device and >1 when it is a bond device. if len(raw_macs) == 1: vlan_raw_device = sys_interfaces.get(raw_macs[0]) else: vlan_raw_device = interface['vlan_link'] interface_name = "{0}.{1}".format(vlan_raw_device, interface['vlan_id']) elif 'bond_mode' in interface: # It is possible our interface does not have a link, so fall back # to iname which is the link id. interface_name = interface.get('link', iname) else: interface_name = sys_interfaces[interface['mac_address']] iface_path = os.path.join(eni_d_path, '%s.cfg' % interface_name) if os.path.exists(iface_path): continue if iface_path not in files_to_write: # Write the header if this is the first time editing # this interface. result = "auto {0}\n".format(interface_name) else: # Append to existing config result = files_to_write[iface_path] if interface['type'] == 'ipv4_dhcp': result += "iface {0} inet dhcp\n".format(interface_name) if vlan_raw_device is not None: result += " vlan-raw-device {0}\n".format(vlan_raw_device) result += " hw-mac-address {0}\n".format( interface['mac_address']) elif interface['type'] == 'manual': result += "iface {0} inet manual\n".format(interface_name) else: # Static ipv4 and ipv6 if interface['type'] == 'ipv6': link_type = "inet6" elif interface['type'] == 'ipv4': link_type = "inet" else: # We do not know this type of entry continue result += "iface {name} {link_type} static\n".format( name=interface_name, link_type=link_type) if vlan_raw_device: result += " vlan-raw-device {0}\n".format(vlan_raw_device) result += " address {0}\n".format(interface['ip_address']) if interface['type'] == 'ipv4': result += " netmask {0}\n".format(interface['netmask']) else: result += " netmask {0}\n".format( utils.ipv6_netmask_length(interface['netmask'])) for route in interface.get('routes', ()): if ((route['network'] == '0.0.0.0' and route['netmask'] == '0.0.0.0') or (route['network'] == '::' and route['netmask'] == '::')): result += " gateway {0}\n".format(route['gateway']) else: if interface['type'] == 'ipv4': route_add = (" up route add -net {net} netmask " "{mask} gw {gw} || true\n") route_del = (" down route del -net {net} netmask " "{mask} gw {gw} || true\n") _netmask = route['netmask'] else: route_add = (" up ip -6 route add {net}/{mask} " "via {gw} dev {interface} || true\n") route_del = (" down ip -6 route del {net}/{mask} " "via {gw} dev {interface} || true\n") _netmask = utils.ipv6_netmask_length(route['netmask']) result += route_add.format( net=route['network'], mask=_netmask, gw=route['gateway'], interface=interface_name) result += route_del.format( net=route['network'], mask=_netmask, gw=route['gateway'], interface=interface_name) if 'bond_master' in interface: result += " bond-master {0}\n".format( interface['bond_master']) if 'bond_mode' in interface: result += _write_debian_bond_conf(interface_name, interface, sys_interfaces) files_to_write[iface_path] = result # Configure any interfaces not mentioned in the config drive data for DHCP. for mac, iname in sorted( sys_interfaces.items(), key=lambda x: x[1]): iface_path = os.path.join(eni_d_path, '%s.cfg' % iname) if os.path.exists(iface_path): # This interface already has a config file, move on continue inter_macs = [intf['mac_address'] for intf in interfaces.values()] link_macs = [intf.get('link_mac') for intf in interfaces.values() if 'vlan_id' in intf] if mac in inter_macs or mac in link_macs: # We have a config drive config, move on continue result = "auto {0}\n".format(iname) result += "iface {0} inet dhcp\n".format(iname) files_to_write[iface_path] = result return files_to_write def write_dns_info(dns_servers): # will fail on non-systemd systems (what we want) # will exit 1 if not enabled (what we want) # will exit 0 if enabled (or indirectly enabled) resolved_enabled = os.system('systemctl is-enabled systemd-resolved') resolve_confs = {} # write resolv.conf if the file can be written to (if symlink is pointing # to a non-existant file, writing will fail), will return false if the # pointer is incomplete if resolved_enabled != 0: log.debug("resolved not in use, writing to /etc/resolv.conf") resolv_nameservers = "" for server in dns_servers: resolv_nameservers += "nameserver {0}\n".format(server) resolve_confs['/etc/resolv.conf'] = resolv_nameservers # set up resolved if enabled if resolved_enabled == 0: log.debug("resolved in use, writing to /etc/systemd/resolved.conf") # read the existing config so we only overwrite what's needed resolved_conf = configparser.ConfigParser() resolved_conf.optionxform = str resolved_conf.read('/etc/systemd/resolved.conf') # create config section if not created if not resolved_conf.has_section('Resolve'): resolved_conf.add_section('Resolve') # write space separated dns servers resolved_conf.set('Resolve', 'DNS', " ".join(dns_servers)) # use stringio to output the resulting config to string # configparser only outputs to file descriptors resolved_conf_fd = StringIO("") resolved_conf.write(resolved_conf_fd) resolved_conf_output = resolved_conf_fd.getvalue() resolved_conf_fd.close() # add the config to files to be written resolve_confs['/etc/systemd/resolved.conf'] = resolved_conf_output return resolve_confs def get_config_drive_interfaces(net): interfaces = {} if 'networks' not in net or 'links' not in net: log.debug("No config-drive interfaces defined") return interfaces networks = {} for network in net['networks']: networks[network['link']] = network vlans = {} phys = {} bonds = {} for link in net['links']: if link['type'] == 'vlan': vlans[link['id']] = link elif link['type'] == 'bond': bonds[link['id']] = link else: phys[link['id']] = link for link in vlans.values(): if link['vlan_link'] in phys: vlan_link = phys[link['vlan_link']] link['raw_macs'] = [vlan_link['ethernet_mac_address'].lower()] elif link['vlan_link'] in bonds: vlan_link = bonds[link['vlan_link']] link['raw_macs'] = [] for phy in vlan_link['bond_links']: link['raw_macs'].append( phys[phy]['ethernet_mac_address'].lower()) else: log.warning('vlan_link=%s not matching any ' 'NIC', link['vlan_link']) continue link['mac_address'] = link.pop( 'vlan_mac_address', vlan_link['ethernet_mac_address']).lower() for link in bonds.values(): phy_macs = [] for phy in link['bond_links']: phy_link = phys[phy] phy_link['bond_master'] = link['id'] if phy in phys: phy_macs.append(phy_link['ethernet_mac_address'].lower()) link['raw_macs'] = phy_macs link['mac_address'] = link.pop('ethernet_mac_address').lower() if link['id'] not in networks: link['type'] = 'manual' interfaces[link['id']] = link for link in phys.values(): link['mac_address'] = link.pop('ethernet_mac_address').lower() if link['id'] not in networks: link['type'] = 'manual' interfaces[link['id']] = link for network in net['networks']: link = vlans.get( network['link'], phys.get(network['link'], bonds.get(network['link']))) if not link: continue link.update(network) # NOTE(pabelanger): Make sure we index by the existing network id, # rather then creating out own. interfaces[network['id']] = copy.deepcopy(link) return interfaces def get_dns_from_config_drive(net): if 'services' not in net: log.debug("No DNS info available from config-drive") return [] return [ f['address'] for f in net['services'] if f['type'] == 'dns' ] def write_static_network_info( interfaces, sys_interfaces, files_to_write, args): if args.distro in ('debian', 'ubuntu'): files_to_write.update( write_debian_interfaces(interfaces, sys_interfaces)) elif args.distro in ('redhat', 'centos', 'fedora') or \ _is_suse(args.distro): files_to_write.update( write_redhat_interfaces(interfaces, sys_interfaces, args)) elif args.distro in 'gentoo': files_to_write.update( write_gentoo_interfaces(interfaces, sys_interfaces) ) elif args.distro in 'networkd': files_to_write.update( write_networkd_interfaces(interfaces, sys_interfaces, args) ) else: return False finish_files(files_to_write, args) def finish_files(files_to_write, args): files = sorted(files_to_write.keys()) log.debug("Writing output files") for k in files: if not files_to_write[k]: # Don't write empty files log.debug("%s is blank, skipped" % k) continue if args.noop: sys.stdout.write("### Write {0}\n{1}".format(k, files_to_write[k])) continue retries = 0 while True: try: log.debug("Writing output file : %s" % k) with safe_open(k, 'w') as outfile: outfile.write(files_to_write[k]) log.debug(" ... done") break except IOError as e: # if we got ELOOP the file was a dangling or bad # symlink. We're taking ownership of this, so # overwrite it. if e.errno == errno.ELOOP and retries < 1: log.debug("Dangling symlink <%s>; " "unlinking and trying again" % k) os.unlink(k) retries = 1 continue elif e.errno == errno.EACCES: log.debug(" ... is read only, skipped") break else: raise def is_interface_live(interface, sys_root): try: if open('{root}/{iface}/carrier'.format( root=sys_root, iface=interface)).read().strip() == '1': return True except IOError as e: # We get this error if the link is not up if e.errno != 22: raise return False def interface_live(iface, sys_root, args): log.debug("Checking status of interface %s" % iface) if is_interface_live(iface, sys_root): log.debug("%s has active carrier, including", iface) return True else: log.debug("%s does not have active carrier", iface) if args.noop: return False subprocess.check_call(['ip', 'link', 'set', 'dev', iface, 'up']) return True def is_interface_vlan(iface, distro): if distro in ('debian', 'ubuntu'): file_name = '/etc/network/interfaces.d/%s.cfg' % iface if os.path.exists(file_name): return 'vlan-raw-device' in open(file_name).read() elif distro in ('redhat', 'centos', 'fedora'): file_name = '/etc/sysconfig/network-scripts/ifcfg-%s' % iface if os.path.exists(file_name): return 'VLAN=YES' in open(file_name).read() elif _is_suse(distro): file_name = '/etc/sysconfig/network/ifcfg-%s' % iface if os.path.exists(file_name): return 'ETHERDEVICE' in open(file_name).read() elif distro in ('gentoo'): file_name = '/etc/conf.d/net.%s' % iface if os.path.exists(file_name): return 'vlan_id' in open(file_name).read() return False def is_interface_bridge(iface, distro): if distro in ('debian', 'ubuntu'): file_name = '/etc/network/interfaces.d/%s.cfg' % iface if os.path.exists(file_name): return 'bridge_ports' in open(file_name).read().lower() elif distro in ('redhat', 'centos', 'fedora'): file_name = '/etc/sysconfig/network-scripts/ifcfg-%s' % iface if os.path.exists(file_name): return 'type=bridge' in open(file_name).read().lower() elif _is_suse(distro): file_name = '/etc/sysconfig/network/ifcfg-%s' % iface if os.path.exists(file_name): return 'bridge=yes' in open(file_name).read().lower() elif distro in ('gentoo'): file_name = '/etc/conf.d/net.%s' % iface if os.path.exists(file_name): return 'bridge' in open(file_name).read().lower() return False def get_sys_interfaces(interface, args): log.debug("Probing system interfaces") sys_root = os.path.join(args.root, 'sys/class/net') ignored_interfaces = ('sit', 'tunl', 'bonding_master', 'teql', 'wg', 'ip6gre', 'ip6_vti', 'ip6tnl', 'bond', 'lo') sys_interfaces = {} called_from_udev = False if interface is not None: log.debug("Only considering interface %s from arguments" % interface) interfaces = [interface] # see notes below... called_from_udev = True else: interfaces = [f for f in os.listdir(sys_root) if not f.startswith(ignored_interfaces)] # build interface dict. so we can enumerate through later if_dict = {} for iface in interfaces: # if interface is for an already configured vlan, skip it if is_interface_vlan(iface, args.distro): log.debug("Skipping vlan %s" % iface) continue # if interface is for an already configured bridge, skip it if is_interface_bridge(iface, args.distro): log.debug("Skipping bridge %s" % iface) continue mac_addr_type = open( '%s/%s/addr_assign_type' % (sys_root, iface), 'r').read().strip() # Interfaces without a permanent address are likely created by some # other system on the host like a running neutron agent. In these cases # that system should be responsible for configuring the interface not # glean. if mac_addr_type != PERMANENT_ADDR_TYPE: continue mac = open('%s/%s/address' % (sys_root, iface), 'r').read().strip() # Hack alert! If we have been given a single interface # argument (hence called_from_udev is true), that means we # have been called for just one nic by udev in response to the # "net" "add" action matching. We are going to assume that if # we made it this far (i.e. past the filters above) this # interface should be configured. # # It is unclear, as at 2019-10, if there are active jobs # relying on the "probe" path below. The only way to get into # this path is being called from init scripts on a pre-systemd # platform that does not use udev activiation; this would mean # (as at this writing) Trusty (CentOS 6 being long gone). # # In short, it tries to bring up *all* the interfaces, and if # they don't come up, it figures they're not valid and # excludes them. This introduces a very tricky race -- by # bringing the interface up it can start accepting RA # broadcasts and possibly have the kernel configure it with an # ipv6 addresses. Then, network-manager will see the # interface is already configured, and out of an abdundance of # caution, refuse to re-configure it. You end up with broken # networking. # # This is racy; you might get lucky and the RA timeout is long # enough that network-manager starts before this happens. So # it is not exactly correct to say that the probe path is # completely broken; it is possible users have just not # noticed or are tacitly relying on it. # # While we consider this, assuming that if we are called from # udev that the interface is to be configured here, and not # bringing it "up", avoids this issue. if called_from_udev: log.debug("Interface matched: %s (%s)", iface, mac) sys_interfaces[mac] = iface return sys_interfaces # check if interface is up if not try and bring it up if interface_live(iface, sys_root, args): if_dict[iface] = mac # wait up to 9 seconds all interfaces to reach up log.debug("Waiting for interfaces to become active.") if_up_list = [] for x in range(0, 90): for iface in if_dict: mac = if_dict[iface] if iface in if_up_list: continue log.debug("Checking liveness of %s", mac) if is_interface_live(iface, sys_root): # Add system interface sys_interfaces[mac] = iface log.debug("Added system interface %s (%s)" % (iface, mac)) if_up_list.append(iface) if sorted(if_up_list) == sorted(if_dict.keys()): # all interfaces are up no need to continue looping break time.sleep(.1) if sorted(if_up_list) != sorted(if_dict.keys()): # not all interfaces became active with in the time limit for iface in if_dict: if iface in if_up_list: continue msg = "Skipping system interface %s (%s)" % (iface, if_dict[iface]) log.warning(msg) log.debug("WARNING: interfaces have been brought 'up' during the probing" "process. This may cause problems if IPv6 RA have" "been accepted") return sys_interfaces def get_network_info(args): """Retrieves network info from config-drive. If there is no meta_data.json in config-drive, it means that there is no config drive mounted- which means we know nothing. """ config_drive = os.path.join(args.root, 'mnt/config') network_info_file = '%s/openstack/latest/network_info.json' % config_drive network_data_file = '%s/openstack/latest/network_data.json' % config_drive vendor_data_file = '%s/openstack/latest/vendor_data.json' % config_drive network_info = {} if os.path.exists(network_info_file): log.debug("Found network_info file %s" % network_info_file) network_info = json.load(open(network_info_file)) # network_data.json is the file written by nova that should be there. # Other cloud deployments may use the above network_info.json or # vendor_data.json but the canonical location is this one. if os.path.exists(network_data_file): log.debug("Found network_info file %s" % network_data_file) network_info = json.load(open(network_data_file)) elif os.path.exists(vendor_data_file): log.debug("Found vendor_data_file file %s" % vendor_data_file) vendor_data = json.load(open(vendor_data_file)) if 'network_info' in vendor_data: log.debug("Found network_info in vendor_data_file") network_info = vendor_data['network_info'] else: log.debug("Did not find vendor_data or network_info in config-drive") if not network_info: log.debug("Found no network_info in config-drive! " "Asusming DHCP interfaces") return network_info def write_network_info_from_config_drive(args): """Write network info from config-drive. If there is no meta_data.json in config-drive, it means that there is no config drive mounted- which means we know nothing. Can set 'glean_ignore_interfaces' in nova metadata to ignore the interface configuration specified by the config drive. This will cause it to fallback to using dhcp configuration. Returns False on any issue, which will cause the writing of DHCP network files. """ config_drive = os.path.join(args.root, 'mnt/config') meta_data_path = '%s/openstack/latest/meta_data.json' % config_drive meta_data = {} if os.path.exists(meta_data_path): meta_data = json.load(open(meta_data_path)) network_info = get_network_info(args) dns = {} if not args.skip_dns: dns = write_dns_info(get_dns_from_config_drive(network_info)) interfaces = get_config_drive_interfaces(network_info) if 'meta' in meta_data and 'glean_ignore_interfaces' in meta_data['meta']: # Force DHCP to be used ignoring the interface information. # Some clouds have neutron configured in such a way that we get # interface config drive data that is at odds with the networking # in the cloud. Note we set interfaces to {} so that fallback dhcp # configuration can happen in write_static_network_info(). interfaces = {} sys_interfaces = get_sys_interfaces(args.interface, args) write_static_network_info(interfaces, sys_interfaces, dns, args) def write_ssh_keys(args): """Write ssh-keys from config-drive. If there is no meta_data.json in config-drive, it means that there is no config drive mounted- which means we do nothing. """ config_drive = os.path.join(args.root, 'mnt/config') ssh_path = os.path.join(args.root, 'root/.ssh') meta_data_path = '%s/openstack/latest/meta_data.json' % config_drive if not os.path.exists(meta_data_path): return 0 meta_data = json.load(open(meta_data_path)) if 'public_keys' not in meta_data: return 0 keys_to_write = [] # if we have keys already there, we want to preserve them if os.path.exists('/root/.ssh/authorized_keys'): with open('/root/.ssh/authorized_keys', 'r') as fk: for line in fk: keys_to_write.append(line.strip()) for (name, key) in meta_data['public_keys'].items(): key_title = "# Injected key {name} by keypair extension".format( name=name) if key_title not in keys_to_write: keys_to_write.append(key_title) if key.rstrip() not in keys_to_write: keys_to_write.append(key) files_to_write = { '/root/.ssh/authorized_keys': '\n'.join(keys_to_write) + '\n', } try: os.mkdir(ssh_path, 0o700) except OSError as e: if e.errno != 17: # not File Exists raise finish_files(files_to_write, args) def set_hostname_from_config_drive(args): if args.noop: return config_drive = os.path.join(args.root, 'mnt/config') meta_data_path = '%s/openstack/latest/meta_data.json' % config_drive if not os.path.exists(meta_data_path): return meta_data = json.load(open(meta_data_path)) if 'name' not in meta_data: return hostname = meta_data['name'] log.debug("Got hostname from meta_data.json : %s" % hostname) # underscore is not a valid hostname, but it's easy to name your # host with that on the command-line. be helpful... if '_' in hostname: hostname = hostname.replace('_', '-') log.debug("Fixed up hostname to %s" % hostname) ret = subprocess.call(['hostname', hostname]) if ret != 0: raise RuntimeError('Error setting hostname') else: # gentoo's hostname file is in a different location if args.distro is 'gentoo': with open('/etc/conf.d/hostname', 'w') as fh: fh.write("hostname=\"{host}\"\n".format(host=hostname)) else: with safe_open('/etc/hostname', 'w') as fh: fh.write(hostname) fh.write('\n') # generate the lists of hosts and ips hosts_to_add = {'localhost': '127.0.0.1'} # get information on the network hostname_ip = '127.0.1.1' network_info = get_network_info(args) if network_info: interfaces = get_config_drive_interfaces(network_info) keys = sorted(interfaces.keys()) for key in keys: interface = interfaces[key] if interface and 'ip_address' in interface: hostname_ip = interface['ip_address'] break # check short hostname and generate list for hosts hosts_to_add[hostname] = hostname_ip short_hostname = hostname.split('.')[0] if short_hostname != hostname: hosts_to_add[short_hostname] = hostname_ip for host in hosts_to_add: host_value = hosts_to_add[host] # See if we already have a hosts entry for hostname if os.path.isfile('/etc/hosts'): with safe_open('/etc/hosts', 'r+') as fh: for line in fh: if line.startswith('%s %s' % (host_value, host)): break else: fh.write(u'%s %s\n' % (host_value, host)) else: with safe_open('/etc/hosts', 'a+') as fh: fh.write(u'%s %s\n' % (host_value, host)) def main(argv=None): if argv is None: args = sys.argv[1:] parser = argparse.ArgumentParser(description="Static network config") parser.add_argument( '-n', '--noop', action='store_true', help='Do not write files') _distro = distro.linux_distribution( full_distribution_name=False)[0].lower() parser.add_argument( '--distro', dest='distro', default=_distro, help='Override distro (detected "%s")' % _distro) parser.add_argument( '--root', dest='root', default='/', help='Mounted root for config drive info, defaults to /') parser.add_argument( '-i', '--interface', dest='interface', default=None, help="Interface to process") parser.add_argument( '--ssh', dest='ssh', action='store_true', help="Write ssh key") parser.add_argument( '--hostname', dest='hostname', action='store_true', help="Set the hostname if name is available in config drive.") parser.add_argument( '--skip-network', dest='skip', action='store_true', help="Do not write network info") parser.add_argument( '--use-nm', dest='use_nm', action='store_true', help=('Use NetworkManager instead of legacy' 'configuration scripts to manage interfaces')) parser.add_argument( '--skip-dns', dest='skip_dns', action='store_true', help='Do not write dns info') parser.add_argument( '--debug', dest='debug', action='store_true', help="Enable debugging output") args = parser.parse_args(argv) if args.debug: logging.basicConfig(level=logging.DEBUG) else: logging.basicConfig(level=logging.INFO) log.debug("Starting glean") log.debug("Detected distro : %s" % args.distro) log.debug("Configuring %s NetworkManager" % "with" if args.use_nm else "without") with systemlock.Lock('/tmp/glean.lock'): if args.ssh: write_ssh_keys(args) if args.hostname: set_hostname_from_config_drive(args) if args.interface != 'lo' and not args.skip: write_network_info_from_config_drive(args) log.debug("Done!") return 0 if __name__ == '__main__': sys.exit(main())