Skip to content

Network

Network interface and NetworkManager connection management — IP addressing, link state, MAC handling, and NM connection profiles for test network setup.

sts.network.interface

Network interface discovery and management.

NetworkInterface pydantic-model

Bases: NetworkDevice

Network interface representation.

Example
iface = NetworkInterface()
iface.discover()  # Discovers first available interface
iface = NetworkInterface(name='eth0')
iface.discover()  # Discovers other values
iface = NetworkInterface(mac='F0:DE:F1:0D:D3:C9')
iface.discover()  # Discovers other values
Show JSON schema:
{
  "additionalProperties": false,
  "description": "Network interface representation.\n\nExample:\n    ```python\n    iface = NetworkInterface()\n    iface.discover()  # Discovers first available interface\n    iface = NetworkInterface(name='eth0')\n    iface.discover()  # Discovers other values\n    iface = NetworkInterface(mac='F0:DE:F1:0D:D3:C9')\n    iface.discover()  # Discovers other values\n    ```",
  "properties": {
    "path": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "format": "path",
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "title": "Path"
    },
    "name": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "title": "Name"
    },
    "ip": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "title": "Ip"
    },
    "port": {
      "anyOf": [
        {
          "type": "integer"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "title": "Port"
    },
    "mac": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "title": "Mac"
    },
    "driver": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "title": "Driver"
    },
    "pci_id": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "title": "Pci Id"
    }
  },
  "title": "NetworkInterface",
  "type": "object"
}

Fields:

  • path (PathOrStr | None)
  • name (str | None)
  • ip (str | None)
  • port (int | None)
  • mac (str | None)
  • driver (str | None)
  • pci_id (str | None)

Validators:

  • _derive_fields
