Skip to content

Commit 59d6a88

Browse files
committed
make nic_slot_prefix configurable
According to Venkat, the physical slots used for those interfaces will vary with each environment.
1 parent 4c4c73d commit 59d6a88

File tree

7 files changed

+166
-4
lines changed

7 files changed

+166
-4
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Example NetApp configuration file showing the new netapp_nic_slot_prefix option
2+
3+
[netapp_nvme]
4+
# Required configuration options
5+
netapp_server_hostname = netapp-cluster.example.com
6+
netapp_login = admin
7+
netapp_password = your-secure-password
8+
9+
# Optional: NIC slot prefix for port naming (defaults to 'e4' if not specified)
10+
# This controls the base port name generation in NetappIPInterfaceConfig
11+
# Examples:
12+
# netapp_nic_slot_prefix = e4 # Results in ports like e4a, e4b (default)
13+
# netapp_nic_slot_prefix = e5 # Results in ports like e5a, e5b
14+
# netapp_nic_slot_prefix = e6 # Results in ports like e6a, e6b
15+
netapp_nic_slot_prefix = e5

python/understack-workflows/tests/test_netapp_config.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,21 @@ def valid_config_file(self):
2020
netapp_server_hostname = test-hostname.example.com
2121
netapp_login = test-user
2222
netapp_password = test-password-123
23+
"""
24+
with tempfile.NamedTemporaryFile(mode="w", suffix=".conf", delete=False) as f:
25+
f.write(config_content)
26+
f.flush()
27+
yield f.name
28+
os.unlink(f.name)
29+
30+
@pytest.fixture
31+
def config_with_nic_prefix(self):
32+
"""Create a config file with custom NIC slot prefix."""
33+
config_content = """[netapp_nvme]
34+
netapp_server_hostname = test-hostname.example.com
35+
netapp_login = test-user
36+
netapp_password = test-password-123
37+
netapp_nic_slot_prefix = e5
2338
"""
2439
with tempfile.NamedTemporaryFile(mode="w", suffix=".conf", delete=False) as f:
2540
f.write(config_content)
@@ -48,6 +63,7 @@ def test_successful_initialization(self, valid_config_file):
4863
assert config.hostname == "test-hostname.example.com"
4964
assert config.username == "test-user"
5065
assert config.password == "test-password-123"
66+
assert config.netapp_nic_slot_prefix == "e4" # Default value
5167
assert config.config_path == valid_config_file
5268

5369
def test_default_config_path(self):
@@ -318,6 +334,21 @@ def test_config_with_extra_sections(self, valid_config_file):
318334

319335
os.unlink(f.name)
320336

337+
def test_netapp_nic_slot_prefix_custom_value(self, config_with_nic_prefix):
338+
"""Test NetAppConfig with custom NIC slot prefix."""
339+
config = NetAppConfig(config_with_nic_prefix)
340+
341+
assert config.hostname == "test-hostname.example.com"
342+
assert config.username == "test-user"
343+
assert config.password == "test-password-123"
344+
assert config.netapp_nic_slot_prefix == "e5"
345+
346+
def test_netapp_nic_slot_prefix_default_value(self, valid_config_file):
347+
"""Test NetAppConfig uses default NIC slot prefix when not specified."""
348+
config = NetAppConfig(valid_config_file)
349+
350+
assert config.netapp_nic_slot_prefix == "e4"
351+
321352
def test_config_with_extra_options(self):
322353
"""Test config parsing ignores extra options in netapp_nvme section."""
323354
config_content = """[netapp_nvme]
@@ -338,3 +369,61 @@ def test_config_with_extra_options(self):
338369
assert config.password == "test-password"
339370

