Skip to content

Commit 78210f4

Browse files
committed
Implement ironic inspect hook setting port local_link, physical_network
1 parent 2050345 commit 78210f4

File tree

5 files changed

+613
-0
lines changed

5 files changed

+613
-0
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from dataclasses import dataclass
2+
3+
4+
@dataclass
5+
class InspectedPort:
6+
"""Represents a parsed entry from Ironic inspection (inventory) data."""
7+
8+
mac_address: str
9+
name: str
10+
switch_system_name: str
11+
switch_port_id: str
12+
switch_chassis_id: str
13+
14+
@property
15+
def local_link_connection(self) -> dict:
16+
return {
17+
"port_id": self.switch_port_id,
18+
"switch_id": self.switch_chassis_id,
19+
"switch_info": self.switch_system_name,
20+
}
21+
22+
@property
23+
def parsed_name(self) -> dict[str, str]:
24+
parts = self.switch_system_name.split(".", maxsplit=1)
25+
if len(parts) != 2:
26+
raise ValueError(
27+
"Failed to parse switch hostname - expecting name.dc in %s", self
28+
)
29+
switch_name, data_center_name = parts
30+
31+
parts = switch_name.rsplit("-", maxsplit=1)
32+
if len(parts) != 2:
33+
raise ValueError(
34+
f"Unknown switch name format: {switch_name} - this hook requires "
35+
f"that switch names follow the convention <cabinet-name>-<suffix>"
36+
)
37+
38+
rack_name, switch_suffix = parts
39+
40+
return {
41+
"rack_name": rack_name,
42+
"switch_suffix": switch_suffix,
43+
"data_center_name": data_center_name,
44+
}
45+
46+
@property
47+
def rack_name(self) -> str:
48+
return self.parsed_name["rack_name"]
49+
50+
@property
51+
def switch_suffix(self) -> str:
52+
return self.parsed_name["switch_suffix"]
53+
54+
@property
55+
def data_center_name(self) -> str:
56+
return self.parsed_name["data_center_name"]
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import logging
2+
3+
import ironic.objects
4+
from oslo_utils import uuidutils
5+
6+
import ironic_understack
7+
from ironic_understack.update_baremetal_port import UpdateBaremetalPortsHook
8+
9+
# load some metaprgramming normally taken care of during Ironic initialization:
10+
ironic.objects.register_all()
11+
12+
_INVENTORY = {}
13+
_PLUGIN_DATA = {
14+
"all_interfaces": {
15+
"ex1": {
16+
"name": "ex1",
17+
"mac_address": "11:11:11:11:11:11",
18+
},
19+
"ex2": {
20+
"name": "ex2",
21+
"mac_address": "22:22:22:22:22:22",
22+
},
23+
"ex3": {
24+
"name": "ex3",
25+
"mac_address": "33:33:33:33:33:33",
26+
},
27+
"ex4": {
28+
"name": "ex4",
29+
"mac_address": "44:44:44:44:44:44",
30+
},
31+
},
32+
"parsed_lldp": {
33+
"ex1": {
34+
"switch_chassis_id": "88:5a:92:ec:54:59",
35+
"switch_port_id": "Ethernet1/18",
36+
"switch_system_name": "f20-3-1.iad3",
37+
},
38+
"ex2": {
39+
"switch_chassis_id": "88:5a:92:ec:54:59",
40+
"switch_port_id": "Ethernet1/18",
41+
"switch_system_name": "f20-3-2.iad3",
42+
},
43+
"ex3": {
44+
"switch_chassis_id": "88:5a:92:ec:54:59",
45+
"switch_port_id": "Ethernet1/18",
46+
"switch_system_name": "f20-3-1f.iad3",
47+
},
48+
"ex4": {
49+
"switch_chassis_id": "88:5a:92:ec:54:59",
50+
"switch_port_id": "Ethernet1/18",
51+
"switch_system_name": "f20-3-2f.iad3",
52+
},
53+
},
54+
}
55+
56+
MAPPING = {
57+
"1": "network",
58+
"2": "network",
59+
"1f": "storage",
60+
"2f": "storage",
61+
"-1d": "bmc",
62+
}
63+
64+
65+
def test_with_valid_data(mocker, caplog):
66+
caplog.set_level(logging.DEBUG)
67+
68+
node_uuid = uuidutils.generate_uuid()
69+
mock_traits = mocker.Mock()
70+
mock_context = mocker.Mock()
71+
mock_node = mocker.Mock(id=1234, traits=mock_traits)
72+
mock_task = mocker.Mock(node=mock_node, context=mock_context)
73+
mock_port = mocker.Mock(
74+
uuid=uuidutils.generate_uuid(),
75+
node_id=node_uuid,
76+
address="11:11:11:11:11:11",
77+
local_link_connection={},
78+
physical_network="original_value",
79+
)
80+
81+
mocker.patch(
82+
"ironic_understack.update_baremetal_port.ironic_ports_for_node",
83+
return_value=[mock_port],
84+
)
85+
mocker.patch(
86+
"ironic_understack.update_baremetal_port.CONF.ironic_understack.switch_name_vlan_group_mapping",
87+
MAPPING,
88+
)
89+
mocker.patch("ironic_understack.update_baremetal_port.objects.TraitList.create")
90+
91+
mock_traits.get_trait_names.return_value = ["CUSTOM_BMC_SWITCH", "bar"]
92+
93+
UpdateBaremetalPortsHook().__call__(mock_task, _INVENTORY, _PLUGIN_DATA)
94+
95+
assert mock_port.local_link_connection == {
96+
"port_id": "Ethernet1/18",
97+
"switch_id": "88:5a:92:ec:54:59",
98+
"switch_info": "f20-3-1.iad3",
99+
}
100+
assert mock_port.physical_network == "f20-3-network"
101+
mock_port.save.assert_called()
102+
103+
mock_traits.get_trait_names.assert_called_once()
104+
mock_traits.destroy.assert_called_once_with("CUSTOM_BMC_SWITCH")
105+
ironic_understack.update_baremetal_port.objects.TraitList.create.assert_called_once_with(
106+
mock_context, 1234, ["CUSTOM_NETWORK_SWITCH", "CUSTOM_STORAGE_SWITCH"]
107+
)
108+
mock_node.save.assert_called_once()
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import pytest
2+
3+
from ironic_understack.inspected_port import InspectedPort
4+
from ironic_understack.vlan_group_name_convention import TopologyError
5+
from ironic_understack.vlan_group_name_convention import vlan_group_names
6+
7+
mapping = {
8+
"1": "network",
9+
"2": "network",
10+
"3": "network",
11+
"4": "network",
12+
"1f": "storage",
13+
"2f": "storage",
14+
"3f": "storage-appliance",
15+
"4f": "storage-appliance",
16+
"1d": "bmc",
17+
}
18+
19+
20+
def port(switch: str):
21+
return InspectedPort(
22+
mac_address="",
23+
name="",
24+
switch_system_name=switch,
25+
switch_chassis_id="",
26+
switch_port_id="",
27+
)
28+
29+
30+
def test_vlan_group_name_single_cab():
31+
assert vlan_group_names(
32+
[
33+
port("a1-1-1.abc1"),
34+
port("a1-1-2.abc1"),
35+
port("a1-1-1f.abc1"),
36+
port("a1-1-2f.abc1"),
37+
],
38+
mapping,
39+
) == {
40+
"a1-1-1.abc1": "a1-1-network",
41+
"a1-1-2.abc1": "a1-1-network",
42+
"a1-1-1f.abc1": "a1-1-storage",
43+
"a1-1-2f.abc1": "a1-1-storage",
44+
}
45+
46+
47+
def test_vlan_group_name_pair_cab():
48+
assert vlan_group_names(
49+
[
50+
port("a1-1-1.abc1"),
51+
port("a1-2-1.abc1"),
52+
port("a1-1-1f.abc1"),
53+
port("a1-2-1f.abc1"),
54+
],
55+
mapping,
56+
) == {
57+
"a1-1-1.abc1": "a1-1/a1-2-network",
58+
"a1-2-1.abc1": "a1-1/a1-2-network",
59+
"a1-1-1f.abc1": "a1-1/a1-2-storage",
60+
"a1-2-1f.abc1": "a1-1/a1-2-storage",
61+
}
62+
63+
64+
def test_vlan_group_name_with_domain():
65+
assert vlan_group_names(
66+
[
67+
port("a1-1-1.abc1.domain"),
68+
port("a1-1-2.abc1.domain"),
69+
port("a1-1-1f.abc1.domain"),
70+
port("a1-1-2f.abc1.domain"),
71+
],
72+
mapping,
73+
) == {
74+
"a1-1-1.abc1.domain": "a1-1-network",
75+
"a1-1-2.abc1.domain": "a1-1-network",
76+
"a1-1-1f.abc1.domain": "a1-1-storage",
77+
"a1-1-2f.abc1.domain": "a1-1-storage",
78+
}
79+
80+
81+
def test_vlan_group_name_invalid_format():
82+
with pytest.raises(ValueError, match="Unknown switch name format"):
83+
vlan_group_names([port("invalid.abc1")], mapping)
84+
85+
with pytest.raises(ValueError, match="Unknown switch name format"):
86+
vlan_group_names([port(".abc1")], mapping)
87+
88+
89+
def test_vlan_group_name_unknown_suffix():
90+
with pytest.raises(TopologyError, match="suffix a1-1-99.abc1 is not present"):
91+
vlan_group_names([port("a1-1-99.abc1")], mapping)
92+
93+
with pytest.raises(TopologyError, match="suffix a1-1-5f.abc1 is not present"):
94+
vlan_group_names([port("a1-1-5f.abc1")], mapping)
95+
96+
with pytest.raises(TopologyError, match="suffix a1-1-xyz.abc1 is not present"):
97+
vlan_group_names([port("a1-1-xyz.abc1")], mapping)
98+
99+
100+
def test_vlan_group_name_many_dc():
101+
with pytest.raises(TopologyError, match="multiple"):
102+
vlan_group_names(
103+
[
104+
port("a1-1-1.abc1.domain"),
105+
port("a1-1-1.xyz2.domain"),
106+
],
107+
mapping,
108+
)
109+
110+
111+
def test_vlan_group_name_too_many_racks():
112+
with pytest.raises(TopologyError, match="more than two racks"):
113+
vlan_group_names(
114+
[
115+
port("a1-1-1.abc1.domain"),
116+
port("a1-2-1.abc1.domain"),
117+
port("a1-3-1.abc1.domain"),
118+
],
119+
mapping,
120+
)
121+
122+
123+
def test_vlan_group_name_too_many_switches():
124+
with pytest.raises(TopologyError, match="exactly two network switches"):
125+
vlan_group_names(
126+
[
127+
port("a1-1-1.abc1.domain"),
128+
port("a1-1-2.abc1.domain"),
129+
port("a1-1-3.abc1.domain"),
130+
],
131+
mapping,
132+
)
133+
134+
135+
def test_vlan_group_name_not_enough_switches():
136+
with pytest.raises(TopologyError, match="exactly two network switches"):
137+
vlan_group_names(
138+
[
139+
port("a1-1-1.abc1.domain"),
140+
port("a1-1-1.abc1.domain"),
141+
],
142+
mapping,
143+
)

0 commit comments

Comments
 (0)