diff --git a/netplan_cli/cli/commands/apply.py b/netplan_cli/cli/commands/apply.py index 8bf8a9477..066f4968c 100644 --- a/netplan_cli/cli/commands/apply.py +++ b/netplan_cli/cli/commands/apply.py @@ -176,9 +176,12 @@ def command_apply(self, run_generate=True, sync=False, exit_on_error=True, state else: logging.debug('no netplan generated networkd configuration exists') + loopback_connection = '' if restart_nm: logging.debug('netplan generated NM configuration changed, restarting NM') if utils.nm_running(): + if 'lo' in nm_ifaces: + loopback_connection = utils.nm_get_connection_for_interface('lo') # restarting NM does not cause new config to be applied, need to shut down devices first for device in devices: if device not in nm_ifaces: @@ -286,12 +289,20 @@ def command_apply(self, run_generate=True, sync=False, exit_on_error=True, state if restart_nm: # Flush all IP addresses of NM managed interfaces, to avoid NM creating # new, non netplan-* connection profiles, using the existing IPs. - for iface in utils.nm_interfaces(restart_nm_glob, devices): + nm_interfaces = utils.nm_interfaces(restart_nm_glob, devices) + for iface in nm_interfaces: utils.ip_addr_flush(iface) # clear NM state, especially the [device].managed=true config, as that might have been # re-set via an udev rule setting "NM_UNMANAGED=1" shutil.rmtree('/run/NetworkManager/devices', ignore_errors=True) utils.systemctl_network_manager('start', sync=sync) + + # If 'lo' is in the nm_interfaces set we flushed it's IPs (see above) and disconnected it. + # NM will not bring it back automatically after restarting and we need to do that manually. + # For that, we need NM up and ready to accept commands + if 'lo' in nm_interfaces: + sync = True + if sync: # 'nmcli' could be /usr/bin/nmcli or # /snap/bin/nmcli -> /snap/bin/network-manager.nmcli @@ -309,6 +320,13 @@ def command_apply(self, run_generate=True, sync=False, exit_on_error=True, state break time.sleep(0.5) + # If "lo" is managed by NM through Netplan, apply will flush its addresses and disconnect it. + # NM will not bring it back automatically. + # This is a possible scenario with netplan-everywhere. If a user tries to change the 'lo' + # connection with nmcli for example, NM will create a persistent nmconnection file and emit a YAML for it. + if 'lo' in nm_interfaces and loopback_connection: + utils.nm_bring_interface_up(loopback_connection) + @staticmethod def is_composite_member(composites, phy): """ diff --git a/netplan_cli/cli/utils.py b/netplan_cli/cli/utils.py index d0a85dffc..a5f60c979 100644 --- a/netplan_cli/cli/utils.py +++ b/netplan_cli/cli/utils.py @@ -78,6 +78,20 @@ def nm_interfaces(paths, devices): return interfaces +def nm_get_connection_for_interface(interface: str) -> str: + output = nmcli_out(['-m', 'tabular', '-f', 'GENERAL.CONNECTION', 'device', 'show', interface]) + lines = output.strip().split('\n') + connection = lines[1] + return connection if connection != '--' else '' + + +def nm_bring_interface_up(connection: str) -> None: # pragma: nocover (must be covered by NM autopkgtests) + try: + nmcli(['connection', 'up', connection]) + except subprocess.CalledProcessError: + pass + + def systemctl_network_manager(action, sync=False): # If the network-manager snap is installed use its service # name rather than the one of the deb packaged NetworkManager diff --git a/tests/test_utils.py b/tests/test_utils.py index ec57087e6..b2a1657e1 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -371,3 +371,15 @@ def test_ip_addr_flush(self): self.assertEqual(self.mock_cmd.calls(), [ ['ip', 'addr', 'flush', 'eth42'] ]) + + @patch('netplan_cli.cli.utils.nmcli_out') + def test_nm_get_connection_for_interface(self, nmcli): + nmcli.return_value = 'CONNECTION \nlo \n' + out = utils.nm_get_connection_for_interface('lo') + self.assertEqual(out, 'lo') + + @patch('netplan_cli.cli.utils.nmcli_out') + def test_nm_get_connection_for_interface_no_connection(self, nmcli): + nmcli.return_value = 'CONNECTION \n-- \n' + out = utils.nm_get_connection_for_interface('asd0') + self.assertEqual(out, '')