Source code in sts_libs/src/sts/network/interface.py
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
class NetworkInterface(NetworkDevice):
    """Network interface representation.

    Example:
        ```python
        iface = NetworkInterface()
        iface.discover()  # Discovers first available interface
        iface = NetworkInterface(name='eth0')
        iface.discover()  # Discovers other values
        iface = NetworkInterface(mac='F0:DE:F1:0D:D3:C9')
        iface.discover()  # Discovers other values
        ```
    """

    # Optional parameters (name inherited from Device)
    mac: str | None = None
    driver: str | None = None
    pci_id: str | None = None

    def discover(self) -> Self:
        """Discover interface properties from the system.

        Runs subprocess commands to find name, MAC, driver, and PCI ID.
        Call after construction when system discovery is needed.
        """
        # Discover first available interface if no parameters provided
        if not any([self.name, self.mac]):
            self._discover_first_name()

        # Discover interface info if needed
        if self.name:
            self._discover_properties_from_name()

        # Discover interface by MAC if provided
        elif self.mac:
            self._discover_name_from_mac()

        return self

    def _discover_first_name(self) -> None:
        """Discover the first available interface name."""
        result = run('ls /sys/class/net')
        if result.succeeded:
            for name in result.stdout.splitlines():
                # Skip special interfaces
                if any(re.match(pattern, name) for pattern in self._get_skip_patterns()):
                    continue
                self.name = name
                break

    def _discover_properties_from_name(self) -> None:
        """Discover MAC, driver, and PCI ID from interface name."""
        if not self.name:
            return

        # Get MAC address if not provided
        if not self.mac:
            mac_result = run(f'ip -o link show {self.name}')
            if mac_result.succeeded:
                tokens = mac_result.stdout.split()
                with suppress(ValueError, IndexError):
                    mac = tokens[tokens.index('link/ether') + 1]
                    if self.is_valid_mac(mac):
                        self.mac = mac

        # Get driver if not provided
        if not self.driver:
            driver_path = self.NET_PATH / self.name / 'device/driver'
            if driver_path.exists():
                self.driver = driver_path.resolve().name

        # Get PCI ID if not provided
        if not self.pci_id:
            device_path = self.NET_PATH / self.name
            if device_path.exists():
                link = device_path.resolve().as_posix()
                pci_match = re.search(r'([0-9a-f]{4}:[0-9a-f]{2}:[0-9a-f]{2}\.[0-9a-f])/net', link)
                if pci_match:
                    self.pci_id = pci_match.group(1)

    def _discover_name_from_mac(self) -> None:
        """Discover interface name and properties from MAC address."""
        if not self.is_valid_mac(self.mac):  # type: ignore[arg-type]
            raise NetworkError(f'Invalid MAC format: {self.mac}')
        for interface in self.get_all():
            if interface.mac and self.mac and interface.mac.upper() == self.mac.upper():
                self.name = interface.name
                self.driver = interface.driver
                self.pci_id = interface.pci_id
                break

    @staticmethod
    def _get_skip_patterns() -> list[str]:
        """Get regex patterns for interfaces to skip during discovery."""
        return [
            r'^lo$',  # Loopback
            r'^tun[0-9]+$',  # TUN/TAP
            r'^vboxnet[0-9]+$',  # VirtualBox
            r'\.',  # Sub-interfaces
        ]

    @staticmethod
    def is_valid_mac(mac: str) -> bool:
        """Check if MAC address is valid."""
        return bool(MAC_PATTERN.match(mac))

    @staticmethod
    def standardize_mac(mac: str) -> str | None:
        """Standardize MAC address to colon-separated uppercase format.

        Example:
            ```python
            NetworkInterface.standardize_mac('F0-DE-F1-0D-D3-C9')
            'F0:DE:F1:0D:D3:C9'
            ```
        """
        if not mac:
            return None

        # Remove 0x and non-hex characters
        mac = re.sub(r'^0x', '', mac.upper())
        mac = re.sub(r'[^0-9A-F]', '', mac)

        # Add : every 2 characters
        if len(mac) == MAC_ADDRESS_LENGTH:
            mac = ':'.join(mac[i : i + 2] for i in range(0, MAC_ADDRESS_LENGTH, 2))

        return mac if NetworkInterface.is_valid_mac(mac) else None

    @property
    def ip_addresses(self) -> list[str]:
        """Get IP addresses assigned to this interface."""
        if not self.name:
            return []

        result = run(f'ip addr show {self.name}')
        if result.failed:
            return []

        addresses: list[str] = []
        for line in result.stdout.splitlines():
            if 'inet' in line:
                # Parse line like: inet 192.168.1.100/24
                try:
                    addr = line.split()[1].split('/')[0]
                    addresses.append(addr)
                except (IndexError, ValueError):
                    continue

        return addresses

    @staticmethod
    def get_ip_version(addr: str) -> Literal[4, 6] | None:
        """Get IP version (4 or 6), or None if invalid."""
        try:
            return ipaddress.ip_address(addr).version
        except ValueError:
            return None

    def up(self) -> bool:
        """Bring interface up."""
        if not self.name:
            logger.error('Interface name required')
            return False

        result = run(f'ip link set {self.name} up')
        return result.succeeded

    def down(self) -> bool:
        """Bring interface down."""
        if not self.name:
            logger.error('Interface name required')
            return False

        result = run(f'ip link set {self.name} down')
        return result.succeeded

    @classmethod
    def get_all(cls) -> list[NetworkInterface]:
        """Get all network interfaces with valid MAC addresses."""
        interfaces: list[NetworkInterface] = []

        # Get all interfaces
        result = run('ls /sys/class/net')
        if result.failed:
            return interfaces

        for name in result.stdout.splitlines():
            # Skip special interfaces
            if any(re.match(pattern, name) for pattern in cls._get_skip_patterns()):
                continue

            # Create interface
            try:
                interface = cls(name=name)
                interface.discover()
                if interface.mac:  # Only add if MAC was discovered
                    interfaces.append(interface)
            except (OSError, ValueError, NetworkError) as e:
                logger.warning(f'Failed to create interface: {e}')

        return interfaces

    @classmethod
    def get_by_mac(cls, mac: str) -> NetworkInterface | None:
        """Get interface by MAC address."""
        std_mac = cls.standardize_mac(mac)
        if not std_mac:
            return None

        try:
            iface = cls(mac=std_mac)
            iface.discover()
        except NetworkError:
            return None
        else:
            return iface

    @classmethod
    def get_by_ip(cls, ip: str) -> NetworkInterface | None:
        """Get interface by IP address."""
        for interface in cls.get_all():
            if ip in interface.ip_addresses:
                return interface

        return None