340371
os.unlink(f.name)
372+
def test_integration_netapp_config_with_from_nautobot_response(self, config_with_nic_prefix):
373+
"""Test integration between NetAppConfig and NetappIPInterfaceConfig.from_nautobot_response."""
374+
from unittest.mock import MagicMock
375+
from understack_workflows.netapp.value_objects import NetappIPInterfaceConfig
376+
import ipaddress
377+
378+
# Create config with custom NIC prefix
379+
config = NetAppConfig(config_with_nic_prefix)
380+
assert config.netapp_nic_slot_prefix == "e5"
381+
382+
# Create a mock nautobot response
383+
mock_interface_a = MagicMock()
384+
mock_interface_a.name = "N1-test-A"
385+
mock_interface_a.address = "192.168.1.10/24"
386+
mock_interface_a.vlan = 100
387+
388+
mock_interface_b = MagicMock()
389+
mock_interface_b.name = "N1-test-B"
390+
mock_interface_b.address = "192.168.1.11/24"
391+
mock_interface_b.vlan = 100
392+
393+
mock_response = MagicMock()
394+
mock_response.interfaces = [mock_interface_a, mock_interface_b]
395+
396+
# Test that from_nautobot_response uses the custom prefix
397+
configs = NetappIPInterfaceConfig.from_nautobot_response(mock_response, config)
398+
399+
assert len(configs) == 2
400+
assert configs[0].base_port_name == "e5a"
401+
assert configs[1].base_port_name == "e5b"
402+
assert configs[0].nic_slot_prefix == "e5"
403+
assert configs[1].nic_slot_prefix == "e5"
404+
def test_from_nautobot_response_default_prefix(self, valid_config_file):
405+
"""Test that from_nautobot_response uses default prefix when no config provided."""
406+
from unittest.mock import MagicMock
407+
from understack_workflows.netapp.value_objects import NetappIPInterfaceConfig
408+
409+
# Create a mock nautobot response
410+
mock_interface = MagicMock()
411+
mock_interface.name = "N1-test-A"
412+
mock_interface.address = "192.168.1.10/24"
413+
mock_interface.vlan = 100
414+
415+
mock_response = MagicMock()
416+
mock_response.interfaces = [mock_interface]
417+
418+
# Test without config (should use default)
419+
configs = NetappIPInterfaceConfig.from_nautobot_response(mock_response)
420+
assert len(configs) == 1
421+
assert configs[0].base_port_name == "e4a"
422+
assert configs[0].nic_slot_prefix == "e4"
423+
424+
# Test with config that has default prefix
425+
config = NetAppConfig(valid_config_file)
426+
configs_with_config = NetappIPInterfaceConfig.from_nautobot_response(mock_response, config)
427+
assert len(configs_with_config) == 1
428+
assert configs_with_config[0].base_port_name == "e4a"
429+
assert configs_with_config[0].nic_slot_prefix == "e4"

python/understack-workflows/tests/test_netapp_manager.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,30 @@ def test_base_port_name_b(self):
666666

667667
assert config.base_port_name == "e4b"
668668

