Skip to content

Commit 33e23f7

Browse files
committed
feat: Add Linux VLAN management functionality
Fixes #5 This PR implements VLAN management functionality for Linux network interfaces, providing comprehensive support for creating, listing, and removing VLANs. # Changes Made # Added get_vlan_ids(): Retrieves all VLAN IDs configured on a Linux network interface using ifconfig and regex parsing Added get_vlan_id(): Returns the first VLAN ID found, or 0 if none exist Added add_vlan(vlan_id): Creates a new VLAN interface using ip link add command Added remove_vlan(vlan_id): Removes a VLAN interface using ip link del command, with automatic detection if no VLAN ID is specified # Technical Details # Uses ifconfig with regex pattern matching to discover existing VLANs Leverages ip link commands for VLAN creation and deletion Includes duplicate prevention in VLAN ID collection Provides comprehensive debug logging for troubleshooting Returns boolean success indicators for add/remove operations # Testing # All methods include validation by re-querying the system state to confirm successful operations. Signed-off-by: Kacper Tokarzewski <[email protected]>
1 parent 0873e1d commit 33e23f7

File tree

2 files changed

+207
-0
lines changed
  • mfd_network_adapter/network_interface/feature/vlan
  • tests/unit/test_mfd_network_adapter/test_network_interface/test_feature/test_vlan

2 files changed

+207
-0
lines changed

mfd_network_adapter/network_interface/feature/vlan/linux.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import logging
66

77
from mfd_common_libs import add_logging_level, log_levels
8+
from mfd_common_libs.log_levels import MFD_DEBUG
9+
810
from .base import BaseFeatureVLAN
911

1012
logger = logging.getLogger(__name__)
@@ -13,3 +15,91 @@
1315