ip_addresses property

Get IP addresses assigned to this interface.

discover()

Discover interface properties from the system.

Runs subprocess commands to find name, MAC, driver, and PCI ID. Call after construction when system discovery is needed.

Source code in sts_libs/src/sts/network/interface.py
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
def discover(self) -> Self:
    """Discover interface properties from the system.

    Runs subprocess commands to find name, MAC, driver, and PCI ID.
    Call after construction when system discovery is needed.
    """
    # Discover first available interface if no parameters provided
    if not any([self.name, self.mac]):
        self._discover_first_name()

    # Discover interface info if needed
    if self.name:
        self._discover_properties_from_name()

    # Discover interface by MAC if provided
    elif self.mac:
        self._discover_name_from_mac()

    return self

down()

Bring interface down.

Source code in sts_libs/src/sts/network/interface.py
195
196
197
198
199
200
201
202
def down(self) -> bool:
    """Bring interface down."""
    if not self.name:
        logger.error('Interface name required')
        return False

    result = run(f'ip link set {self.name} down')
    return result.succeeded

get_all() classmethod

Get all network interfaces with valid MAC addresses.

Source code in sts_libs/src/sts/network/interface.py
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
@classmethod
def get_all(cls) -> list[NetworkInterface]:
    """Get all network interfaces with valid MAC addresses."""
    interfaces: list[NetworkInterface] = []

    # Get all interfaces
    result = run('ls /sys/class/net')
    if result.failed:
        return interfaces

    for name in result.stdout.splitlines():
        # Skip special interfaces
        if any(re.match(pattern, name) for pattern in cls._get_skip_patterns()):
            continue

        # Create interface
        try:
            interface = cls(name=name)
            interface.discover()
            if interface.mac:  # Only add if MAC was discovered
                interfaces.append(interface)
        except (OSError, ValueError, NetworkError) as e:
            logger.warning(f'Failed to create interface: {e}')

    return interfaces

get_by_ip(ip) classmethod

Get interface by IP address.

Source code in sts_libs/src/sts/network/interface.py
245
246
247
248
249
250
251
252
@classmethod
def get_by_ip(cls, ip: str) -> NetworkInterface | None:
    """Get interface by IP address."""
    for interface in cls.get_all():
        if ip in interface.ip_addresses:
            return interface

    return None

get_by_mac(mac) classmethod

Get interface by MAC address.

Source code in sts_libs/src/sts/network/interface.py
230
231
232
233
234
235
236
237
238
239
240
241
242
243
@classmethod
def get_by_mac(cls, mac: str) -> NetworkInterface | None:
    """Get interface by MAC address."""
    std_mac = cls.standardize_mac(mac)
    if not std_mac:
        return None

    try:
        iface = cls(mac=std_mac)
        iface.discover()
    except NetworkError:
        return None
    else:
        return iface

get_ip_version(addr) staticmethod

Get IP version (4 or 6), or None if invalid.

Source code in sts_libs/src/sts/network/interface.py
178
179
180
181
182
183
184
@staticmethod
def get_ip_version(addr: str) -> Literal[4, 6] | None:
    """Get IP version (4 or 6), or None if invalid."""
    try:
        return ipaddress.ip_address(addr).version
    except ValueError:
        return None

