Skip to content

Commit 9f52e36

Browse files
authored
Merge pull request #1230 from rackerlabs/pydantic-netapp
chore: convert NetApp related dataclasses to pydantic
2 parents 2c0efd1 + f070bbd commit 9f52e36

12 files changed

+1160
-256
lines changed

python/understack-workflows/tests/test_netapp_client.py

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import pytest
99
from netapp_ontap.error import NetAppRestError
10+
from pydantic import ValidationError
1011

1112
from understack_workflows.netapp.client import NetAppClient
1213
from understack_workflows.netapp.client import NetAppClientInterface
@@ -327,7 +328,7 @@ def test_create_ip_interface_success(self, mock_interface_class, netapp_client):
327328
assert isinstance(result, InterfaceResult)
328329
assert result.name == "test-interface"
329330
assert result.uuid == "interface-uuid-123"
330-
assert result.address == "192.168.1.10"
331+
assert str(result.address) == "192.168.1.10"
331332
assert result.netmask == "255.255.255.0"
332333
assert result.enabled is True
333334
assert result.svm_name == "test-svm"
@@ -540,7 +541,7 @@ def test_create_route_success(self, mock_route_class, netapp_client):
540541

541542
# Verify route configuration
542543
assert mock_route_instance.svm == {"name": "os-test-project"}
543-
assert mock_route_instance.gateway == "100.127.0.17"
544+
assert str(mock_route_instance.gateway) == "100.127.0.17"
544545
assert mock_route_instance.destination == {
545546
"address": "100.126.0.0",
546547
"netmask": "255.255.128.0",
@@ -550,7 +551,7 @@ def test_create_route_success(self, mock_route_class, netapp_client):
550551
# Verify result
551552
assert result.uuid == "route-uuid-123"
552553
assert result.gateway == "100.127.0.17"
553-
assert result.destination == ipaddress.IPv4Network("100.126.0.0/17")
554+
assert result.destination == "100.126.0.0/17"
554555
assert result.svm_name == "os-test-project"
555556

556557
@patch("understack_workflows.netapp.client.NetworkRoute")
@@ -584,7 +585,7 @@ def test_create_route_netapp_rest_error(self, mock_route_class, netapp_client):
584585
assert isinstance(call_args[0][0], NetAppRestError)
585586
assert call_args[0][1] == "Route creation"
586587
assert call_args[0][2]["svm_name"] == "os-nonexistent-project"
587-
assert call_args[0][2]["gateway"] == "100.127.0.17"
588+
assert str(call_args[0][2]["gateway"]) == "100.127.0.17"
588589
assert call_args[0][2]["destination"] == ipaddress.IPv4Network("100.126.0.0/17")
589590

590591
@patch("understack_workflows.netapp.client.NetworkRoute")
@@ -638,19 +639,12 @@ def test_create_route_gateway_unreachable_error(
638639
)
639640
)
640641

641-
route_spec = RouteSpec(
642-
svm_name="os-test-project",
643-
gateway="192.168.1.1", # Invalid gateway for this network
644-
destination=ipaddress.IPv4Network("100.126.0.0/17"),
645-
)
646-
647-
with pytest.raises(NetworkOperationError):
648-
netapp_client.create_route(route_spec)
649-
650-
# Verify error context includes gateway information
651-
call_args = netapp_client._error_handler.handle_netapp_error.call_args
652-
assert call_args[0][2]["gateway"] == "192.168.1.1"
653-
assert call_args[0][2]["destination"] == ipaddress.IPv4Network("100.126.0.0/17")
642+
with pytest.raises(ValidationError):
643+
RouteSpec(
644+
svm_name="os-test-project",
645+
gateway="192.168.1.1", # Invalid gateway for this network
646+
destination=ipaddress.IPv4Network("100.126.0.0/17"),
647+
)
654648

