Enable new routing rules on the fly without restarting interfaces

This change enables os-net-config to add and delete routing rules
without restarting interfaces. The iproute2 binary is called for
each rule that is added or deleted, and the updates happen without
the interface being restarted. The config files will be updated so
that on the next interface restart the changes will be permanent.
This change fixes bug 1865123.

Change-Id: I83f8b26000148a9b311ddf43cd423475373be404
Closes-bug: 1865123
(cherry picked from commit a007ca238b)
This commit is contained in:
Dan Sneddon 2020-03-10 12:03:13 -07:00 committed by Bob Fournier
parent 74d32121f6
commit 049314b09b
2 changed files with 110 additions and 0 deletions

View File

@ -170,6 +170,14 @@ class IfcfgNetConfig(os_net_config.NetConfig):
routes.append(line)
return routes
def parse_ifcfg_rules(self, ifcfg_data):
"""Break out the individual rules from an ifcfg rule file."""
rules = []
for line in ifcfg_data.split("\n"):
if not line.startswith("#"):
rules.append(line)
return rules
def enumerate_ifcfg_changes(self, ifcfg_data_old, ifcfg_data_new):
"""Determine which values are added/modified/removed
@ -208,6 +216,24 @@ class IfcfgNetConfig(os_net_config.NetConfig):
route_changes.append((route, 'added'))
return route_changes
def enumerate_ifcfg_rule_changes(self, old_rules, new_rules):
"""Determine which routes are added or removed.
:param file_values: contents of existing interface route rule file
:param data_values: contents of replacement interface route rule file
:return: list of tuples representing changes (rule, state), where
state is one of added or removed
"""
rule_changes = []
for rule in old_rules:
if rule not in new_rules:
rule_changes.append((rule, 'removed'))
for rule in new_rules:
if rule not in old_rules:
rule_changes.append((rule, 'added'))
return rule_changes
def ifcfg_requires_restart(self, filename, new_data):
"""Determine if changes to the ifcfg file require a restart to apply.
@ -327,6 +353,31 @@ class IfcfgNetConfig(os_net_config.NetConfig):
commands.append('route add ' + route[0])
return commands
def iproute2_rule_commands(self, filename, data):
"""Return a list of commands for 'ip route' to modify routing rules.
The list of commands is generated by comparing the old and new
configs, and calculating which rules need to be added and which
need to be removed.
:param filename: path to the original interface route rule file
:param data: data that is to be written to new route rule file
:return: list of commands to feed to 'ip' to reconfigure route rules
"""
file_values = self.parse_ifcfg_rules(utils.get_file_data(filename))
data_values = self.parse_ifcfg_rules(data)
rule_changes = self.enumerate_ifcfg_rule_changes(file_values,
data_values)
commands = []
for rule in rule_changes:
if rule[1] == 'removed':
commands.append('rule del ' + rule[0])
elif rule[1] == 'added':
commands.append('rule add ' + rule[0])
return commands
def child_members(self, name):
children = set()
try:
@ -1160,6 +1211,7 @@ class IfcfgNetConfig(os_net_config.NetConfig):
apply_interfaces = []
apply_bridges = []
apply_routes = []
apply_rules = []
update_files = {}
all_file_names = []
ivs_uplinks = [] # ivs physical uplinks
@ -1214,6 +1266,8 @@ class IfcfgNetConfig(os_net_config.NetConfig):
apply_routes.append((interface_name, route6_data))
if utils.diff(rule_path, rule_data):
update_files[rule_path] = rule_data
if interface_name not in restart_interfaces:
apply_rules.append((interface_name, rule_data))
for interface_name, iface_data in self.ivsinterface_data.items():
route_data = self.route_data.get(interface_name, '')
@ -1248,6 +1302,8 @@ class IfcfgNetConfig(os_net_config.NetConfig):
apply_routes.append((interface_name, route6_data))
if utils.diff(rule_path, rule_data):
update_files[rule_path] = rule_data
if interface_name not in restart_interfaces:
apply_rules.append((interface_name, rule_data))
for iface_name, iface_data in self.nfvswitch_intiface_data.items():
route_data = self.route_data.get(iface_name, '')
@ -1282,6 +1338,8 @@ class IfcfgNetConfig(os_net_config.NetConfig):
apply_routes.append((iface_name, route6_data))
if utils.diff(rule_path, rule_data):
update_files[rule_path] = rule_data
if iface_name not in restart_interfaces:
apply_rules.append((iface_name, rule_data))
for bridge_name, bridge_data in self.bridge_data.items():
route_data = self.route_data.get(bridge_name, '')
@ -1319,6 +1377,8 @@ class IfcfgNetConfig(os_net_config.NetConfig):
apply_routes.append((bridge_name, route6_data))
if utils.diff(br_rule_path, rule_data):
update_files[br_rule_path] = rule_data
if bridge_name not in restart_interfaces:
apply_rules.append((bridge_name, rule_data))
for bridge_name, bridge_data in self.linuxbridge_data.items():
route_data = self.route_data.get(bridge_name, '')
@ -1356,6 +1416,8 @@ class IfcfgNetConfig(os_net_config.NetConfig):
apply_routes.append((bridge_name, route6_data))
if utils.diff(br_rule_path, rule_data):
update_files[br_rule_path] = rule_data
if bridge_name not in restart_bridges:
apply_rules.append((bridge_name, rule_data))
for team_name, team_data in self.linuxteam_data.items():
route_data = self.route_data.get(team_name, '')
@ -1469,6 +1531,8 @@ class IfcfgNetConfig(os_net_config.NetConfig):
apply_routes.append((interface_name, route6_data))
if utils.diff(rule_path, rule_data):
update_files[rule_path] = rule_data
if interface_name not in restart_interfaces:
apply_rules.append((interface_name, rule_data))
# NOTE(hjensas): Process the VLAN's last so that we know if the vlan's
# parent interface is being restarted.
@ -1513,6 +1577,8 @@ class IfcfgNetConfig(os_net_config.NetConfig):
apply_routes.append((vlan_name, route6_data))
if utils.diff(vlan_rule_path, rule_data):
update_files[vlan_rule_path] = rule_data
if vlan_name not in restart_vlans:
apply_rules.append((vlan_name, rule_data))
if self.vpp_interface_data or self.vpp_bond_data:
vpp_path = self.root_dir + vpp_config_path()
@ -1589,6 +1655,24 @@ class IfcfgNetConfig(os_net_config.NetConfig):
self.child_members(interface[0]))
break
for interface in apply_rules:
logger.debug('Applying rules for interface %s' % interface[0])
filename = self.root_dir + route_rule_config_path(interface[0])
commands = self.iproute2_rule_commands(filename, interface[1])
for command in commands:
args = command.split()
try:
if len(args) > 0:
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 vlan in restart_vlans:
self.ifdown(vlan)