is_valid_mac(mac) staticmethod

Check if MAC address is valid.

Source code in sts_libs/src/sts/network/interface.py
128
129
130
131
@staticmethod
def is_valid_mac(mac: str) -> bool:
    """Check if MAC address is valid."""
    return bool(MAC_PATTERN.match(mac))

standardize_mac(mac) staticmethod

Standardize MAC address to colon-separated uppercase format.

Example
NetworkInterface.standardize_mac('F0-DE-F1-0D-D3-C9')
'F0:DE:F1:0D:D3:C9'
Source code in sts_libs/src/sts/network/interface.py
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
@staticmethod
def standardize_mac(mac: str) -> str | None:
    """Standardize MAC address to colon-separated uppercase format.

    Example:
        ```python
        NetworkInterface.standardize_mac('F0-DE-F1-0D-D3-C9')
        'F0:DE:F1:0D:D3:C9'
        ```
    """
    if not mac:
        return None

    # Remove 0x and non-hex characters
    mac = re.sub(r'^0x', '', mac.upper())
    mac = re.sub(r'[^0-9A-F]', '', mac)

    # Add : every 2 characters
    if len(mac) == MAC_ADDRESS_LENGTH:
        mac = ':'.join(mac[i : i + 2] for i in range(0, MAC_ADDRESS_LENGTH, 2))

    return mac if NetworkInterface.is_valid_mac(mac) else None

up()

Bring interface up.

Source code in sts_libs/src/sts/network/interface.py
186
187
188
189
190
191
192
193
def up(self) -> bool:
    """Bring interface up."""
    if not self.name:
        logger.error('Interface name required')
        return False

    result = run(f'ip link set {self.name} up')
    return result.succeeded

sts.network.nm

NetworkManager connection management via nmcli.

NetworkConnection pydantic-model

Bases: StsBaseModel

NetworkManager connection representation.

Example
conn = NetworkConnection()
conn.discover()  # Discovers first available connection
conn = NetworkConnection(name='eth0')
conn.discover()  # Discovers other values
Show JSON schema:
{
  "additionalProperties": false,
  "description": "NetworkManager connection representation.\n\nExample:\n    ```python\n    conn = NetworkConnection()\n    conn.discover()  # Discovers first available connection\n    conn = NetworkConnection(name='eth0')\n    conn.discover()  # Discovers other values\n    ```",
  "properties": {
    "name": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "title": "Name"
    },
    "uuid": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "title": "Uuid"
    },
    "interface": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "title": "Interface"
    },
    "conn_type": {
      "default": "ethernet",
      "enum": [
        "ethernet",
        "wifi",
        "bond",
        "bridge",
        "team",
        "vlan"
      ],
      "title": "Conn Type",
      "type": "string"
    }
  },
  "title": "NetworkConnection",
  "type": "object"
}

Fields:

  • name (str | None)
  • uuid (str | None)
  • interface (str | None)
  • conn_type (ConnectionType)
