diff --git a/os_net_config/impl_ifcfg.py b/os_net_config/impl_ifcfg.py index 3b14de00..d3dc3d57 100644 --- a/os_net_config/impl_ifcfg.py +++ b/os_net_config/impl_ifcfg.py @@ -16,6 +16,7 @@ import glob import logging +import netaddr import os import re @@ -127,6 +128,183 @@ class IfcfgNetConfig(os_net_config.NetConfig): self.bond_primary_ifaces = {} logger.info('Ifcfg net config provider created.') + def parse_ifcfg(self, ifcfg_data): + """Break out the key/value pairs from ifcfg_data + + Return the keys and values without quotes. + """ + ifcfg_values = {} + for line in ifcfg_data.split("\n"): + if not line.startswith("#") and line.find("=") > 0: + k, v = line.split("=", 1) + ifcfg_values[k] = v.strip("\"'") + return ifcfg_values + + def parse_ifcfg_routes(self, ifcfg_data): + """Break out the individual routes from an ifcfg route file.""" + routes = [] + for line in ifcfg_data.split("\n"): + if not line.startswith("#"): + routes.append(line) + return routes + + def enumerate_ifcfg_changes(self, ifcfg_data_old, ifcfg_data_new): + """Determine which values are added/modified/removed + + :param ifcfg_data_old: content of existing ifcfg file + :param ifcfg_data_new: content of replacement ifcfg file + :return: dict of changed values and states (added, removed, modified) + """ + + changed_values = {} + for key in ifcfg_data_old: + if key in ifcfg_data_new: + if ifcfg_data_old[key].upper() != ifcfg_data_new[key].upper(): + changed_values[key] = "modified" + else: + changed_values[key] = "removed" + for key in ifcfg_data_new: + if key not in ifcfg_data_old: + changed_values[key] = "added" + return changed_values + + def enumerate_ifcfg_route_changes(self, old_routes, new_routes): + """Determine which routes are added or removed. + + :param file_values: contents of existing interface route file + :param data_values: contents of replacement interface route file + :return: list of tuples representing changes (route, state), where + state is one of added or removed + """ + + route_changes = [] + for route in old_routes: + if route not in new_routes: + route_changes.append((route, 'removed')) + for route in new_routes: + if route not in old_routes: + route_changes.append((route, 'added')) + return route_changes + + def ifcfg_requires_restart(self, filename, new_data): + """Determine if changes to the ifcfg file require a restart to apply. + + Simple changes like IP, MTU, and routes can be directly applied + without restarting the interface. + + :param filename: The ifcfg- filename. + :type filename: string + :param new_data: The data for the new ifcfg- file. + :type new_data: string + :returns: boolean value for whether a restart is required + """ + + file_data = utils.get_file_data(filename) + logger.debug("Original ifcfg file:\n%s" % file_data) + logger.debug("New ifcfg file:\n%s" % new_data) + file_values = self.parse_ifcfg(file_data) + new_values = self.parse_ifcfg(new_data) + restart_required = False + # Certain changes can be applied without restarting the interface + permitted_changes = [ + "IPADDR", + "NETMASK", + "MTU", + "ONBOOT" + ] + # Check whether any of the changes require restart + for change in self.enumerate_ifcfg_changes(file_values, new_values): + if change not in permitted_changes: + # Moving to DHCP requires restarting interface + if change in ["BOOTPROTO", "OVSBOOTPROTO"]: + if change in new_values: + if (new_values[change].upper() == "DHCP"): + restart_required = True + logger.debug( + "DHCP on %s requires restart" % change) + else: + restart_required = True + if not restart_required: + logger.debug("Changes do not require restart") + return restart_required + + def iproute2_apply_commands(self, device_name, filename, data): + """Return list of commands needed to implement changes. + + Given ifcfg data for an interface, return commands required to + apply the configuration using 'ip' commands. + + :param device_name: The name of the int, bridge, or bond + :type device_name: string + :param filename: The ifcfg- filename. + :type filename: string + :param data: The data for the new ifcfg- file. + :type data: string + :returns: commands (commands to be run) + """ + + previous_cfg = utils.get_file_data(filename) + file_values = self.parse_ifcfg(previous_cfg) + data_values = self.parse_ifcfg(data) + logger.debug("File values:\n%s" % file_values) + logger.debug("Data values:\n%s" % data_values) + changes = self.enumerate_ifcfg_changes(file_values, data_values) + commands = [] + new_cidr = 0 + old_cidr = 0 + # Convert dot notation netmask to CIDR length notation + if "NETMASK" in file_values: + netmask = file_values["NETMASK"] + old_cidr = netaddr.IPAddress(netmask).netmask_bits() + if "NETMASK" in data_values: + netmask = data_values["NETMASK"] + new_cidr = netaddr.IPAddress(netmask).netmask_bits() + if "IPADDR" in changes: + if changes["IPADDR"] == "removed" or changes[ + "IPADDR"] == "modified": + if old_cidr: + commands.append("addr del %s/%s dev %s" % + (file_values["IPADDR"], old_cidr, + device_name)) + else: + # Cannot remove old IP specifically if netmask not known + commands.append("addr flush dev %s" % device_name) + if changes["IPADDR"] == "added" or changes["IPADDR"] == "modified": + commands.insert(0, "addr add %s/%s dev %s" % + (data_values["IPADDR"], new_cidr, device_name)) + if "MTU" in changes: + if changes["MTU"] == "added" or changes["MTU"] == "modified": + commands.append("link set dev %s mtu %s" % + (device_name, data_values["MTU"])) + elif changes["MTU"] == "removed": + commands.append("link set dev %s mtu 1500" % device_name) + return commands + + def iproute2_route_commands(self, filename, data): + """Return a list of commands for 'ip route' to modify routing table. + + The list of commands is generated by comparing the old and new + configs, and calculating which routes need to be added and which + need to be removed. + + :param filename: path to the original interface route file + :param data: data that is to be written to new route file + :return: list of commands to feed to 'ip' to reconfigure routes + """ + + file_values = self.parse_ifcfg_routes(utils.get_file_data(filename)) + data_values = self.parse_ifcfg_routes(data) + route_changes = self.enumerate_ifcfg_route_changes(file_values, + data_values) + commands = [] + + for route in route_changes: + if route[1] == 'removed': + commands.append('route del ' + route[0]) + elif route[1] == 'added': + commands.append('route add ' + route[0]) + return commands + def child_members(self, name): children = set() try: @@ -840,6 +1018,9 @@ class IfcfgNetConfig(os_net_config.NetConfig): restart_linux_bonds = [] restart_linux_teams = [] restart_vpp = False + apply_interfaces = [] + apply_bridges = [] + apply_routes = [] update_files = {} all_file_names = [] ivs_uplinks = [] # ivs physical uplinks @@ -850,6 +1031,7 @@ class IfcfgNetConfig(os_net_config.NetConfig): ovs_needs_restart = False vpp_interfaces = self.vpp_interface_data.values() vpp_bonds = self.vpp_bond_data.values() + ipcmd = utils.iproute2_path() for interface_name, iface_data in self.interface_data.items(): route_data = self.route_data.get(interface_name, '') @@ -864,25 +1046,31 @@ class IfcfgNetConfig(os_net_config.NetConfig): ivs_uplinks.append(interface_name) if "NFVSWITCH_BRIDGE" in iface_data: nfvswitch_interfaces.append(interface_name) - all_file_names.append(route6_path) - if (utils.diff(interface_path, iface_data) or - utils.diff(route_path, route_data) or - utils.diff(route6_path, route6_data)): - restart_interfaces.append(interface_name) - restart_interfaces.extend(self.child_members(interface_name)) + if utils.diff(interface_path, iface_data): + if self.ifcfg_requires_restart(interface_path, iface_data): + restart_interfaces.append(interface_name) + # Openvswitch needs to be restarted when OVSDPDKPort or + # OVSDPDKBond is added + if "OVSDPDK" in iface_data: + ovs_needs_restart = True + else: + apply_interfaces.append( + (interface_name, interface_path, iface_data)) update_files[interface_path] = iface_data - update_files[route_path] = route_data - update_files[route6_path] = route6_data if "BOOTPROTO=dhcp" not in iface_data: stop_dhclient_interfaces.append(interface_name) - # Openvswitch needs to be restarted when OVSDPDKPort or - # OVSDPDKBond is added - if "OVSDPDK" in iface_data: - ovs_needs_restart = True else: logger.info('No changes required for interface: %s' % interface_name) + if utils.diff(route_path, route_data): + update_files[route_path] = route_data + if interface_name not in restart_interfaces: + apply_routes.append((interface_name, route_data)) + if utils.diff(route6_path, route6_data): + update_files[route6_path] = route6_data + if interface_name not in restart_interfaces: + apply_routes.append((interface_name, route6_data)) for interface_name, iface_data in self.ivsinterface_data.items(): route_data = self.route_data.get(interface_name, '') @@ -894,16 +1082,24 @@ class IfcfgNetConfig(os_net_config.NetConfig): all_file_names.append(route_path) all_file_names.append(route6_path) ivs_interfaces.append(interface_name) - if (utils.diff(interface_path, iface_data) or - utils.diff(route_path, route_data)): - restart_interfaces.append(interface_name) - restart_interfaces.extend(self.child_members(interface_name)) + if utils.diff(interface_path, iface_data): + if self.ifcfg_requires_restart(interface_path, iface_data): + restart_interfaces.append(interface_name) + else: + apply_interfaces.append( + (interface_name, interface_path, iface_data)) update_files[interface_path] = iface_data - update_files[route_path] = route_data - update_files[route6_path] = route6_data else: logger.info('No changes required for ivs interface: %s' % interface_name) + if utils.diff(route_path, route_data): + update_files[route_path] = route_data + if interface_name not in restart_interfaces: + apply_routes.append((interface_name, route_data)) + if utils.diff(route6_path, route6_data): + update_files[route6_path] = route6_data + if interface_name not in restart_interfaces: + apply_routes.append((interface_name, route6_data)) for iface_name, iface_data in self.nfvswitch_intiface_data.items(): route_data = self.route_data.get(iface_name, '') @@ -915,16 +1111,24 @@ class IfcfgNetConfig(os_net_config.NetConfig): all_file_names.append(route_path) all_file_names.append(route6_path) nfvswitch_internal_ifaces.append(iface_name) - if (utils.diff(iface_path, iface_data) or - utils.diff(route_path, route_data)): - restart_interfaces.append(iface_name) - restart_interfaces.extend(self.child_members(iface_name)) + if utils.diff(iface_path, iface_data): + if self.ifcfg_requires_restart(iface_path, iface_data): + restart_interfaces.append(iface_name) + else: + apply_interfaces.append( + (iface_name, iface_path, iface_data)) update_files[iface_path] = iface_data - update_files[route_path] = route_data - update_files[route6_path] = route6_data else: logger.info('No changes required for nfvswitch interface: %s' % iface_name) + if utils.diff(route_path, route_data): + update_files[route_path] = route_data + if iface_name not in restart_interfaces: + apply_routes.append((iface_name, route_data)) + if utils.diff(route6_path, route6_data): + update_files[route6_path] = route6_data + if iface_name not in restart_interfaces: + apply_routes.append((iface_name, route6_data)) for vlan_name, vlan_data in self.vlan_data.items(): route_data = self.route_data.get(vlan_name, '') @@ -935,16 +1139,24 @@ class IfcfgNetConfig(os_net_config.NetConfig): all_file_names.append(vlan_path) all_file_names.append(vlan_route_path) all_file_names.append(vlan_route6_path) - if (utils.diff(vlan_path, vlan_data) or - utils.diff(vlan_route_path, route_data)): - restart_vlans.append(vlan_name) - restart_vlans.extend(self.child_members(vlan_name)) + if utils.diff(vlan_path, vlan_data): + if self.ifcfg_requires_restart(vlan_path, vlan_data): + restart_vlans.append(vlan_name) + else: + apply_interfaces.append( + (vlan_name, vlan_path, vlan_data)) update_files[vlan_path] = vlan_data - update_files[vlan_route_path] = route_data - update_files[vlan_route6_path] = route6_data else: logger.info('No changes required for vlan interface: %s' % vlan_name) + if utils.diff(vlan_route_path, route_data): + update_files[vlan_route_path] = route_data + if vlan_name not in restart_vlans: + apply_routes.append((vlan_name, route_data)) + if utils.diff(vlan_route6_path, route6_data): + update_files[vlan_route6_path] = route6_data + if vlan_name not in restart_vlans: + apply_routes.append((vlan_name, route6_data)) for bridge_name, bridge_data in self.bridge_data.items(): route_data = self.route_data.get(bridge_name, '') @@ -955,20 +1167,28 @@ class IfcfgNetConfig(os_net_config.NetConfig): all_file_names.append(bridge_path) all_file_names.append(br_route_path) all_file_names.append(br_route6_path) - if (utils.diff(bridge_path, bridge_data) or - utils.diff(br_route_path, route_data) or - utils.diff(br_route6_path, route6_data)): - restart_bridges.append(bridge_name) - # Avoid duplicate interface being added to the restart list - children = self.child_members(bridge_name) - for child in children: - if child not in restart_interfaces: - restart_interfaces.append(child) + if utils.diff(bridge_path, bridge_data): + if self.ifcfg_requires_restart(bridge_path, bridge_data): + restart_bridges.append(bridge_name) + # Avoid duplicate interface being added to the restart list + children = self.child_members(bridge_name) + for child in children: + if child not in restart_interfaces: + restart_interfaces.append(child) + else: + apply_bridges.append((bridge_name, bridge_path, + bridge_data)) update_files[bridge_path] = bridge_data - update_files[br_route_path] = route_data - update_files[br_route6_path] = route6_data else: logger.info('No changes required for bridge: %s' % bridge_name) + if utils.diff(br_route_path, route_data): + update_files[br_route_path] = route_data + if bridge_name not in restart_interfaces: + apply_routes.append((bridge_name, route_data)) + if utils.diff(br_route6_path, route6_data): + update_files[br_route6_path] = route6_data + if bridge_name not in restart_interfaces: + apply_routes.append((bridge_name, route6_data)) for bridge_name, bridge_data in self.linuxbridge_data.items(): route_data = self.route_data.get(bridge_name, '') @@ -979,16 +1199,28 @@ class IfcfgNetConfig(os_net_config.NetConfig): all_file_names.append(bridge_path) all_file_names.append(br_route_path) all_file_names.append(br_route6_path) - if (utils.diff(bridge_path, bridge_data) or - utils.diff(br_route_path, route_data) or - utils.diff(br_route6_path, route6_data)): - restart_bridges.append(bridge_name) - restart_interfaces.extend(self.child_members(bridge_name)) + if utils.diff(bridge_path, bridge_data): + if self.ifcfg_requires_restart(bridge_path, bridge_data): + restart_bridges.append(bridge_name) + # Avoid duplicate interface being added to the restart list + children = self.child_members(bridge_name) + for child in children: + if child not in restart_interfaces: + restart_interfaces.append(child) + else: + apply_bridges.append((bridge_name, bridge_path, + bridge_data)) update_files[bridge_path] = bridge_data - update_files[br_route_path] = route_data - update_files[br_route6_path] = route6_data else: logger.info('No changes required for bridge: %s' % bridge_name) + if utils.diff(br_route_path, route_data): + update_files[br_route_path] = route_data + if bridge_name not in restart_bridges: + apply_routes.append((bridge_name, route_data)) + if utils.diff(route6_path, route6_data): + update_files[route6_path] = route6_data + if bridge_name not in restart_bridges: + apply_routes.append((bridge_name, route6_data)) for team_name, team_data in self.linuxteam_data.items(): route_data = self.route_data.get(team_name, '') @@ -999,17 +1231,29 @@ class IfcfgNetConfig(os_net_config.NetConfig): all_file_names.append(team_path) all_file_names.append(team_route_path) all_file_names.append(team_route6_path) - if (utils.diff(team_path, team_data) or - utils.diff(team_route_path, route_data) or - utils.diff(team_route6_path, route6_data)): - restart_linux_teams.append(team_name) - restart_interfaces.extend(self.child_members(team_name)) + if utils.diff(team_path, team_data): + if self.ifcfg_requires_restart(team_path, team_data): + restart_linux_teams.append(team_name) + # Avoid duplicate interface being added to the restart list + children = self.child_members(team_name) + for child in children: + if child not in restart_interfaces: + restart_interfaces.append(child) + else: + apply_interfaces.append( + (team_name, team_path, team_data)) update_files[team_path] = team_data - update_files[team_route_path] = route_data - update_files[team_route6_path] = route6_data else: logger.info('No changes required for linux team: %s' % team_name) + if utils.diff(team_route_path, route_data): + update_files[team_route_path] = route_data + if team_name not in restart_linux_teams: + apply_routes.append((team_name, route_data)) + if utils.diff(team_route6_path, route6_data): + update_files[team_route6_path] = route6_data + if team_name not in restart_linux_teams: + apply_routes.append((team_name, route6_data)) for bond_name, bond_data in self.linuxbond_data.items(): route_data = self.route_data.get(bond_name, '') @@ -1020,17 +1264,29 @@ class IfcfgNetConfig(os_net_config.NetConfig): all_file_names.append(bond_path) all_file_names.append(bond_route_path) all_file_names.append(bond_route6_path) - if (utils.diff(bond_path, bond_data) or - utils.diff(bond_route_path, route_data) or - utils.diff(bond_route6_path, route6_data)): - restart_linux_bonds.append(bond_name) - restart_interfaces.extend(self.child_members(bond_name)) + if utils.diff(bond_path, bond_data): + if self.ifcfg_requires_restart(bond_path, bond_data): + restart_linux_bonds.append(bond_name) + # Avoid duplicate interface being added to the restart list + children = self.child_members(bond_name) + for child in children: + if child not in restart_interfaces: + restart_interfaces.append(child) + else: + apply_interfaces.append( + (bond_name, bond_path, bond_data)) update_files[bond_path] = bond_data - update_files[bond_route_path] = route_data - update_files[bond_route6_path] = route6_data else: logger.info('No changes required for linux bond: %s' % bond_name) + if utils.diff(bond_route_path, route_data): + update_files[bond_route_path] = route_data + if bond_name not in restart_linux_bonds: + apply_routes.append((bond_name, route_data)) + if utils.diff(bond_route6_path, route6_data): + update_files[bond_route6_path] = route6_data + if bond_name not in restart_linux_bonds: + apply_routes.append((bond_name, route6_data)) # Infiniband interfaces are handled similarly to Ethernet interfaces for interface_name, iface_data in self.ib_interface_data.items(): @@ -1045,18 +1301,24 @@ class IfcfgNetConfig(os_net_config.NetConfig): # TODO(dsneddon) determine if InfiniBand can be used with IVS if "IVS_BRIDGE" in iface_data: ivs_uplinks.append(interface_name) - all_file_names.append(route6_path) - if (utils.diff(interface_path, iface_data) or - utils.diff(route_path, route_data) or - utils.diff(route6_path, route6_data)): - restart_interfaces.append(interface_name) - restart_interfaces.extend(self.child_members(interface_name)) + if utils.diff(interface_path, iface_data): + if self.ifcfg_requires_restart(interface_path, iface_data): + restart_interfaces.append(interface_name) + else: + apply_interfaces.append( + (interface_name, interface_path, iface_data)) update_files[interface_path] = iface_data - update_files[route_path] = route_data - update_files[route6_path] = route6_data else: logger.info('No changes required for InfiniBand iface: %s' % interface_name) + if utils.diff(route_path, route_data): + update_files[route_path] = route_data + if interface_name not in restart_interfaces: + apply_routes.append((interface_name, route_data)) + if utils.diff(route6_path, route6_data): + update_files[route6_path] = route6_data + if interface_name not in restart_interfaces: + apply_routes.append((interface_name, route6_data)) if self.vpp_interface_data or self.vpp_bond_data: vpp_path = self.root_dir + vpp_config_path() @@ -1079,6 +1341,57 @@ class IfcfgNetConfig(os_net_config.NetConfig): self.remove_config(ifcfg_file) if activate: + for interface in apply_interfaces: + logger.debug('Running ip commands on interface: %s' % + interface[0]) + commands = self.iproute2_apply_commands(interface[0], + interface[1], + interface[2]) + for command in commands: + try: + args = command.split() + self.execute('Running ip %s' % command, ipcmd, *args) + except Exception as e: + logger.warning("Error in 'ip %s', restarting %s:\n%s" % + (command, interface[0], str(e))) + restart_interfaces.append(interface[0]) + restart_interfaces.extend( + self.child_members(interface[0])) + break + + for bridge in apply_bridges: + logger.debug('Running ip commands on bridge: %s' % + interface[0]) + commands = self.iproute2_apply_commands(interface[0], + interface[1], + interface[2]) + for command in commands: + try: + args = command.split() + self.execute('Running ip %s' % command, ipcmd, *args) + except Exception as e: + logger.warning("Error in 'ip %s', restarting %s:\n%s" % + (command, interface[0], str(e))) + restart_bridges.append(interface[0]) + restart_interfaces.extend( + self.child_members(interface[0])) + break + + for interface in apply_routes: + logger.debug('Applying routes for interface %s' % interface[0]) + commands = self.iproute2_route_commands(interface[0], + interface[1]) + for command in commands: + try: + self.execute('Running ip %s' % command, ipcmd, command) + except Exception as e: + logger.warning("Error in 'ip %s', restarting %s:\n%s" % + (command, interface[0], str(e))) + restart_interfaces.append(interface[0]) + restart_interfaces.extend( + self.child_members(interface[0])) + break + for vlan in restart_vlans: self.ifdown(vlan) diff --git a/os_net_config/tests/test_impl_ifcfg.py b/os_net_config/tests/test_impl_ifcfg.py index 410a00f0..17d0410b 100644 --- a/os_net_config/tests/test_impl_ifcfg.py +++ b/os_net_config/tests/test_impl_ifcfg.py @@ -15,6 +15,7 @@ # under the License. import os.path +import shutil import tempfile from oslo_concurrency import processutils @@ -86,6 +87,50 @@ VLAN=yes BOOTPROTO=none """ +_IFCFG_DHCP = """# This file is autogenerated by os-net-config +DEVICE="eth0" +BOOTPROTO="dhcp" +ONBOOT="yes" +TYPE="Ethernet" +""" +_IFCFG_STATIC1 = """# This file is autogenerated by os-net-config +DEVICE=eth0 +BOOTPROTO=static +IPADDR=10.0.0.1 +NETMASK=255.255.255.0 +TYPE=Ethernet +ONBOOT=yes +""" + +_IFCFG_STATIC1_MTU = _IFCFG_STATIC1 + "\nMTU=9000" + +_IFCFG_STATIC2 = """DEVICE=eth0 +BOOTPROTO=static +IPADDR=10.0.1.2 +NETMASK=255.255.254.0 +TYPE=Ethernet +ONBOOT=yes +""" + +_IFCFG_STATIC2_MTU = _IFCFG_STATIC2 + "\nMTU=9000" + +_IFCFG_OVS = """DEVICE=eth0 +ONBOOT=yes +DEVICETYPE=ovs +TYPE=OVSPort +OVS_BRIDGE=brctlplane +BOOTPROTO=none +HOTPLUG=no +""" + +_IFCFG_ROUTES1 = """default via 192.0.2.1 dev eth0 +192.0.2.1/24 via 192.0.2.1 dev eth0 +""" + +_IFCFG_ROUTES2 = """default via 192.0.1.1 dev eth0 +192.0.1.1/24 via 192.0.3.1 dev eth1 +""" + _V4_IFCFG_MAPPED = _V4_IFCFG.replace('em1', 'nic1') + "HWADDR=a1:b2:c3:d4:e5\n" @@ -1413,6 +1458,7 @@ class TestIfcfgNetConfigApply(base.TestCase): self.ifup_interface_names = [] self.ovs_appctl_cmds = [] self.stop_dhclient_interfaces = [] + self.ip_reconfigure_commands = [] def test_ifcfg_path(name): return self.temp_ifcfg_file.name @@ -1451,6 +1497,8 @@ class TestIfcfgNetConfigApply(base.TestCase): self.ifup_interface_names.append(args[1]) elif args[0] == '/bin/ovs-appctl': self.ovs_appctl_cmds.append(' '.join(args)) + elif args[0] == '/sbin/ip' or args[0] == '/usr/sbin/ip': + self.ip_reconfigure_commands.append(' '.join(args[1:])) pass self.stubs.Set(processutils, 'execute', test_execute) @@ -1548,6 +1596,125 @@ class TestIfcfgNetConfigApply(base.TestCase): ovs_appctl_cmds = '/bin/ovs-appctl bond/set-active-slave bond1 em1' self.assertIn(ovs_appctl_cmds, self.ovs_appctl_cmds) + def test_reconfigure_and_apply(self): + route1 = objects.Route('192.168.1.1', default=True) + route2 = objects.Route('192.168.1.1', '172.19.0.0/24') + v4_addr1 = objects.Address('192.168.1.2/24') + interface1 = objects.Interface('em1', addresses=[v4_addr1], + routes=[route1, route2]) + self.provider.add_interface(interface1) + self.provider.apply() + self.assertIn('em1', self.ifup_interface_names) + + expected_commands = ['addr add 192.168.0.2/23 dev em1', + 'addr del 192.168.1.2/24 dev em1', + 'link set dev em1 mtu 9000'] + + v4_addr2 = objects.Address('192.168.0.2/23') + interface2 = objects.Interface('em1', addresses=[v4_addr2], + routes=[route1, route2], mtu=9000) + self.provider.add_interface(interface2) + self.provider.apply() + self.assertEqual(expected_commands, self.ip_reconfigure_commands) + + self.ip_reconfigure_commands = [] + expected_commands = ['addr add 192.168.1.2/24 dev em1', + 'addr del 192.168.0.2/23 dev em1', + 'link set dev em1 mtu 1500', + 'route add default via 192.168.0.1 dev em1', + 'route add 172.19.0.0/24 via 192.168.0.1 dev em1'] + + route3 = objects.Route('192.168.0.1', default=True) + route4 = objects.Route('192.168.0.1', '172.19.0.0/24') + interface3 = objects.Interface('em1', addresses=[v4_addr1], + routes=[route3, route4]) + self.provider.add_interface(interface3) + self.provider.apply() + self.assertEqual(expected_commands, self.ip_reconfigure_commands) + + def test_change_restart_required(self): + + tmpdir = tempfile.mkdtemp() + interface = "eth0" + interface_filename = tmpdir + '/ifcfg-' + interface + file = open(interface_filename, 'w') + file.write(_IFCFG_DHCP) + file.close() + + # Changing a dhcp interface to static should not require restart + self.assertFalse( + self.provider.ifcfg_requires_restart(interface_filename, + _IFCFG_STATIC1)) + + # Changing a standard interface to ovs should require restart + self.assertTrue( + self.provider.ifcfg_requires_restart(interface_filename, + _IFCFG_OVS)) + + # Changing a static interface to dhcp should require restart + file = open(interface_filename, 'w') + file.write(_IFCFG_STATIC1) + file.close() + self.assertTrue(self.provider.ifcfg_requires_restart( + interface_filename, _IFCFG_DHCP)) + + # Configuring a previously unconfigured interface requires restart + self.assertTrue(self.provider.ifcfg_requires_restart('/doesnotexist', + _IFCFG_DHCP)) + + shutil.rmtree(tmpdir) + + def test_ifcfg_route_commands(self): + + tmpdir = tempfile.mkdtemp() + interface = "eth0" + interface_filename = tmpdir + '/route-' + interface + file = open(interface_filename, 'w') + file.write(_IFCFG_ROUTES1) + file.close() + + # Changing only the routes should delete and add routes + command_list1 = ['route del default via 192.0.2.1 dev eth0', + 'route del 192.0.2.1/24 via 192.0.2.1 dev eth0', + 'route add default via 192.0.1.1 dev eth0', + 'route add 192.0.1.1/24 via 192.0.3.1 dev eth1'] + commands = self.provider.iproute2_route_commands(interface_filename, + _IFCFG_ROUTES2) + self.assertTrue(commands == command_list1) + + def test_ifcfg_ipmtu_commands(self): + + tmpdir = tempfile.mkdtemp() + interface = "eth0" + interface_filename = tmpdir + '/ifcfg-' + interface + file = open(interface_filename, 'w') + file.write(_IFCFG_STATIC1) + file.close() + + # Changing only the IP should delete and add the IP + command_list1 = ['addr add 10.0.1.2/23 dev eth0', + 'addr del 10.0.0.1/24 dev eth0'] + commands = self.provider.iproute2_apply_commands(interface, + interface_filename, + _IFCFG_STATIC2) + self.assertTrue(commands == command_list1) + + # Changing only the MTU should just set the interface MTU + command_list2 = ['link set dev eth0 mtu 9000'] + commands = self.provider.iproute2_apply_commands(interface, + interface_filename, + _IFCFG_STATIC1_MTU) + self.assertTrue(commands == command_list2) + + # Changing both the IP and MTU should delete IP, add IP, and set MTU + command_list3 = ['addr add 10.0.1.2/23 dev eth0', + 'addr del 10.0.0.1/24 dev eth0', + 'link set dev eth0 mtu 9000'] + commands = self.provider.iproute2_apply_commands(interface, + interface_filename, + _IFCFG_STATIC2_MTU) + self.assertTrue(commands == command_list3) + def test_restart_children_on_change(self): # setup and apply a bridge interface = objects.Interface('em1') diff --git a/os_net_config/utils.py b/os_net_config/utils.py index 1613f7e3..a80050a1 100644 --- a/os_net_config/utils.py +++ b/os_net_config/utils.py @@ -757,3 +757,26 @@ def update_vpp_mapping(vpp_interfaces, vpp_bonds): # Enable VPP service to make the VPP interface configuration # persistent. processutils.execute('systemctl', 'enable', 'vpp') + + +def is_ovs_installed(): + """Check if OpenVswitch is installed + + Verify that OpenVswitch is installed by checking if + ovs-appctl is on the system. If OVS is not installed + it will limit os-net-config's ability to set up ovs-bonds, + ovs-bridges etc. + """ + return os.path.exists("/usr/bin/ovs-appctl") + + +def iproute2_path(): + """Find 'ip' executable.""" + if os.access('/sbin/ip', os.X_OK): + ipcmd = '/sbin/ip' + elif os.access('/usr/sbin/ip', os.X_OK): + ipcmd = '/usr/sbin/ip' + else: + logger.warning("Could not execute /sbin/ip or /usr/sbin/ip") + return False + return ipcmd diff --git a/releasenotes/notes/modify-interface-without-restart-d55949572017d52f.yaml b/releasenotes/notes/modify-interface-without-restart-d55949572017d52f.yaml new file mode 100644 index 00000000..4dc25454 --- /dev/null +++ b/releasenotes/notes/modify-interface-without-restart-d55949572017d52f.yaml @@ -0,0 +1,12 @@ +--- +features: + - | + Some changes can now be made to interfaces without restarting. Changes to + routes, IP addresses, netmask, or MTU will now be applied using iproute2 + without restarting the interface, and the ifcfg file will be updated. +other: + - | + Since this change uses iproute2 to make changes to live interfaces, it + does not allow MTU on DPDK interfaces to be modified in place. DPDK + requires that ovs-vsctl be run to modify MTU. For DPDK interfaces, MTU + changes will result in an interface restart.