Merge "Add initial support for systemd-networkd link configuration"

This commit is contained in:
Zuul 2024-09-11 15:19:25 +00:00 committed by Gerrit Code Review
commit da4bcb59b6
7 changed files with 134 additions and 24 deletions

View File

@ -41,3 +41,9 @@
systemd_networkd_cleanup: true systemd_networkd_cleanup: true
systemd_networkd_cleanup_patterns: systemd_networkd_cleanup_patterns:
- "{{ networkd_prefix }}*" - "{{ networkd_prefix }}*"
- name: Ensure udev is triggered on links changes
become: true
command: "udevadm trigger --verbose --subsystem-match=net --action=add"
changed_when: false
when: network_interfaces | networkd_links | length

View File

@ -429,6 +429,34 @@ To configure a network called ``example`` with an Ethernet interface on
example_interface: eth0 example_interface: eth0
Advanced: Configuring (Renaming) Ethernet Interfaces System Name
----------------------------------------------------------------
The name of the Ethernet interface may be explicitly configured by binding
known MAC address of the specific interface to its name by setting the
``macaddress`` attribute for a network.
.. warning::
Supported only on Ubuntu/Debian operating systems.
To configure a network called ``example`` with known MAC address
``aa:bb:cc:dd:ee:ff`` and rename it from a system name (might be ``eth0``,
``ens3``, or any other name) to the ``lan0`` (new name):
.. code-block:: yaml
:caption: ``inventory/group_vars/<group>/network-interfaces``
example_interface: lan0
example_macaddress: "aa:bb:cc:dd:ee:ff"
.. warning::
The network interface must be down before changing its name. See
`issue <https://github.com/systemd/systemd/issues/26601>`__ in the systemd
project. So the configured node reboot might be required right after the
``seed host configure`` or ``overcloud host configure`` Kayobe commands.
.. _configuring-bridge-interfaces: .. _configuring-bridge-interfaces:
Configuring Bridge Interfaces Configuring Bridge Interfaces

View File

@ -479,6 +479,35 @@ def _veth_peer_network(context, veth, inventory_hostname):
return _filter_options(config) return _filter_options(config)
def _ether_link(context, name, inventory_hostname):
"""Return a networkd link configuration for a ether.
:param context: a Jinja2 Context object.
:param name: name of the network.
:param inventory_hostname: Ansible inventory hostname.
"""
config = []
device = networks.net_interface(context, name, inventory_hostname)
macaddress = networks.net_macaddress(context, name, inventory_hostname)
if macaddress is not None:
config = [
{
'Match': [
{'PermanentMACAddress': macaddress},
],
},
{
'Link': [
{'Name': device},
],
}
]
return _filter_options(config)
def _add_to_result(result, prefix, device, config): def _add_to_result(result, prefix, device, config):
"""Add configuration for an interface to a filter result. """Add configuration for an interface to a filter result.
@ -561,8 +590,20 @@ def networkd_links(context, names, inventory_hostname=None):
:param inventory_hostname: Ansible inventory hostname. :param inventory_hostname: Ansible inventory hostname.
:returns: a dict representation of networkd link configuration. :returns: a dict representation of networkd link configuration.
""" """
# NOTE(mgoddard): We do not currently support link configuration. # Prefix for configuration file names.
return {} prefix = utils.get_hostvar(context, "networkd_prefix", inventory_hostname)
result = {}
# only ethers
for name in networks.net_select_ethers(context, names, inventory_hostname):
device = networks.get_and_validate_interface(context, name,
inventory_hostname)
ether_link = _ether_link(context, name, inventory_hostname)
if ether_link:
_add_to_result(result, prefix, device, ether_link)
return result
@jinja2.pass_context @jinja2.pass_context

View File