Source code in sts_libs/src/sts/network/nm.py
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
class NetworkConnection(StsBaseModel):
    """NetworkManager connection representation.

    Example:
        ```python
        conn = NetworkConnection()
        conn.discover()  # Discovers first available connection
        conn = NetworkConnection(name='eth0')
        conn.discover()  # Discovers other values
        ```
    """

    # Optional parameters
    name: str | None = None
    uuid: str | None = None
    interface: str | None = None
    conn_type: ConnectionType = 'ethernet'

    def discover(self) -> Self:
        """Discover connection properties from NetworkManager.

        Runs nmcli commands to find name, UUID, and interface.
        Call after construction when NM discovery is needed.
        """
        # Discover connection info if needed
        if not self.name or not self.uuid:
            # Try by UUID first
            if self.uuid:
                result = run(f'nmcli -g connection.id,connection.interface-name conn show "{self.uuid}"')
                if result.succeeded:
                    with suppress(ValueError):
                        name, interface = result.stdout.splitlines()
                        self.name = name
                        self.interface = interface if interface != '--' else None

            # Try by name
            elif self.name:
                result = run(f'nmcli -g connection.uuid conn show "{self.name}"')
                if result.succeeded:
                    self.uuid = result.stdout.strip()
                    # Get interface
                    result = run(f'nmcli -g connection.interface-name conn show "{self.uuid}"')
                    if result.succeeded:
                        interface = result.stdout.strip()
                        self.interface = interface if interface != '--' else None

            # Try by interface
            elif self.interface:
                result = run(f'nmcli -g GENERAL.CONNECTION device show {self.interface}')
                if result.succeeded:
                    conn_name = result.stdout.strip()
                    if conn_name and conn_name != '--':
                        self.name = conn_name
                        # Get UUID
                        result = run(f'nmcli -g connection.uuid conn show "{self.name}"')
                        if result.succeeded:
                            self.uuid = result.stdout.strip()

        return self

    def exists(self) -> bool:
        """Check if connection exists in NetworkManager."""
        if not self.uuid and not self.name:
            return False

        if self.uuid:
            result = run(f'nmcli -g connection.uuid conn show "{self.uuid}"')
            return result.succeeded

        result = run(f'nmcli -g connection.uuid conn show "{self.name}"')
        return result.succeeded

    def up(self) -> bool:
        """Activate connection."""
        if not self.uuid and not self.name:
            logger.error('Connection UUID or name required')
            return False

        result = run(f'nmcli connection up "{self.uuid or self.name}"')
        return result.succeeded

    def down(self) -> bool:
        """Deactivate connection."""
        if not self.uuid and not self.name:
            logger.error('Connection UUID or name required')
            return False

        result = run(f'nmcli connection down "{self.uuid or self.name}"')
        return result.succeeded

    def delete(self) -> bool:
        """Delete connection."""
        if not self.uuid and not self.name:
            logger.error('Connection UUID or name required')
            return False

        result = run(f'nmcli connection delete "{self.uuid or self.name}"')
        return result.succeeded

    def modify(self, setting: str, value: str) -> bool:
        """Modify connection setting (e.g. ``ipv4.method``)."""
        if not self.uuid and not self.name:
            logger.error('Connection UUID or name required')
            return False

        result = run(f'nmcli connection modify "{self.uuid or self.name}" {setting} {value}')
        return result.succeeded

    def set_ip(self, ip: str, prefix: str | int = 24) -> bool:
        """Set IP address, switch to manual mode, and reactivate."""
        try:
            addr = ipaddress.ip_address(ip)
            method = 'ipv4' if addr.version == IPV4_VERSION else 'ipv6'
        except ValueError:
            return False

        # Set manual method
        if not self.modify(f'{method}.method', 'manual'):
            return False

        # Set IP address
        if not self.modify(f'{method}.addresses', f'{ip}/{prefix}'):
            return False

        # Reactivate connection
        return self.up()

delete()

Delete connection.

Source code in sts_libs/src/sts/network/nm.py
127
128
129
130
131
132
133
134
def delete(self) -> bool:
    """Delete connection."""
    if not self.uuid and not self.name:
        logger.error('Connection UUID or name required')
        return False

    result = run(f'nmcli connection delete "{self.uuid or self.name}"')
    return result.succeeded

discover()

Discover connection properties from NetworkManager.

Runs nmcli commands to find name, UUID, and interface. Call after construction when NM discovery is needed.