View File

@ -142,6 +142,14 @@ _IFCFG_ROUTES2 = """default via 192.0.1.1 dev eth0
192.0.1.1/24 via 192.0.3.1 dev eth1
"""
_IFCFG_RULES1 = """to 192.168.2.0/24 table main priority 500
from 192.168.2.0/24 table 200 priority 501
"""
_IFCFG_RULES2 = """to 192.168.1.0/24 table main priority 500
from 192.168.1.0/24 table 200 priority 501
"""
_V4_IFCFG_MAPPED = _V4_IFCFG.replace('em1', 'nic1') + "HWADDR=a1:b2:c3:d4:e5\n"
@ -2183,6 +2191,24 @@ class TestIfcfgNetConfigApply(base.TestCase):
_IFCFG_ROUTES2)
self.assertTrue(commands == command_list1)
def test_ifcfg_rule_commands(self):
tmpdir = tempfile.mkdtemp()
interface = "eth0"
rule_filename = tmpdir + '/rule-' + interface
file = open(rule_filename, 'w')
file.write(_IFCFG_RULES1)
file.close()
# Changing only the rules should delete and add rules
command_list1 = ['rule del to 192.168.2.0/24 table main priority 500',
'rule del from 192.168.2.0/24 table 200 priority 501',
'rule add to 192.168.1.0/24 table main priority 500',
'rule add from 192.168.1.0/24 table 200 priority 501']
commands = self.provider.iproute2_rule_commands(rule_filename,
_IFCFG_RULES2)
self.assertTrue(commands == command_list1)
def test_ifcfg_ipmtu_commands(self):
tmpdir = tempfile.mkdtemp()