Apply IP/netmask/route/MTU changes without bouncing interfaces.

Modifications to impl_ifcfg.py to support making changes to IP
address, MTU, and routes live, rather than running ifdown/ifup on the
interface. Bouncing the interface is very disruptive, especially with
bridges where all the child interfaces are also bounced.

If only the IP address, netmask, MTU, routes, and/or boot status are
changed, the file will be written and 'ip' or 'ip route' commands
will be run to apply the new config on the fly. If any commands fail,
the interface will be restarted instead.

Closes-bug: 1795129
Change-Id: I75239fd3f273a88feb80f09a67aa5a947d64ac30
(cherry picked from commit c9bf347a23)
This commit is contained in:
Dan Sneddon 2018-09-29 04:28:14 -07:00 committed by Harald Jensås
parent e78d45f368
commit 6252d770ba
4 changed files with 574 additions and 70 deletions

View File

@ -16,6 +16,7 @@
import glob import glob
import logging import logging
import netaddr
import os import os
import re import re
@ -127,6 +128,183 @@ class IfcfgNetConfig(os_net_config.NetConfig):
self.bond_primary_ifaces = {} self.bond_primary_ifaces = {}
logger.info('Ifcfg net config provider created.') 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-<int> filename.
:type filename: string
:param new_data: The data for the new ifcfg-<int> 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-<int> filename.
:type filename: string
:param data: The data for the new ifcfg-<int> 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): def child_members(self, name):
children = set() children = set()
try: try:
@ -834,6 +1012,9 @@ class IfcfgNetConfig(os_net_config.NetConfig):
restart_linux_bonds = [] restart_linux_bonds = []
restart_linux_teams = [] restart_linux_teams = []
restart_vpp = False restart_vpp = False
apply_interfaces = []
apply_bridges = []
apply_routes = []
update_files = {} update_files = {}
all_file_names = [] all_file_names = []
ivs_uplinks = [] # ivs physical uplinks ivs_uplinks = [] # ivs physical uplinks
@ -844,6 +1025,7 @@ class IfcfgNetConfig(os_net_config.NetConfig):
ovs_needs_restart = False ovs_needs_restart = False
vpp_interfaces = self.vpp_interface_data.values() vpp_interfaces = self.vpp_interface_data.values()
vpp_bonds = self.vpp_bond_data.values() vpp_bonds = self.vpp_bond_data.values()
ipcmd = utils.iproute2_path()
for interface_name, iface_data in self.interface_data.items(): for interface_name, iface_data in self.interface_data.items():
route_data = self.route_data.get(interface_name, '') route_data = self.route_data.get(interface_name, '')
@ -858,25 +1040,31 @@ class IfcfgNetConfig(os_net_config.NetConfig):
ivs_uplinks.append(interface_name) ivs_uplinks.append(interface_name)
if "NFVSWITCH_BRIDGE" in iface_data: if "NFVSWITCH_BRIDGE" in iface_data:
nfvswitch_interfaces.append(interface_name) nfvswitch_interfaces.append(interface_name)
all_file_names.append(route6_path) if utils.diff(interface_path, iface_data):
if (utils.diff(interface_path, iface_data) or if self.ifcfg_requires_restart(interface_path, iface_data):
utils.diff(route_path, route_data) or restart_interfaces.append(interface_name)
utils.diff(route6_path, route6_data)): # Openvswitch needs to be restarted when OVSDPDKPort or
restart_interfaces.append(interface_name) # OVSDPDKBond is added
restart_interfaces.extend(self.child_members(interface_name)) 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[interface_path] = iface_data
update_files[route_path] = route_data
update_files[route6_path] = route6_data
if "BOOTPROTO=dhcp" not in iface_data: if "BOOTPROTO=dhcp" not in iface_data:
stop_dhclient_interfaces.append(interface_name) 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: else:
logger.info('No changes required for interface: %s' % logger.info('No changes required for interface: %s' %
interface_name) 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(): for interface_name, iface_data in self.ivsinterface_data.items():
route_data = self.route_data.get(interface_name, '') route_data = self.route_data.get(interface_name, '')
@ -888,16 +1076,24 @@ class IfcfgNetConfig(os_net_config.NetConfig):
all_file_names.append(route_path) all_file_names.append(route_path)
all_file_names.append(route6_path) all_file_names.append(route6_path)
ivs_interfaces.append(interface_name) ivs_interfaces.append(interface_name)
if (utils.diff(interface_path, iface_data) or if utils.diff(interface_path, iface_data):
utils.diff(route_path, route_data)): if self.ifcfg_requires_restart(interface_path, iface_data):
restart_interfaces.append(interface_name) restart_interfaces.append(interface_name)
restart_interfaces.extend(self.child_members(interface_name)) else:
apply_interfaces.append(
(interface_name, interface_path, iface_data))
update_files[interface_path] = iface_data update_files[interface_path] = iface_data
update_files[route_path] = route_data
update_files[route6_path] = route6_data
else: else:
logger.info('No changes required for ivs interface: %s' % logger.info('No changes required for ivs interface: %s' %
interface_name) 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(): for iface_name, iface_data in self.nfvswitch_intiface_data.items():
route_data = self.route_data.get(iface_name, '') route_data = self.route_data.get(iface_name, '')
@ -909,16 +1105,24 @@ class IfcfgNetConfig(os_net_config.NetConfig):
all_file_names.append(route_path) all_file_names.append(route_path)
all_file_names.append(route6_path) all_file_names.append(route6_path)
nfvswitch_internal_ifaces.append(iface_name) nfvswitch_internal_ifaces.append(iface_name)
if (utils.diff(iface_path, iface_data) or if utils.diff(iface_path, iface_data):
utils.diff(route_path, route_data)): if self.ifcfg_requires_restart(iface_path, iface_data):
restart_interfaces.append(iface_name) restart_interfaces.append(iface_name)
restart_interfaces.extend(self.child_members(iface_name)) else:
apply_interfaces.append(
(iface_name, iface_path, iface_data))
update_files[iface_path] = iface_data update_files[iface_path] = iface_data
update_files[route_path] = route_data
update_files[route6_path] = route6_data
else: else:
logger.info('No changes required for nfvswitch interface: %s' % logger.info('No changes required for nfvswitch interface: %s' %
iface_name) 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(): for vlan_name, vlan_data in self.vlan_data.items():
route_data = self.route_data.get(vlan_name, '') route_data = self.route_data.get(vlan_name, '')
@ -929,16 +1133,24 @@ class IfcfgNetConfig(os_net_config.NetConfig):
all_file_names.append(vlan_path) all_file_names.append(vlan_path)
all_file_names.append(vlan_route_path) all_file_names.append(vlan_route_path)
all_file_names.append(vlan_route6_path) all_file_names.append(vlan_route6_path)
if (utils.diff(vlan_path, vlan_data) or if utils.diff(vlan_path, vlan_data):
utils.diff(vlan_route_path, route_data)): if self.ifcfg_requires_restart(vlan_path, vlan_data):
restart_vlans.append(vlan_name) restart_vlans.append(vlan_name)
restart_vlans.extend(self.child_members(vlan_name)) else:
apply_interfaces.append(
(vlan_name, vlan_path, vlan_data))
update_files[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: else:
logger.info('No changes required for vlan interface: %s' % logger.info('No changes required for vlan interface: %s' %
vlan_name) 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(): for bridge_name, bridge_data in self.bridge_data.items():
route_data = self.route_data.get(bridge_name, '') route_data = self.route_data.get(bridge_name, '')
@ -949,20 +1161,28 @@ class IfcfgNetConfig(os_net_config.NetConfig):
all_file_names.append(bridge_path) all_file_names.append(bridge_path)
all_file_names.append(br_route_path) all_file_names.append(br_route_path)
all_file_names.append(br_route6_path) all_file_names.append(br_route6_path)
if (utils.diff(bridge_path, bridge_data) or if utils.diff(bridge_path, bridge_data):
utils.diff(br_route_path, route_data) or if self.ifcfg_requires_restart(bridge_path, bridge_data):
utils.diff(br_route6_path, route6_data)): restart_bridges.append(bridge_name)
restart_bridges.append(bridge_name) # Avoid duplicate interface being added to the restart list
# Avoid duplicate interface being added to the restart list children = self.child_members(bridge_name)
children = self.child_members(bridge_name) for child in children:
for child in children: if child not in restart_interfaces:
if child not in restart_interfaces: restart_interfaces.append(child)
restart_interfaces.append(child) else:
apply_bridges.append((bridge_name, bridge_path,
bridge_data))
update_files[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: else:
logger.info('No changes required for bridge: %s' % bridge_name) 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(): for bridge_name, bridge_data in self.linuxbridge_data.items():
route_data = self.route_data.get(bridge_name, '') route_data = self.route_data.get(bridge_name, '')
@ -973,16 +1193,28 @@ class IfcfgNetConfig(os_net_config.NetConfig):
all_file_names.append(bridge_path) all_file_names.append(bridge_path)
all_file_names.append(br_route_path) all_file_names.append(br_route_path)
all_file_names.append(br_route6_path) all_file_names.append(br_route6_path)
if (utils.diff(bridge_path, bridge_data) or if utils.diff(bridge_path, bridge_data):
utils.diff(br_route_path, route_data) or if self.ifcfg_requires_restart(bridge_path, bridge_data):
utils.diff(br_route6_path, route6_data)): restart_bridges.append(bridge_name)
restart_bridges.append(bridge_name) # Avoid duplicate interface being added to the restart list
restart_interfaces.extend(self.child_members(bridge_name)) 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[bridge_path] = bridge_data
update_files[br_route_path] = route_data
update_files[br_route6_path] = route6_data
else: else:
logger.info('No changes required for bridge: %s' % bridge_name) 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(): for team_name, team_data in self.linuxteam_data.items():
route_data = self.route_data.get(team_name, '') route_data = self.route_data.get(team_name, '')
@ -993,17 +1225,29 @@ class IfcfgNetConfig(os_net_config.NetConfig):
all_file_names.append(team_path) all_file_names.append(team_path)
all_file_names.append(team_route_path) all_file_names.append(team_route_path)
all_file_names.append(team_route6_path) all_file_names.append(team_route6_path)
if (utils.diff(team_path, team_data) or if utils.diff(team_path, team_data):
utils.diff(team_route_path, route_data) or if self.ifcfg_requires_restart(team_path, team_data):
utils.diff(team_route6_path, route6_data)): restart_linux_teams.append(team_name)
restart_linux_teams.append(team_name) # Avoid duplicate interface being added to the restart list
restart_interfaces.extend(self.child_members(team_name)) 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_path] = team_data
update_files[team_route_path] = route_data
update_files[team_route6_path] = route6_data
else: else:
logger.info('No changes required for linux team: %s' % logger.info('No changes required for linux team: %s' %
team_name) 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(): for bond_name, bond_data in self.linuxbond_data.items():
route_data = self.route_data.get(bond_name, '') route_data = self.route_data.get(bond_name, '')
@ -1014,17 +1258,29 @@ class IfcfgNetConfig(os_net_config.NetConfig):
all_file_names.append(bond_path) all_file_names.append(bond_path)
all_file_names.append(bond_route_path) all_file_names.append(bond_route_path)
all_file_names.append(bond_route6_path) all_file_names.append(bond_route6_path)
if (utils.diff(bond_path, bond_data) or if utils.diff(bond_path, bond_data):
utils.diff(bond_route_path, route_data) or if self.ifcfg_requires_restart(bond_path, bond_data):
utils.diff(bond_route6_path, route6_data)): restart_linux_bonds.append(bond_name)
restart_linux_bonds.append(bond_name) # Avoid duplicate interface being added to the restart list
restart_interfaces.extend(self.child_members(bond_name)) 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_path] = bond_data
update_files[bond_route_path] = route_data
update_files[bond_route6_path] = route6_data
else: else:
logger.info('No changes required for linux bond: %s' % logger.info('No changes required for linux bond: %s' %
bond_name) 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 # Infiniband interfaces are handled similarly to Ethernet interfaces
for interface_name, iface_data in self.ib_interface_data.items(): for interface_name, iface_data in self.ib_interface_data.items():
@ -1039,18 +1295,24 @@ class IfcfgNetConfig(os_net_config.NetConfig):
# TODO(dsneddon) determine if InfiniBand can be used with IVS # TODO(dsneddon) determine if InfiniBand can be used with IVS
if "IVS_BRIDGE" in iface_data: if "IVS_BRIDGE" in iface_data:
ivs_uplinks.append(interface_name) ivs_uplinks.append(interface_name)
all_file_names.append(route6_path) if utils.diff(interface_path, iface_data):
if (utils.diff(interface_path, iface_data) or if self.ifcfg_requires_restart(interface_path, iface_data):
utils.diff(route_path, route_data) or restart_interfaces.append(interface_name)
utils.diff(route6_path, route6_data)): else:
restart_interfaces.append(interface_name) apply_interfaces.append(
restart_interfaces.extend(self.child_members(interface_name)) (interface_name, interface_path, iface_data))
update_files[interface_path] = iface_data update_files[interface_path] = iface_data
update_files[route_path] = route_data
update_files[route6_path] = route6_data
else: else:
logger.info('No changes required for InfiniBand iface: %s' % logger.info('No changes required for InfiniBand iface: %s' %
interface_name) 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: if self.vpp_interface_data or self.vpp_bond_data:
vpp_path = self.root_dir + vpp_config_path() vpp_path = self.root_dir + vpp_config_path()
@ -1073,6 +1335,57 @@ class IfcfgNetConfig(os_net_config.NetConfig):
self.remove_config(ifcfg_file) self.remove_config(ifcfg_file)
if activate: 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: for vlan in restart_vlans:
self.ifdown(vlan) self.ifdown(vlan)

View File

@ -16,6 +16,7 @@
import os.path import os.path
import random import random
import shutil
import tempfile import tempfile
from oslo_concurrency import processutils from oslo_concurrency import processutils
@ -87,6 +88,50 @@ VLAN=yes
BOOTPROTO=none 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" _V4_IFCFG_MAPPED = _V4_IFCFG.replace('em1', 'nic1') + "HWADDR=a1:b2:c3:d4:e5\n"
@ -1597,6 +1642,7 @@ class TestIfcfgNetConfigApply(base.TestCase):
self.ifup_interface_names = [] self.ifup_interface_names = []
self.ovs_appctl_cmds = [] self.ovs_appctl_cmds = []
self.stop_dhclient_interfaces = [] self.stop_dhclient_interfaces = []
self.ip_reconfigure_commands = []
def test_ifcfg_path(name): def test_ifcfg_path(name):
return self.temp_ifcfg_file.name return self.temp_ifcfg_file.name
@ -1640,6 +1686,8 @@ class TestIfcfgNetConfigApply(base.TestCase):
self.ifup_interface_names.append(args[1]) self.ifup_interface_names.append(args[1])
elif args[0] == '/bin/ovs-appctl': elif args[0] == '/bin/ovs-appctl':
self.ovs_appctl_cmds.append(' '.join(args)) 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 pass
self.stub_out('oslo_concurrency.processutils.execute', test_execute) self.stub_out('oslo_concurrency.processutils.execute', test_execute)
@ -1742,6 +1790,125 @@ class TestIfcfgNetConfigApply(base.TestCase):
ovs_appctl_cmds = '/bin/ovs-appctl bond/set-active-slave bond1 em1' ovs_appctl_cmds = '/bin/ovs-appctl bond/set-active-slave bond1 em1'
self.assertIn(ovs_appctl_cmds, self.ovs_appctl_cmds) 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): def test_restart_children_on_change(self):
# setup and apply a bridge # setup and apply a bridge
interface = objects.Interface('em1') interface = objects.Interface('em1')

View File

@ -835,3 +835,15 @@ def is_ovs_installed():
ovs-bridges etc. ovs-bridges etc.
""" """
return os.path.exists("/usr/bin/ovs-appctl") 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

View File

@ -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.