Source code in sts_libs/src/sts/network/nm.py
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
def discover(self) -> Self:
    """Discover connection properties from NetworkManager.

    Runs nmcli commands to find name, UUID, and interface.
    Call after construction when NM discovery is needed.
    """
    # Discover connection info if needed
    if not self.name or not self.uuid:
        # Try by UUID first
        if self.uuid:
            result = run(f'nmcli -g connection.id,connection.interface-name conn show "{self.uuid}"')
            if result.succeeded:
                with suppress(ValueError):
                    name, interface = result.stdout.splitlines()
                    self.name = name
                    self.interface = interface if interface != '--' else None

        # Try by name
        elif self.name:
            result = run(f'nmcli -g connection.uuid conn show "{self.name}"')
            if result.succeeded:
                self.uuid = result.stdout.strip()
                # Get interface
                result = run(f'nmcli -g connection.interface-name conn show "{self.uuid}"')
                if result.succeeded:
                    interface = result.stdout.strip()
                    self.interface = interface if interface != '--' else None

        # Try by interface
        elif self.interface:
            result = run(f'nmcli -g GENERAL.CONNECTION device show {self.interface}')
            if result.succeeded:
                conn_name = result.stdout.strip()
                if conn_name and conn_name != '--':
                    self.name = conn_name
                    # Get UUID
                    result = run(f'nmcli -g connection.uuid conn show "{self.name}"')
                    if result.succeeded:
                        self.uuid = result.stdout.strip()

    return self

down()

Deactivate connection.

Source code in sts_libs/src/sts/network/nm.py
118
119
120
121
122
123
124
125
def down(self) -> bool:
    """Deactivate connection."""
    if not self.uuid and not self.name:
        logger.error('Connection UUID or name required')
        return False

    result = run(f'nmcli connection down "{self.uuid or self.name}"')
    return result.succeeded

exists()

Check if connection exists in NetworkManager.

Source code in sts_libs/src/sts/network/nm.py
 97
 98
 99
100
101
102
103
104
105
106
107
def exists(self) -> bool:
    """Check if connection exists in NetworkManager."""
    if not self.uuid and not self.name:
        return False

    if self.uuid:
        result = run(f'nmcli -g connection.uuid conn show "{self.uuid}"')
        return result.succeeded

    result = run(f'nmcli -g connection.uuid conn show "{self.name}"')
    return result.succeeded

modify(setting, value)

Modify connection setting (e.g. ipv4.method).

Source code in sts_libs/src/sts/network/nm.py
136
137
138
139
140
141
142
143
def modify(self, setting: str, value: str) -> bool:
    """Modify connection setting (e.g. ``ipv4.method``)."""
    if not self.uuid and not self.name:
        logger.error('Connection UUID or name required')
        return False

    result = run(f'nmcli connection modify "{self.uuid or self.name}" {setting} {value}')
    return result.succeeded

set_ip(ip, prefix=24)

Set IP address, switch to manual mode, and reactivate.

Source code in sts_libs/src/sts/network/nm.py
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
def set_ip(self, ip: str, prefix: str | int = 24) -> bool:
    """Set IP address, switch to manual mode, and reactivate."""
    try:
        addr = ipaddress.ip_address(ip)
        method = 'ipv4' if addr.version == IPV4_VERSION else 'ipv6'
    except ValueError:
        return False

    # Set manual method
    if not self.modify(f'{method}.method', 'manual'):
        return False

    # Set IP address
    if not self.modify(f'{method}.addresses', f'{ip}/{prefix}'):
        return False

    # Reactivate connection
    return self.up()

up()

Activate connection.

Source code in sts_libs/src/sts/network/nm.py
109
110
111
112
113
114
115
116
def up(self) -> bool:
    """Activate connection."""
    if not self.uuid and not self.name:
        logger.error('Connection UUID or name required')
        return False

    result = run(f'nmcli connection up "{self.uuid or self.name}"')
    return result.succeeded

NetworkManager

NetworkManager operations.

