Merge "Ubuntu: support policy-based routing in systemd-networkd"

This commit is contained in:
Zuul 2021-04-23 11:50:29 +00:00 committed by Gerrit Code Review
commit 8eb7e053db
7 changed files with 166 additions and 16 deletions

View File

@ -4,6 +4,15 @@
when: resolv_is_managed | bool
become: True
- name: Ensure IP routing tables are defined for iproute2
become: true
blockinfile:
dest: /etc/iproute2/rt_tables
block: |
{% for table in network_route_tables %}
{{ table.id }} {{ table.name }}
{% endfor %}
- name: Remove netplan.io packages
become: true
package:

View File

@ -67,8 +67,10 @@ supported:
table to which the route will be added. ``options`` is a list of option
strings to add to the route.
``rules``
List of IP routing rules. Each item should be an ``iproute2`` IP routing
rule.
List of IP routing rules.
On CentOS, each item should be a string describing an ``iproute2`` IP
routing rule.
``physical_network``
Name of the physical network on which this network exists. This aligns with
the physical network concept in neutron.
@ -258,8 +260,14 @@ Configuring IP Routing Policy Rules
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
IP routing policy rules may be configured by setting the ``rules`` attribute
for a network to a list of rules. The format of a rule is the string which
would be appended to ``ip rule <add|del>`` to create or delete the rule.
for a network to a list of rules. The format of each rule currently differs
between CentOS and Ubuntu.
CentOS
""""""
The format of a rule is the string which would be appended to ``ip rule
<add|del>`` to create or delete the rule.
To configure a network called ``example`` with an IP routing policy rule to
handle traffic from the subnet ``10.1.0.0/24`` using the routing table
@ -273,6 +281,25 @@ handle traffic from the subnet ``10.1.0.0/24`` using the routing table
These rules will be configured on all hosts to which the network is mapped.
Ubuntu
""""""
The format of a rule is a dictionary with optional items ``from``, ``to``,
``priority``, and ``table``.
To configure a network called ``example`` with an IP routing policy rule to
handle traffic from the subnet ``10.1.0.0/24`` using the routing table
``exampleroutetable``:
.. code-block:: yaml
:caption: ``networks.yml``
example_rules:
- from: 10.1.0.0/24
table: exampleroutetable
These rules will be configured on all hosts to which the network is mapped.
Configuring IP Routes on Specific Tables
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -194,6 +194,53 @@ def _veth_netdev(context, veth, inventory_hostname):
return _filter_options(config)
def _network_routes(routes, route_tables):
"""Return a list of routes for a networkd network.
:param routes: a list of route dictionaries.
:param route_tables: a dict mapping route table names to IDs.
:returns: a list of routes for a networkd network.
"""
return [
{
'Route': [
# FIXME(mgoddard): No support for 'options'.
{'Destination': route['cidr']},
{'Gateway': route.get('gateway')},
{'Table': route_tables.get(route.get('table'),
route.get('table'))},
]
}
for route in routes or []
]
def _network_rules(rules, route_tables):
"""Return a list of routing policy rules for a networkd network.
:param rules: a list of rule dictionaries.
:param route_tables: a dict mapping route table names to IDs.
:returns: a list of rules for a networkd network.
"""
for rule in rules or []:
if not isinstance(rule, dict):
raise errors.AnsibleFilterError(
"Routing policy rules must be defined in dictionary "
"format for systemd-networkd")
return [
{
'RoutingPolicyRule': [
{'From': rule.get("from")},
{'To': rule.get("to")},
{'Priority': rule.get("priority")},
{'Table': route_tables.get(rule.get('table'),
rule.get('table'))},
]
}
for rule in rules or []
]
def _network(context, name, inventory_hostname, bridge, bond, vlan_interfaces):
"""Return a networkd network for an interface.
@ -205,7 +252,7 @@ def _network(context, name, inventory_hostname, bridge, bond, vlan_interfaces):
:param bond: Name of a bond of which the interface is a member, or None.
:param vlan_interfaces: List of VLAN subinterfaces of the interface.
"""
# FIXME(mgoddard): Currently does not support: rules, ethtool_opts, zone,
# FIXME(mgoddard): Currently does not support: ethtool_opts, zone,
# allowed_addresses.
device = networks.net_interface(context, name, inventory_hostname)
ip = networks.net_ip(context, name, inventory_hostname)
@ -223,6 +270,7 @@ def _network(context, name, inventory_hostname, bridge, bond, vlan_interfaces):
mtu = networks.net_mtu(context, name, inventory_hostname)
routes = networks.net_routes(context, name, inventory_hostname)
rules = networks.net_rules(context, name, inventory_hostname)
bootproto = networks.net_bootproto(context, name, inventory_hostname)
defroute = networks.net_defroute(context, name, inventory_hostname)
if defroute is not None:
@ -256,17 +304,16 @@ def _network(context, name, inventory_hostname, bridge, bond, vlan_interfaces):
]
},
]
if routes:
config += [
{
'Route': [
# FIXME(mgoddard): No support for 'options'.
{'Destination': route['cidr']},
{'Gateway': route.get('gateway')},
]
}
for route in routes or []
]
# NOTE(mgoddard): Systemd-networkd does not support named route tables
# until v248. Until then, translate names to numeric IDs using the
# network_route_tables variable.
route_tables = utils.get_hostvar(context, "network_route_tables",
inventory_hostname)
route_tables = {table["name"]: table["id"] for table in route_tables}
config += _network_routes(routes, route_tables)
config += _network_rules(rules, route_tables)
return _filter_options(config)