1416
class LinuxVLAN(BaseFeatureVLAN):
1517
"""Linux class for VLAN feature."""
18+
19+
def get_vlan_ids(self) -> list[int]:
20+
"""
21+
Get all VLAN IDs on Linux network interface via terminal command.
22+
23+
:return: List of VLAN IDs
24+
"""
25+
vlan_ids = []
26+
logger.log(
27+
level=MFD_DEBUG,
28+
msg=f"Getting VLAN IDs on interface {self._interface().name}.",
29+
)
30+
result = self.owner._connection.execute_command(
31+
f"ifconfig | grep -oP '{self._interface().name}.\\d+'",
32+
expected_return_codes={0},
33+
).stdout
34+
35+
vlan_ids_set = set()
36+
if result:
37+
lines = result.strip().splitlines()
38+
for line in lines:
39+
if f"{self._interface().name}." in line:
40+
vlan_id_str = line.replace(f"{self._interface().name}.", "")
41+
if vlan_id_str.isdigit():
42+
vlan_ids_set.add(int(vlan_id_str))
43+
vlan_ids = list(vlan_ids_set)
44+
logger.log(
45+
level=MFD_DEBUG,
46+
msg=f"VLAN IDs on interface {self._interface().name}: {vlan_ids}.",
47+
)
48+
else:
49+
logger.log(level=MFD_DEBUG, msg=f"No VLANs set on {self._interface().name}.")
50+
51+
return vlan_ids
52+
53+
def get_vlan_id(self) -> int:
54+
"""
55+
Get first VLAN ID on Linux network interface via terminal command.
56+
57+
:return: First VLAN ID or 0 if none found
58+
"""
59+
vlan_ids = self.get_vlan_ids()
60+
return vlan_ids[0] if vlan_ids else 0
61+
62+
def add_vlan(self, vlan_id: int) -> bool:
63+
"""
64+
Add VLAN on Linux network interface via terminal command.
65+
66+
:param vlan_id: VLAN ID to create
67+
:return: True if VLAN was added successfully, False otherwise
68+
"""
69+
logger.log(
70+
level=MFD_DEBUG,
71+
msg=f"Adding VLAN {vlan_id} on interface {self._interface().name}.",
72+
)
73+
command = (
74+
f"ip link add link {self._interface().name} name {self._interface().name}.{vlan_id} "
75+
f"type vlan id {vlan_id}"
76+
)
77+
78+
self.owner._connection.execute_command(command, expected_return_codes={0})
79+
80+
return vlan_id in self.get_vlan_ids()
81+
82+
def remove_vlan(self, vlan_id: int = 0) -> bool:
83+
"""
84+
Remove VLAN on Linux network interface via terminal command.
85+
86+
:param vlan_id: VLAN ID to remove, if 0 then first collected VLAN ID will be used
87+
:return: True if VLAN was removed successfully, False otherwise
88+
"""
89+
if vlan_id == 0:
90+
vlan_id = self.get_vlan_id()
91+
if vlan_id == 0:
92+
logger.log(
93+
level=MFD_DEBUG,
94+
msg=f"No VLANs found on interface {self._interface().name}.",
95+
)
96+
return False
97+
logger.log(level=MFD_DEBUG, msg=f"Removing VLAN on interface {self._interface().name}.")
98+
command = (
99+
f"ip link del link {self._interface().name} name {self._interface().name}.{vlan_id} "
100+
f"type vlan id {vlan_id}"
101+
)
102+
103+
self.owner._connection.execute_command(command, expected_return_codes={0})
104+
105+
return vlan_id not in self.get_vlan_ids()
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# Copyright (C) 2025 Intel Corporation
2+
# SPDX-License-Identifier: MIT
3+
import pytest
4+
from mfd_connect import RPyCConnection
5+
from mfd_connect.base import ConnectionCompletedProcess
6+
from mfd_typing import PCIAddress, OSName
7+
from mfd_typing.network_interface import LinuxInterfaceInfo
8+
9+
from mfd_network_adapter.network_adapter_owner.linux import LinuxNetworkAdapterOwner
10+
from mfd_network_adapter.network_interface.feature.vlan import LinuxVLAN
11+
from mfd_network_adapter.network_interface.linux import LinuxNetworkInterface
12+
13+
14+
class TestVlanLinux:
15+
@pytest.fixture()
16+
def vlan(self, mocker):
17+
pci_address = PCIAddress(0, 0, 0, 0)
18+
name = "eth1"
19+
mock_connection = mocker.create_autospec(RPyCConnection)
20+
mock_connection.get_os_name.return_value = OSName.LINUX
21+
22+
interface = LinuxNetworkInterface(
23+
connection=mock_connection,
24+
interface_info=LinuxInterfaceInfo(pci_address=pci_address, name=name),
25+
)
26+
27+
mock_owner = mocker.create_autospec(LinuxNetworkAdapterOwner)
28+
mock_owner._connection = mock_connection
29+
30+
vlan = LinuxVLAN(connection=mock_connection, interface=interface)
31+
vlan.owner = mock_owner
32+
vlan._interface = lambda: interface
33+
34+
return vlan
35+
36+
def test_get_vlan_id_vlan_exist(self, vlan):
37+
vlan.owner._connection.execute_command.return_value = ConnectionCompletedProcess(
38+
return_code=0, args="command", stdout="eth1.100\n", stderr="stderr"
39+
)
40+
result = vlan.get_vlan_id()
41+
vlan.owner._connection.execute_command.assert_called_once_with(
42+
"ifconfig | grep -oP 'eth1.\\d+'",
43+
expected_return_codes={0},
44+
)
45+
assert result == 100
46+
47+
def test_get_vlan_id_vlan_does_not_exist(self, vlan):
48+
vlan.owner._connection.execute_command.return_value = ConnectionCompletedProcess(
49+
return_code=0, args="command", stdout="", stderr="stderr"
50+
)
51+
result = vlan.get_vlan_id()
52+
vlan.owner._connection.execute_command.assert_called_once_with(
53+
"ifconfig | grep -oP 'eth1.\\d+'",
54+
expected_return_codes={0},
55+
)
56+
assert result == 0
57+
58+
def test_add_vlan_positive(self, vlan, mocker):
59+
mocker.patch.object(vlan, "get_vlan_ids", return_value=[100])
60+
result = vlan.add_vlan(100)
61+
vlan.owner._connection.execute_command.assert_called_once_with(
62+
"ip link add link eth1 name eth1.100 type vlan id 100",
63+
expected_return_codes={0},
64+
)
65+
assert result
66+
67+
def test_add_vlan_negative(self, vlan, mocker):
68+
mocker.patch.object(vlan, "get_vlan_ids", return_value=[])
69+
result = vlan.add_vlan(100)
70+
vlan.owner._connection.execute_command.assert_called_once_with(
71+
"ip link add link eth1 name eth1.100 type vlan id 100",
72+
expected_return_codes={0},
73+
)
74+
assert not result
75+
76+
def test_remove_vlan_positive(self, vlan, mocker):
77+
mocker.patch.object(vlan, "get_vlan_id", return_value=100)
78+
mocker.patch.object(vlan, "get_vlan_ids", return_value=[])
79+
result = vlan.remove_vlan()
80+
vlan.owner._connection.execute_command.assert_called_once_with(
81+
"ip link del link eth1 name eth1.100 type vlan id 100",
82+
expected_return_codes={0},
83+
)
84+
assert result
85+
86+
def test_remove_vlan_negative(self, vlan, mocker):
87+
mocker.patch.object(vlan, "get_vlan_id", return_value=100)
88+
mocker.patch.object(vlan, "get_vlan_ids", return_value=[100])
89+
result = vlan.remove_vlan()
90+
vlan.owner._connection.execute_command.assert_called_once_with(
91+
"ip link del link eth1 name eth1.100 type vlan id 100",
92+
expected_return_codes={0},
93+
)
94+
assert not result
95+
96+
def test_remove_vlan_negative_no_vlan(self, vlan, mocker):
97+
mocker.patch.object(vlan, "get_vlan_id", return_value=0)
98+
result = vlan.remove_vlan()
99+
assert not result
100+
101+
def test_remove_specified_vlan_positive(self, vlan, mocker):
102+
mocker.patch.object(vlan, "get_vlan_ids", return_value=[100])
103+
result = vlan.remove_vlan(vlan_id=101)
104+
vlan.owner._connection.execute_command.assert_called_once_with(
105+
"ip link del link eth1 name eth1.101 type vlan id 101",
106+
expected_return_codes={0},
107+
)
108+
assert result
109+
110+
def test_remove_specified_vlan_negative(self, vlan, mocker):
111+
mocker.patch.object(vlan, "get_vlan_ids", return_value=[100, 101])
112+
result = vlan.remove_vlan(vlan_id=101)
113+
vlan.owner._connection.execute_command.assert_called_once_with(
114+
"ip link del link eth1 name eth1.101 type vlan id 101",
115+
expected_return_codes={0},
116+
)
117+
assert not result

0 commit comments

Comments
 (0)