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 when: resolv_is_managed | bool
become: True 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 - name: Remove netplan.io packages
become: true become: true
package: package:

View File

@ -67,8 +67,10 @@ supported:
table to which the route will be added. ``options`` is a list of option table to which the route will be added. ``options`` is a list of option
strings to add to the route. strings to add to the route.
``rules`` ``rules``
List of IP routing rules. Each item should be an ``iproute2`` IP routing List of IP routing rules.
rule.
On CentOS, each item should be a string describing an ``iproute2`` IP
routing rule.
``physical_network`` ``physical_network``
Name of the physical network on which this network exists. This aligns with Name of the physical network on which this network exists. This aligns with
the physical network concept in neutron. 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 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 for a network to a list of rules. The format of each rule currently differs
would be appended to ``ip rule <add|del>`` to create or delete the rule. 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 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 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. 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 Configuring IP Routes on Specific Tables
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -194,6 +194,53 @@ def _veth_netdev(context, veth, inventory_hostname):
return _filter_options(config) 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): def _network(context, name, inventory_hostname, bridge, bond, vlan_interfaces):
"""Return a networkd network for an interface. """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 bond: Name of a bond of which the interface is a member, or None.
:param vlan_interfaces: List of VLAN subinterfaces of the interface. :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. # allowed_addresses.
device = networks.net_interface(context, name, inventory_hostname) device = networks.net_interface(context, name, inventory_hostname)
ip = networks.net_ip(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) mtu = networks.net_mtu(context, name, inventory_hostname)
routes = networks.net_routes(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) bootproto = networks.net_bootproto(context, name, inventory_hostname)
defroute = networks.net_defroute(context, name, inventory_hostname) defroute = networks.net_defroute(context, name, inventory_hostname)
if defroute is not None: if defroute is not None:
@ -256,17 +304,16 @@ def _network(context, name, inventory_hostname, bridge, bond, vlan_interfaces):
] ]
}, },
] ]
if routes:
config += [ # NOTE(mgoddard): Systemd-networkd does not support named route tables
{ # until v248. Until then, translate names to numeric IDs using the
'Route': [ # network_route_tables variable.
# FIXME(mgoddard): No support for 'options'. route_tables = utils.get_hostvar(context, "network_route_tables",
{'Destination': route['cidr']}, inventory_hostname)
{'Gateway': route.get('gateway')}, route_tables = {table["name"]: table["id"] for table in route_tables}
] config += _network_routes(routes, route_tables)
} config += _network_rules(rules, route_tables)
for route in routes or []
]
return _filter_options(config) return _filter_options(config)

View File

@ -307,6 +307,19 @@ def _route_obj(route):
return route_obj 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 @jinja2.contextfilter
def net_interface_obj(context, name, inventory_hostname=None): def net_interface_obj(context, name, inventory_hostname=None):
"""Return a dict representation of a network interface. """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) zone = net_zone(context, name, inventory_hostname)
vip_address = net_vip_address(context, name, inventory_hostname) vip_address = net_vip_address(context, name, inventory_hostname)
allowed_addresses = [vip_address] if vip_address else None allowed_addresses = [vip_address] if vip_address else None
_validate_rules(rules)
interface = { interface = {
'device': device, 'device': device,
'address': ip, 'address': ip,
@ -390,6 +404,7 @@ def net_bridge_obj(context, name, inventory_hostname=None):
zone = net_zone(context, name, inventory_hostname) zone = net_zone(context, name, inventory_hostname)
vip_address = net_vip_address(context, name, inventory_hostname) vip_address = net_vip_address(context, name, inventory_hostname)
allowed_addresses = [vip_address] if vip_address else None allowed_addresses = [vip_address] if vip_address else None
_validate_rules(rules)
interface = { interface = {
'device': device, 'device': device,
'address': ip, 'address': ip,
@ -450,6 +465,7 @@ def net_bond_obj(context, name, inventory_hostname=None):
zone = net_zone(context, name, inventory_hostname) zone = net_zone(context, name, inventory_hostname)
vip_address = net_vip_address(context, name, inventory_hostname) vip_address = net_vip_address(context, name, inventory_hostname)
allowed_addresses = [vip_address] if vip_address else None allowed_addresses = [vip_address] if vip_address else None
_validate_rules(rules)
interface = { interface = {
'device': device, 'device': device,
'address': ip, 'address': ip,

View File

@ -48,6 +48,8 @@ class BaseNetworkdTest(unittest.TestCase):
"network_patch_prefix": "p-", "network_patch_prefix": "p-",
"network_patch_suffix_ovs": "-ovs", "network_patch_suffix_ovs": "-ovs",
"network_patch_suffix_phy": "-phy", "network_patch_suffix_phy": "-phy",
# List of route tables.
"network_route_tables": [],
} }
def setUp(self): def setUp(self):
@ -317,6 +319,18 @@ class TestNetworkdNetworks(BaseNetworkdTest):
}, },
{ {
"cidr": "1.2.6.0/24", "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", "net1_bootproto": "dhcp",
@ -358,6 +372,20 @@ class TestNetworkdNetworks(BaseNetworkdTest):
{ {
"Route": [ "Route": [
{"Destination": "1.2.6.0/24"}, {"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
- test_net_bond_vlan - test_net_bond_vlan
# Custom IP routing tables.
network_route_tables:
- id: 2
name: kayobe-test-route-table
# dummy2: Ethernet interface. # dummy2: Ethernet interface.
test_net_eth_cidr: 192.168.34.0/24 test_net_eth_cidr: 192.168.34.0/24
test_net_eth_routes: 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_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_interface: "{% raw %}{{ test_net_eth_interface }}.{{ test_net_eth_vlan_vlan }}{% endraw %}"
test_net_eth_vlan_vlan: 42 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. # br0: bridge with ports dummy3, dummy4.
test_net_bridge_cidr: 192.168.36.0/24 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 interface.exists
assert '192.168.35.1' in interface.addresses assert '192.168.35.1' in interface.addresses
assert host.file('/sys/class/net/dummy2.42/lower_dummy2').exists 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): def test_network_bridge(host):