Example
nm = NetworkManager()
conn = nm.get_connection('eth0')
conn.up()
Source code in sts_libs/src/sts/network/nm.py
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
class NetworkManager:
    """NetworkManager operations.

    Example:
        ```python
        nm = NetworkManager()
        conn = nm.get_connection('eth0')
        conn.up()
        ```
    """

    @staticmethod
    def reload() -> bool:
        """Reload NetworkManager configuration."""
        result = run('nmcli connection reload')
        return result.succeeded

    def get_connection(self, name_or_uuid: str | None = None) -> NetworkConnection | None:
        """Get connection by name or UUID, or the first available if not specified."""
        if not name_or_uuid:
            # Get first available connection
            result = run('nmcli -g connection.id,connection.uuid,connection.interface-name conn show')
            if result.succeeded and result.stdout:
                with suppress(ValueError):
                    name, uuid, interface = result.stdout.splitlines()[0].split(':')
                    conn = NetworkConnection(
                        name=name,
                        uuid=uuid,
                        interface=interface if interface != '--' else None,
                    )
                    conn.discover()
                    return conn
            return None

        # Try by UUID first
        result = run(f'nmcli -g connection.id,connection.uuid,connection.interface-name conn show "{name_or_uuid}"')
        if result.succeeded:
            with suppress(ValueError):
                name, uuid, interface = result.stdout.splitlines()
                conn = NetworkConnection(
                    name=name,
                    uuid=uuid,
                    interface=interface if interface != '--' else None,
                )
                conn.discover()
                return conn

        # Try by name
        result = run(f'nmcli -g connection.uuid conn show "{name_or_uuid}"')
        if result.succeeded:
            uuid = result.stdout.strip()
            return self.get_connection(uuid)

        return None

    def get_connection_by_interface(self, interface: str | NetworkInterface | None = None) -> NetworkConnection | None:
        """Get connection by interface, or the first available if not specified."""
        if not interface:
            # Get first available interface
            result = run('nmcli -g GENERAL.DEVICE,GENERAL.CONNECTION device show')
            if result.succeeded and result.stdout:
                with suppress(ValueError):
                    _device, conn_name = result.stdout.splitlines()[0].split(':')
                    if conn_name and conn_name != '--':
                        return self.get_connection(conn_name)
            return None

        name = interface.name if isinstance(interface, NetworkInterface) else interface
        result = run(f'nmcli -g GENERAL.CONNECTION device show {name}')
        if result.failed:
            return None

        conn_name = result.stdout.strip()
        if not conn_name or conn_name == '--':
            return None

        return self.get_connection(conn_name)

    def add_connection(
        self,
        name: str | None = None,
        interface: str | NetworkInterface | None = None,
        conn_type: ConnectionType = 'ethernet',
        **settings: str,
    ) -> NetworkConnection | None:
        """Add new connection.

        Setting keys are converted to nmcli format via ``_convert_setting_name``
        (e.g. ``ipv4_method`` becomes ``ipv4.method``).
        """
        # Get interface name if provided
        ifname = None
        if interface:
            ifname = interface.name if isinstance(interface, NetworkInterface) else interface
            # Use interface name as connection name if not provided
            if not name:
                name = ifname

        # Generate connection name if not provided
        if not name:
            # Find first unused name
            i = 0
            while True:
                name = f'connection{i}'
                if not self.get_connection(name):
                    break
                i += 1

        # Build command
        cmd = ['nmcli', 'connection', 'add', 'type', conn_type, 'con-name', name]

        # Add interface if specified
        if ifname:
            cmd.extend(['ifname', ifname])

        # Add settings
        for key, value in settings.items():
            cmd.extend([_convert_setting_name(key), value])

        # Create connection
        result = run(' '.join(cmd))
        if result.failed:
            return None

        # Get new connection
        return self.get_connection(name)

add_connection(name=None, interface=None, conn_type='ethernet', **settings)

Add new connection.

Setting keys are converted to nmcli format via _convert_setting_name (e.g. ipv4_method becomes ipv4.method).