655649
@patch("understack_workflows.netapp.client.NetworkRoute")
656650
def test_create_route_logging_behavior(self, mock_route_class, netapp_client):
@@ -694,7 +688,7 @@ def test_create_route_logging_behavior(self, mock_route_class, netapp_client):
694688
assert start_log[0][1]["destination"] == ipaddress.IPv4Network(
695689
"100.126.128.0/17"
696690
)
697-
assert start_log[0][1]["gateway"] == "100.127.128.17"
691+
assert str(start_log[0][1]["gateway"]) == "100.127.128.17"
698692
assert start_log[0][1]["svm_name"] == "os-logging-test"
699693

700694
# Verify route completion log

python/understack-workflows/tests/test_netapp_config.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -384,12 +384,12 @@ def test_integration_netapp_config_with_from_nautobot_response(
384384

385385
# Create a mock nautobot response
386386
mock_interface_a = MagicMock()
387-
mock_interface_a.name = "N1-test-A"
387+
mock_interface_a.name = "N1-lif-A"
388388
mock_interface_a.address = "192.168.1.10/24"
389389
mock_interface_a.vlan = 100
390390

391391
mock_interface_b = MagicMock()
392-
mock_interface_b.name = "N1-test-B"
392+
mock_interface_b.name = "N1-lif-B"
393393
mock_interface_b.address = "192.168.1.11/24"
394394
mock_interface_b.vlan = 100
395395

@@ -413,7 +413,7 @@ def test_from_nautobot_response_default_prefix(self, valid_config_file):
413413

414414
# Create a mock nautobot response
415415
mock_interface = MagicMock()
416-
mock_interface.name = "N1-test-A"
416+
mock_interface.name = "N1-lif-A"
417417
mock_interface.address = "192.168.1.10/24"
418418
mock_interface.vlan = 100
419419

python/understack-workflows/tests/test_netapp_configure_net.py

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,9 @@
66
from unittest.mock import patch
77

88
import pytest
9+
from pydantic import ValidationError
910

1011
from understack_workflows.main.netapp_configure_net import VIRTUAL_MACHINES_QUERY
11-
from understack_workflows.main.netapp_configure_net import InterfaceInfo
12-
from understack_workflows.main.netapp_configure_net import VirtualMachineNetworkInfo
1312
from understack_workflows.main.netapp_configure_net import argument_parser
1413
from understack_workflows.main.netapp_configure_net import construct_device_name
1514
from understack_workflows.main.netapp_configure_net import execute_graphql_query
@@ -19,6 +18,8 @@
1918
from understack_workflows.main.netapp_configure_net import (
2019
validate_and_transform_response,
2120
)
21+
from understack_workflows.netapp.value_objects import InterfaceInfo
22+
from understack_workflows.netapp.value_objects import VirtualMachineNetworkInfo
2223

2324

2425
def load_json_sample(filename: str) -> dict:
@@ -657,6 +658,52 @@ def test_error_messages_contain_interface_details(self):
657658
assert "200" in error_message
658659
assert "300" in error_message
659660

661+
def test_vlan_validation_valid_range(self):
662+
"""Test VLAN validation with valid VLAN IDs (1-4094)."""
663+
valid_vlans = [1, 100, 2002, 4094]
664+
665+
for vlan in valid_vlans:
666+
interface = InterfaceInfo(
667+
name="test-interface", address="192.168.1.10/24", vlan=vlan
668+
)
669+
assert interface.vlan == vlan
670+
671+
def test_vlan_validation_invalid_range(self):
672+
"""Test VLAN validation with invalid VLAN IDs (outside 1-4094 range)."""
673+
from pydantic import ValidationError
674+
675+
invalid_vlans = [0, -1, 4095, 5000, 65536]
676+
677+
for vlan in invalid_vlans:
678+
with pytest.raises(
679+
ValidationError, match="VLAN ID must be between 1 and 4094"
680+
):
681+
InterfaceInfo(
682+
name="test-interface", address="192.168.1.10/24", vlan=vlan
683+
)
684+
685+
def test_vlan_validation_in_from_graphql_interface(self):
686+
"""Test VLAN validation works in from_graphql_interface method."""
687+
# Test valid VLAN
688+
interface_data = {
689+
"name": "test-interface",
690+
"ip_addresses": [{"address": "192.168.1.10/24"}],
691+
"tagged_vlans": [{"vid": 2002}],
692+
}
693+
694+
interface = InterfaceInfo.from_graphql_interface(interface_data)
695+
assert interface.vlan == 2002
696+
697+
# Test invalid VLAN
698+
interface_data_invalid = {
699+
"name": "test-interface",
700+
"ip_addresses": [{"address": "192.168.1.10/24"}],
701+
"tagged_vlans": [{"vid": 4095}], # Invalid VLAN
702+
}
703+
704+
with pytest.raises(ValidationError, match="VLAN ID must be between 1 and 4094"):
705+
InterfaceInfo.from_graphql_interface(interface_data_invalid)
706+
660707

661708
class TestVirtualMachineNetworkInfo:
662709
"""Test cases for VirtualMachineNetworkInfo data class and validation."""
@@ -1142,7 +1189,7 @@ def test_handling_of_empty_query_results(self, mock_logger):
11421189

11431190
assert result["data"]["virtual_machines"] == []
11441191
mock_logger.info.assert_called_with(
1145-
"GraphQL query successful. Found %s virtual machine(s) " "for device: %s",
1192+
"GraphQL query successful. Found %s virtual machine(s) for device: %s",
11461193
0,
11471194
"os-empty-project",
11481195
)
@@ -1175,7 +1222,7 @@ def test_handling_of_empty_query_results(self, mock_logger):
11751222
assert len(result["data"]["virtual_machines"]) == 1
11761223
assert result["data"]["virtual_machines"][0]["interfaces"] == []
11771224
mock_logger.info.assert_called_with(
1178-
"GraphQL query successful. Found %s virtual machine(s) " "for device: %s",
1225+
"GraphQL query successful. Found %s virtual machine(s) for device: %s",
11791226
1,
11801227
"os-empty-interfaces-project",
11811228
)
@@ -1204,7 +1251,7 @@ def test_graphql_query_logging_behavior(self, mock_logger):
12041251

12051252
# Verify info logging
12061253
mock_logger.info.assert_called_with(
1207-
"GraphQL query successful. Found %s virtual machine(s) " "for device: %s",
1254+
"GraphQL query successful. Found %s virtual machine(s) for device: %s",
12081255
1,
12091256
"os-logging-test-project",
12101257
)
@@ -1733,6 +1780,10 @@ def test_main_function_initializes_netapp_manager_with_default_path(
17331780
# Mock NetAppManager
17341781
mock_netapp_manager_instance = Mock()
17351782
mock_netapp_manager_instance.create_routes_for_project.return_value = []
1783+
# Properly mock the config property with netapp_nic_slot_prefix
1784+
mock_config = Mock()
1785+
mock_config.netapp_nic_slot_prefix = "e4"
1786+
mock_netapp_manager_instance.config = mock_config
17361787
mock_netapp_manager_class.return_value = mock_netapp_manager_instance
17371788

17381789
# Mock sys.argv with default netapp config path
@@ -1789,6 +1840,10 @@ def test_main_function_initializes_netapp_manager_with_custom_path(
17891840

17901841
# Mock NetAppManager
17911842
mock_netapp_manager_instance = Mock()
1843+
# Mock the config property to return a proper config object
1844+
mock_config = Mock()
1845+
mock_config.netapp_nic_slot_prefix = "e4"
1846+
mock_netapp_manager_instance.config = mock_config
17921847
mock_netapp_manager_instance.create_routes_for_project.return_value = []
17931848
mock_netapp_manager_class.return_value = mock_netapp_manager_instance
17941849

0 commit comments

Comments
 (0)