Add Policy Based Routing Support to os-net-config

Add support for routing tables and rules, as well as a
property to specify a routing table for each route to
the impl_ifcfg implementation.

This change adds a new top-level object in the
network_config with type "route_table" to specify
which route table IDs and names should be added to
the /etc/iproute2/rt_table file.

This change adds a "table" property to routes for each
interface type, that allows the selection of which
routing table to apply the route to.

This change also adds a "rules" property to each
interface type, which can be used to specify a list
of rules. Each rule contains a rule string and an
optional comment, both of which will be added to the
/etc/sysconfig/network-scripts/rule-<iface> file.

This change includes tests and changes to the schema
for validation.

Note that this change was based on the abandoned patch
https://review.openstack.org/#/c/575712
and was recreated when that patch diverged too far
from master branch.

Change-Id: I6906d3b6923845af177faba6579fa255a2195bec
Closes-bug: #1783297
(cherry picked from commit 35832347e1)
This commit is contained in:
Dan Sneddon 2018-12-19 15:06:39 -08:00
parent 683650abeb
commit 18fa8b6946
10 changed files with 882 additions and 255 deletions

View File

@ -0,0 +1,52 @@
network_config:
-
type: route_table
name: custom
table_id: 200
-
type: route_table
name: alternate
table_id: 201
-
type: interface
name: em1
use_dhcp: false
addresses:
-
ip_netmask: 192.0.2.1/24
routes:
-
ip_netmask: 10.1.3.0/24
next_hop: 192.0.2.5
route_options: "metric 10"
table: 200 # Use table ID or table name
-
ip_netmask: 0.0.0.0/0
next_hop: 192.0.2.254
default: true
table: 200
rules:
- rule: "iif em1 table 200"
comment: "Route incoming traffic to em1 with table 200"
- rule: "from 192.0.2.0/24 table 200"
comment: "Route all traffic from 192.0.2.0/24 with table 200"
- rule: "add blackhole from 172.19.40.0/24 table 200"
- rule: "add unreachable iif em1 from 192.168.1.0/24"
-
type: interface
name: em2
use_dhcp: false
addresses:
- ip_netmask: 10.0.2.1/24
routes:
-
ip_netmask: 10.1.3.0/24
next_hop: 10.0.2.253
table: alternate # Use table ID or table name
-
default: true
next_hop: 10.0.3.254
route_options: "table alternate"
rules:
- rule: "iif em2 table alternate"
- rule: "from 10.0.2.0/24 table alternate"

View File

@ -50,6 +50,8 @@ class NetConfig(object):
:param obj: The object to add.
"""
if isinstance(obj, objects.RouteTable):
self.add_route_table(obj)
if isinstance(obj, objects.Interface):
self.add_interface(obj)
elif isinstance(obj, objects.Vlan):
@ -115,6 +117,13 @@ class NetConfig(object):
elif isinstance(obj, objects.ContrailVrouterDpdk):
self.add_contrail_vrouter_dpdk(obj)
def add_route_table(self, route_table):
"""Add a route table object to the net config object.
:param route_table: The RouteTable object to add.
"""
raise NotImplementedError("add_route_table is not implemented.")
def add_interface(self, interface):
"""Add an Interface object to the net config object.

View File

@ -245,8 +245,9 @@ def main(argv=sys.argv):
return 1
for iface_json in iface_array:
iface_json.update({'nic_mapping': iface_mapping})
iface_json.update({'persist_mapping': persist_mapping})
if iface_json.get('type') != 'route_table':
iface_json.update({'nic_mapping': iface_mapping})
iface_json.update({'persist_mapping': persist_mapping})
validation_errors = validator.validate_config(iface_array)
if validation_errors:

View File

