From cac4b125ed00e0b1dbcaaff3dd03366f73337e60 Mon Sep 17 00:00:00 2001 From: Danilo Egea Gondolfo Date: Mon, 16 Sep 2024 11:19:57 +0100 Subject: [PATCH 01/10] Revert "cli:apply: improve networkd restart logic for non-existent networkd config" This reverts commit a527c51fad3427423c86eaee493e78253e6075b0. --- netplan_cli/cli/commands/apply.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/netplan_cli/cli/commands/apply.py b/netplan_cli/cli/commands/apply.py index f6b94cdcb..d4d4b738e 100644 --- a/netplan_cli/cli/commands/apply.py +++ b/netplan_cli/cli/commands/apply.py @@ -122,11 +122,8 @@ def command_apply(self, run_generate=True, sync=False, exit_on_error=True, state restart_networkd = False with tempfile.TemporaryDirectory() as tmp_dir: # needs to be a subfolder as copytree wants to create it - run_systemd_network = '/run/systemd/network' old_files_dir = os.path.join(tmp_dir, 'cfg') - has_old_networkd_config = os.path.isdir(run_systemd_network) - if has_old_networkd_config: - shutil.copytree(run_systemd_network, old_files_dir) + shutil.copytree('/run/systemd/network', old_files_dir) generator_call = [] generate_out = None @@ -142,13 +139,9 @@ def command_apply(self, run_generate=True, sync=False, exit_on_error=True, state raise ConfigurationError("the configuration could not be generated") # Restart networkd if something in the configuration changed - has_new_networkd_config = os.path.isdir(run_systemd_network) - if has_old_networkd_config != has_new_networkd_config: + comp = filecmp.dircmp('/run/systemd/network', old_files_dir) + if comp.left_only or comp.right_only or comp.diff_files: restart_networkd = True - elif has_old_networkd_config and has_new_networkd_config: - comp = filecmp.dircmp(run_systemd_network, old_files_dir) - if comp.left_only or comp.right_only or comp.diff_files: - restart_networkd = True devices = utils.get_interfaces() From 1ab755c9a148407142faf215a02835d1e9e60c37 Mon Sep 17 00:00:00 2001 From: Danilo Egea Gondolfo Date: Mon, 16 Sep 2024 11:21:27 +0100 Subject: [PATCH 02/10] Revert "apply: compare full configuration to decide to restart networkd" This reverts commit a9c1433cbf39c90eccdfb8e355c6bb93f662f135. --- netplan_cli/cli/commands/apply.py | 45 +++++++++++++++---------------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/netplan_cli/cli/commands/apply.py b/netplan_cli/cli/commands/apply.py index d4d4b738e..7e17cee1f 100644 --- a/netplan_cli/cli/commands/apply.py +++ b/netplan_cli/cli/commands/apply.py @@ -25,8 +25,6 @@ import glob import subprocess import shutil -import tempfile -import filecmp import time from .. import utils @@ -110,6 +108,7 @@ def command_apply(self, run_generate=True, sync=False, exit_on_error=True, state return ovs_cleanup_service = '/run/systemd/system/netplan-ovs-cleanup.service' + old_files_networkd = bool(glob.glob('/run/systemd/network/*netplan-*')) old_ovs_glob = glob.glob('/run/systemd/system/netplan-ovs-*') # Ignore netplan-ovs-cleanup.service, as it can always be there if ovs_cleanup_service in old_ovs_glob: @@ -119,32 +118,30 @@ def command_apply(self, run_generate=True, sync=False, exit_on_error=True, state nm_ifaces = utils.nm_interfaces(old_nm_glob, utils.get_interfaces()) old_files_nm = bool(old_nm_glob) - restart_networkd = False - with tempfile.TemporaryDirectory() as tmp_dir: - # needs to be a subfolder as copytree wants to create it - old_files_dir = os.path.join(tmp_dir, 'cfg') - shutil.copytree('/run/systemd/network', old_files_dir) + generator_call = [] + generate_out = None + if 'NETPLAN_PROFILE' in os.environ: + generator_call.extend(['valgrind', '--leak-check=full']) + generate_out = subprocess.STDOUT - generator_call = [] - generate_out = None - if 'NETPLAN_PROFILE' in os.environ: - generator_call.extend(['valgrind', '--leak-check=full']) - generate_out = subprocess.STDOUT - - generator_call.append(utils.get_generator_path()) - if run_generate and subprocess.call(generator_call, stderr=generate_out) != 0: - if exit_on_error: - sys.exit(os.EX_CONFIG) - else: - raise ConfigurationError("the configuration could not be generated") - - # Restart networkd if something in the configuration changed - comp = filecmp.dircmp('/run/systemd/network', old_files_dir) - if comp.left_only or comp.right_only or comp.diff_files: - restart_networkd = True + generator_call.append(utils.get_generator_path()) + if run_generate and subprocess.call(generator_call, stderr=generate_out) != 0: + if exit_on_error: + sys.exit(os.EX_CONFIG) + else: + raise ConfigurationError("the configuration could not be generated") devices = utils.get_interfaces() + # Re-start service when + # 1. We have configuration files for it + # 2. Previously we had config files for it but not anymore + # Ideally we should compare the content of the *netplan-* files before and + # after generation to minimize the number of re-starts, but the conditions + # above works too. + restart_networkd = bool(glob.glob('/run/systemd/network/*netplan-*')) + if not restart_networkd and old_files_networkd: + restart_networkd = True restart_ovs_glob = glob.glob('/run/systemd/system/netplan-ovs-*') # Ignore netplan-ovs-cleanup.service, as it can always be there if ovs_cleanup_service in restart_ovs_glob: From 9cab923b0df192079ef26043f4289274cd40a1ab Mon Sep 17 00:00:00 2001 From: Danilo Egea Gondolfo Date: Tue, 24 Sep 2024 14:25:26 +0100 Subject: [PATCH 03/10] status: fix SSID parsing The networkctl output was changed from "WiFi access point" to "Wi-Fi access point" as part of systemd commit 8fff78a1dded105e1ee87bc66e29ef2fd61bf8c9. Use a regex that matches both formats to keep it compatible with older versions of networkd. --- netplan_cli/cli/state.py | 10 +++++++--- tests/cli/test_state.py | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/netplan_cli/cli/state.py b/netplan_cli/cli/state.py index b6437d88f..c5a46b094 100644 --- a/netplan_cli/cli/state.py +++ b/netplan_cli/cli/state.py @@ -59,6 +59,10 @@ 'vrf': 'vrf', 'vxlan': 'tunnel', + # Used for wifi testing. + # It's the type of the interface hwsim0 created by the mac80211_hwsim driver + 'ieee80211_radiotap': 'wifi', + # Netplan netdef types 'wifis': 'wifi', 'ethernets': 'ethernet', @@ -332,9 +336,9 @@ def ssid(self) -> str: # https://github.com/systemd/systemd/commit/da7c995 for line in self._networkctl.splitlines(): line = line.strip() - key = 'WiFi access point: ' - if line.startswith(key): - ssid = line[len(key):-len(' (xB:SS:ID:xx:xx:xx)')].strip() + key = r'^Wi-?Fi access point: (.*) \(.*\)' + if match := re.match(key, line): + ssid = match.group(1) return ssid if ssid else None return None diff --git a/tests/cli/test_state.py b/tests/cli/test_state.py index 646d7b5c4..11e7ec981 100644 --- a/tests/cli/test_state.py +++ b/tests/cli/test_state.py @@ -417,6 +417,27 @@ def test_json_nm_wlan0(self, networkctl_mock, nm_ssid_mock): self.assertEqual(len(json.get('dns_search')), 1) self.assertEqual(len(json.get('routes')), 6) + @patch('netplan_cli.cli.state.Interface.query_nm_ssid') + @patch('netplan_cli.cli.state.Interface.query_networkctl') + def test_json_nm_wlan0_2(self, networkctl_mock, nm_ssid_mock): + SSID = 'MYCON' + nm_ssid_mock.return_value = SSID + # networkctl mock output reduced to relevant lines + # Newer versions of systemd changed WiFi to Wi-Fi + # See systemd commit 8fff78a1dded105e1ee87bc66e29ef2fd61bf8c9 + networkctl_mock.return_value = \ + 'Wi-Fi access point: {} (b4:fb:e4:75:c6:21)'.format(SSID) + + data = next((itf for itf in yaml.safe_load(IPROUTE2) if itf['ifindex'] == 5), {}) + nd = SystemConfigState.process_networkd(NETWORKD) + nm = SystemConfigState.process_nm(NMCLI) + dns = (DNS_ADDRESSES, DNS_SEARCH) + routes = (SystemConfigState.process_generic(ROUTE4), SystemConfigState.process_generic(ROUTE6)) + + itf = Interface(data, nd, nm, dns, routes) + _, json = itf.json() + self.assertEqual(json.get('ssid'), 'MYCON') + @patch('netplan_cli.cli.state.Interface.query_networkctl') def test_json_nd_enp0s31f6(self, networkctl_mock): # networkctl mock output reduced to relevant lines From 7de9410d0a67abd8d5b7761ffe83c0ddb5cf2af5 Mon Sep 17 00:00:00 2001 From: Danilo Egea Gondolfo Date: Tue, 24 Sep 2024 15:19:43 +0100 Subject: [PATCH 04/10] cli: set LC_ALL to C.UTF-8 Non-ascii characters returned from external programs (such as nmcli) are being replaced by "?". --- netplan_cli/cli/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netplan_cli/cli/core.py b/netplan_cli/cli/core.py index 7850c182d..918df9997 100644 --- a/netplan_cli/cli/core.py +++ b/netplan_cli/cli/core.py @@ -35,7 +35,7 @@ def __init__(self): description='Network configuration in YAML', leaf=False) os.environ.update({ - 'LC_ALL': 'C', + 'LC_ALL': 'C.UTF-8', 'PATH': os.getenv('PATH', FALLBACK_PATH)}) def parse_args(self): From ded34bad0e8c30159f91f6e8aa940f719a11284f Mon Sep 17 00:00:00 2001 From: Danilo Egea Gondolfo Date: Wed, 25 Sep 2024 10:06:16 +0100 Subject: [PATCH 05/10] state/Interface: improve netdef ID handling with NetworkManager The ID is extracted from the file name. For Wi-Fi connections, the SSID will also be present in the file name and we need to remove it in order to extract the netdef ID. When non-ascii characters are present in the SSID, NM will represent it using a byte sequence separated with ;. The semicolons are escaped in the file name when we emit the .nmconnection file. In order to find and remove the SSID from the file name, so we can get the netdef ID, we need to convert it to the same representation. To detect this situation we check if the SSID is present in the file name. If it's not, it likely because the SSID contains non-ascii characters. --- netplan_cli/cli/state.py | 19 ++++++++++++++++++- tests/cli/test_state.py | 23 +++++++++++++++++++++-- tests/cli/test_status.py | 2 +- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/netplan_cli/cli/state.py b/netplan_cli/cli/state.py index c5a46b094..734efdcc0 100644 --- a/netplan_cli/cli/state.py +++ b/netplan_cli/cli/state.py @@ -28,6 +28,7 @@ from io import StringIO from socket import AF_INET, AF_INET6, inet_ntop from typing import Dict, List, Type, Union +from urllib import parse import yaml @@ -318,7 +319,23 @@ def netdef_id(self) -> str: 'run/NetworkManager/system-connections/netplan-')[1].split('.nmconnection')[0] if self.nm.get('type', '') == '802-11-wireless': ssid = self.query_nm_ssid(self.nm.get('name')) - if ssid: # XXX: escaping needed? + if ssid not in netdef: + # If the plain SSID in not found in the netdef here + # it's probably because it contains non-ascii characters that + # were escaped in the file name. We need to do the same here to + # be able to extract it from the file name. + # In this case, Network Manager will save the SSID using the format "b1;b2;b3...;" + # instead of a non-ascii string. + # In src/nm.c we use g_uri_escape_string() to create the file name. + + # Transform the SSID to the same format used by Network Manager + ssid_encoded = ssid.encode('utf-8') + ssid_bytes = [str(b) for b in ssid_encoded] + ssid_nm_escaped = ';'.join(ssid_bytes) + ';' + + # Escape characters in the same way we do in src/nm.c. + ssid = parse.quote(ssid_nm_escaped) + if ssid: netdef = netdef.split('-' + ssid)[0] return netdef return None diff --git a/tests/cli/test_state.py b/tests/cli/test_state.py index 11e7ec981..fa2914ec8 100644 --- a/tests/cli/test_state.py +++ b/tests/cli/test_state.py @@ -85,8 +85,8 @@ def test_query_nm(self, mock): mock.assert_called_with(['nmcli', '-t', '-f', 'DEVICE,NAME,UUID,FILENAME,TYPE,AUTOCONNECT', 'con', 'show'], text=True) - self.assertEqual(len(res), 1) - self.assertListEqual([itf.get('device') for itf in res], ['wlan0']) + self.assertEqual(len(res), 2) + self.assertListEqual([itf.get('device') for itf in res], ['wlan0', 'wlan1']) @patch('subprocess.check_output') def test_query_nm_fail(self, mock): @@ -438,6 +438,25 @@ def test_json_nm_wlan0_2(self, networkctl_mock, nm_ssid_mock): _, json = itf.json() self.assertEqual(json.get('ssid'), 'MYCON') + @patch('netplan_cli.cli.state.Interface.query_nm_ssid') + @patch('netplan_cli.cli.state.Interface.query_networkctl') + def test_json_nm_wlan1(self, networkctl_mock, nm_ssid_mock): + SSID = 'áéíóúççção€€€' + nm_ssid_mock.return_value = SSID + # networkctl mock output reduced to relevant lines + # Newer versions of systemd changed WiFi to Wi-Fi + # See systemd commit 8fff78a1dded105e1ee87bc66e29ef2fd61bf8c9 + networkctl_mock.return_value = \ + 'Wi-Fi access point: {} (b4:fb:e4:75:c6:21)'.format(SSID) + + data = {'ifname': 'wlan1', 'ifindex': 123} + nd = [{'Index': 123, 'Type': 'wlan', 'Name': 'wlan1'}] + nm = SystemConfigState.process_nm(NMCLI) + + itf = Interface(data, nd, nm, (None, None), (None, None)) + _, json = itf.json() + self.assertEqual(json.get('ssid'), SSID) + @patch('netplan_cli.cli.state.Interface.query_networkctl') def test_json_nd_enp0s31f6(self, networkctl_mock): # networkctl mock output reduced to relevant lines diff --git a/tests/cli/test_status.py b/tests/cli/test_status.py index 4d38acbfc..4b0f53ee6 100644 --- a/tests/cli/test_status.py +++ b/tests/cli/test_status.py @@ -30,7 +30,7 @@ IPROUTE2 = '[{"ifindex":1,"ifname":"lo","flags":["LOOPBACK","UP","LOWER_UP"],"mtu":65536,"qdisc":"noqueue","operstate":"UNKNOWN","group":"default","txqlen":1000,"link_type":"loopback","address":"00:00:00:00:00:00","broadcast":"00:00:00:00:00:00","promiscuity":0,"min_mtu":0,"max_mtu":0,"num_tx_queues":1,"num_rx_queues":1,"gso_max_size":65536,"gso_max_segs":65535,"addr_info":[{"family":"inet","local":"127.0.0.1","prefixlen":8,"scope":"host","label":"lo","valid_life_time":4294967295,"preferred_life_time":4294967295},{"family":"inet6","local":"::1","prefixlen":128,"scope":"host","valid_life_time":4294967295,"preferred_life_time":4294967295}]},{"ifindex":2,"ifname":"enp0s31f6","flags":["BROADCAST","MULTICAST","UP","LOWER_UP"],"mtu":1500,"qdisc":"fq_codel","operstate":"UP","group":"default","txqlen":1000,"link_type":"ether","address":"54:e1:ad:5f:24:b4","broadcast":"ff:ff:ff:ff:ff:ff","promiscuity":0,"min_mtu":68,"max_mtu":9000,"num_tx_queues":1,"num_rx_queues":1,"gso_max_size":65536,"gso_max_segs":65535,"parentbus":"pci","parentdev":"0000:00:1f.6","addr_info":[{"family":"inet","local":"192.168.178.62","prefixlen":24,"metric":100,"broadcast":"192.168.178.255","scope":"global","dynamic":true,"label":"enp0s31f6","valid_life_time":850698,"preferred_life_time":850698},{"family":"inet6","local":"2001:9e8:a19f:1c00:56e1:adff:fe5f:24b4","prefixlen":64,"scope":"global","dynamic":true,"mngtmpaddr":true,"noprefixroute":true,"valid_life_time":6821,"preferred_life_time":3221},{"family":"inet6","local":"fe80::56e1:adff:fe5f:24b4","prefixlen":64,"scope":"link","valid_life_time":4294967295,"preferred_life_time":4294967295}]},{"ifindex":5,"ifname":"wlan0","flags":["BROADCAST","MULTICAST","UP","LOWER_UP"],"mtu":1500,"qdisc":"noqueue","operstate":"UP","group":"default","txqlen":1000,"link_type":"ether","address":"1c:4d:70:e4:e4:0e","broadcast":"ff:ff:ff:ff:ff:ff","promiscuity":0,"min_mtu":256,"max_mtu":2304,"num_tx_queues":1,"num_rx_queues":1,"gso_max_size":65536,"gso_max_segs":65535,"parentbus":"pci","parentdev":"0000:04:00.0","addr_info":[{"family":"inet","local":"192.168.178.142","prefixlen":24,"broadcast":"192.168.178.255","scope":"global","dynamic":true,"noprefixroute":true,"label":"wlan0","valid_life_time":850700,"preferred_life_time":850700},{"family":"inet6","local":"2001:9e8:a19f:1c00:7011:2d1:951:ad03","prefixlen":64,"scope":"global","temporary":true,"dynamic":true,"valid_life_time":6822,"preferred_life_time":3222},{"family":"inet6","local":"2001:9e8:a19f:1c00:f24f:f724:5dd1:d0ad","prefixlen":64,"scope":"global","dynamic":true,"mngtmpaddr":true,"noprefixroute":true,"valid_life_time":6822,"preferred_life_time":3222},{"family":"inet6","local":"fe80::fec1:6ced:5268:b46c","prefixlen":64,"scope":"link","noprefixroute":true,"valid_life_time":4294967295,"preferred_life_time":4294967295}]},{"ifindex":41,"ifname":"wg0","flags":["POINTOPOINT","NOARP","UP","LOWER_UP"],"mtu":1420,"qdisc":"noqueue","operstate":"UNKNOWN","group":"default","txqlen":1000,"link_type":"none","promiscuity":0,"min_mtu":0,"max_mtu":2147483552,"linkinfo":{"info_kind":"wireguard"},"num_tx_queues":1,"num_rx_queues":1,"gso_max_size":65536,"gso_max_segs":65535,"addr_info":[{"family":"inet","local":"10.10.0.2","prefixlen":24,"scope":"global","label":"wg0","valid_life_time":4294967295,"preferred_life_time":4294967295}]},{"ifindex":46,"ifname":"wwan0","flags":["BROADCAST","MULTICAST","NOARP"],"mtu":1500,"qdisc":"noop","operstate":"DOWN","group":"default","txqlen":1000,"link_type":"ether","address":"a2:23:44:c4:4e:f8","broadcast":"ff:ff:ff:ff:ff:ff","promiscuity":0,"min_mtu":0,"max_mtu":2048,"num_tx_queues":1,"num_rx_queues":1,"gso_max_size":65536,"gso_max_segs":65535,"parentbus":"usb","parentdev":"1-6:1.12","addr_info":[]},{"ifindex":48,"link":null,"ifname":"tun0","flags":["POINTOPOINT","NOARP","UP","LOWER_UP"],"mtu":1480,"qdisc":"noqueue","operstate":"UNKNOWN","group":"default","txqlen":1000,"link_type":"sit","address":"1.1.1.1","link_pointtopoint":true,"broadcast":"2.2.2.2","promiscuity":0,"min_mtu":1280,"max_mtu":65555,"linkinfo":{"info_kind":"sit","info_data":{"proto":"ip6ip","remote":"2.2.2.2","local":"1.1.1.1","ttl":0,"pmtudisc":true,"prefix":"2002::","prefixlen":16}},"num_tx_queues":1,"num_rx_queues":1,"gso_max_size":65536,"gso_max_segs":65535,"addr_info":[{"family":"inet6","local":"2001:dead:beef::2","prefixlen":64,"scope":"global","valid_life_time":4294967295,"preferred_life_time":4294967295}]},{"ifindex":49,"ifname":"tun1","flags":["POINTOPOINT","MULTICAST","NOARP","UP","LOWER_UP"],"mtu":1500,"qdisc":"pfifo_fast","operstate":"UNKNOWN","link_type":"none","linkinfo":{"info_kind":"tun","info_data":{"type":"tun"}}}]' # nopep8 NETWORKD = '{"Interfaces":[{"Index":1,"Name":"lo","AlternativeNames":[],"Type":"loopback","Driver":null,"SetupState":"unmanaged","OperationalState":"carrier","CarrierState":"carrier","AddressState":"off","IPv4AddressState":"off","IPv6AddressState":"off","OnlineState":null,"LinkFile":null,"Path":null,"Vendor":null,"Model":null},{"Index":2,"Name":"enp0s31f6","AlternativeNames":[],"Type":"ether","Driver":"e1000e","SetupState":"configured","OperationalState":"routable","CarrierState":"carrier","AddressState":"routable","IPv4AddressState":"routable","IPv6AddressState":"routable","OnlineState":"online","NetworkFile":"/run/systemd/network/10-netplan-enp0s31f6.network","LinkFile":"/usr/lib/systemd/network/99-default.link","Path":"pci-0000:00:1f.6","Vendor":"Intel Corporation","Model":"Ethernet Connection I219-LM"},{"Index":5,"Name":"wlan0","AlternativeNames":[],"Type":"wlan","Driver":"iwlwifi","SetupState":"unmanaged","OperationalState":"routable","CarrierState":"carrier","AddressState":"routable","IPv4AddressState":"routable","IPv6AddressState":"routable","OnlineState":"online","NetworkFile":"/run/systemd/network/10-netplan-wlan0.network","LinkFile":"/usr/lib/systemd/network/80-iwd.link","Path":"pci-0000:04:00.0","Vendor":"Intel Corporation","Model":"Wireless 8260 (Dual Band Wireless-AC 8260)"},{"Index":41,"Name":"wg0","AlternativeNames":[],"Type":"wireguard","Driver":null,"SetupState":"configured","OperationalState":"routable","CarrierState":"carrier","AddressState":"routable","IPv4AddressState":"routable","IPv6AddressState":"off","OnlineState":"online","NetworkFile":"/run/systemd/network/10-netplan-wg0.network","LinkFile":"/usr/lib/systemd/network/99-default.link","Path":null,"Vendor":null,"Model":null},{"Index":46,"Name":"wwan0","AlternativeNames":[],"Type":"wwan","Driver":"cdc_mbim","SetupState":"unmanaged","OperationalState":"off","CarrierState":"off","AddressState":"off","IPv4AddressState":"off","IPv6AddressState":"off","OnlineState":null,"LinkFile":"/usr/lib/systemd/network/73-usb-net-by-mac.link","Path":"pci-0000:00:14.0-usb-0:6:1.12","Vendor":"Sierra Wireless, Inc.","Model":"EM7455"},{"Index":48,"Name":"tun0","AlternativeNames":[],"Type":"sit","Driver":null,"SetupState":"configured","OperationalState":"routable","CarrierState":"carrier","AddressState":"routable","IPv4AddressState":"off","IPv6AddressState":"routable","OnlineState":"online","NetworkFile":"/run/systemd/network/10-netplan-tun0.network","LinkFile":"/usr/lib/systemd/network/99-default.link","Path":null,"Vendor":null,"Model":null}, {"Index":43,"Name":"mybr0","Type":"bridge","Driver":"bridge","OperationalState":"degraded","CarrierState":"carrier","AddressState":"degraded","IPv4AddressState":"degraded","IPv6AddressState":"degraded","OnlineState":null}, {"Index":45,"Name":"mybond0","Type":"bond","Driver":"bonding","OperationalState":"degraded","CarrierState":"carrier","AddressState":"degraded","IPv4AddressState":"degraded","IPv6AddressState":"degraded","OnlineState":null}, {"Index":47,"Name":"myvrf0","Type":"ether","Kind":"vrf","Driver":"vrf","OperationalState":"degraded","CarrierState":"carrier","AddressState":"degraded","IPv4AddressState":"degraded","IPv6AddressState":"degraded","OnlineState":null},{"Index":49,"Name":"tun1","Kind":"tun","Type":"none","Driver":"tun"}]}' # nopep8 -NMCLI = 'wlan0:MYCON:b6b7a21d-186e-45e1-b3a6-636da1735563:/run/NetworkManager/system-connections/netplan-NM-b6b7a21d-186e-45e1-b3a6-636da1735563-MYCON.nmconnection:802-11-wireless:yes' # nopep8 +NMCLI = 'wlan0:MYCON:b6b7a21d-186e-45e1-b3a6-636da1735563:/run/NetworkManager/system-connections/netplan-NM-b6b7a21d-186e-45e1-b3a6-636da1735563-MYCON.nmconnection:802-11-wireless:yes\nwlan1:áéíóúççção€€€:1a1fc964-78df-4a93-8ed3-1416f02c5cdf:/run/NetworkManager/system-connections/netplan-NM-1a1fc964-78df-4a93-8ed3-1416f02c5cdf-195%3B161%3B195%3B169%3B195%3B173%3B195%3B179%3B195%3B186%3B195%3B167%3B195%3B167%3B195%3B167%3B195%3B163%3B111%3B226%3B130%3B172%3B226%3B130%3B172%3B226%3B130%3B172%3B.nmconnection:802-11-wireless:yes' # nopep8 ROUTE4 = '[{"family":2,"type":"unicast","dst":"default","gateway":"192.168.178.1","dev":"enp0s31f6","table":"main","protocol":"dhcp","scope":"global","prefsrc":"192.168.178.62","metric":100,"flags":[]},{"family":2,"type":"unicast","dst":"default","gateway":"192.168.178.1","dev":"wlan0","table":"main","protocol":"dhcp","scope":"global","metric":600,"flags":[]},{"family":2,"type":"unicast","dst":"10.10.0.0/24","dev":"wg0","table":"main","protocol":"kernel","scope":"link","prefsrc":"10.10.0.2","flags":[]},{"family":2,"type":"unicast","dst":"192.168.178.0/24","dev":"enp0s31f6","table":"main","protocol":"kernel","scope":"link","prefsrc":"192.168.178.62","metric":100,"flags":[]},{"family":2,"type":"unicast","dst":"192.168.178.0/24","dev":"wlan0","table":"main","protocol":"kernel","scope":"link","prefsrc":"192.168.178.142","metric":600,"flags":[]},{"family":2,"type":"unicast","dst":"192.168.178.1","dev":"enp0s31f6","table":"1234","protocol":"dhcp","scope":"link","prefsrc":"192.168.178.62","metric":100,"flags":[]},{"family":2,"type":"broadcast","dst":"192.168.178.255","dev":"enp0s31f6","table":"local","protocol":"kernel","scope":"link","prefsrc":"192.168.178.62","flags":[]}]' # nopep8 ROUTE6 = '[{"family":10,"type":"unicast","dst":"::1","dev":"lo","table":"main","protocol":"kernel","scope":"global","metric":256,"flags":[],"pref":"medium"},{"family":10,"type":"unicast","dst":"2001:9e8:a19f:1c00::/64","dev":"enp0s31f6","table":"main","protocol":"ra","scope":"global","metric":100,"flags":[],"expires":7199,"pref":"medium"},{"family":10,"type":"unicast","dst":"2001:9e8:a19f:1c00::/64","dev":"wlan0","table":"main","protocol":"ra","scope":"global","metric":600,"flags":[],"pref":"medium"},{"family":10,"type":"unicast","dst":"2001:9e8:a19f:1c00::/56","gateway":"fe80::cece:1eff:fe3d:c737","dev":"enp0s31f6","table":"main","protocol":"ra","scope":"global","metric":100,"flags":[],"expires":1799,"pref":"medium"},{"family":10,"type":"unicast","dst":"2001:9e8:a19f:1c00::/56","gateway":"fe80::cece:1eff:fe3d:c737","dev":"wlan0","table":"main","protocol":"ra","scope":"global","metric":600,"flags":[],"pref":"medium"},{"family":10,"type":"unicast","dst":"2001:dead:beef::/64","dev":"tun0","table":"main","protocol":"kernel","scope":"global","metric":256,"flags":[],"pref":"medium"},{"family":10,"type":"unicast","dst":"fe80::/64","dev":"enp0s31f6","table":"main","protocol":"kernel","scope":"global","metric":256,"flags":[],"pref":"medium"},{"family":10,"type":"unicast","dst":"fe80::/64","dev":"wlan0","table":"main","protocol":"kernel","scope":"global","metric":1024,"flags":[],"pref":"medium"},{"family":10,"type":"unicast","dst":"default","gateway":"fe80::cece:1eff:fe3d:c737","dev":"enp0s31f6","table":"1234","protocol":"ra","scope":"global","metric":100,"flags":[],"expires":1799,"metrics":[{"mtu":1492}],"pref":"medium"},{"family":10,"type":"unicast","dst":"default","gateway":"fe80::cece:1eff:fe3d:c737","dev":"wlan0","table":"main","protocol":"ra","scope":"global","metric":20600,"flags":[],"pref":"medium"}]' # nopep8 DNS_IP4 = ([192, 168, 178, 1]) From 54d59d17c750f6098a29f9e2e54d8615c46f28fc Mon Sep 17 00:00:00 2001 From: Danilo Egea Gondolfo Date: Wed, 25 Sep 2024 11:16:28 +0100 Subject: [PATCH 06/10] state/SSID: handle non-ascii SSIDs This workaround is intended to properly decode non-ascii SSIDs returned by networkctl. In such a case, a sequence of bytes (in octal notation) will be returned and we were displaying this string in netplan status. --- netplan_cli/cli/state.py | 7 +++++++ tests/cli/test_state.py | 11 ++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/netplan_cli/cli/state.py b/netplan_cli/cli/state.py index 734efdcc0..3ae7646e3 100644 --- a/netplan_cli/cli/state.py +++ b/netplan_cli/cli/state.py @@ -351,11 +351,18 @@ def ssid(self) -> str: if self.type == 'wifi': # XXX: available from networkctl's JSON output as of v250: # https://github.com/systemd/systemd/commit/da7c995 + # TODO: Retrieving the SSID from systemd seems to not be reliable. + # Sometimes it will return "(null)". for line in self._networkctl.splitlines(): line = line.strip() key = r'^Wi-?Fi access point: (.*) \(.*\)' if match := re.match(key, line): ssid = match.group(1) + # TODO: Find a better way to retrieve the SSID + # networkctl will return a non-ascii SSID using the octal notation below: + # '\\303\\241\\303\\251\\303\\255\\303\\263\\303... + # Here we handle the escaping, the encoding of individual bytes and the final decoding to utf-8 + ssid = ssid.encode('latin1').decode('unicode-escape').encode('latin1').decode('utf-8') return ssid if ssid else None return None diff --git a/tests/cli/test_state.py b/tests/cli/test_state.py index fa2914ec8..7d0e6172e 100644 --- a/tests/cli/test_state.py +++ b/tests/cli/test_state.py @@ -440,14 +440,15 @@ def test_json_nm_wlan0_2(self, networkctl_mock, nm_ssid_mock): @patch('netplan_cli.cli.state.Interface.query_nm_ssid') @patch('netplan_cli.cli.state.Interface.query_networkctl') - def test_json_nm_wlan1(self, networkctl_mock, nm_ssid_mock): - SSID = 'áéíóúççção€€€' - nm_ssid_mock.return_value = SSID + def test_json_nm_wlan1_non_ascii(self, networkctl_mock, nm_ssid_mock): + ND_SSID = '\\303\\241\\303\\251\\303\\255\\303\\263\\303\\272\\302\\242\\302\\242\\302\\242\\302\\243\\302\\243\\302\\243' + NM_SSID = 'áéíóú¢¢¢£££' + nm_ssid_mock.return_value = NM_SSID # networkctl mock output reduced to relevant lines # Newer versions of systemd changed WiFi to Wi-Fi # See systemd commit 8fff78a1dded105e1ee87bc66e29ef2fd61bf8c9 networkctl_mock.return_value = \ - 'Wi-Fi access point: {} (b4:fb:e4:75:c6:21)'.format(SSID) + 'Wi-Fi access point: {} (b4:fb:e4:75:c6:21)'.format(ND_SSID) data = {'ifname': 'wlan1', 'ifindex': 123} nd = [{'Index': 123, 'Type': 'wlan', 'Name': 'wlan1'}] @@ -455,7 +456,7 @@ def test_json_nm_wlan1(self, networkctl_mock, nm_ssid_mock): itf = Interface(data, nd, nm, (None, None), (None, None)) _, json = itf.json() - self.assertEqual(json.get('ssid'), SSID) + self.assertEqual(json.get('ssid'), NM_SSID) @patch('netplan_cli.cli.state.Interface.query_networkctl') def test_json_nd_enp0s31f6(self, networkctl_mock): From ff65879a91ca92b094933b4e3d6399b8125b1abd Mon Sep 17 00:00:00 2001 From: Danilo Egea Gondolfo Date: Thu, 26 Sep 2024 10:27:02 +0100 Subject: [PATCH 07/10] state: get the SSID from NM if it's the backend networkctl might return "(null)" as the SSID value. Use Network Manager to get the SSID when it's the backend. --- netplan_cli/cli/state.py | 2 ++ tests/cli/test_state.py | 18 ++++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/netplan_cli/cli/state.py b/netplan_cli/cli/state.py index 3ae7646e3..ff5115940 100644 --- a/netplan_cli/cli/state.py +++ b/netplan_cli/cli/state.py @@ -349,6 +349,8 @@ def vendor(self) -> str: @property def ssid(self) -> str: if self.type == 'wifi': + if self.backend == "NetworkManager": + return self.query_nm_ssid(self.nm.get('name', '')) # XXX: available from networkctl's JSON output as of v250: # https://github.com/systemd/systemd/commit/da7c995 # TODO: Retrieving the SSID from systemd seems to not be reliable. diff --git a/tests/cli/test_state.py b/tests/cli/test_state.py index 7d0e6172e..699227f7b 100644 --- a/tests/cli/test_state.py +++ b/tests/cli/test_state.py @@ -440,7 +440,7 @@ def test_json_nm_wlan0_2(self, networkctl_mock, nm_ssid_mock): @patch('netplan_cli.cli.state.Interface.query_nm_ssid') @patch('netplan_cli.cli.state.Interface.query_networkctl') - def test_json_nm_wlan1_non_ascii(self, networkctl_mock, nm_ssid_mock): + def test_json_nd_wlan1_non_ascii(self, networkctl_mock, nm_ssid_mock): ND_SSID = '\\303\\241\\303\\251\\303\\255\\303\\263\\303\\272\\302\\242\\302\\242\\302\\242\\302\\243\\302\\243\\302\\243' NM_SSID = 'áéíóú¢¢¢£££' nm_ssid_mock.return_value = NM_SSID @@ -451,7 +451,21 @@ def test_json_nm_wlan1_non_ascii(self, networkctl_mock, nm_ssid_mock): 'Wi-Fi access point: {} (b4:fb:e4:75:c6:21)'.format(ND_SSID) data = {'ifname': 'wlan1', 'ifindex': 123} - nd = [{'Index': 123, 'Type': 'wlan', 'Name': 'wlan1'}] + nd = [{'Index': 123, 'Type': 'wlan', 'Name': 'wlan1', 'SetupState': 'managed', + 'NetworkFile': '/run/systemd/network/10-netplan-wlan1.network'}] + nm = SystemConfigState.process_nm(NMCLI) + + itf = Interface(data, nd, nm, (None, None), (None, None)) + _, json = itf.json() + self.assertEqual(json.get('ssid'), NM_SSID) + + @patch('netplan_cli.cli.state.Interface.query_nm_ssid') + def test_json_nm_wlan1_non_ascii(self, nm_ssid_mock): + NM_SSID = 'áéíóú¢¢¢£££' + nm_ssid_mock.return_value = NM_SSID + + data = {'ifname': 'wlan1', 'ifindex': 123} + nd = [{'Index': 123, 'Type': 'wlan', 'Name': 'wlan1', 'SetupState': 'unmanaged'}] nm = SystemConfigState.process_nm(NMCLI) itf = Interface(data, nd, nm, (None, None), (None, None)) From 88202316a523409ca5f28b41c8c530dd731769b7 Mon Sep 17 00:00:00 2001 From: Danilo Egea Gondolfo Date: Fri, 4 Oct 2024 12:45:14 +0100 Subject: [PATCH 08/10] wpa_supplicant: add ExecReload to the service unit Add an ExecReload command to call "wpa_cli reconfigure" on the interface. With that we can use "systemctl reload" to reconfigure a wireless device without having to call stop/start. --- src/networkd.c | 2 ++ tests/generator/base.py | 1 + 2 files changed, 3 insertions(+) diff --git a/src/networkd.c b/src/networkd.c index 814fabc6e..aed0273f6 100644 --- a/src/networkd.c +++ b/src/networkd.c @@ -1341,6 +1341,8 @@ write_wpa_unit(const NetplanNetDefinition* def, const char* rootdir) g_string_append(s, " -Dnl80211,wext\n"); } + g_string_append_printf(s, "ExecReload=/sbin/wpa_cli -i %s reconfigure\n", stdouth); + g_autofree char* new_s = _netplan_scrub_systemd_unit_contents(s->str); g_string_free(s, TRUE); s = g_string_new(new_s); diff --git a/tests/generator/base.py b/tests/generator/base.py index b32b04234..30f42adde 100644 --- a/tests/generator/base.py +++ b/tests/generator/base.py @@ -99,6 +99,7 @@ [Service] Type=simple ExecStart=/sbin/wpa_supplicant -c /run/netplan/wpa-%(iface)s.conf -i%(iface)s -D%(drivers)s +ExecReload=/sbin/wpa_cli -i %(iface)s reconfigure ''' NM_MANAGED = 'SUBSYSTEM=="net", ACTION=="add|change|move", ENV{ID_NET_NAME}=="%s", ENV{NM_UNMANAGED}="0"\n' NM_UNMANAGED = 'SUBSYSTEM=="net", ACTION=="add|change|move", ENV{ID_NET_NAME}=="%s", ENV{NM_UNMANAGED}="1"\n' From cfac5eb95d67b2db0f5cb2cd72adc0de0960f521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Thu, 10 Oct 2024 11:30:31 +0200 Subject: [PATCH 09/10] include: fix apidoc warnings about undocumented parameters --- include/netdef.h | 4 ++-- include/state.h | 8 ++++---- include/types.h | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/include/netdef.h b/include/netdef.h index f84cd23be..595dcd321 100644 --- a/include/netdef.h +++ b/include/netdef.h @@ -54,7 +54,7 @@ netplan_netdef_get_filepath(const NetplanNetDefinition* netdef, char* out_buffer /** * @brief Get the specific @ref NetplanBackend defined for this @ref NetplanNetDefinition. - * @param[in] np_state The @ref NetplanState to query + * @param[in] netdef The @ref NetplanNetDefinition to query * @return Enumeration value, specifiying the @ref NetplanBackend */ NETPLAN_PUBLIC NetplanBackend @@ -62,7 +62,7 @@ netplan_netdef_get_backend(const NetplanNetDefinition* netdef); /** * @brief Get the interface type for a given @ref NetplanNetDefinition. - * @param[in] np_state The @ref NetplanState to query + * @param[in] netdef The @ref NetplanNetDefinition to query * @return Enumeration value of @ref NetplanDefType, specifiying the interface type */ NETPLAN_PUBLIC NetplanDefType diff --git a/include/state.h b/include/state.h index 5416e04a9..a44392838 100644 --- a/include/state.h +++ b/include/state.h @@ -124,10 +124,10 @@ netplan_state_update_yaml_hierarchy( /** * @brief Dump the whole @ref NetplanState into a single YAML file. * @details Ignoring the origin of each @ref NetplanNetDefinition. - * @param[in] np_state The @ref NetplanState for which to generate the configuration - * @param[in] out_fd File descriptor to an opened file into which to dump the content - * @param[out] error Filled with a @ref NetplanError in case of failure - * @return Indication of success or failure + * @param[in] np_state The @ref NetplanState for which to generate the configuration + * @param[in] output_fd File descriptor to an opened file into which to dump the content + * @param[out] error Filled with a @ref NetplanError in case of failure + * @return Indication of success or failure */ NETPLAN_PUBLIC gboolean netplan_state_dump_yaml( diff --git a/include/types.h b/include/types.h index df4ef1954..55bbcf4d8 100644 --- a/include/types.h +++ b/include/types.h @@ -106,7 +106,7 @@ typedef struct _NetplanStateIterator NetplanStateIterator; * @note The idea is based on the GLib implementation of iterators. */ struct _NetplanStateIterator { - void* placeholder; + void* placeholder; ///< Just a placeholder in memory }; /* From 83a8d8cc6f27cb001b6f81b5c59a47bf980f1029 Mon Sep 17 00:00:00 2001 From: Danilo Egea Gondolfo Date: Fri, 11 Oct 2024 14:38:53 +0100 Subject: [PATCH 10/10] networkmanager: add support for stable-ssid MAC option 802-11-wireless.cloned-mac-address = stable-ssid was added in Network Manager 1.46 and is available through the GUI applet. tests: add integration test for stable-ssid In order to create a predictable MAC address we need to set a fixed /etc/machine-id and /var/lib/NetworkManager/secret_key. Note that this test is currently unstable and skipped by default. --- doc/netplan-yaml.md | 2 +- src/parse.c | 4 ++-- src/util-internal.h | 2 +- src/util.c | 5 ++-- tests/generator/test_ethernets.py | 16 ++++++++++++- tests/generator/test_wifis.py | 36 ++++++++++++++++++++++++++++ tests/integration/wifi.py | 40 +++++++++++++++++++++++++++++++ 7 files changed, 98 insertions(+), 7 deletions(-) diff --git a/doc/netplan-yaml.md b/doc/netplan-yaml.md index 998b06c05..2857e7eab 100644 --- a/doc/netplan-yaml.md +++ b/doc/netplan-yaml.md @@ -499,7 +499,7 @@ Match devices by MAC when setting options like: `wakeonlan` or `*-offload`. > "XX:XX:XX:XX:XX:XX". The following special options are also accepted: > `permanent` and `random`. > In addition to these options, the NetworkManager renderer also accepts - > `stable` and `preserve`. + > `stable`, `stable-ssid` (Wi-Fi only) and `preserve`. > > **Note:** This will not work reliably for devices matched by name > only and rendered by networkd, due to interactions with device diff --git a/src/parse.c b/src/parse.c index 79c5c7145..6500c7a9c 100644 --- a/src/parse.c +++ b/src/parse.c @@ -392,7 +392,7 @@ handle_special_macaddress_option(NetplanParser* npp, yaml_node_t* node, void* en g_assert(entryptr != NULL); g_assert(node->type == YAML_SCALAR_NODE); - if (!_is_macaddress_special_nm_option(scalar(node)) && + if (!_is_macaddress_special_nm_option(npp->current.netdef, scalar(node)) && !_is_macaddress_special_nd_option(scalar(node))) return FALSE; @@ -695,7 +695,7 @@ handle_netdef_set_mac(NetplanParser* npp, yaml_node_t* node, const void* data, G if (!handle_special_macaddress_option(npp, node, npp->current.netdef, data, NULL)) { return yaml_error(npp, node, error, "Invalid MAC address '%s', must be XX:XX:XX:XX:XX:XX, XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX" - " or one of 'permanent', 'random', 'stable', 'preserve'.", + " or one of 'permanent', 'random', 'stable', 'preserve', 'stable-ssid' (Wi-Fi only).", scalar(node)); } } diff --git a/src/util-internal.h b/src/util-internal.h index bed36d493..bfc4dbebc 100644 --- a/src/util-internal.h +++ b/src/util-internal.h @@ -136,7 +136,7 @@ gboolean _is_auth_key_management_psk(const NetplanAuthenticationSettings* auth); gboolean -_is_macaddress_special_nm_option(const char* value); +_is_macaddress_special_nm_option(const NetplanNetDefinition* netdef, const char* value); gboolean _is_macaddress_special_nd_option(const char* value); diff --git a/src/util.c b/src/util.c index 0fc06f222..294a2f457 100644 --- a/src/util.c +++ b/src/util.c @@ -1239,12 +1239,13 @@ _is_auth_key_management_psk(const NetplanAuthenticationSettings* auth) } gboolean -_is_macaddress_special_nm_option(const char* value) +_is_macaddress_special_nm_option(const NetplanNetDefinition* netdef, const char* value) { return ( !g_strcmp0(value, "preserve") || !g_strcmp0(value, "permanent") || !g_strcmp0(value, "random") - || !g_strcmp0(value, "stable")); + || !g_strcmp0(value, "stable") + || (!g_strcmp0(value, "stable-ssid") && netdef->type == NETPLAN_DEF_TYPE_WIFI)); } gboolean diff --git a/tests/generator/test_ethernets.py b/tests/generator/test_ethernets.py index 9f01fcf28..0e3ad0d5d 100644 --- a/tests/generator/test_ethernets.py +++ b/tests/generator/test_ethernets.py @@ -504,7 +504,21 @@ def test_eth_set_mac_special_values_error(self): error = ("Invalid MAC address 'preservetypo', must be XX:XX:XX:XX:XX:XX, " "XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX or " - "one of 'permanent', 'random', 'stable', 'preserve'") + "one of 'permanent', 'random', 'stable', 'preserve', 'stable-ssid' (Wi-Fi only)") + self.assertIn(error, res) + + def test_eth_set_mac_special_values_ethernet_stable_ssid(self): + res = self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + eth0: + macaddress: stable-ssid + dhcp4: true''', expect_fail=True) + + error = ("Invalid MAC address 'stable-ssid', must be XX:XX:XX:XX:XX:XX, " + "XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX or " + "one of 'permanent', 'random', 'stable', 'preserve', 'stable-ssid' (Wi-Fi only)") self.assertIn(error, res) def test_eth_match_by_driver(self): diff --git a/tests/generator/test_wifis.py b/tests/generator/test_wifis.py index 6dc8df7f8..5809b40e7 100644 --- a/tests/generator/test_wifis.py +++ b/tests/generator/test_wifis.py @@ -1114,6 +1114,42 @@ def test_wifi_regdom(self): new_config = f.read() self.assertIn('ExecStart=/usr/sbin/iw reg set DE\n', new_config) + def test_wlan_set_mac_special_values(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + wifis: + wlan0: + macaddress: stable-ssid + dhcp4: true + access-points: + "mynetwork": + password: mypassword''') + + self.assert_networkd(None) + + self.assert_nm({'wlan0-mynetwork': '''[connection] +id=netplan-wlan0-mynetwork +type=wifi +interface-name=wlan0 + +[wifi] +cloned-mac-address=stable-ssid +ssid=mynetwork +mode=infrastructure + +[ipv4] +method=auto + +[ipv6] +method=ignore + +[wifi-security] +key-mgmt=wpa-psk +pmf=2 +psk=mypassword +'''}) + class TestConfigErrors(TestBase): diff --git a/tests/integration/wifi.py b/tests/integration/wifi.py index 26200d0c1..4e8079ad7 100644 --- a/tests/integration/wifi.py +++ b/tests/integration/wifi.py @@ -181,5 +181,45 @@ def test_wifi_ap_open(self): self.assertIn('ssid fake net', out) self.assert_iface_up(self.dev_w_ap, ['inet 10.']) + @unittest.skip("Test if flaky. NM might generate a different MAC address.") + def test_wifi_cloned_macaddress_stable_ssid(self): + self.setup_ap('''hw_mode=g +channel=1 +ssid=fake net +wpa=1 +wpa_key_mgmt=WPA-PSK +wpa_pairwise=TKIP +wpa_passphrase=12345678 +''', None) + + with open(self.config, 'w') as f: + f.write('''network: + renderer: NetworkManager + wifis: + %(wc)s: + addresses: ["192.168.1.42/24"] + dhcp4: false + dhcp6: false + macaddress: stable-ssid + access-points: + "fake net": + password: 12345678''' % {'wc': self.dev_w_client}) + + subprocess.check_call(['systemctl', 'start', 'NetworkManager']) + + # Make the generated MAC address predictable + # See nm_utils_hw_addr_gen_stable_eth() in NM for details + # TODO: save and restore these files to avoid any impact on the + # entire test suite. + with open('/etc/machine-id', 'w') as f: + f.write('ee7ac3602b6306061bd984a41eb1c045\n') + with open('/var/lib/NetworkManager/secret_key', 'w') as f: + f.write('nm-v2:hnIHoHp4p9kaEWU5/+dO+gFREirN1AsMoO1MPaoYxCc=') + + subprocess.check_call(['systemctl', 'restart', 'NetworkManager']) + + self.generate_and_settle([self.state_up(self.dev_w_client)]) + self.assert_iface_up(self.dev_w_client, ['ether 5e:ba:fe:fd:89:03']) + unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2))