@ -274,6 +274,11 @@ def net_mtu(context, name, inventory_hostname=None):
return mtu return mtu
@jinja2.pass_context
def net_macaddress(context, name, inventory_hostname=None):
return net_attr(context, name, 'macaddress', inventory_hostname)
@jinja2.pass_context @jinja2.pass_context
def net_bridge_stp(context, name, inventory_hostname=None): def net_bridge_stp(context, name, inventory_hostname=None):
"""Return the Spanning Tree Protocol (STP) state for a bridge. """Return the Spanning Tree Protocol (STP) state for a bridge.
@ -394,6 +399,7 @@ def net_interface_obj(context, name, inventory_hostname=None, names=None):
netmask = None netmask = None
vlan = net_vlan(context, name, inventory_hostname) vlan = net_vlan(context, name, inventory_hostname)
mtu = net_mtu(context, name, inventory_hostname) mtu = net_mtu(context, name, inventory_hostname)
macaddress = net_macaddress(context, name, inventory_hostname)
# NOTE(priteau): do not pass MTU for VLAN interfaces on bridges when it is # NOTE(priteau): do not pass MTU for VLAN interfaces on bridges when it is
# identical to the parent bridge, to work around a NetworkManager bug. # identical to the parent bridge, to work around a NetworkManager bug.
@ -433,6 +439,7 @@ def net_interface_obj(context, name, inventory_hostname=None, names=None):
'gateway': gateway, 'gateway': gateway,
'vlan': vlan, 'vlan': vlan,
'mtu': mtu, 'mtu': mtu,
'macaddress': macaddress,
'route': routes, 'route': routes,
'rules': rules, 'rules': rules,
'bootproto': bootproto or 'static', 'bootproto': bootproto or 'static',
@ -789,6 +796,7 @@ def get_filters():
'net_neutron_gateway': net_neutron_gateway, 'net_neutron_gateway': net_neutron_gateway,
'net_vlan': net_vlan, 'net_vlan': net_vlan,
'net_mtu': net_mtu, 'net_mtu': net_mtu,
'net_macaddress': net_macaddress,
'net_routes': net_routes, 'net_routes': net_routes,
'net_rules': net_rules, 'net_rules': net_rules,
'net_physical_network': net_physical_network, 'net_physical_network': net_physical_network,

View File

@ -33,6 +33,7 @@ class BaseNetworkdTest(unittest.TestCase):
"net1_interface": "eth0", "net1_interface": "eth0",
"net1_cidr": "1.2.3.0/24", "net1_cidr": "1.2.3.0/24",
"net1_ips": {"test-host": "1.2.3.4"}, "net1_ips": {"test-host": "1.2.3.4"},
"net1_macaddress": "aa:bb:cc:dd:ee:ff",
# net2: VLAN on eth0.2 with VLAN 2 on interface eth0. # net2: VLAN on eth0.2 with VLAN 2 on interface eth0.
"net2_interface": "eth0.2", "net2_interface": "eth0.2",
"net2_vlan": 2, "net2_vlan": 2,
@ -351,9 +352,27 @@ class TestNetworkdNetDevs(BaseNetworkdTest):
class TestNetworkdLinks(BaseNetworkdTest): class TestNetworkdLinks(BaseNetworkdTest):
def test_empty(self): def test_empty(self):
links = networkd.networkd_links(self.context, ['net1']) links = networkd.networkd_links(self.context, ['net2'])
self.assertEqual({}, links) self.assertEqual({}, links)
def test_link_name(self):
links = networkd.networkd_links(self.context, ['net1'])
expected = {
"50-kayobe-eth0": [
{
"Match": [
{"PermanentMACAddress": "aa:bb:cc:dd:ee:ff"}
]
},
{
"Link": [
{"Name": "eth0"},
]
},
]
}
self.assertEqual(expected, links)
class TestNetworkdNetworks(BaseNetworkdTest): class TestNetworkdNetworks(BaseNetworkdTest):

View File