@ -31,6 +31,17 @@ logger = logging.getLogger(__name__)
# Import the raw NetConfig object so we can call its methods
netconfig = os_net_config.NetConfig()
_ROUTE_TABLE_DEFAULT = """# reserved values
#
255\tlocal
254\tmain
253\tdefault
0\tunspec
#
# local
#
#1\tinr.ruhep\n"""
def ifcfg_config_path(name):
return "/etc/sysconfig/network-scripts/ifcfg-%s" % name
@ -68,6 +79,14 @@ def route6_config_path(name):
return "/etc/sysconfig/network-scripts/route6-%s" % name
def route_rule_config_path(name):
return "/etc/sysconfig/network-scripts/rule-%s" % name
def route_table_config_path():
return "/etc/iproute2/rt_tables"
def cleanup_pattern():
return "/etc/sysconfig/network-scripts/ifcfg-*"
@ -117,6 +136,8 @@ class IfcfgNetConfig(os_net_config.NetConfig):
self.vlan_data = {}
self.route_data = {}
self.route6_data = {}
self.route_table_data = {}
self.rule_data = {}
self.bridge_data = {}
self.linuxbridge_data = {}
self.linuxbond_data = {}
@ -603,33 +624,62 @@ class IfcfgNetConfig(os_net_config.NetConfig):
first_line6 = ""
for route in routes:
options = ""
table = ""
if route.route_options:
options = " %s" % (route.route_options)
options = " %s" % route.route_options
if route.route_table:
if route.route_options.find('table ') == -1:
table = " table %s" % route.route_table
if ":" not in route.next_hop:
# Route is an IPv4 route
if route.default:
first_line = "default via %s dev %s%s\n" % (
first_line = "default via %s dev %s%s%s\n" % (
route.next_hop, interface_name,
options)
table, options)
else:
data += "%s via %s dev %s%s\n" % (
data += "%s via %s dev %s%s%s\n" % (
route.ip_netmask, route.next_hop,
interface_name, options)
interface_name, table, options)
else:
# Route is an IPv6 route
if route.default:
first_line6 = "default via %s dev %s%s\n" % (
first_line6 = "default via %s dev %s%s%s\n" % (
route.next_hop, interface_name,
options)
table, options)
else:
data6 += "%s via %s dev %s%s\n" % (
data6 += "%s via %s dev %s%s%s\n" % (
route.ip_netmask, route.next_hop,
interface_name, options)
interface_name, table, options)
self.route_data[interface_name] = first_line + data
self.route6_data[interface_name] = first_line6 + data6
logger.debug('route data: %s' % self.route_data[interface_name])
logger.debug('ipv6 route data: %s' % self.route6_data[interface_name])
def _add_rules(self, interface, rules):
"""Add RouteRule objects to an interface.
:param interface: the name of the interface to apply rules.
:param rules: the list of rules to apply to the interface.
"""
logger.info('adding route rules for interface: %s' % interface)
data = ""
first_line = "# This file is autogenerated by os-net-config\n"
for rule in rules:
if rule.comment:
data += "# %s\n" % rule.comment
data += "%s\n" % rule.rule
self.rule_data[interface] = first_line + data
logger.debug('rules for interface: %s' % self.rule_data[interface])
def add_route_table(self, route_table):
"""Add a RouteTable object to the net config object.
:param route_table: the RouteTable object to add.
"""
logger.info('adding route table: %s %s' % (route_table.table_id,
route_table.name))
self.route_table_data[int(route_table.table_id)] = route_table.name
def add_interface(self, interface):
"""Add an Interface object to the net config object.
@ -641,6 +691,8 @@ class IfcfgNetConfig(os_net_config.NetConfig):
self.interface_data[interface.name] = data
if interface.routes:
self._add_routes(interface.name, interface.routes)
if interface.rules:
self._add_rules(interface.name, interface.rules)
if interface.renamed:
logger.info("Interface %s being renamed to %s"
@ -658,6 +710,8 @@ class IfcfgNetConfig(os_net_config.NetConfig):
self.vlan_data[vlan.name] = data
if vlan.routes:
self._add_routes(vlan.name, vlan.routes)
if vlan.rules:
self._add_rules(vlan.name, vlan.rules)
def add_ivs_interface(self, ivs_interface):
"""Add a ivs_interface object to the net config object.
@ -670,6 +724,8 @@ class IfcfgNetConfig(os_net_config.NetConfig):
self.ivsinterface_data[ivs_interface.name] = data
if ivs_interface.routes:
self._add_routes(ivs_interface.name, ivs_interface.routes)
if ivs_interface.rules:
self._add_rules(ivs_interface.name, ivs_interface.rules)
def add_nfvswitch_internal(self, nfvswitch_internal):
"""Add a nfvswitch_internal interface object to the net config object.
@ -683,6 +739,8 @@ class IfcfgNetConfig(os_net_config.NetConfig):
self.nfvswitch_intiface_data[iface_name] = data
if nfvswitch_internal.routes:
self._add_routes(iface_name, nfvswitch_internal.routes)
if nfvswitch_internal.rules:
self._add_rules(iface_name, nfvswitch_internal.rules)
def add_bridge(self, bridge):
"""Add an OvsBridge object to the net config object.
@ -695,6 +753,8 @@ class IfcfgNetConfig(os_net_config.NetConfig):
self.bridge_data[bridge.name] = data
if bridge.routes:
self._add_routes(bridge.name, bridge.routes)
if bridge.routes:
self._add_rules(bridge.name, bridge.rules)
def add_ovs_user_bridge(self, bridge):
"""Add an OvsUserBridge object to the net config object.
@ -707,6 +767,8 @@ class IfcfgNetConfig(os_net_config.NetConfig):
self.bridge_data[bridge.name] = data
if bridge.routes:
self._add_routes(bridge.name, bridge.routes)
if bridge.rules:
self._add_rules(bridge.name, bridge.rules)
def add_linux_bridge(self, bridge):
"""Add a LinuxBridge object to the net config object.
@ -719,6 +781,8 @@ class IfcfgNetConfig(os_net_config.NetConfig):
self.linuxbridge_data[bridge.name] = data
if bridge.routes:
self._add_routes(bridge.name, bridge.routes)
if bridge.rules:
self._add_rules(bridge.name, bridge.rules)
def add_ivs_bridge(self, bridge):
"""Add a IvsBridge object to the net config object.
@ -753,6 +817,8 @@ class IfcfgNetConfig(os_net_config.NetConfig):
self.interface_data[bond.name] = data
if bond.routes:
self._add_routes(bond.name, bond.routes)
if bond.rules:
self._add_rules(bond.name, bond.rules)
def add_linux_bond(self, bond):
"""Add a LinuxBond object to the net config object.
@ -765,6 +831,8 @@ class IfcfgNetConfig(os_net_config.NetConfig):
self.linuxbond_data[bond.name] = data
if bond.routes:
self._add_routes(bond.name, bond.routes)
if bond.rules:
self._add_rules(bond.name, bond.rules)
def add_linux_team(self, team):
"""Add a LinuxTeam object to the net config object.
@ -777,6 +845,8 @@ class IfcfgNetConfig(os_net_config.NetConfig):
self.linuxteam_data[team.name] = data
if team.routes:
self._add_routes(team.name, team.routes)
if team.rules:
self._add_rules(team.name, team.rules)
def add_ovs_tunnel(self, tunnel):
"""Add a OvsTunnel object to the net config object.
@ -809,6 +879,8 @@ class IfcfgNetConfig(os_net_config.NetConfig):
self.ib_interface_data[ib_interface.name] = data
if ib_interface.routes:
self._add_routes(ib_interface.name, ib_interface.routes)
if ib_interface.rules:
self._add_rules(ib_interface.name, ib_interface.rules)
if ib_interface.renamed:
logger.info("InfiniBand interface %s being renamed to %s"
@ -856,6 +928,8 @@ class IfcfgNetConfig(os_net_config.NetConfig):
self.interface_data[ovs_dpdk_bond.name] = data
if ovs_dpdk_bond.routes:
self._add_routes(ovs_dpdk_bond.name, ovs_dpdk_bond.routes)
if ovs_dpdk_bond.rules:
self._add_rules(ovs_dpdk_bond.name, ovs_dpdk_bond.rules)
def add_sriov_pf(self, sriov_pf):
"""Add a SriovPF object to the net config object
@ -882,6 +956,8 @@ class IfcfgNetConfig(os_net_config.NetConfig):
self.interface_data[sriov_vf.name] = data
if sriov_vf.routes:
self._add_routes(sriov_vf.name, sriov_vf.routes)
if sriov_vf.rules:
self._add_rules(sriov_vf.name, sriov_vf.rules)
def add_vpp_interface(self, vpp_interface):
"""Add a VppInterface object to the net config object
@ -926,8 +1002,9 @@ class IfcfgNetConfig(os_net_config.NetConfig):
logger.debug('contrail data: %s' % data)
self.interface_data[contrail_vrouter.name] = data
if contrail_vrouter.routes:
self._add_routes(contrail_vrouter.name,
contrail_vrouter.routes)
self._add_routes(contrail_vrouter.name, contrail_vrouter.routes)
if contrail_vrouter.rules:
self._add_rules(contrail_vrouter.name, contrail_vrouter.rules)
def add_contrail_vrouter_dpdk(self, contrail_vrouter_dpdk):
"""Add a ContraiVrouterDpdk object to the net config object
@ -956,6 +1033,9 @@ class IfcfgNetConfig(os_net_config.NetConfig):
if contrail_vrouter_dpdk.routes:
self._add_routes(contrail_vrouter_dpdk.name,
contrail_vrouter_dpdk.routes)
if contrail_vrouter_dpdk.rules:
self._add_rules(contrail_vrouter_dpdk.name,
contrail_vrouter_dpdk.rules)
def generate_ivs_config(self, ivs_uplinks, ivs_interfaces):
"""Generate configuration content for ivs."""
@ -1000,6 +1080,56 @@ class IfcfgNetConfig(os_net_config.NetConfig):
data = "SETUP_ARGS=\"%s%s%s\"" % (options_str, iface_str, internal_str)
return data
def generate_route_table_config(self, route_tables):
"""Generate configuration content for routing tables.
This method first extracts the existing route table definitions. If
any non-default tables exist, they will be kept unless they conflict
with new tables defined in the route_tables dict.
:param route_tables: A dict of RouteTable objects
"""
custom_tables = {}
res_ids = ['0', '253', '254', '255']
res_names = ['unspec', 'default', 'main', 'local']
rt_config = utils.get_file_data(route_table_config_path()).split('\n')
rt_defaults = _ROUTE_TABLE_DEFAULT.split("\n")
data = _ROUTE_TABLE_DEFAULT
for line in (line for line in rt_config if line not in rt_defaults):
# Leave non-standard comments intact in file
if line.startswith('#') and not line.strip() in rt_defaults:
data += "%s\n" % line
# Ignore old managed entries, will be added back if in new config.
elif line.find("# os-net-config managed table") == -1:
id_name = line.split()
# Keep custom tables if there is no conflict with new tables.
if id_name[0].isdigit() and len(id_name) > 1:
if not id_name[0] in res_ids:
if not id_name[1] in res_names:
if not int(id_name[0]) in route_tables:
if not id_name[1] in route_tables.values():
# Replicate line with any comments appended
custom_tables[id_name[0]] = id_name[1]
data += "%s\n" % line
if custom_tables:
logger.debug("Existing route tables: %s" % custom_tables)
for id in sorted(route_tables):
if str(id) in res_ids:
message = "Table %s(%s) conflicts with reserved table %s(%s)" \
% (route_tables[id], id,
res_names[res_ids.index(str(id))], id)
raise os_net_config.ConfigurationError(message)
elif route_tables[id] in res_names:
message = "Table %s(%s) conflicts with reserved table %s(%s)" \
% (route_tables[id], id, route_tables[id],
res_ids[res_names.index(route_tables[id])])
raise os_net_config.ConfigurationError(message)
else:
data += "%s\t%s # os-net-config managed table\n" \
% (id, route_tables[id])
return data
def apply(self, cleanup=False, activate=True):
"""Apply the network configuration.
@ -1040,9 +1170,11 @@ class IfcfgNetConfig(os_net_config.NetConfig):
for interface_name, iface_data in self.interface_data.items():
route_data = self.route_data.get(interface_name, '')
route6_data = self.route6_data.get(interface_name, '')
rule_data = self.rule_data.get(interface_name, '')
interface_path = self.root_dir + ifcfg_config_path(interface_name)
route_path = self.root_dir + route_config_path(interface_name)
route6_path = self.root_dir + route6_config_path(interface_name)
rule_path = self.root_dir + route_rule_config_path(interface_name)
all_file_names.append(interface_path)
all_file_names.append(route_path)
all_file_names.append(route6_path)
@ -1075,16 +1207,21 @@ class IfcfgNetConfig(os_net_config.NetConfig):
update_files[route6_path] = route6_data
if interface_name not in restart_interfaces:
apply_routes.append((interface_name, route6_data))
if utils.diff(rule_path, rule_data):
update_files[rule_path] = rule_data
for interface_name, iface_data in self.ivsinterface_data.items():
route_data = self.route_data.get(interface_name, '')
route6_data = self.route6_data.get(interface_name, '')
rule_data = self.rule_data.get(interface_name, '')
interface_path = self.root_dir + ifcfg_config_path(interface_name)
route_path = self.root_dir + route_config_path(interface_name)
route6_path = self.root_dir + route6_config_path(interface_name)
rule_path = self.root_dir + route_rule_config_path(interface_name)
all_file_names.append(interface_path)
all_file_names.append(route_path)
all_file_names.append(route6_path)
all_file_names.append(rule_path)
ivs_interfaces.append(interface_name)
if utils.diff(interface_path, iface_data):
if self.ifcfg_requires_restart(interface_path, iface_data):
@ -1104,16 +1241,21 @@ class IfcfgNetConfig(os_net_config.NetConfig):
update_files[route6_path] = route6_data
if interface_name not in restart_interfaces:
apply_routes.append((interface_name, route6_data))
if utils.diff(rule_path, rule_data):
update_files[rule_path] = rule_data
for iface_name, iface_data in self.nfvswitch_intiface_data.items():
route_data = self.route_data.get(iface_name, '')
route6_data = self.route6_data.get(iface_name, '')
rule_data = self.rule_data.get(iface_name, '')
iface_path = self.root_dir + ifcfg_config_path(iface_name)
route_path = self.root_dir + route_config_path(iface_name)
route6_path = self.root_dir + route6_config_path(iface_name)
rule_path = self.root_dir + route_rule_config_path(iface_name)
all_file_names.append(iface_path)
all_file_names.append(route_path)
all_file_names.append(route6_path)
all_file_names.append(rule_path)
nfvswitch_internal_ifaces.append(iface_name)
if utils.diff(iface_path, iface_data):
if self.ifcfg_requires_restart(iface_path, iface_data):
@ -1133,16 +1275,21 @@ class IfcfgNetConfig(os_net_config.NetConfig):
update_files[route6_path] = route6_data
if iface_name not in restart_interfaces:
apply_routes.append((iface_name, route6_data))
if utils.diff(rule_path, rule_data):
update_files[rule_path] = rule_data
for bridge_name, bridge_data in self.bridge_data.items():
route_data = self.route_data.get(bridge_name, '')
route6_data = self.route6_data.get(bridge_name, '')
rule_data = self.rule_data.get(bridge_name, '')
bridge_path = self.root_dir + bridge_config_path(bridge_name)
br_route_path = self.root_dir + route_config_path(bridge_name)
br_route6_path = self.root_dir + route6_config_path(bridge_name)
br_rule_path = self.root_dir + route_rule_config_path(bridge_name)
all_file_names.append(bridge_path)
all_file_names.append(br_route_path)
all_file_names.append(br_route6_path)
all_file_names.append(br_rule_path)
if utils.diff(bridge_path, bridge_data):
if self.ifcfg_requires_restart(bridge_path, bridge_data):
restart_bridges.append(bridge_name)
@ -1165,16 +1312,21 @@ class IfcfgNetConfig(os_net_config.NetConfig):
update_files[br_route6_path] = route6_data
if bridge_name not in restart_interfaces:
apply_routes.append((bridge_name, route6_data))
if utils.diff(br_rule_path, rule_data):
update_files[br_rule_path] = rule_data
for bridge_name, bridge_data in self.linuxbridge_data.items():
route_data = self.route_data.get(bridge_name, '')
route6_data = self.route6_data.get(bridge_name, '')
rule_data = self.rule_data.get(bridge_name, '')
bridge_path = self.root_dir + bridge_config_path(bridge_name)
br_route_path = self.root_dir + route_config_path(bridge_name)
br_route6_path = self.root_dir + route6_config_path(bridge_name)
br_rule_path = self.root_dir + route_rule_config_path(bridge_name)
all_file_names.append(bridge_path)
all_file_names.append(br_route_path)
all_file_names.append(br_route6_path)
all_file_names.append(br_rule_path)
if utils.diff(bridge_path, bridge_data):
if self.ifcfg_requires_restart(bridge_path, bridge_data):
restart_bridges.append(bridge_name)
@ -1197,16 +1349,21 @@ class IfcfgNetConfig(os_net_config.NetConfig):
update_files[route6_path] = route6_data
if bridge_name not in restart_bridges:
apply_routes.append((bridge_name, route6_data))
if utils.diff(br_rule_path, rule_data):
update_files[br_rule_path] = rule_data
for team_name, team_data in self.linuxteam_data.items():
route_data = self.route_data.get(team_name, '')
route6_data = self.route6_data.get(team_name, '')
rule_data = self.rule_data.get(team_name, '')
team_path = self.root_dir + bridge_config_path(team_name)
team_route_path = self.root_dir + route_config_path(team_name)
team_route6_path = self.root_dir + route6_config_path(team_name)
team_rule_path = self.root_dir + route_rule_config_path(team_name)
all_file_names.append(team_path)
all_file_names.append(team_route_path)
all_file_names.append(team_route6_path)
all_file_names.append(team_rule_path)
if utils.diff(team_path, team_data):
if self.ifcfg_requires_restart(team_path, team_data):
restart_linux_teams.append(team_name)
@ -1230,16 +1387,21 @@ class IfcfgNetConfig(os_net_config.NetConfig):
update_files[team_route6_path] = route6_data
if team_name not in restart_linux_teams:
apply_routes.append((team_name, route6_data))
if utils.diff(team_rule_path, rule_data):
update_files[team_rule_path] = rule_data
for bond_name, bond_data in self.linuxbond_data.items():
route_data = self.route_data.get(bond_name, '')
route6_data = self.route6_data.get(bond_name, '')
rule_data = self.rule_data.get(bond_name, '')
bond_path = self.root_dir + bridge_config_path(bond_name)
bond_route_path = self.root_dir + route_config_path(bond_name)
bond_route6_path = self.root_dir + route6_config_path(bond_name)
bond_rule_path = self.root_dir + route_rule_config_path(bond_name)
all_file_names.append(bond_path)
all_file_names.append(bond_route_path)
all_file_names.append(bond_route6_path)
all_file_names.append(bond_rule_path)
if utils.diff(bond_path, bond_data):
if self.ifcfg_requires_restart(bond_path, bond_data):
restart_linux_bonds.append(bond_name)
@ -1263,17 +1425,22 @@ class IfcfgNetConfig(os_net_config.NetConfig):
update_files[bond_route6_path] = route6_data
if bond_name not in restart_linux_bonds:
apply_routes.append((bond_name, route6_data))
if utils.diff(bond_rule_path, rule_data):
update_files[bond_rule_path] = rule_data
# Infiniband interfaces are handled similarly to Ethernet interfaces
for interface_name, iface_data in self.ib_interface_data.items():
route_data = self.route_data.get(interface_name, '')
route6_data = self.route6_data.get(interface_name, '')
rule_data = self.rule_data.get(interface_name, '')
interface_path = self.root_dir + ifcfg_config_path(interface_name)
route_path = self.root_dir + route_config_path(interface_name)
route6_path = self.root_dir + route6_config_path(interface_name)
rule_path = self.root_dir + route_rule_config_path(interface_name)
all_file_names.append(interface_path)
all_file_names.append(route_path)
all_file_names.append(route6_path)
all_file_names.append(rule_path)
# TODO(dsneddon) determine if InfiniBand can be used with IVS
if "IVS_BRIDGE" in iface_data:
ivs_uplinks.append(interface_name)
@ -1295,18 +1462,23 @@ class IfcfgNetConfig(os_net_config.NetConfig):
update_files[route6_path] = route6_data
if interface_name not in restart_interfaces:
apply_routes.append((interface_name, route6_data))
if utils.diff(rule_path, rule_data):
update_files[rule_path] = rule_data
# NOTE(hjensas): Process the VLAN's last so that we know if the vlan's
# parent interface is being restarted.
for vlan_name, vlan_data in self.vlan_data.items():
route_data = self.route_data.get(vlan_name, '')
route6_data = self.route6_data.get(vlan_name, '')
rule_data = self.rule_data.get(vlan_name, '')
vlan_path = self.root_dir + ifcfg_config_path(vlan_name)
vlan_route_path = self.root_dir + route_config_path(vlan_name)
vlan_route6_path = self.root_dir + route6_config_path(vlan_name)
vlan_rule_path = self.root_dir + route_rule_config_path(vlan_name)
all_file_names.append(vlan_path)
all_file_names.append(vlan_route_path)
all_file_names.append(vlan_route6_path)
all_file_names.append(vlan_rule_path)
restarts_concatenated = itertools.chain(restart_interfaces,
restart_bridges,
restart_linux_bonds,
@ -1334,6 +1506,8 @@ class IfcfgNetConfig(os_net_config.NetConfig):
update_files[vlan_route6_path] = route6_data
if vlan_name not in restart_vlans:
apply_routes.append((vlan_name, route6_data))
if utils.diff(vlan_rule_path, rule_data):
update_files[vlan_rule_path] = rule_data
if self.vpp_interface_data or self.vpp_bond_data:
vpp_path = self.root_dir + vpp_config_path()
@ -1444,6 +1618,11 @@ class IfcfgNetConfig(os_net_config.NetConfig):
for location, data in update_files.items():
self.write_config(location, data)
if self.route_table_data:
location = route_table_config_path()
data = self.generate_route_table_config(self.route_table_data)
self.write_config(location, data)
if ivs_uplinks or ivs_interfaces:
location = ivs_config_path()
data = self.generate_ivs_config(ivs_uplinks, ivs_interfaces)

File diff suppressed because it is too large Load Diff

View File

@ -142,7 +142,8 @@ definitions:
route:
type: object
properties:
oneOf:
- properties:
next_hop:
$ref: "#/definitions/ip_address_string_or_param"
ip_netmask:
@ -151,15 +152,51 @@ definitions:
$ref: "#/definitions/bool_or_param"
route_options:
$ref: "#/definitions/string_or_param"
required:
- next_hop
additionalProperties: False
table:
oneOf:
- $ref: "#/definitions/string_or_param"
- $ref: "#/definitions/int_or_param"
requires:
- next_hop
additionalProperties: False
- properties:
nexthop:
$ref: "#/definitions/ip_address_string_or_param"
destination:
$ref: "#/definitions/ip_cidr_string_or_param"
default:
$ref: "#/definitions/bool_or_param"
route_options:
$ref: "#/definitions/string_or_param"
table:
oneOf:
- $ref: "#/definitions/string_or_param"
- $ref: "#/definitions/int_or_param"
requires:
- nexthop
additionalProperties: False
list_of_route:
type: array
items:
$ref: "#/definitions/route"
minItems: 1
route_rule:
type: object
properties:
rule:
$ref: "#/definitions/string_or_param"
comment:
$ref: "#/definitions/string_or_param"
required:
- rule
additionalProperties: False
list_of_rule:
type: array
items:
$ref: "#/definitions/route_rule"
minItems: 1
nic_mapping:
type: ["object", "null"]
@ -234,6 +271,8 @@ definitions:
$ref: "#/definitions/list_of_address"
routes:
$ref: "#/definitions/list_of_route"
rules:
$ref: "#/definitions/list_of_rule"
mtu:
$ref: "#/definitions/int_or_param"
nic_mapping:
@ -257,6 +296,23 @@ definitions:
- name
additionalProperties: False
route_table:
type: object
properties:
type:
enum: ["route_table"]
name:
$ref: "#/definitions/string_or_param"
table_id:
oneOf:
- $ref: "#/definitions/int_or_param"
- $ref: "#/definitions/string_or_param"
required:
- type
- name
- table_id
additionalProperties: False
sriov_pf:
type: object
properties:
@ -281,6 +337,8 @@ definitions:
$ref: "#/definitions/list_of_address"
routes:
$ref: "#/definitions/list_of_route"
rules:
$ref: "#/definitions/list_of_rule"
mtu:
$ref: "#/definitions/int_or_param"
nic_mapping:
@ -343,6 +401,8 @@ definitions:
$ref: "#/definitions/list_of_address"
routes:
$ref: "#/definitions/list_of_route"
rules:
$ref: "#/definitions/list_of_rule"
mtu:
$ref: "#/definitions/int_or_param"
nic_mapping:
@ -387,6 +447,8 @@ definitions:
$ref: "#/definitions/list_of_address"
routes:
$ref: "#/definitions/list_of_route"
rules:
$ref: "#/definitions/list_of_rule"
mtu:
$ref: "#/definitions/int_or_param"
nic_mapping:
@ -448,6 +510,8 @@ definitions:
$ref: "#/definitions/list_of_address"
routes:
$ref: "#/definitions/list_of_route"
rules:
$ref: "#/definitions/list_of_rule"
mtu:
$ref: "#/definitions/int_or_param"
nic_mapping:
@ -505,6 +569,8 @@ definitions:
$ref: "#/definitions/list_of_address"
routes:
$ref: "#/definitions/list_of_route"
rules:
$ref: "#/definitions/list_of_rule"
mtu:
$ref: "#/definitions/int_or_param"
nic_mapping:
@ -558,6 +624,8 @@ definitions:
$ref: "#/definitions/list_of_address"
routes:
$ref: "#/definitions/list_of_route"
rules:
$ref: "#/definitions/list_of_rule"
mtu:
$ref: "#/definitions/int_or_param"
nic_mapping:
@ -608,6 +676,8 @@ definitions:
$ref: "#/definitions/list_of_address"
routes:
$ref: "#/definitions/list_of_route"
rules:
$ref: "#/definitions/list_of_rule"
mtu:
$ref: "#/definitions/int_or_param"
nic_mapping:
@ -657,6 +727,8 @@ definitions:
$ref: "#/definitions/list_of_address"
routes:
$ref: "#/definitions/list_of_route"
rules:
$ref: "#/definitions/list_of_rule"
mtu:
$ref: "#/definitions/int_or_param"
nic_mapping:
@ -707,6 +779,8 @@ definitions:
$ref: "#/definitions/list_of_address"
routes:
$ref: "#/definitions/list_of_route"
rules:
$ref: "#/definitions/list_of_rule"
mtu:
$ref: "#/definitions/int_or_param"
nic_mapping:
@ -765,6 +839,8 @@ definitions:
$ref: "#/definitions/list_of_address"
routes:
$ref: "#/definitions/list_of_route"
rules:
$ref: "#/definitions/list_of_rule"
mtu:
$ref: "#/definitions/int_or_param"
nic_mapping:
@ -809,6 +885,8 @@ definitions:
$ref: "#/definitions/list_of_address"
routes:
$ref: "#/definitions/list_of_route"
rules:
$ref: "#/definitions/list_of_rule"
mtu:
$ref: "#/definitions/int_or_param"
nic_mapping:
@ -852,6 +930,8 @@ definitions:
$ref: "#/definitions/list_of_address"
routes:
$ref: "#/definitions/list_of_route"
rules:
$ref: "#/definitions/list_of_rule"
mtu:
$ref: "#/definitions/int_or_param"
nic_mapping:
@ -904,6 +984,8 @@ definitions:
$ref: "#/definitions/list_of_address"
routes:
$ref: "#/definitions/list_of_route"
rules:
$ref: "#/definitions/list_of_rule"
mtu:
$ref: "#/definitions/int_or_param"
nic_mapping:
@ -961,6 +1043,8 @@ definitions:
$ref: "#/definitions/list_of_address"
routes:
$ref: "#/definitions/list_of_route"
rules:
$ref: "#/definitions/list_of_rule"
mtu:
$ref: "#/definitions/int_or_param"
nic_mapping:
@ -1004,6 +1088,8 @@ definitions:
$ref: "#/definitions/list_of_address"
routes:
$ref: "#/definitions/list_of_route"
rules:
$ref: "#/definitions/list_of_rule"
mtu:
$ref: "#/definitions/int_or_param"
nic_mapping:
@ -1054,6 +1140,8 @@ definitions:
$ref: "#/definitions/list_of_address"
routes:
$ref: "#/definitions/list_of_route"
rules:
$ref: "#/definitions/list_of_rule"
mtu:
$ref: "#/definitions/int_or_param"
nic_mapping:
@ -1102,6 +1190,8 @@ definitions:
$ref: "#/definitions/list_of_address"
routes:
$ref: "#/definitions/list_of_route"
rules:
$ref: "#/definitions/list_of_rule"
mtu:
$ref: "#/definitions/int_or_param"
nic_mapping:
@ -1146,6 +1236,8 @@ definitions:
$ref: "#/definitions/list_of_address"
routes:
$ref: "#/definitions/list_of_route"
rules:
$ref: "#/definitions/list_of_rule"
mtu:
$ref: "#/definitions/int_or_param"
nic_mapping:
@ -1189,6 +1281,8 @@ definitions:
$ref: "#/definitions/list_of_address"
routes:
$ref: "#/definitions/list_of_route"
rules:
$ref: "#/definitions/list_of_rule"
mtu:
$ref: "#/definitions/int_or_param"
nic_mapping:
@ -1235,6 +1329,8 @@ definitions:
$ref: "#/definitions/list_of_address"
routes:
$ref: "#/definitions/list_of_route"
rules:
$ref: "#/definitions/list_of_rule"
mtu:
$ref: "#/definitions/int_or_param"
nic_mapping:
@ -1279,6 +1375,8 @@ definitions:
$ref: "#/definitions/list_of_address"
routes:
$ref: "#/definitions/list_of_route"
rules:
$ref: "#/definitions/list_of_rule"
mtu:
$ref: "#/definitions/int_or_param"
nic_mapping:
@ -1322,6 +1420,8 @@ definitions:
$ref: "#/definitions/list_of_address"
routes:
$ref: "#/definitions/list_of_route"
rules:
$ref: "#/definitions/list_of_rule"
mtu:
$ref: "#/definitions/int_or_param"
nic_mapping:
@ -1349,6 +1449,7 @@ type: array
items:
oneOf:
- $ref: "#/definitions/interface"
- $ref: "#/definitions/route_table"
- $ref: "#/definitions/sriov_pf"
- $ref: "#/definitions/sriov_vf"
- $ref: "#/definitions/vlan"

View File

@ -194,6 +194,33 @@ _ROUTES = """default via 192.168.1.1 dev em1 metric 10
172.20.0.0/24 via 192.168.1.5 dev em1 metric 100
"""
_ROUTES_WITH_TABLES = """172.19.0.0/24 via 192.168.1.1 dev em1 table table1
172.20.0.0/24 via 192.168.1.1 dev em1 table 201
172.21.0.0/24 via 192.168.1.1 dev em1 table 200
"""
_ROUTE_RULES = """# This file is autogenerated by os-net-config
# test comment
from 192.0.2.0/24 table 200
"""
_RT_DEFAULT = """# reserved values
#
255\tlocal
254\tmain
253\tdefault
0\tunspec
#
# local
#
#1\tinr.ruhep\n"""
_RT_CUSTOM = _RT_DEFAULT + "# Custom\n10\tcustom # Custom table\n20\ttable1\n"
_RT_FULL = _RT_DEFAULT + """# Custom
10\tcustom # os-net-config managed table
200\ttable1 # os-net-config managed table\n"""
_ROUTES_V6 = """default via 2001:db8::1 dev em1
2001:db8:dead:beef:cafe::/56 via fd00:fd00:2000::1 dev em1
2001:db8:dead:beff::/64 via fd00:fd00:2000::1 dev em1 metric 100
@ -539,6 +566,12 @@ class TestIfcfgNetConfig(base.TestCase):
def get_route_config(self, name='em1'):
return self.provider.route_data.get(name, '')
def get_route_table_config(self, name='custom', table_id=200):
return self.provider.route_table_data.get(name, table_id)
def get_rule_config(self, name='em1'):
return self.provider.rule_data.get(name)
def get_route6_config(self, name='em1'):
return self.provider.route6_data.get(name, '')
@ -554,6 +587,35 @@ class TestIfcfgNetConfig(base.TestCase):
if 'em1' in ifname:
return "0000:00:01.0"
def test_add_route_table(self):
route_table1 = objects.RouteTable('table1', 200)
route_table2 = objects.RouteTable('table2', '201')
self.provider.add_route_table(route_table1)
self.provider.add_route_table(route_table2)
self.assertEqual("table1", self.get_route_table_config(200))
self.assertEqual("table2", self.get_route_table_config(201))
def test_add_route_with_table(self):
route_rule1 = objects.RouteRule('from 192.0.2.0/24 table 200',
'test comment')
# Test route table by name
route1 = objects.Route('192.168.1.1', '172.19.0.0/24', False,
route_table="table1")
# Test that table specified in route_options takes precedence
route2 = objects.Route('192.168.1.1', '172.20.0.0/24', False,
'table 201', route_table=200)
# Test route table specified by integer ID
route3 = objects.Route('192.168.1.1', '172.21.0.0/24', False,
route_table=200)
v4_addr = objects.Address('192.168.1.2/24')
interface = objects.Interface('em1', addresses=[v4_addr],
routes=[route1, route2, route3],
rules=[route_rule1])
self.provider.add_interface(interface)
self.assertEqual(_V4_IFCFG, self.get_interface_config())
self.assertEqual(_ROUTES_WITH_TABLES, self.get_route_config())
self.assertEqual(_ROUTE_RULES, self.get_rule_config())
def test_add_base_interface(self):
interface = objects.Interface('em1')
self.provider.add_interface(interface)
@ -1687,6 +1749,8 @@ class TestIfcfgNetConfigApply(base.TestCase):
self.temp_bond_file = tempfile.NamedTemporaryFile()
self.temp_route_file = tempfile.NamedTemporaryFile()
self.temp_route6_file = tempfile.NamedTemporaryFile()
self.temp_route_table_file = tempfile.NamedTemporaryFile()
self.temp_rule_file = tempfile.NamedTemporaryFile()
self.temp_bridge_file = tempfile.NamedTemporaryFile()
self.temp_cleanup_file = tempfile.NamedTemporaryFile(delete=False)
self.ifup_interface_names = []
@ -1716,6 +1780,18 @@ class TestIfcfgNetConfigApply(base.TestCase):
self.stub_out(
'os_net_config.impl_ifcfg.route6_config_path', test_routes6_path)
def test_rules_path(name):
return self.temp_rule_file.name
self.stub_out(
'os_net_config.impl_ifcfg.route_rule_config_path', test_rules_path)
def test_route_table_path():
return self.temp_route_table_file.name
self.stub_out(
'os_net_config.impl_ifcfg.route_table_config_path',
test_route_table_path)
utils.write_config(self.temp_route_table_file.name, _RT_CUSTOM)
def test_bridge_path(name):
return self.temp_bridge_file.name
self.stub_out(
@ -1752,6 +1828,40 @@ class TestIfcfgNetConfigApply(base.TestCase):
self.temp_cleanup_file.close()
super(TestIfcfgNetConfigApply, self).tearDown()
def test_route_table_apply(self):
# Test overriding an existing route table
route_table1 = objects.RouteTable('custom', 10)
self.provider.add_route_table(route_table1)
route_table2 = objects.RouteTable('table1', 200)
self.provider.add_route_table(route_table2)
def test_route_table_path():
return self.temp_route_table_file.name
self.stub_out(
'os_net_config.impl_ifcfg.route_table_config_path',
test_route_table_path)
utils.write_config(self.temp_route_table_file.name, _RT_CUSTOM)
self.provider.apply()
route_table_data = utils.get_file_data(
self.temp_route_table_file.name)
self.assertEqual(_RT_FULL, route_table_data)
def test_reserved_route_table_id(self):
route_table1 = objects.RouteTable('table1', 253)
self.provider.add_route_table(route_table1)
self.assertRaises(os_net_config.ConfigurationError,
self.provider.apply)
def test_reserved_route_table_name(self):
route_table2 = objects.RouteTable('default', 200)
self.provider.add_route_table(route_table2)
self.assertRaises(os_net_config.ConfigurationError,
self.provider.apply)
def test_network_apply(self):
route1 = objects.Route('192.168.1.1', default=True,
route_options="metric 10")

View File

@ -30,12 +30,13 @@ class TestRoute(base.TestCase):
def test_from_json(self):
data = '{"next_hop": "172.19.0.1", "ip_netmask": "172.19.0.0/24", ' \
'"route_options": "metric 10"}'
'"route_options": "metric 10", "table": "200"}'
route = objects.Route.from_json(json.loads(data))
self.assertEqual("172.19.0.1", route.next_hop)
self.assertEqual("172.19.0.0/24", route.ip_netmask)
self.assertFalse(route.default)
self.assertEqual("metric 10", route.route_options)
self.assertEqual("200", route.route_table)
def test_from_json_default_route(self):
data = '{"next_hop": "172.19.0.1", "ip_netmask": "172.19.0.0/24", ' \
@ -55,6 +56,45 @@ class TestRoute(base.TestCase):
self.assertEqual("metric 10", route.route_options)
class TestRouteTable(base.TestCase):
def test_from_json(self):
data = '{"type": "route_table", "name": "custom", "table_id": 200}'
route_table = objects.RouteTable.from_json(json.loads(data))
self.assertEqual("custom", route_table.name)
self.assertEqual(200, route_table.table_id)
def test_from_json_invalid(self):
self.assertRaises(objects.InvalidConfigException,
objects.RouteTable.from_json,
{})
data = '{"type": "route_table", "table_id": 200}'
json_data = json.loads(data)
self.assertRaises(objects.InvalidConfigException,
objects.RouteTable.from_json,
json_data)
data = '{"type": "route_table", "name": "custom"}'
json_data = json.loads(data)
self.assertRaises(objects.InvalidConfigException,
objects.RouteTable.from_json,
json_data)
class TestRouteRule(base.TestCase):
def test_rule(self):
rule1 = objects.RouteRule('from 192.0.2.0/24 table 200 prio 1000')
self.assertEqual('from 192.0.2.0/24 table 200 prio 1000', rule1.rule)
def test_rule_from_json(self):
data = '{"rule":"from 172.19.0.0/24 table 200", "comment":"test"}'
route_rule = objects.RouteRule.from_json(json.loads(data))
self.assertEqual("from 172.19.0.0/24 table 200", route_rule.rule)
self.assertEqual("test", route_rule.comment)
class TestAddress(base.TestCase):
def test_ipv4_address(self):

View File

@ -211,6 +211,28 @@ class TestDerivedTypes(base.TestCase):
self.assertFalse(v.is_valid([]))
self.assertFalse(v.is_valid(None))
def test_route_table(self):
schema = validator.get_schema_for_defined_type("route_table")
v = jsonschema.Draft4Validator(schema)
data = {"type": "route_table", "name": "custom", "table_id": "20"}
self.assertTrue(v.is_valid(data))
data["unkown_property"] = "value"
self.assertFalse(v.is_valid(data))
self.assertFalse(v.is_valid({}))
self.assertFalse(v.is_valid([]))
self.assertFalse(v.is_valid(None))
def test_route_rule(self):
schema = validator.get_schema_for_defined_type("route_rule")
v = jsonschema.Draft4Validator(schema)
data = {"rule": "iif em2 table 20"}
self.assertTrue(v.is_valid(data))
data["unkown_property"] = "value"
self.assertFalse(v.is_valid(data))
self.assertFalse(v.is_valid({}))
self.assertFalse(v.is_valid([]))
self.assertFalse(v.is_valid(None))
class TestDeviceTypes(base.TestCase):

View File

@ -0,0 +1,10 @@
---
features:
- |
Support for configuring policy-based routing has been added. A new
top-level object "route_table" has been added, which allows the user to
add tables to the system route table at /etc/iproute2/rt_tables. Routes
have a new "table" property for specifying which table to apply the route.
Interfaces now have a "rules" property that allows the user to add
arbitrary rules for when the system should use a particular routing table,
such as input interface or source IP address.