View File

@ -307,6 +307,19 @@ def _route_obj(route):
return route_obj
def _validate_rules(rules):
"""Validate the format of policy-based routing rules.
:param rules: a list of rules or None.
:raises: AnsibleFilterError if any rule is invalid.
"""
for rule in rules or []:
if not isinstance(rule, str):
raise errors.AnsibleFilterError(
"Routing policy rules must be defined in string format "
"for CentOS")
@jinja2.contextfilter
def net_interface_obj(context, name, inventory_hostname=None):
"""Return a dict representation of a network interface.
@ -338,6 +351,7 @@ def net_interface_obj(context, name, inventory_hostname=None):
zone = net_zone(context, name, inventory_hostname)
vip_address = net_vip_address(context, name, inventory_hostname)
allowed_addresses = [vip_address] if vip_address else None
_validate_rules(rules)
interface = {
'device': device,
'address': ip,
@ -390,6 +404,7 @@ def net_bridge_obj(context, name, inventory_hostname=None):
zone = net_zone(context, name, inventory_hostname)
vip_address = net_vip_address(context, name, inventory_hostname)
allowed_addresses = [vip_address] if vip_address else None
_validate_rules(rules)
interface = {
'device': device,
'address': ip,
@ -450,6 +465,7 @@ def net_bond_obj(context, name, inventory_hostname=None):
zone = net_zone(context, name, inventory_hostname)
vip_address = net_vip_address(context, name, inventory_hostname)
allowed_addresses = [vip_address] if vip_address else None
_validate_rules(rules)
interface = {
'device': device,
'address': ip,

View File

@ -48,6 +48,8 @@ class BaseNetworkdTest(unittest.TestCase):
"network_patch_prefix": "p-",
"network_patch_suffix_ovs": "-ovs",
"network_patch_suffix_phy": "-phy",
# List of route tables.
"network_route_tables": [],
}
def setUp(self):
@ -317,6 +319,18 @@ class TestNetworkdNetworks(BaseNetworkdTest):
},
{
"cidr": "1.2.6.0/24",
"table": 42,
},
],
"net1_rules": [
{
"from": "1.2.7.0/24",
"table": 43,
},
{
"to": "1.2.8.0/24",
"table": 44,
"priority": 1,
},
],
"net1_bootproto": "dhcp",
@ -358,6 +372,20 @@ class TestNetworkdNetworks(BaseNetworkdTest):
{
"Route": [
{"Destination": "1.2.6.0/24"},
{"Table": 42},
]
},
{
"RoutingPolicyRule": [
{"From": "1.2.7.0/24"},
{"Table": 43},
]
},
{
"RoutingPolicyRule": [
{"To": "1.2.8.0/24"},
{"Priority": 1},
{"Table": 44},
]
},
]

View File

@ -19,6 +19,11 @@ controller_extra_network_interfaces:
- test_net_bond
- test_net_bond_vlan
# Custom IP routing tables.
network_route_tables:
- id: 2
name: kayobe-test-route-table
# dummy2: Ethernet interface.
test_net_eth_cidr: 192.168.34.0/24
test_net_eth_routes:
@ -30,6 +35,17 @@ test_net_eth_interface: dummy2
test_net_eth_vlan_cidr: 192.168.35.0/24
test_net_eth_vlan_interface: "{% raw %}{{ test_net_eth_interface }}.{{ test_net_eth_vlan_vlan }}{% endraw %}"
test_net_eth_vlan_vlan: 42
test_net_eth_vlan_routes:
- cidr: 192.168.40.0/24
gateway: 192.168.35.254
table: kayobe-test-route-table
test_net_eth_vlan_rules:
{% if ansible_os_family == 'RedHat' %}
- from 192.168.35.0/24 table kayobe-test-route-table
{% else %}
- from: 192.168.35.0/24
table: kayobe-test-route-table
{% endif %}
# br0: bridge with ports dummy3, dummy4.
test_net_bridge_cidr: 192.168.36.0/24

View File

@ -28,6 +28,13 @@ def test_network_ethernet_vlan(host):
assert interface.exists
assert '192.168.35.1' in interface.addresses
assert host.file('/sys/class/net/dummy2.42/lower_dummy2').exists
routes = host.check_output(
'/sbin/ip route show dev dummy2.42 table kayobe-test-route-table')
assert '192.168.40.0/24 via 192.168.35.254' in routes
rules = host.check_output(
'/sbin/ip rule show table kayobe-test-route-table')
expected = 'from 192.168.35.0/24 lookup kayobe-test-route-table'
assert expected in rules
def test_network_bridge(host):