@ -5,7 +5,6 @@
import ipaddress import ipaddress
import os import os
import time
import distro import distro
import pytest import pytest
@ -54,17 +53,18 @@ def test_network_bridge(host):
interface = host.interface('br0') interface = host.interface('br0')
assert interface.exists assert interface.exists
assert '192.168.36.1' in interface.addresses assert '192.168.36.1' in interface.addresses
stp_status = host.file('/sys/class/net/br0/bridge/stp_state').content_string.strip() state_file = "/sys/class/net/br0/bridge/stp_state"
stp_status = host.file(state_file).content_string.strip()
assert '0' == stp_status assert '0' == stp_status
ports = ['dummy3', 'dummy4'] ports = ['dummy3', 'dummy4']
sys_ports = host.check_output('ls -1 /sys/class/net/br0/brif') sys_ports = host.check_output('ls -1 /sys/class/net/br0/brif')
assert sys_ports == "\n".join(ports) assert sys_ports == "\n".join(ports)
for port in ports: for port in ports:
interface = host.interface(port) interface = host.interface(port)
assert interface.exists assert interface.exists
v4_addresses = [a for a in interface.addresses v4_addresses = [a for a in interface.addresses
if ipaddress.ip_address(a).version == '4'] if ipaddress.ip_address(a).version == '4']
assert not v4_addresses assert not v4_addresses
def test_network_bridge_vlan(host): def test_network_bridge_vlan(host):
@ -84,9 +84,9 @@ def test_network_bond(host):
slaves = set(['dummy5', 'dummy6']) slaves = set(['dummy5', 'dummy6'])
assert sys_slaves == slaves assert sys_slaves == slaves
for slave in slaves: for slave in slaves:
interface = host.interface(slave) interface = host.interface(slave)
assert interface.exists assert interface.exists
assert not interface.addresses assert not interface.addresses
def test_network_bond_vlan(host): def test_network_bond_vlan(host):
@ -99,8 +99,9 @@ def test_network_bond_vlan(host):
def test_network_bridge_no_ip(host): def test_network_bridge_no_ip(host):
interface = host.interface('br1') interface = host.interface('br1')
assert interface.exists assert interface.exists
assert not '192.168.40.1' in interface.addresses assert '192.168.40.1' not in interface.addresses
stp_status = host.file('/sys/class/net/br1/bridge/stp_state').content_string.strip() state_file = "/sys/class/net/br1/bridge/stp_state"
stp_status = host.file(state_file).content_string.strip()
assert '1' == stp_status assert '1' == stp_status
@ -113,13 +114,13 @@ def test_network_systemd_vlan(host):
def test_additional_user_account(host): def test_additional_user_account(host):
user = host.user("kayobe-test-user") user = host.user("kayobe-test-user")
assert user.name == "kayobe-test-user" assert user.name == "kayobe-test-user"
assert user.group == "kayobe-test-user" assert user.group == "kayobe-test-user"
assert set(user.groups) == {"kayobe-test-user", "stack"} assert set(user.groups) == {"kayobe-test-user", "stack"}
assert user.gecos == "Kayobe test user" assert user.gecos == "Kayobe test user"
with host.sudo(): with host.sudo():
assert user.password == 'kayobe-test-user-password' assert user.password == 'kayobe-test-user-password'
def test_software_RAID(host): def test_software_RAID(host):
@ -238,7 +239,8 @@ def test_apt_auth(host):
@pytest.mark.parametrize('repo', ["appstream", "baseos", "extras", "epel"]) @pytest.mark.parametrize('repo', ["appstream", "baseos", "extras", "epel"])
@pytest.mark.skipif(not _is_dnf_mirror(), reason="DNF OpenDev mirror only for CentOS 8") @pytest.mark.skipif(not _is_dnf_mirror(),
reason="DNF OpenDev mirror only for CentOS 8")
def test_dnf_local_package_mirrors(host, repo): def test_dnf_local_package_mirrors(host, repo):
# Depends on SITE_MIRROR_FQDN environment variable. # Depends on SITE_MIRROR_FQDN environment variable.
assert os.getenv('SITE_MIRROR_FQDN') assert os.getenv('SITE_MIRROR_FQDN')
@ -265,7 +267,7 @@ def test_dnf_automatic(host):
@pytest.mark.skipif(not _is_dnf(), @pytest.mark.skipif(not _is_dnf(),
reason="tuned profile setting only supported on CentOS/Rocky") reason="tuned profiles only supported on CentOS/Rocky")
def test_tuned_profile_is_active(host): def test_tuned_profile_is_active(host):
tuned_output = host.check_output("tuned-adm active") tuned_output = host.check_output("tuned-adm active")
assert "throughput-performance" in tuned_output assert "throughput-performance" in tuned_output

View File

@ -0,0 +1,6 @@
---
features:
- |
Added initial support for systemd-networkd link configuration, now you can
configure and rename the name of a network interface if you know the MAC
address of the interface.