Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions python/understack-workflows/tests/test_enroll_fw.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def _actual_port(
address=mac,
physical_network=physnet,
category="network",
extra={"bios_name": label},
local_link_connection={
"switch_id": switch_id,
"switch_info": switch,
Expand Down Expand Up @@ -109,6 +110,7 @@ def test_enroll_fw_hands_metadata_to_the_engine(mocker):
"management_switch_port": "Ethernet1/24",
},
extra={"mate_serial": "026701010045"},
properties={},
)


Expand Down
7 changes: 7 additions & 0 deletions python/understack-workflows/tests/test_netdev_reconciler.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ def existing_port(label, mac, switch, interface, name=None, category="network"):
name=name or f"leaf01:{label}",
physical_network="f20-1-network",
category=category,
# bios_name defaults to the label, matching what the engine writes, so
# an otherwise-matching port is a true no-op.
extra={"bios_name": label},
local_link_connection={
"switch_id": "00:00:00:00:00:00",
"switch_info": switch,
Expand Down Expand Up @@ -90,6 +93,7 @@ def test_enroll_creates_node_ports_logs_and_makes_available(mocker, caplog):
call(
address="00:11:22:33:44:55",
category="network",
extra={"bios_name": "port1"},
local_link_connection={
"switch_id": "00:00:00:00:00:00",
"switch_info": "spine01.example.net",
Expand All @@ -102,6 +106,7 @@ def test_enroll_creates_node_ports_logs_and_makes_available(mocker, caplog):
call(
address="00:11:22:33:44:66",
category="network",
extra={"bios_name": "port2"},
local_link_connection={
"switch_id": "00:00:00:00:00:00",
"switch_info": "spine02.example.net",
Expand Down Expand Up @@ -328,6 +333,7 @@ def test_enroll_updates_existing_port_and_creates_missing_one(mocker):
fake_ironic.port.create.assert_called_once_with(
address="00:11:22:33:44:66",
category="network",
extra={"bios_name": "port2"},
local_link_connection={
"switch_id": "00:00:00:00:00:00",
"switch_info": "spine02.example.net",
Expand Down Expand Up @@ -856,6 +862,7 @@ def test_enroll_switch_id_override_on_create(mocker):
fake_ironic.port.create.assert_called_once_with(
address="00:11:22:33:44:55",
category="network",
extra={"bios_name": "port1"},
local_link_connection={
"switch_id": "aa:bb:cc:dd:ee:ff",
"switch_info": "spine01.example.net",
Expand Down
38 changes: 28 additions & 10 deletions python/understack-workflows/understack_workflows/firewall.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,18 @@ def firewall_metadata(
management_switch: str = "",
management_switch_port: str = "",
mate_serial: str = "",
) -> tuple[dict, dict]:
"""Build (driver_info, extra) from the firewall fields (non-empty only).
serial: str = "",
vendor: str = "",
model: str = "",
) -> tuple[dict, dict, dict]:
"""Build (driver_info, extra, properties) from the firewall fields.

Management access goes in driver_info (mirroring how servers store
redfish_address); the HA mate serial goes in extra. external_cmdb_id is not
handled here -- the caller decides where it goes (the enroll engine folds it
into extra; the metadata patch adds it explicitly).
Only non-empty values are included. Management access goes in driver_info
(mirroring how servers store redfish_address); the device serial and HA mate
serial go in extra; vendor/model go in properties (which the Nautobot device
sync reads). external_cmdb_id is not handled here -- the caller decides where
it goes (the enroll engine folds it into extra; the metadata patch adds it
explicitly).
"""
driver_info = {
key: value
Expand All @@ -35,18 +40,28 @@ def firewall_metadata(
}.items()
if value
}
extra = {"mate_serial": mate_serial} if mate_serial else {}
return driver_info, extra
extra = {
key: value
for key, value in {"serial": serial, "mate_serial": mate_serial}.items()
if value
}
properties = {
key: value for key, value in {"vendor": vendor, "model": model}.items() if value
}
return driver_info, extra, properties


def apply_node_metadata(client, node, driver_info: dict, extra: dict) -> None:
"""Diff-patch driver_info/extra onto an existing node (any provision state).
def apply_node_metadata(
client, node, driver_info: dict, extra: dict, properties: dict | None = None
) -> None:
"""Diff-patch driver_info/extra/properties onto a node (any provision state).

Only the supplied keys are considered; keys the request does not mention are
left untouched.
"""
node_driver_info = getattr(node, "driver_info", None) or {}
node_extra = getattr(node, "extra", None) or {}
node_properties = getattr(node, "properties", None) or {}

updates = []
for key, value in driver_info.items():
Expand All @@ -55,6 +70,9 @@ def apply_node_metadata(client, node, driver_info: dict, extra: dict) -> None:
for key, value in extra.items():
if node_extra.get(key) != value:
updates.append(f"extra/{key}={value}")
for key, value in (properties or {}).items():
if node_properties.get(key) != value:
updates.append(f"properties/{key}={value}")

if not updates:
logger.info("[node:%s] Firewall metadata already up to date", node.uuid)
Expand Down
39 changes: 36 additions & 3 deletions python/understack-workflows/understack_workflows/main/enroll_fw.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ def main() -> None:
management_switch=args.management_switch,
management_switch_port=args.management_switch_port,
mate_serial=args.mate_serial,
serial=args.serial,
vendor=args.vendor,
model=args.model,
)


Expand Down Expand Up @@ -80,16 +83,22 @@ def enroll_fw(
management_switch: str = "",
management_switch_port: str = "",
mate_serial: str = "",
serial: str = "",
vendor: str = "",
model: str = "",
) -> None:
resource_class = _require_specific_resource_class(resource_class)
management_switch, management_switch_port = _require_management_location(
management_switch, management_switch_port
)
driver_info, extra = firewall.firewall_metadata(
driver_info, extra, properties = firewall.firewall_metadata(
management_ip=management_ip,
management_switch=management_switch,
management_switch_port=management_switch_port,
mate_serial=mate_serial,
serial=serial,
vendor=vendor,
model=model,
)

# Look up the node first so we can tell an in-service (active) firewall from
Expand All @@ -110,6 +119,7 @@ def enroll_fw(
external_cmdb_id=external_cmdb_id,
driver_info=driver_info,
extra=extra,
properties=properties,
)
return

Expand All @@ -124,6 +134,7 @@ def enroll_fw(
external_cmdb_id=external_cmdb_id,
driver_info=driver_info,
extra=extra,
properties=properties,
)


Expand All @@ -146,6 +157,7 @@ def _update_active_firewall(
external_cmdb_id: int | str | None,
driver_info: dict,
extra: dict,
properties: dict,
) -> None:
"""Update firewall metadata on an in-service (active) node, in place.

Expand Down Expand Up @@ -187,7 +199,7 @@ def _update_active_firewall(
"updating firewall metadata only",
node.uuid,
)
firewall.apply_node_metadata(client, node, driver_info, active_extra)
firewall.apply_node_metadata(client, node, driver_info, active_extra, properties)


def _reject_structural_drift(
Expand Down Expand Up @@ -242,7 +254,10 @@ def argument_parser():
parser.add_argument(
"--ports",
required=True,
help="JSON array of ports (same format as enroll-netdev)",
help="JSON array of ports (same format as enroll-netdev). 'switch' must "
"be the switch FQDN (e.g. n11-22-1.dfw3.rackspace.net) so the Nautobot "
"sync can resolve the cable. Optional per-port 'bios_name' (the device "
"interface name); defaults to the label.",
)
parser.add_argument(
"--resource-class",
Expand Down Expand Up @@ -279,6 +294,24 @@ def argument_parser():
default="",
help="HA mate serial number -> extra.mate_serial",
)
parser.add_argument(
"--serial",
required=False,
default="",
help="Device's own serial number -> extra.serial (Nautobot sync)",
)
parser.add_argument(
"--vendor",
required=False,
default="Palo Alto",
help="Device vendor -> properties.vendor (Nautobot sync)",
)
parser.add_argument(
"--model",
required=False,
default="",
help="Device model, e.g. PA-1410 -> properties.model (Nautobot sync)",
)
return parser


Expand Down
Loading
Loading