From e25761f04118baf42716b3ee7b743af788683eb3 Mon Sep 17 00:00:00 2001 From: ushiboy Date: Sat, 20 Dec 2025 16:59:44 +0900 Subject: [PATCH 1/3] Update the compatibility table --- README.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 18f75bf..7fa6d86 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ except Exception as e: | general | hostname | supported | | general | permissions | not supported | | general | logging | not supported | +| general | reload | not supported | | networking | | supported | | networking | on | supported | | networking | off | supported | @@ -61,6 +62,7 @@ except Exception as e: | connection | clone | not supported | | connection | edit | not supported | | connection | delete | supported | +| connection | monitor | not supported | | connection | reload | supported | | connection | load | not supported | | connection | import | not supported | @@ -69,17 +71,20 @@ except Exception as e: | device | status | supported | | device | show | supported | | device | set | not supported | +| device | up | not supported | | device | connect | supported | | device | reapply | supported | | device | modify | not supported | +| device | down | not supported | | device | disconnect | supported | | device | delete | supported | | device | monitor | not supported | -| device | wifi | supported | -| device | wifi connect | supported | -| device | wifi rescan | supported | -| device | wifi hotspot | supported | -| device | lldp | not supported | +| device | wifi | supported | +| device | wifi connect | supported | +| device | wifi rescan | supported | +| device | wifi hotspot | supported | +| device | wifi show-password | not supported | +| device | lldp | not supported | | agent | | not supported | | agent | secret | not supported | | agent | polkit | not supported | From 0cff316ed1db716c1538822bb4bf96bfe7d3ca4e Mon Sep 17 00:00:00 2001 From: ushiboy Date: Sat, 20 Dec 2025 17:12:36 +0900 Subject: [PATCH 2/3] Add device up and device down --- README.md | 24 ++++++++++++++++++++++-- nmcli/_device.py | 16 ++++++++++++++++ nmcli/dummy/_device.py | 20 +++++++++++++++++++- tests/test_device.py | 23 +++++++++++++++++++++++ 4 files changed, 80 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7fa6d86..9f8914d 100644 --- a/README.md +++ b/README.md @@ -71,11 +71,11 @@ except Exception as e: | device | status | supported | | device | show | supported | | device | set | not supported | -| device | up | not supported | +| device | up | supported | | device | connect | supported | | device | reapply | supported | | device | modify | not supported | -| device | down | not supported | +| device | down | supported | | device | disconnect | supported | | device | delete | supported | | device | monitor | not supported | @@ -221,6 +221,16 @@ The `fields` argument applies the same effect to the command as the `-f | --fiel nmcli.device.show_all(fields: str = None) -> List[DeviceDetails] ``` +#### nmcli.device.up + +Connect the device. + +The `wait` argument applies the same effect to the command as the `--wait` option. If it is omitted, the default behavior is followed. + +``` +nmcli.device.up(ifname: str, wait: int = None) -> None +``` + #### nmcli.device.connect Connect the device. @@ -231,6 +241,16 @@ The `wait` argument applies the same effect to the command as the `--wait` optio nmcli.device.connect(ifname: str, wait: int = None) -> None ``` +#### nmcli.device.down + +Disconnect a device and prevent the device from automatically activating further connections without user/manual intervention. + +The `wait` argument applies the same effect to the command as the `--wait` option. If it is omitted, the default behavior is followed. + +``` +nmcli.device.down(ifname: str, wait: int = None) -> None +``` + #### nmcli.device.disconnect Disconnect devices. diff --git a/nmcli/_device.py b/nmcli/_device.py index 9f71c0a..ea60321 100644 --- a/nmcli/_device.py +++ b/nmcli/_device.py @@ -52,9 +52,15 @@ def show(self, ifname: str, fields: str = None) -> DeviceDetails: def show_all(self, fields: str = None) -> List[DeviceDetails]: raise NotImplementedError + def up(self, ifname: str, wait: int = None) -> None: + raise NotImplementedError + def connect(self, ifname: str, wait: int = None) -> None: raise NotImplementedError + def down(self, ifname: str, wait: int = None) -> None: + raise NotImplementedError + def disconnect(self, ifname: str, wait: int = None) -> None: raise NotImplementedError @@ -131,11 +137,21 @@ def show_all(self, fields: str = None) -> List[DeviceDetails]: details[key] = None if value in ('--', '""') else value return results + def up(self, ifname: str, wait: int = None) -> None: + cmd = add_wait_option_if_needed( + wait) + ['device', 'up', ifname] + self._syscmd.nmcli(cmd) + def connect(self, ifname: str, wait: int = None) -> None: cmd = add_wait_option_if_needed( wait) + ['device', 'connect', ifname] self._syscmd.nmcli(cmd) + def down(self, ifname: str, wait: int = None) -> None: + cmd = add_wait_option_if_needed( + wait) + ['device', 'down', ifname] + self._syscmd.nmcli(cmd) + def disconnect(self, ifname: str, wait: int = None) -> None: cmd = add_wait_option_if_needed( wait) + ['device', 'disconnect', ifname] diff --git a/nmcli/dummy/_device.py b/nmcli/dummy/_device.py index d6e1bc0..10d9c01 100644 --- a/nmcli/dummy/_device.py +++ b/nmcli/dummy/_device.py @@ -5,16 +5,24 @@ from ..data.hotspot import Hotspot -class DummyDeviceControl(DeviceControlInterface): +class DummyDeviceControl(DeviceControlInterface): # pylint: disable=too-many-public-methods @property def show_args(self): return self._show_args + @property + def up_args(self): + return self._up_args + @property def connect_args(self): return self._connect_args + @property + def down_args(self): + return self._down_args + @property def disconnect_args(self): return self._disconnect_args @@ -57,7 +65,9 @@ def __init__(self, self._result_show_all = result_show_all or [] self._result_wifi_hotspot = result_wifi_hotspot self._show_args: List[Tuple] = [] + self._up_args: List[Tuple] = [] self._connect_args: List[Tuple] = [] + self._down_args: List[Tuple] = [] self._disconnect_args: List[Tuple] = [] self._reapply_args: List[str] = [] self._delete_args: List[Tuple] = [] @@ -85,10 +95,18 @@ def show_all(self, fields: str = None) -> List[DeviceDetails]: self._raise_error_if_needed() return self._result_show_all + def up(self, ifname: str, wait: int = None) -> None: + self._raise_error_if_needed() + self._up_args.append((ifname, wait)) + def connect(self, ifname: str, wait: int = None) -> None: self._raise_error_if_needed() self._connect_args.append((ifname, wait)) + def down(self, ifname: str, wait: int = None) -> None: + self._raise_error_if_needed() + self._down_args.append((ifname, wait)) + def disconnect(self, ifname: str, wait: int = None) -> None: self._raise_error_if_needed() self._disconnect_args.append((ifname, wait)) diff --git a/tests/test_device.py b/tests/test_device.py index 164e3ef..18ff45c 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -171,6 +171,17 @@ def test_show_all(): assert s2.passed_parameters == ['-f', 'all', 'device', 'show'] +def test_up(): + s = DummySystemCommand() + device = DeviceControl(s) + ifname = 'eth0' + device.up(ifname) + assert s.passed_parameters == ['device', 'up', ifname] + + device.up(ifname, wait=10) + assert s.passed_parameters == ['--wait', '10', 'device', 'up', ifname] + + def test_connect(): s = DummySystemCommand() device = DeviceControl(s) @@ -182,6 +193,18 @@ def test_connect(): assert s.passed_parameters == ['--wait', '10', 'device', 'connect', ifname] +def test_down(): + s = DummySystemCommand() + device = DeviceControl(s) + ifname = 'eth0' + device.down(ifname) + assert s.passed_parameters == ['device', 'down', ifname] + + device.down(ifname, wait=10) + assert s.passed_parameters == [ + '--wait', '10', 'device', 'down', ifname] + + def test_disconnect(): s = DummySystemCommand() device = DeviceControl(s) From b4f06c7d49dbe1787e9b80b75e39fc48f508bebc Mon Sep 17 00:00:00 2001 From: ushiboy Date: Sat, 20 Dec 2025 22:08:34 +0900 Subject: [PATCH 3/3] Add general reload --- README.md | 17 ++++++++++++++++- nmcli/_general.py | 20 ++++++++++++++++++++ nmcli/dummy/_general.py | 11 ++++++++++- tests/test_general.py | 40 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9f8914d..80b8930 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ except Exception as e: | general | hostname | supported | | general | permissions | not supported | | general | logging | not supported | -| general | reload | not supported | +| general | reload | supported | | networking | | supported | | networking | on | supported | | networking | off | supported | @@ -355,6 +355,21 @@ Change persistent system hostname. nmcli.general.set_hostname(hostname: str) -> None ``` +#### nmcli.general.reload + +Reload NetworkManager's configuration and perform certain updates. + +The `flags` argument specifies which configurations to reload. Valid flags are: +- `conf`: Reload NetworkManager.conf configuration from disk +- `dns-rc`: Update DNS configuration (equivalent to SIGUSR1) +- `dns-full`: Restart the DNS plugin + +If no flags are provided, everything that is supported is reloaded. + +``` +nmcli.general.reload(flags: Optional[List[str]] = None) -> None +``` + ### networking #### nmcli.networking diff --git a/nmcli/_general.py b/nmcli/_general.py index 4821afd..b64f6bb 100644 --- a/nmcli/_general.py +++ b/nmcli/_general.py @@ -1,3 +1,5 @@ +from typing import List, Optional + from ._system import SystemCommand, SystemCommandInterface from .data import General @@ -16,9 +18,14 @@ def get_hostname(self) -> str: def set_hostname(self, hostname: str): raise NotImplementedError + def reload(self, flags: Optional[List[str]] = None) -> None: + raise NotImplementedError + class GeneralControl(GeneralControlInterface): + VALID_RELOAD_FLAGS = ['conf', 'dns-rc', 'dns-full'] + def __init__(self, syscmd: SystemCommandInterface = None): self._syscmd = syscmd or SystemCommand() @@ -35,3 +42,16 @@ def get_hostname(self) -> str: def set_hostname(self, hostname: str): self._syscmd.nmcli(['general', 'hostname', hostname]) + + def reload(self, flags: Optional[List[str]] = None) -> None: + if flags is not None: + for flag in flags: + if flag not in self.VALID_RELOAD_FLAGS: + raise ValueError( + f"Invalid reload flag '{flag}'. " + f"Valid flags are: {', '.join(self.VALID_RELOAD_FLAGS)}" + ) + cmd = ['general', 'reload'] + if flags: + cmd += flags + self._syscmd.nmcli(cmd) diff --git a/nmcli/dummy/_general.py b/nmcli/dummy/_general.py index ca2c7c9..3269649 100644 --- a/nmcli/dummy/_general.py +++ b/nmcli/dummy/_general.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Optional from .._general import GeneralControlInterface from ..data.general import General @@ -10,6 +10,10 @@ class DummyGeneralControl(GeneralControlInterface): def set_hostname_args(self): return self._set_hostname_args + @property + def reload_args(self): + return self._reload_args + def __init__(self, result_call: General = None, result_status: General = None, @@ -20,6 +24,7 @@ def __init__(self, self._result_status = result_status self._result_hostname = result_hostname self._set_hostname_args: List[str] = [] + self._reload_args: List[Optional[List[str]]] = [] def __call__(self) -> General: self._raise_error_if_needed() @@ -41,6 +46,10 @@ def set_hostname(self, hostname: str): self._raise_error_if_needed() self._set_hostname_args.append(hostname) + def reload(self, flags: Optional[List[str]] = None) -> None: + self._raise_error_if_needed() + self._reload_args.append(flags) + def _raise_error_if_needed(self): if not self._raise_error is None: raise self._raise_error diff --git a/tests/test_general.py b/tests/test_general.py index 23656e1..870828f 100644 --- a/tests/test_general.py +++ b/tests/test_general.py @@ -1,3 +1,5 @@ +import pytest + from nmcli._const import NetworkConnectivity, NetworkManagerState from nmcli._general import GeneralControl from nmcli.data import General @@ -37,3 +39,41 @@ def test_set_hostname(): general = GeneralControl(s) general.set_hostname('test') assert s.passed_parameters == ['general', 'hostname', 'test'] + + +def test_reload_without_flags(): + s = DummySystemCommand() + general = GeneralControl(s) + general.reload() + assert s.passed_parameters == ['general', 'reload'] + + +def test_reload_with_single_flag(): + s = DummySystemCommand() + general = GeneralControl(s) + general.reload(['conf']) + assert s.passed_parameters == ['general', 'reload', 'conf'] + + +def test_reload_with_all_valid_flags(): + s = DummySystemCommand() + general = GeneralControl(s) + general.reload(['conf', 'dns-rc', 'dns-full']) + assert s.passed_parameters == ['general', 'reload', 'conf', 'dns-rc', 'dns-full'] + + +def test_reload_with_invalid_flag(): + s = DummySystemCommand() + general = GeneralControl(s) + with pytest.raises(ValueError) as exc_info: + general.reload(['invalid-flag']) + assert "Invalid reload flag 'invalid-flag'" in str(exc_info.value) + assert "Valid flags are: conf, dns-rc, dns-full" in str(exc_info.value) + + +def test_reload_with_mixed_valid_and_invalid_flags(): + s = DummySystemCommand() + general = GeneralControl(s) + with pytest.raises(ValueError) as exc_info: + general.reload(['conf', 'invalid']) + assert "Invalid reload flag 'invalid'" in str(exc_info.value)