669+
def test_base_port_name_custom_prefix_a(self):
670+
"""Test base_port_name with custom prefix for side A."""
671+
config = NetappIPInterfaceConfig(
672+
name="N1-test-A",
673+
address=ipaddress.IPv4Address("192.168.1.10"),
674+
network=ipaddress.IPv4Network("192.168.1.0/24"),
675+
vlan_id=100,
676+
nic_slot_prefix="e5",
677+
)
678+
679+
assert config.base_port_name == "e5a"
680+
681+
def test_base_port_name_custom_prefix_b(self):
682+
"""Test base_port_name with custom prefix for side B."""
683+
config = NetappIPInterfaceConfig(
684+
name="N1-test-B",
685+
address=ipaddress.IPv4Address("192.168.1.10"),
686+
network=ipaddress.IPv4Network("192.168.1.0/24"),
687+
vlan_id=100,
688+
nic_slot_prefix="e6",
689+
)
690+
691+
assert config.base_port_name == "e6b"
692+
669693
def test_broadcast_domain_name_a(self):
670694
"""Test broadcast_domain_name for side A."""
671695
config = NetappIPInterfaceConfig(

python/understack-workflows/understack_workflows/main/netapp_configure_net.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ def netapp_create_interfaces(
352352
Exception: If SVM for the project is not found
353353
NetAppRestError: If LIF creation fails on the NetApp system
354354
"""
355-
configs = NetappIPInterfaceConfig.from_nautobot_response(nautobot_response)
355+
configs = NetappIPInterfaceConfig.from_nautobot_response(nautobot_response, mgr.config)
356356
for interface_config in configs:
357357
logger.info("Creating LIF %s for project %s", interface_config.name, project_id)
358358
mgr.create_lif(project_id, interface_config)

python/understack-workflows/understack_workflows/netapp/config.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,19 @@ def _parse_config(self) -> dict[str, str]:
4949
) from e
5050

5151
try:
52-
return {
52+
config_data = {
5353
"hostname": parser.get("netapp_nvme", "netapp_server_hostname"),
5454
"username": parser.get("netapp_nvme", "netapp_login"),
5555
"password": parser.get("netapp_nvme", "netapp_password"),
5656
}
57+
58+
# Optional netapp_nic_slot_prefix with default value
59+
try:
60+
config_data["netapp_nic_slot_prefix"] = parser.get("netapp_nvme", "netapp_nic_slot_prefix")
61+
except (configparser.NoSectionError, configparser.NoOptionError):
62+
config_data["netapp_nic_slot_prefix"] = "e4"
63+
64+
return config_data
5765
except (configparser.NoSectionError, configparser.NoOptionError) as e:
5866
raise ConfigurationError(
5967
f"Missing required configuration in {self._config_path}: {e}",
@@ -108,6 +116,11 @@ def password(self) -> str:
108116
"""Get the NetApp login password."""
109117
return self._config_data["password"]
110118

119+
@property
120+
def netapp_nic_slot_prefix(self) -> str:
121+
"""Get the NetApp NIC slot prefix."""
122+
return self._config_data["netapp_nic_slot_prefix"]
123+
111124
@property
112125
def config_path(self) -> str:
113126
"""Get the configuration file path."""

python/understack-workflows/understack_workflows/netapp/manager.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,3 +429,8 @@ def _svm_name(self, project_id):
429429

430430
def _volume_name(self, project_id):
431431
return f"vol_{project_id}"
432+
433+
@property
434+
def config(self):
435+
"""Get the NetApp configuration."""
436+
return self._config

python/understack-workflows/understack_workflows/netapp/value_objects.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class NetappIPInterfaceConfig:
2323
address: ipaddress.IPv4Address
2424
network: ipaddress.IPv4Network
2525
vlan_id: int
26+
nic_slot_prefix: str = "e4"
2627

2728
def netmask_long(self):
2829
return self.network.netmask
@@ -50,7 +51,20 @@ def desired_node_number(self) -> int:
5051
raise ValueError("Cannot determine node index from name %s", self.name)
5152

5253
@classmethod
53-
def from_nautobot_response(cls, response: "VirtualMachineNetworkInfo"):
54+
def from_nautobot_response(cls, response: "VirtualMachineNetworkInfo", netapp_config=None):
55+
"""Create NetappIPInterfaceConfig instances from Nautobot response.
56+
57+
Args:
58+
response: The Nautobot response containing network interface information
59+
netapp_config: Optional NetApp configuration to get NIC slot prefix from
60+
61+
Returns:
62+
List of NetappIPInterfaceConfig instances
63+
"""
64+
nic_slot_prefix = "e4" # Default value
65+
if netapp_config:
66+
nic_slot_prefix = netapp_config.netapp_nic_slot_prefix
67+
5468
result = []
5569
for interface in response.interfaces:
5670
address, _ = interface.address.split("/")
@@ -60,13 +74,15 @@ def from_nautobot_response(cls, response: "VirtualMachineNetworkInfo"):
6074
address=ipaddress.IPv4Address(address),
6175
network=ipaddress.IPv4Network(interface.address, strict=False),
6276
vlan_id=interface.vlan,
77+
nic_slot_prefix=nic_slot_prefix,
6378
)
6479
)
6580
return result
6681

6782
@cached_property
6883
def base_port_name(self):
69-
return f"e4{self.side.lower()}"
84+
"""Get the base port name using the configured NIC slot prefix."""
85+
return f"{self.nic_slot_prefix}{self.side.lower()}"
7086

7187
@cached_property
7288
def broadcast_domain_name(self):

0 commit comments

Comments
 (0)