Source code in sts_libs/src/sts/network/nm.py
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
def add_connection(
    self,
    name: str | None = None,
    interface: str | NetworkInterface | None = None,
    conn_type: ConnectionType = 'ethernet',
    **settings: str,
) -> NetworkConnection | None:
    """Add new connection.

    Setting keys are converted to nmcli format via ``_convert_setting_name``
    (e.g. ``ipv4_method`` becomes ``ipv4.method``).
    """
    # Get interface name if provided
    ifname = None
    if interface:
        ifname = interface.name if isinstance(interface, NetworkInterface) else interface
        # Use interface name as connection name if not provided
        if not name:
            name = ifname

    # Generate connection name if not provided
    if not name:
        # Find first unused name
        i = 0
        while True:
            name = f'connection{i}'
            if not self.get_connection(name):
                break
            i += 1

    # Build command
    cmd = ['nmcli', 'connection', 'add', 'type', conn_type, 'con-name', name]

    # Add interface if specified
    if ifname:
        cmd.extend(['ifname', ifname])

    # Add settings
    for key, value in settings.items():
        cmd.extend([_convert_setting_name(key), value])

    # Create connection
    result = run(' '.join(cmd))
    if result.failed:
        return None

    # Get new connection
    return self.get_connection(name)

get_connection(name_or_uuid=None)

Get connection by name or UUID, or the first available if not specified.

Source code in sts_libs/src/sts/network/nm.py
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
def get_connection(self, name_or_uuid: str | None = None) -> NetworkConnection | None:
    """Get connection by name or UUID, or the first available if not specified."""
    if not name_or_uuid:
        # Get first available connection
        result = run('nmcli -g connection.id,connection.uuid,connection.interface-name conn show')
        if result.succeeded and result.stdout:
            with suppress(ValueError):
                name, uuid, interface = result.stdout.splitlines()[0].split(':')
                conn = NetworkConnection(
                    name=name,
                    uuid=uuid,
                    interface=interface if interface != '--' else None,
                )
                conn.discover()
                return conn
        return None

    # Try by UUID first
    result = run(f'nmcli -g connection.id,connection.uuid,connection.interface-name conn show "{name_or_uuid}"')
    if result.succeeded:
        with suppress(ValueError):
            name, uuid, interface = result.stdout.splitlines()
            conn = NetworkConnection(
                name=name,
                uuid=uuid,
                interface=interface if interface != '--' else None,
            )
            conn.discover()
            return conn

    # Try by name
    result = run(f'nmcli -g connection.uuid conn show "{name_or_uuid}"')
    if result.succeeded:
        uuid = result.stdout.strip()
        return self.get_connection(uuid)

    return None

get_connection_by_interface(interface=None)

Get connection by interface, or the first available if not specified.

Source code in sts_libs/src/sts/network/nm.py
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
def get_connection_by_interface(self, interface: str | NetworkInterface | None = None) -> NetworkConnection | None:
    """Get connection by interface, or the first available if not specified."""
    if not interface:
        # Get first available interface
        result = run('nmcli -g GENERAL.DEVICE,GENERAL.CONNECTION device show')
        if result.succeeded and result.stdout:
            with suppress(ValueError):
                _device, conn_name = result.stdout.splitlines()[0].split(':')
                if conn_name and conn_name != '--':
                    return self.get_connection(conn_name)
        return None

    name = interface.name if isinstance(interface, NetworkInterface) else interface
    result = run(f'nmcli -g GENERAL.CONNECTION device show {name}')
    if result.failed:
        return None

    conn_name = result.stdout.strip()
    if not conn_name or conn_name == '--':
        return None

    return self.get_connection(conn_name)

reload() staticmethod

Reload NetworkManager configuration.

Source code in sts_libs/src/sts/network/nm.py
176
177
178
179
180
@staticmethod
def reload() -> bool:
    """Reload NetworkManager configuration."""
    result = run('nmcli connection reload')
    return result.succeeded