Skip to content

Commit fcbc395

Browse files
committed
feat(understack_workflows): refactor LIF creation to be more dynamic
The broadcast_domain_name could not be hard coded. What we really want is to create home ports and the LIFs bound to those home ports based on the node and the ports that the data traffic is routing over. This takes those two items as input and then makes the rest dynamically looked up from there while returning detailed exceptions when things are missing or incorrect.
1 parent 9c68224 commit fcbc395

8 files changed

Lines changed: 142 additions & 74 deletions

File tree

python/understack-workflows/tests/test_netapp_client.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,28 @@ def test_get_or_create_port_failure(self, mock_port_class, netapp_client):
403403

404404
assert exc_info.value.__cause__ is not None
405405

406+
@patch("understack_workflows.netapp.client.Port")
407+
def test_get_broadcast_domain_name_success(self, mock_port_class, netapp_client):
408+
"""Test successful broadcast domain lookup."""
409+
port = MagicMock()
410+
port.broadcast_domain.name = "Fabric-A"
411+
mock_port_class.get_collection.return_value = [port]
412+
413+
result = netapp_client.get_broadcast_domain_name("node-01", "e4a")
414+
415+
assert result == "Fabric-A"
416+
417+
@patch("understack_workflows.netapp.client.Port")
418+
def test_get_broadcast_domain_name_not_found(self, mock_port_class, netapp_client):
419+
"""Test broadcast domain lookup when no matching port exists."""
420+
mock_port_class.get_collection.return_value = []
421+
422+
with pytest.raises(NetworkOperationError) as exc_info:
423+
netapp_client.get_broadcast_domain_name("node-01", "e4a")
424+
425+
assert exc_info.value.context["node_name"] == "node-01"
426+
assert exc_info.value.context["port_name"] == "e4a"
427+
406428
@patch("understack_workflows.netapp.client.Node")
407429
def test_get_nodes_success(self, mock_node_class, netapp_client):
408430
"""Test successful node retrieval."""
@@ -566,6 +588,7 @@ def test_interface_methods_are_abstract(self):
566588
"find_volume",
567589
"get_or_create_ip_interface",
568590
"get_or_create_port",
591+
"get_broadcast_domain_name",
569592
"get_aggregates",
570593
"get_nodes",
571594
"get_namespaces",

python/understack-workflows/tests/test_netapp_lif_service.py

Lines changed: 21 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
from understack_workflows.netapp.value_objects import PortSpec
1818
from understack_workflows.netapp.value_objects import SvmResult
1919

20+
DYNAMIC_BROADCAST_DOMAIN = "test-domain"
21+
2022

2123
class TestLifService:
2224
"""Test cases for LifService class."""
@@ -56,6 +58,7 @@ def test_create_lif_success(self, lif_service, mock_client, sample_config):
5658
uuid="port-uuid-123", name="e4a-100", node_name="node-01", port_type="vlan"
5759
)
5860
mock_client.get_or_create_port.return_value = mock_port
61+
mock_client.get_broadcast_domain_name.return_value = DYNAMIC_BROADCAST_DOMAIN
5962

6063
# Mock interface creation
6164
mock_interface = InterfaceResult(
@@ -71,6 +74,7 @@ def test_create_lif_success(self, lif_service, mock_client, sample_config):
7174
# Mock node identification
7275
mock_node = NodeResult(name="node-01", uuid="node-uuid-1")
7376
mock_client.get_nodes.return_value = [mock_node]
77+
mock_client.get_broadcast_domain_name.return_value = DYNAMIC_BROADCAST_DOMAIN
7478

7579
lif_service.create_lif(project_id, sample_config)
7680

@@ -91,6 +95,7 @@ def test_create_lif_success(self, lif_service, mock_client, sample_config):
9195
assert interface_call_args.name == sample_config.name
9296
assert interface_call_args.svm_name == expected_svm_name
9397
assert interface_call_args.home_port_uuid == mock_port.uuid
98+
mock_client.get_broadcast_domain_name.assert_called_once_with("node-01", "e4a")
9499

95100
def test_create_lif_svm_not_found(self, lif_service, mock_client, sample_config):
96101
"""Test LIF creation when SVM is not found."""
@@ -125,6 +130,7 @@ def test_create_lif_port_creation_error(
125130
# Mock node identification
126131
mock_node = NodeResult(name="node-01", uuid="node-uuid-1")
127132
mock_client.get_nodes.return_value = [mock_node]
133+
mock_client.get_broadcast_domain_name.return_value = DYNAMIC_BROADCAST_DOMAIN
128134

129135
# Mock port creation failure
130136
mock_client.get_or_create_port.side_effect = Exception("Port creation failed")
@@ -139,15 +145,17 @@ def test_create_home_port_success(self, lif_service, mock_client, sample_config)
139145
"""Test successful home port creation."""
140146
# Mock node identification
141147
mock_node = NodeResult(name="node-01", uuid="node-uuid-1")
142-
mock_client.get_nodes.return_value = [mock_node]
143148

144149
# Mock port creation
145150
mock_port = PortResult(
146151
uuid="port-uuid-123", name="e4a-100", node_name="node-01", port_type="vlan"
147152
)
148153
mock_client.get_or_create_port.return_value = mock_port
154+
mock_client.get_broadcast_domain_name.return_value = DYNAMIC_BROADCAST_DOMAIN
149155

150-
result = lif_service.create_home_port(sample_config)
156+
result = lif_service.create_home_port(
157+
sample_config, mock_node, DYNAMIC_BROADCAST_DOMAIN
158+
)
151159

152160
assert result == mock_port
153161

@@ -158,21 +166,7 @@ def test_create_home_port_success(self, lif_service, mock_client, sample_config)
158166
assert call_args.node_name == "node-01"
159167
assert call_args.vlan_id == 100
160168
assert call_args.base_port_name == sample_config.base_port_name
161-
assert call_args.broadcast_domain_name == sample_config.broadcast_domain_name
162-
163-
def test_create_home_port_no_node(self, lif_service, mock_client, sample_config):
164-
"""Test home port creation when no suitable node is found."""
165-
# Mock no matching nodes
166-
mock_client.get_nodes.return_value = [
167-
NodeResult(name="node-03", uuid="node-uuid-3"),
168-
NodeResult(name="node-04", uuid="node-uuid-4"),
169-
]
170-
171-
with pytest.raises(HomeNodeNotFoundError, match="Could not find home node"):
172-
lif_service.create_home_port(sample_config)
173-
174-
# Verify no port creation was attempted
175-
mock_client.get_or_create_port.assert_not_called()
169+
assert call_args.broadcast_domain_name == DYNAMIC_BROADCAST_DOMAIN
176170

177171
def test_identify_home_node_success(self, lif_service, mock_client, sample_config):
178172
"""Test successful node identification."""
@@ -222,19 +216,17 @@ def test_identify_home_node_not_found(
222216
]
223217
mock_client.get_nodes.return_value = mock_nodes
224218

225-
result = lif_service.identify_home_node(sample_config)
226-
227-
assert result is None
219+
with pytest.raises(HomeNodeNotFoundError, match="Could not find home node"):
220+
lif_service.identify_home_node(sample_config)
228221

229222
def test_identify_home_node_exception(
230223
self, lif_service, mock_client, sample_config
231224
):
232225
"""Test node identification when client raises an exception."""
233226
mock_client.get_nodes.side_effect = Exception("NetApp error")
234227

235-
result = lif_service.identify_home_node(sample_config)
236-
237-
assert result is None
228+
with pytest.raises(HomeNodeNotFoundError, match="Could not find home node"):
229+
lif_service.identify_home_node(sample_config)
238230

239231
def test_svm_name_generation(self, lif_service):
240232
"""Test SVM name generation follows naming convention."""
@@ -260,6 +252,7 @@ def test_interface_spec_creation(self, lif_service, mock_client, sample_config):
260252
uuid="port-uuid-123", name="e4a-100", node_name="node-01", port_type="vlan"
261253
)
262254
mock_client.get_or_create_port.return_value = mock_port
255+
mock_client.get_broadcast_domain_name.return_value = DYNAMIC_BROADCAST_DOMAIN
263256

264257
# Mock interface creation
265258
mock_client.get_or_create_ip_interface.return_value = InterfaceResult(
@@ -273,6 +266,7 @@ def test_interface_spec_creation(self, lif_service, mock_client, sample_config):
273266
# Mock node identification
274267
mock_node = NodeResult(name="node-01", uuid="node-uuid-1")
275268
mock_client.get_nodes.return_value = [mock_node]
269+
mock_client.get_broadcast_domain_name.return_value = DYNAMIC_BROADCAST_DOMAIN
276270

277271
lif_service.create_lif(project_id, sample_config)
278272

@@ -283,33 +277,27 @@ def test_interface_spec_creation(self, lif_service, mock_client, sample_config):
283277
assert interface_call_args.netmask == str(sample_config.network.netmask)
284278
assert interface_call_args.svm_name == expected_svm_name
285279
assert interface_call_args.home_port_uuid == mock_port.uuid
286-
assert (
287-
interface_call_args.broadcast_domain_name
288-
== sample_config.broadcast_domain_name
289-
)
280+
assert interface_call_args.broadcast_domain_name == DYNAMIC_BROADCAST_DOMAIN
290281
assert interface_call_args.service_policy == "default-data-nvme-tcp"
282+
mock_client.get_broadcast_domain_name.assert_called_once_with("node-01", "e4a")
291283

292284
def test_port_spec_creation(self, lif_service, mock_client, sample_config):
293285
"""Test that port specification is created correctly."""
294286
# Mock node identification
295287
mock_node = NodeResult(name="node-01", uuid="node-uuid-1")
296-
mock_client.get_nodes.return_value = [mock_node]
297288

298289
# Mock port creation
299290
mock_client.get_or_create_port.return_value = PortResult(
300291
uuid="port-uuid-123", name="e4a-100", node_name="node-01", port_type="vlan"
301292
)
302-
303-
lif_service.create_home_port(sample_config)
293+
lif_service.create_home_port(sample_config, mock_node, DYNAMIC_BROADCAST_DOMAIN)
304294

305295
# Verify the port spec is created correctly
306296
port_call_args = mock_client.get_or_create_port.call_args[0][0]
307297
assert port_call_args.node_name == "node-01"
308298
assert port_call_args.vlan_id == sample_config.vlan_id
309299
assert port_call_args.base_port_name == sample_config.base_port_name
310-
assert (
311-
port_call_args.broadcast_domain_name == sample_config.broadcast_domain_name
312-
)
300+
assert port_call_args.broadcast_domain_name == DYNAMIC_BROADCAST_DOMAIN
313301

314302
def test_node_number_extraction_logic(self, lif_service, mock_client):
315303
"""Test the node number extraction logic with various node names."""

python/understack-workflows/tests/test_netapp_manager.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from understack_workflows.netapp.manager import NetAppManager
1313
from understack_workflows.netapp.value_objects import AggregateResult
1414
from understack_workflows.netapp.value_objects import NetappIPInterfaceConfig
15+
from understack_workflows.netapp.value_objects import NodeResult
1516

1617

1718
class TestNetAppManagerOrchestration:
@@ -392,7 +393,12 @@ def test_public_api_contract_maintained(
392393
manager._volume_service.get_mapped_namespaces = MagicMock(return_value=[])
393394
manager._lif_service.create_lif = MagicMock()
394395
manager._lif_service.create_home_port = MagicMock()
395-
manager._lif_service.identify_home_node = MagicMock()
396+
manager._lif_service.identify_home_node = MagicMock(
397+
return_value=NodeResult(name="node-01", uuid="node-uuid-1")
398+
)
399+
manager._client.get_broadcast_domain_name = MagicMock(
400+
return_value="test-domain"
401+
)
396402

397403
# Test all public methods can be called with expected signatures
398404
try:

python/understack-workflows/tests/test_netapp_manager_integration.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from understack_workflows.netapp.exceptions import SvmOperationError
1111
from understack_workflows.netapp.exceptions import VolumeOperationError
1212
from understack_workflows.netapp.manager import NetAppManager
13+
from understack_workflows.netapp.value_objects import NodeResult
1314

1415

1516
class TestNetAppManagerIntegration:
@@ -445,6 +446,12 @@ def test_backward_compatibility_maintained(
445446
network=ipaddress.IPv4Network("192.168.1.0/24"),
446447
vlan_id=100,
447448
)
449+
manager._lif_service.identify_home_node = MagicMock(
450+
return_value=NodeResult(name="node-03", uuid="node-uuid-3")
451+
)
452+
manager._client.get_broadcast_domain_name = MagicMock(
453+
return_value="test-domain"
454+
)
448455
manager.create_lif("project", config_obj)
449456
manager.create_home_port(config_obj)
450457
manager.identify_home_node(config_obj)

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,10 @@ def get_or_create_port(self, port_spec: PortSpec) -> PortResult:
155155
NetworkOperationError: If port creation or load fails
156156
"""
157157

158+
@abstractmethod
159+
def get_broadcast_domain_name(self, node_name: str, port_name: str) -> str:
160+
"""Get the broadcast domain name for a port."""
161+
158162
@abstractmethod
159163
def get_nodes(self) -> list[NodeResult]:
160164
"""Get all nodes in the cluster.
@@ -599,6 +603,36 @@ def get_or_create_port(self, port_spec: PortSpec) -> PortResult:
599603
},
600604
) from e
601605

606+
def get_broadcast_domain_name(self, node_name: str, port_name: str) -> str:
607+
"""Get the broadcast domain name for a port."""
608+
try:
609+
ports = Port.get_collection(
610+
name=port_name,
611+
fields="node.name,name,broadcast_domain",
612+
**{"node.name": node_name}, # pyright: ignore[reportArgumentType]
613+
)
614+
615+
port = cast(Port, next(iter(ports)))
616+
return str(port.broadcast_domain.name)
617+
618+
except NetAppRestError as e:
619+
raise NetworkOperationError(
620+
f"NetApp broadcast domain lookup failed: {e}",
621+
context={
622+
"node_name": node_name,
623+
"port_name": port_name,
624+
"netapp_error": str(e),
625+
},
626+
) from e
627+
except StopIteration:
628+
raise NetworkOperationError(
629+
"No broadcast domain found for the requested port",
630+
context={
631+
"node_name": node_name,
632+
"port_name": port_name,
633+
},
634+
) from None
635+
602636
def get_nodes(self) -> list[NodeResult]:
603637
"""Get all nodes in the cluster."""
604638
try:

0 commit comments

Comments
 (0)