From e863e9a12a5d245ddb0f44da1f5cc6f377adea4b Mon Sep 17 00:00:00 2001 From: Doug Goldstein Date: Thu, 1 Aug 2024 10:48:13 -0500 Subject: [PATCH 1/6] feat: refactor creation of ironic nodes in workflows Added a common from argo event parser for the IronicNodeConfiguration class and create tests with a sample event. Utilize this event parser to create the node in ironic. The code follows the python-ironicclient a little bit more closely here. --- .../json_samples/event-interface-update.json | 487 ++++++++++++++++++ .../tests/test_node_config.py | 26 + .../main/synchronize_obm_creds.py | 25 +- .../main/synchronize_server.py | 33 +- .../node_configuration.py | 76 ++- .../redfish_driver_info.py | 2 +- 6 files changed, 583 insertions(+), 66 deletions(-) create mode 100644 python/understack-workflows/tests/json_samples/event-interface-update.json create mode 100644 python/understack-workflows/tests/test_node_config.py diff --git a/python/understack-workflows/tests/json_samples/event-interface-update.json b/python/understack-workflows/tests/json_samples/event-interface-update.json new file mode 100644 index 00000000..cfae1af5 --- /dev/null +++ b/python/understack-workflows/tests/json_samples/event-interface-update.json @@ -0,0 +1,487 @@ +{ + "event": "updated", + "timestamp": "2024-07-30 18:04:41.828756+00:00", + "model": "interface", + "username": "person@company.com", + "request_id": "de6f6f70-3907-4aaa-acde-f68b6ceae71e", + "data": { + "id": "67e80b37-1efa-4591-baf1-5d68d1ed5981", + "lag": null, + "mtu": null, + "url": "/api/dcim/interfaces/67e80b37-1efa-4591-baf1-5d68d1ed5981/", + "vrf": null, + "mode": null, + "name": "iDRAC", + "tags": [], + "type": { "label": "1000BASE-T (1GE)", "value": "1000base-t" }, + "cable": null, + "label": "", + "bridge": null, + "device": { + "id": "0ccdd4ae-3e49-42cd-9b70-f742ece59312", + "url": "/api/dcim/devices/0ccdd4ae-3e49-42cd-9b70-f742ece59312/", + "face": { "label": "Front", "value": "front" }, + "name": "1327198-GP2S.3.understack.iad3", + "rack": { + "id": "f5cc7552-061b-48a0-93f9-404a553eb99d", + "url": "/api/dcim/racks/f5cc7552-061b-48a0-93f9-404a553eb99d/", + "object_type": "dcim.rack" + }, + "role": { + "id": "8c4ab2e6-f84a-4474-9e78-d81b8f9d167c", + "url": "/api/extras/roles/8c4ab2e6-f84a-4474-9e78-d81b8f9d167c/", + "object_type": "extras.role" + }, + "serial": "", + "status": { + "id": "3709c38e-1f4f-4cdf-a9d4-45ad14e9f3dd", + "url": "/api/extras/statuses/3709c38e-1f4f-4cdf-a9d4-45ad14e9f3dd/", + "object_type": "extras.status" + }, + "tenant": { + "id": "fc0147e3-c716-4b03-a52a-c1ee7ad30221", + "url": "/api/tenancy/tenants/fc0147e3-c716-4b03-a52a-c1ee7ad30221/", + "object_type": "tenancy.tenant" + }, + "cluster": null, + "created": "2024-07-30T17:37:18.661137Z", + "display": "1327198-GP2S.3.understack.iad3", + "comments": "", + "location": { + "id": "3a8f1bb1-71fe-4b7e-b1a0-31402e8117f8", + "url": "/api/dcim/locations/3a8f1bb1-71fe-4b7e-b1a0-31402e8117f8/", + "object_type": "dcim.location" + }, + "platform": null, + "position": 5, + "asset_tag": null, + "notes_url": "/api/dcim/devices/0ccdd4ae-3e49-42cd-9b70-f742ece59312/notes/", + "parent_bay": null, + "device_type": { + "id": "2ddd7ac2-1843-4d7a-8547-f91a1f523383", + "url": "/api/dcim/device-types/2ddd7ac2-1843-4d7a-8547-f91a1f523383/", + "object_type": "dcim.devicetype" + }, + "object_type": "dcim.device", + "primary_ip4": null, + "primary_ip6": null, + "vc_position": null, + "vc_priority": null, + "last_updated": "2024-07-30T17:40:11.713297Z", + "natural_slug": "1327198-gp2s-3-understack-iad3_core-5836965_spine1-1_iad3_iad_company_global_0ccd", + "custom_fields": { + "core_number": 1327198, + "connected_to_network": "provisioning", + "ironic_provisioning_status": "" + }, + "secrets_group": null, + "virtual_chassis": null, + "software_version": null, + "device_redundancy_group": null, + "local_config_context_data": null, + "local_config_context_schema": null, + "controller_managed_device_group": null, + "device_redundancy_group_priority": null, + "local_config_context_data_owner_object_id": null, + "local_config_context_data_owner_content_type": null + }, + "status": { + "id": "3709c38e-1f4f-4cdf-a9d4-45ad14e9f3dd", + "url": "/api/extras/statuses/3709c38e-1f4f-4cdf-a9d4-45ad14e9f3dd/", + "name": "Active", + "color": "4calf50", + "created": "2024-04-29T00:00:00Z", + "display": "Active", + "notes_url": "/api/extras/statuses/3709c38e-1f4f-4cdf-a9d4-45ad14e9f3dd/notes/", + "description": "Unit is active", + "object_type": "extras.status", + "last_updated": "2024-05-23T15:20:38.861176Z", + "natural_slug": "active_3709", + "custom_fields": {} + }, + "created": "2024-07-30T17:37:18.750296Z", + "display": "iDRAC", + "enabled": true, + "mgmt_only": true, + "notes_url": "/api/dcim/interfaces/67e80b37-1efa-4591-baf1-5d68d1ed5981/notes/", + "cable_peer": null, + "description": "test", + "mac_address": "", + "object_type": "dcim.interface", + "ip_addresses": [ + { + "id": "b1739ed2-d6c6-4722-9f39-b6a8bab02b60", + "url": "/api/ipam/ip-addresses/b1739ed2-d6c6-4722-9f39-b6a8bab02b60/", + "host": "10.46.96.156", + "role": null, + "type": "host", + "parent": { + "id": "f3b3041f-c449-4706-be7a-aa6ac144c5a2", + "url": "/api/ipam/prefixes/f3b3041f-c449-4706-be7a-aa6ac144c5a2/", + "object_type": "ipam.prefix" + }, + "status": { + "id": "3709c38e-1f4f-4cdf-a9d4-45ad14e9f3dd", + "url": "/api/extras/statuses/3709c38e-1f4f-4cdf-a9d4-45ad14e9f3dd/", + "object_type": "extras.status" + }, + "tenant": null, + "address": "10.46.96.156/26", + "created": "2024-07-16T16:23:55.723588Z", + "display": "10.46.96.156/26", + "dns_name": "", + "notes_url": "/api/ipam/ip-addresses/b1739ed2-d6c6-4722-9f39-b6a8bab02b60/notes/", + "interfaces": [ + { + "id": "67e80b37-1efa-4591-baf1-5d68d1ed5981", + "url": "/api/dcim/interfaces/67e80b37-1efa-4591-baf1-5d68d1ed5981/", + "object_type": "dcim.interface" + } + ], + "ip_version": 4, + "nat_inside": null, + "description": "", + "mask_length": 26, + "object_type": "ipam.ipaddress", + "last_updated": "2024-07-16T16:23:55.723616Z", + "natural_slug": "company_10-46-96-156_b173", + "custom_fields": {}, + "vm_interfaces": [], + "nat_outside_list": [] + } + ], + "last_updated": "2024-07-30T18:04:41.623727Z", + "natural_slug": "idrac_1327198-gp2s-3-understack-iad3_core-5836965_spine1-1_iad3_iad_company_global_67e8", + "tagged_vlans": [], + "custom_fields": {}, + "untagged_vlan": null, + "cable_peer_type": null, + "ip_address_count": 1, + "parent_interface": null, + "connected_endpoint": null, + "connected_endpoint_type": null, + "connected_endpoint_reachable": null + }, + "snapshots": { + "prechange": null, + "postchange": { + "id": "67e80b37-1efa-4591-baf1-5d68d1ed5981", + "lag": null, + "mtu": null, + "url": "/api/dcim/interfaces/67e80b37-1efa-4591-baf1-5d68d1ed5981/", + "vrf": null, + "mode": null, + "name": "iDRAC", + "tags": [], + "type": { "label": "1000BASE-T (1GE)", "value": "1000base-t" }, + "cable": null, + "label": "", + "bridge": null, + "device": { + "id": "0ccdd4ae-3e49-42cd-9b70-f742ece59312", + "url": "/api/dcim/devices/0ccdd4ae-3e49-42cd-9b70-f742ece59312/", + "face": { "label": "Front", "value": "front" }, + "name": "1327198-GP2S.3.understack.iad3", + "rack": { + "id": "f5cc7552-061b-48a0-93f9-404a553eb99d", + "url": "/api/dcim/racks/f5cc7552-061b-48a0-93f9-404a553eb99d/", + "object_type": "dcim.rack" + }, + "role": { + "id": "8c4ab2e6-f84a-4474-9e78-d81b8f9d167c", + "url": "/api/extras/roles/8c4ab2e6-f84a-4474-9e78-d81b8f9d167c/", + "object_type": "extras.role" + }, + "serial": "", + "status": { + "id": "3709c38e-1f4f-4cdf-a9d4-45ad14e9f3dd", + "url": "/api/extras/statuses/3709c38e-1f4f-4cdf-a9d4-45ad14e9f3dd/", + "object_type": "extras.status" + }, + "tenant": { + "id": "fc0147e3-c716-4b03-a52a-c1ee7ad30221", + "url": "/api/tenancy/tenants/fc0147e3-c716-4b03-a52a-c1ee7ad30221/", + "object_type": "tenancy.tenant" + }, + "cluster": null, + "created": "2024-07-30T17:37:18.661137Z", + "display": "1327198-GP2S.3.understack.iad3", + "comments": "", + "location": { + "id": "3a8f1bb1-71fe-4b7e-b1a0-31402e8117f8", + "url": "/api/dcim/locations/3a8f1bb1-71fe-4b7e-b1a0-31402e8117f8/", + "object_type": "dcim.location" + }, + "platform": null, + "position": 5, + "asset_tag": null, + "notes_url": "/api/dcim/devices/0ccdd4ae-3e49-42cd-9b70-f742ece59312/notes/", + "parent_bay": null, + "device_type": { + "id": "2ddd7ac2-1843-4d7a-8547-f91a1f523383", + "url": "/api/dcim/device-types/2ddd7ac2-1843-4d7a-8547-f91a1f523383/", + "object_type": "dcim.devicetype" + }, + "object_type": "dcim.device", + "primary_ip4": null, + "primary_ip6": null, + "vc_position": null, + "vc_priority": null, + "last_updated": "2024-07-30T17:40:11.713297Z", + "natural_slug": "1327198-gp2s-3-understack-iad3_core-5836965_spine1-1_iad3_iad_company_global_0ccd", + "custom_fields": { + "core_number": 1327198, + "connected_to_network": "provisioning", + "ironic_provisioning_status": "" + }, + "secrets_group": null, + "virtual_chassis": null, + "software_version": null, + "device_redundancy_group": null, + "local_config_context_data": null, + "local_config_context_schema": null, + "controller_managed_device_group": null, + "device_redundancy_group_priority": null, + "local_config_context_data_owner_object_id": null, + "local_config_context_data_owner_content_type": null + }, + "status": { + "id": "3709c38e-1f4f-4cdf-a9d4-45ad14e9f3dd", + "url": "/api/extras/statuses/3709c38e-1f4f-4cdf-a9d4-45ad14e9f3dd/", + "name": "Active", + "color": "4calf50", + "created": "2024-04-29T00:00:00Z", + "display": "Active", + "notes_url": "/api/extras/statuses/3709c38e-1f4f-4cdf-a9d4-45ad14e9f3dd/notes/", + "description": "Unit is active", + "object_type": "extras.status", + "last_updated": "2024-05-23T15:20:38.861176Z", + "natural_slug": "active_3709", + "custom_fields": {} + }, + "created": "2024-07-30T17:37:18.750296Z", + "display": "iDRAC", + "enabled": true, + "mgmt_only": true, + "notes_url": "/api/dcim/interfaces/67e80b37-1efa-4591-baf1-5d68d1ed5981/notes/", + "cable_peer": null, + "description": "test", + "mac_address": "", + "object_type": "dcim.interface", + "ip_addresses": [ + { + "id": "b1739ed2-d6c6-4722-9f39-b6a8bab02b60", + "url": "/api/ipam/ip-addresses/b1739ed2-d6c6-4722-9f39-b6a8bab02b60/", + "host": "10.46.96.156", + "role": null, + "type": "host", + "parent": { + "id": "f3b3041f-c449-4706-be7a-aa6ac144c5a2", + "url": "/api/ipam/prefixes/f3b3041f-c449-4706-be7a-aa6ac144c5a2/", + "object_type": "ipam.prefix" + }, + "status": { + "id": "3709c38e-1f4f-4cdf-a9d4-45ad14e9f3dd", + "url": "/api/extras/statuses/3709c38e-1f4f-4cdf-a9d4-45ad14e9f3dd/", + "object_type": "extras.status" + }, + "tenant": null, + "address": "10.46.96.156/26", + "created": "2024-07-16T16:23:55.723588Z", + "display": "10.46.96.156/26", + "dns_name": "", + "notes_url": "/api/ipam/ip-addresses/b1739ed2-d6c6-4722-9f39-b6a8bab02b60/notes/", + "interfaces": [ + { + "id": "67e80b37-1efa-4591-baf1-5d68d1ed5981", + "url": "/api/dcim/interfaces/67e80b37-1efa-4591-baf1-5d68d1ed5981/", + "object_type": "dcim.interface" + } + ], + "ip_version": 4, + "nat_inside": null, + "description": "", + "mask_length": 26, + "object_type": "ipam.ipaddress", + "last_updated": "2024-07-16T16:23:55.723616Z", + "natural_slug": "company_10-46-96-156_b173", + "custom_fields": {}, + "vm_interfaces": [], + "nat_outside_list": [] + } + ], + "last_updated": "2024-07-30T18:04:41.623727Z", + "natural_slug": "idrac_1327198-gp2s-3-understack-iad3_core-5836965_spine1-1_iad3_iad_company_global_67e8", + "tagged_vlans": [], + "custom_fields": {}, + "untagged_vlan": null, + "cable_peer_type": null, + "ip_address_count": 1, + "parent_interface": null, + "connected_endpoint": null, + "connected_endpoint_type": null, + "connected_endpoint_reachable": null + }, + "differences": { + "removed": null, + "added": { + "id": "67e80b37-1efa-4591-baf1-5d68d1ed5981", + "lag": null, + "mtu": null, + "url": "/api/dcim/interfaces/67e80b37-1efa-4591-baf1-5d68d1ed5981/", + "vrf": null, + "mode": null, + "name": "iDRAC", + "tags": [], + "type": { "label": "1000BASE-T (1GE)", "value": "1000base-t" }, + "cable": null, + "label": "", + "bridge": null, + "device": { + "id": "0ccdd4ae-3e49-42cd-9b70-f742ece59312", + "url": "/api/dcim/devices/0ccdd4ae-3e49-42cd-9b70-f742ece59312/", + "face": { "label": "Front", "value": "front" }, + "name": "1327198-GP2S.3.understack.iad3", + "rack": { + "id": "f5cc7552-061b-48a0-93f9-404a553eb99d", + "url": "/api/dcim/racks/f5cc7552-061b-48a0-93f9-404a553eb99d/", + "object_type": "dcim.rack" + }, + "role": { + "id": "8c4ab2e6-f84a-4474-9e78-d81b8f9d167c", + "url": "/api/extras/roles/8c4ab2e6-f84a-4474-9e78-d81b8f9d167c/", + "object_type": "extras.role" + }, + "serial": "", + "status": { + "id": "3709c38e-1f4f-4cdf-a9d4-45ad14e9f3dd", + "url": "/api/extras/statuses/3709c38e-1f4f-4cdf-a9d4-45ad14e9f3dd/", + "object_type": "extras.status" + }, + "tenant": { + "id": "fc0147e3-c716-4b03-a52a-c1ee7ad30221", + "url": "/api/tenancy/tenants/fc0147e3-c716-4b03-a52a-c1ee7ad30221/", + "object_type": "tenancy.tenant" + }, + "cluster": null, + "created": "2024-07-30T17:37:18.661137Z", + "display": "1327198-GP2S.3.understack.iad3", + "comments": "", + "location": { + "id": "3a8f1bb1-71fe-4b7e-b1a0-31402e8117f8", + "url": "/api/dcim/locations/3a8f1bb1-71fe-4b7e-b1a0-31402e8117f8/", + "object_type": "dcim.location" + }, + "platform": null, + "position": 5, + "asset_tag": null, + "notes_url": "/api/dcim/devices/0ccdd4ae-3e49-42cd-9b70-f742ece59312/notes/", + "parent_bay": null, + "device_type": { + "id": "2ddd7ac2-1843-4d7a-8547-f91a1f523383", + "url": "/api/dcim/device-types/2ddd7ac2-1843-4d7a-8547-f91a1f523383/", + "object_type": "dcim.devicetype" + }, + "object_type": "dcim.device", + "primary_ip4": null, + "primary_ip6": null, + "vc_position": null, + "vc_priority": null, + "last_updated": "2024-07-30T17:40:11.713297Z", + "natural_slug": "1327198-gp2s-3-understack-iad3_core-5836965_spine1-1_iad3_iad_company_global_0ccd", + "custom_fields": { + "core_number": 1327198, + "connected_to_network": "provisioning", + "ironic_provisioning_status": "" + }, + "secrets_group": null, + "virtual_chassis": null, + "software_version": null, + "device_redundancy_group": null, + "local_config_context_data": null, + "local_config_context_schema": null, + "controller_managed_device_group": null, + "device_redundancy_group_priority": null, + "local_config_context_data_owner_object_id": null, + "local_config_context_data_owner_content_type": null + }, + "status": { + "id": "3709c38e-1f4f-4cdf-a9d4-45ad14e9f3dd", + "url": "/api/extras/statuses/3709c38e-1f4f-4cdf-a9d4-45ad14e9f3dd/", + "name": "Active", + "color": "4calf50", + "created": "2024-04-29T00:00:00Z", + "display": "Active", + "notes_url": "/api/extras/statuses/3709c38e-1f4f-4cdf-a9d4-45ad14e9f3dd/notes/", + "description": "Unit is active", + "object_type": "extras.status", + "last_updated": "2024-05-23T15:20:38.861176Z", + "natural_slug": "active_3709", + "custom_fields": {} + }, + "created": "2024-07-30T17:37:18.750296Z", + "display": "iDRAC", + "enabled": true, + "mgmt_only": true, + "notes_url": "/api/dcim/interfaces/67e80b37-1efa-4591-baf1-5d68d1ed5981/notes/", + "cable_peer": null, + "description": "test", + "mac_address": "", + "object_type": "dcim.interface", + "ip_addresses": [ + { + "id": "b1739ed2-d6c6-4722-9f39-b6a8bab02b60", + "url": "/api/ipam/ip-addresses/b1739ed2-d6c6-4722-9f39-b6a8bab02b60/", + "host": "10.46.96.156", + "role": null, + "type": "host", + "parent": { + "id": "f3b3041f-c449-4706-be7a-aa6ac144c5a2", + "url": "/api/ipam/prefixes/f3b3041f-c449-4706-be7a-aa6ac144c5a2/", + "object_type": "ipam.prefix" + }, + "status": { + "id": "3709c38e-1f4f-4cdf-a9d4-45ad14e9f3dd", + "url": "/api/extras/statuses/3709c38e-1f4f-4cdf-a9d4-45ad14e9f3dd/", + "object_type": "extras.status" + }, + "tenant": null, + "address": "10.46.96.156/26", + "created": "2024-07-16T16:23:55.723588Z", + "display": "10.46.96.156/26", + "dns_name": "", + "notes_url": "/api/ipam/ip-addresses/b1739ed2-d6c6-4722-9f39-b6a8bab02b60/notes/", + "interfaces": [ + { + "id": "67e80b37-1efa-4591-baf1-5d68d1ed5981", + "url": "/api/dcim/interfaces/67e80b37-1efa-4591-baf1-5d68d1ed5981/", + "object_type": "dcim.interface" + } + ], + "ip_version": 4, + "nat_inside": null, + "description": "", + "mask_length": 26, + "object_type": "ipam.ipaddress", + "last_updated": "2024-07-16T16:23:55.723616Z", + "natural_slug": "company_10-46-96-156_b173", + "custom_fields": {}, + "vm_interfaces": [], + "nat_outside_list": [] + } + ], + "last_updated": "2024-07-30T18:04:41.623727Z", + "natural_slug": "idrac_1327198-gp2s-3-understack-iad3_core-5836965_spine1-1_iad3_iad_company_global_67e8", + "tagged_vlans": [], + "custom_fields": {}, + "untagged_vlan": null, + "cable_peer_type": null, + "ip_address_count": 1, + "parent_interface": null, + "connected_endpoint": null, + "connected_endpoint_type": null, + "connected_endpoint_reachable": null + } + } + } +} diff --git a/python/understack-workflows/tests/test_node_config.py b/python/understack-workflows/tests/test_node_config.py new file mode 100644 index 00000000..3cc60a0c --- /dev/null +++ b/python/understack-workflows/tests/test_node_config.py @@ -0,0 +1,26 @@ +import json +import pathlib + +import pytest + +from understack_workflows.node_configuration import IronicNodeConfiguration + + +@pytest.fixture +def interface_event() -> dict: + here = pathlib.Path(__file__).parent + ref = here.joinpath("json_samples/event-interface-update.json") + with ref.open("r") as f: + return json.load(f) + + +def test_node_config_from_event_none_event(): + with pytest.raises(ValueError): + _ = IronicNodeConfiguration.from_event({}) + + +def test_node_config_from_event_interface_event(interface_event): + node = IronicNodeConfiguration.from_event(interface_event) + assert node.uuid == interface_event["data"]["device"]["id"] + assert node.name == interface_event["data"]["device"]["name"] + assert node.driver == "redfish" diff --git a/python/understack-workflows/understack_workflows/main/synchronize_obm_creds.py b/python/understack-workflows/understack_workflows/main/synchronize_obm_creds.py index 0fb46ed3..cbbdf564 100644 --- a/python/understack-workflows/understack_workflows/main/synchronize_obm_creds.py +++ b/python/understack-workflows/understack_workflows/main/synchronize_obm_creds.py @@ -11,18 +11,6 @@ logger = setup_logger(__name__) -def event_to_node_configuration(event: dict) -> IronicNodeConfiguration: - node_config = IronicNodeConfiguration() - node_config.conductor_group = None - node_config.driver = "redfish" - - node_config.chassis_uuid = None - node_config.uuid = event["device"]["id"] - node_config.name = event["device"]["name"] - - return node_config - - def credential_secrets(): """Reads Kubernetes Secret files with username/password credentials.""" username = None @@ -64,22 +52,21 @@ def main(): interface_update_event = json.loads(sys.argv[1]) logger.debug(f"Received: {interface_update_event}") - update_data = interface_update_event["data"] - node_id = update_data["device"]["id"] - logger.debug(f"Checking if node with UUID: {node_id} exists in Ironic.") + node = IronicNodeConfiguration.from_event(interface_update_event) + logger.debug(f"Checking if node with UUID {node.uuid} exists in Ironic.") try: - ironic_node = client.get_node(node_id) + ironic_node = client.get_node(node.uuid) except ironicclient.common.apiclient.exceptions.NotFound: - logger.debug(f"Node: {node_id} not found in Ironic.") + logger.debug(f"Node: {node.uuid} not found in Ironic.") ironic_node = None sys.exit(1) STATES_ALLOWING_UPDATES = ["enroll"] if ironic_node.provision_state not in STATES_ALLOWING_UPDATES: logger.info( - f"Device {node_id} is in a {ironic_node.provision_state} " + f"Device {node.uuid} is in a {ironic_node.provision_state} " f"provisioning state, so the updates are not allowed." ) sys.exit(0) @@ -100,6 +87,6 @@ def main(): ] patches = [p for p in patches if p is not None] - response = client.update_node(node_id, patches) + response = client.update_node(node.uuid, patches) logger.info(f"Patching: {patches}") logger.info(f"Updated: {response}") diff --git a/python/understack-workflows/understack_workflows/main/synchronize_server.py b/python/understack-workflows/understack_workflows/main/synchronize_server.py index 65969200..21dc3f4d 100644 --- a/python/understack-workflows/understack_workflows/main/synchronize_server.py +++ b/python/understack-workflows/understack_workflows/main/synchronize_server.py @@ -20,17 +20,6 @@ def replace_or_add_field(path, current_val, expected_val): return {"op": "replace", "path": path, "value": expected_val} -def event_to_node_configuration(event: dict) -> IronicNodeConfiguration: - node_config = IronicNodeConfiguration() - node_config.conductor_group = None - node_config.driver = "redfish" - node_config.chassis_uuid = None - node_config.uuid = event["device"]["id"] - node_config.name = event["device"]["name"] - - return node_config - - def main(): if len(sys.argv) < 1: raise ValueError( @@ -47,28 +36,24 @@ def main(): ) interface_update_event = json.loads(sys.argv[1]) - logger.debug(f"Received: {interface_update_event}") + logger.debug(f"Received: {json.dumps(interface_update_event, indent=2)}") update_data = interface_update_event["data"] - node_id = update_data["device"]["id"] - logger.debug(f"Checking if node with UUID: {node_id} exists in Ironic.") + node = IronicNodeConfiguration.from_event(interface_update_event) + logger.debug(f"Checking if node UUID {node.uuid} exists in Ironic.") try: - ironic_node = client.get_node(node_id) + ironic_node = client.get_node(node.uuid) except ironicclient.common.apiclient.exceptions.NotFound: - logger.debug(f"Node: {node_id} not found in Ironic.") - ironic_node = None + logger.debug(f"Node: {node.uuid} not found in Ironic, creating") + ironic_node = node.create_node(client) - if not ironic_node: - node_config = event_to_node_configuration(update_data) - response = client.create_node(node_config.create_arguments()) - logger.debug(response) - ironic_node = client.get_node(node_id) + logger.debug("Got Ironic node: %s", json.dumps(ironic_node.to_dict(), indent=2)) STATES_ALLOWING_UPDATES = ["enroll"] if ironic_node.provision_state not in STATES_ALLOWING_UPDATES: logger.info( - f"Device {node_id} is in a {ironic_node.provision_state} " + f"Device {node.uuid} is in a {ironic_node.provision_state} " f"provisioning state, so the updates are not allowed." ) sys.exit(0) @@ -88,6 +73,6 @@ def main(): ] patches = [p for p in patches if p is not None] - response = client.update_node(node_id, patches) + response = client.update_node(node.uuid, patches) logger.info(f"Patching: {patches}") logger.info(f"Updated: {response}") diff --git a/python/understack-workflows/understack_workflows/node_configuration.py b/python/understack-workflows/understack_workflows/node_configuration.py index 6b3d4482..ad0ea87b 100644 --- a/python/understack-workflows/understack_workflows/node_configuration.py +++ b/python/understack-workflows/understack_workflows/node_configuration.py @@ -1,7 +1,12 @@ +from __future__ import annotations + from dataclasses import asdict from dataclasses import dataclass from dataclasses import field +from ironicclient.common.utils import args_array_to_dict +from ironicclient.v1.node import Node + from understack_workflows.redfish_driver_info import RedfishDriverInfo @@ -9,6 +14,21 @@ class IronicNodeConfiguration: """The boot interface for a Node, e.g. “pxe”.""" + uuid: str + """The UUID for the resource.""" + + name: str + """Human-readable identifier for the Node resource. May be undefined. + Certain words are reserved.""" + + driver: str + """The name of the driver used to manage this Node.""" + + driver_info: RedfishDriverInfo = field(default_factory=RedfishDriverInfo) + """All the metadata required by the driver to manage this Node. List of + fields varies between drivers, and can be retrieved from the + /v1/drivers//properties resource.""" + boot_interface: str | None = None conductor_group: str | None = None @@ -21,14 +41,6 @@ class IronicNodeConfiguration: deploy_interface: str | None = None """The deploy interface for a node, e.g. “iscsi”.""" - driver_info: dict | RedfishDriverInfo = field(default_factory=dict) - """All the metadata required by the driver to manage this Node. List of - fields varies between drivers, and can be retrieved from the - /v1/drivers//properties resource.""" - - driver: str = "" - """The name of the driver used to manage this Node.""" - extra: dict = field(default_factory=dict) """A set of one or more arbitrary metadata key and value pairs.""" @@ -38,10 +50,6 @@ class IronicNodeConfiguration: management_interface: str | None = None """Interface for out-of-band node management, e.g. “ipmitool”.""" - name: str = "" - """Human-readable identifier for the Node resource. May be undefined. - Certain words are reserved.""" - network_interface: str | None = None """Which Network Interface provider to use when plumbing the network connections for this Node.""" @@ -68,9 +76,6 @@ class IronicNodeConfiguration: """Interface used for attaching and detaching volumes on this node, e.g. “cinder”.""" - uuid: str = "" - """The UUID for the resource.""" - vendor_interface: str | None = None """Interface for vendor-specific functionality on this node, e.g. “no-vendor”.""" @@ -156,10 +161,37 @@ class IronicNodeConfiguration: "parent_node", ] - def create_arguments(self): - arguments = { - k: v - for k, v in asdict(self).items() - if k not in self.CREATE_EXCLUDED_KEYWORDS - } - return arguments + def create_node(self, client) -> Node: + """Create a node from our config.""" + # this follows the code in the python-ironicclient + field_list = ["uuid", "name", "driver", "driver_info"] + fields = dict( + (k, v) + for (k, v) in asdict(self).items() + if k in field_list and v is not None + ) + fields = args_array_to_dict(fields, "driver_info") + return client.create_node(fields) + + @staticmethod + def from_event(event: dict) -> IronicNodeConfiguration: + # check for events we support + model = event.get("model") + if model not in ["interface"]: + raise ValueError(f"'{model}' events not supported") + + data = event["data"] + + driver = "redfish" + + di = RedfishDriverInfo( + redfish_address=f"https://{data['ip_addresses'][0]['host']}", + redfish_verify_ca=False, + ) + + return IronicNodeConfiguration( + data["device"]["id"], + data["device"]["name"], + driver, + driver_info=di, + ) diff --git a/python/understack-workflows/understack_workflows/redfish_driver_info.py b/python/understack-workflows/understack_workflows/redfish_driver_info.py index 72ef5bf0..4dbf6538 100644 --- a/python/understack-workflows/understack_workflows/redfish_driver_info.py +++ b/python/understack-workflows/understack_workflows/redfish_driver_info.py @@ -3,7 +3,7 @@ @dataclass class RedfishDriverInfo: - redfish_address: str + redfish_address: str | None = None """The URL address to the Redfish controller""" redfish_system_id: str | None = None From e69b33881d4b68d69fcdf5bda872785744f22c56 Mon Sep 17 00:00:00 2001 From: Doug Goldstein Date: Fri, 2 Aug 2024 08:37:56 -0500 Subject: [PATCH 2/6] feat: set correct ironic driver for node If we're working with an iDRAC based interface then we need to use the Ironic idrac driver, otherwise use the redfish based driver. this integrates some of the behaviors of #168. The communication with Nautobot is going to ultimately be the best approach. But the configuration of the Ironic side is better in this code. --- .../tests/test_node_config.py | 2 +- .../main/synchronize_server.py | 32 ++++++++++++------- .../node_configuration.py | 4 ++- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/python/understack-workflows/tests/test_node_config.py b/python/understack-workflows/tests/test_node_config.py index 3cc60a0c..97ef54b8 100644 --- a/python/understack-workflows/tests/test_node_config.py +++ b/python/understack-workflows/tests/test_node_config.py @@ -23,4 +23,4 @@ def test_node_config_from_event_interface_event(interface_event): node = IronicNodeConfiguration.from_event(interface_event) assert node.uuid == interface_event["data"]["device"]["id"] assert node.name == interface_event["data"]["device"]["name"] - assert node.driver == "redfish" + assert node.driver == "idrac" diff --git a/python/understack-workflows/understack_workflows/main/synchronize_server.py b/python/understack-workflows/understack_workflows/main/synchronize_server.py index 21dc3f4d..8e812f4b 100644 --- a/python/understack-workflows/understack_workflows/main/synchronize_server.py +++ b/python/understack-workflows/understack_workflows/main/synchronize_server.py @@ -2,6 +2,7 @@ import sys import ironicclient.common.apiclient.exceptions +from ironicclient.common.utils import args_array_to_patch from understack_workflows.helpers import setup_logger from understack_workflows.ironic.client import IronicClient @@ -60,18 +61,27 @@ def main(): drac_ip = update_data["ip_addresses"][0]["host"] expected_address = f"https://{drac_ip}" - current_address = ironic_node.driver_info.get("redfish_address", None) - current_verify_ca = ironic_node.driver_info.get("redfish_verify_ca", None) - - patches = [ - replace_or_add_field( - "/driver_info/redfish_address", current_address, expected_address - ), - replace_or_add_field( - "/driver_info/redfish_verify_ca", current_verify_ca, False - ), + + updates = [ + f"name={node.name}", + f"driver={node.driver}", + f"driver_info/redfish_address={expected_address}", + "driver_info/redfish_verify_ca=false", + ] + resets = [ + "bios_interface", + "boot_interface", + "inspect_interface", + "management_interface", + "power_interface", + "vendor_interface", + "raid_interface", + "network_interface", ] - patches = [p for p in patches if p is not None] + + # using the behavior from the ironicclient code + patches = args_array_to_patch("add", updates) + patches.extend(args_array_to_patch("remove", resets)) response = client.update_node(node.uuid, patches) logger.info(f"Patching: {patches}") diff --git a/python/understack-workflows/understack_workflows/node_configuration.py b/python/understack-workflows/understack_workflows/node_configuration.py index ad0ea87b..afb91229 100644 --- a/python/understack-workflows/understack_workflows/node_configuration.py +++ b/python/understack-workflows/understack_workflows/node_configuration.py @@ -182,7 +182,9 @@ def from_event(event: dict) -> IronicNodeConfiguration: data = event["data"] - driver = "redfish" + # if we got an iDRAC interface then we'll want to use + # the idrac driver otherwise redfish + driver = "idrac" if data["name"] == "iDRAC" else "redfish" di = RedfishDriverInfo( redfish_address=f"https://{data['ip_addresses'][0]['host']}", From 2b71d5f1e37672daa1ebc8d7e0fad67c8016bd92 Mon Sep 17 00:00:00 2001 From: Doug Goldstein Date: Mon, 5 Aug 2024 10:11:14 -0500 Subject: [PATCH 3/6] fix: update ironic nodes in manage state When our nodes are in the manage state, we need to update their data as well since we'll only be bringing nodes back to the manage state to make changes to them. --- .../understack_workflows/main/synchronize_obm_creds.py | 2 +- .../understack_workflows/main/synchronize_server.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/python/understack-workflows/understack_workflows/main/synchronize_obm_creds.py b/python/understack-workflows/understack_workflows/main/synchronize_obm_creds.py index cbbdf564..9bcd370c 100644 --- a/python/understack-workflows/understack_workflows/main/synchronize_obm_creds.py +++ b/python/understack-workflows/understack_workflows/main/synchronize_obm_creds.py @@ -63,7 +63,7 @@ def main(): ironic_node = None sys.exit(1) - STATES_ALLOWING_UPDATES = ["enroll"] + STATES_ALLOWING_UPDATES = ["enroll", "manage"] if ironic_node.provision_state not in STATES_ALLOWING_UPDATES: logger.info( f"Device {node.uuid} is in a {ironic_node.provision_state} " diff --git a/python/understack-workflows/understack_workflows/main/synchronize_server.py b/python/understack-workflows/understack_workflows/main/synchronize_server.py index 8e812f4b..840d73ab 100644 --- a/python/understack-workflows/understack_workflows/main/synchronize_server.py +++ b/python/understack-workflows/understack_workflows/main/synchronize_server.py @@ -51,7 +51,7 @@ def main(): logger.debug("Got Ironic node: %s", json.dumps(ironic_node.to_dict(), indent=2)) - STATES_ALLOWING_UPDATES = ["enroll"] + STATES_ALLOWING_UPDATES = ["enroll", "manage"] if ironic_node.provision_state not in STATES_ALLOWING_UPDATES: logger.info( f"Device {node.uuid} is in a {ironic_node.provision_state} " From f7f018a96c9b7087efb767908539ce0740cfb7c8 Mon Sep 17 00:00:00 2001 From: Doug Goldstein Date: Mon, 5 Aug 2024 10:46:03 -0500 Subject: [PATCH 4/6] fixup! feat: set correct ironic driver for node --- .../understack_workflows/main/synchronize_server.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/python/understack-workflows/understack_workflows/main/synchronize_server.py b/python/understack-workflows/understack_workflows/main/synchronize_server.py index 840d73ab..d4e6db5d 100644 --- a/python/understack-workflows/understack_workflows/main/synchronize_server.py +++ b/python/understack-workflows/understack_workflows/main/synchronize_server.py @@ -12,15 +12,6 @@ logger = setup_logger(__name__) -def replace_or_add_field(path, current_val, expected_val): - if current_val == expected_val: - return None - if current_val is None: - return {"op": "add", "path": path, "value": expected_val} - else: - return {"op": "replace", "path": path, "value": expected_val} - - def main(): if len(sys.argv) < 1: raise ValueError( From 07b53b8a076c58d421e383967d828054f9cc7193 Mon Sep 17 00:00:00 2001 From: Doug Goldstein Date: Mon, 5 Aug 2024 10:48:27 -0500 Subject: [PATCH 5/6] chore: use ironicclient helper to set user/pass for BMC Set the BMC username and password using the helper function from ironicclient instead of having our own. --- .../main/synchronize_obm_creds.py | 27 +++++-------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/python/understack-workflows/understack_workflows/main/synchronize_obm_creds.py b/python/understack-workflows/understack_workflows/main/synchronize_obm_creds.py index 9bcd370c..7a69907f 100644 --- a/python/understack-workflows/understack_workflows/main/synchronize_obm_creds.py +++ b/python/understack-workflows/understack_workflows/main/synchronize_obm_creds.py @@ -2,6 +2,7 @@ import sys import ironicclient.common.apiclient.exceptions +from ironicclient.common.utils import args_array_to_patch from understack_workflows.helpers import setup_logger from understack_workflows.ironic.client import IronicClient @@ -26,15 +27,6 @@ def credential_secrets(): return [username, password] -def replace_or_add_field(path, current_val, expected_val): - if current_val == expected_val: - return None - if current_val is None: - return {"op": "add", "path": path, "value": expected_val} - else: - return {"op": "replace", "path": path, "value": expected_val} - - def main(): if len(sys.argv) < 1: raise ValueError( @@ -74,18 +66,13 @@ def main(): # Update OBM credentials expected_username, expected_password = credential_secrets() - current_username = ironic_node.driver_info.get("redfish_username", None) - current_password_is_set = ironic_node.driver_info.get("redfish_password", None) - - patches = [ - replace_or_add_field( - "/driver_info/redfish_username", current_username, expected_username - ), - replace_or_add_field( - "/driver_info/redfish_password", current_password_is_set, expected_password - ), + updates = [ + f"driver_info/redfish_username={expected_username}", + f"driver_info/redfish_password={expected_password}", ] - patches = [p for p in patches if p is not None] + + # using the behavior from the ironicclient code + patches = args_array_to_patch("add", updates) response = client.update_node(node.uuid, patches) logger.info(f"Patching: {patches}") From bcfc7b42348e5fc52536b0ae0e1f08e43dd37c96 Mon Sep 17 00:00:00 2001 From: Doug Goldstein Date: Mon, 5 Aug 2024 10:51:02 -0500 Subject: [PATCH 6/6] chore: utilize more shared code in workflows Drop another function local to the obm function and use the shared copy. --- .../main/synchronize_obm_creds.py | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/python/understack-workflows/understack_workflows/main/synchronize_obm_creds.py b/python/understack-workflows/understack_workflows/main/synchronize_obm_creds.py index 7a69907f..4b7eb858 100644 --- a/python/understack-workflows/understack_workflows/main/synchronize_obm_creds.py +++ b/python/understack-workflows/understack_workflows/main/synchronize_obm_creds.py @@ -4,6 +4,7 @@ import ironicclient.common.apiclient.exceptions from ironicclient.common.utils import args_array_to_patch +from understack_workflows.helpers import credential from understack_workflows.helpers import setup_logger from understack_workflows.ironic.client import IronicClient from understack_workflows.ironic.secrets import read_secret @@ -12,21 +13,6 @@ logger = setup_logger(__name__) -def credential_secrets(): - """Reads Kubernetes Secret files with username/password credentials.""" - username = None - password = None - with open("/etc/obm/username") as f: - # strip leading and trailing whitespace - username = f.read().strip() - - with open("/etc/obm/password") as f: - # strip leading and trailing whitespace - password = f.read().strip() - - return [username, password] - - def main(): if len(sys.argv) < 1: raise ValueError( @@ -64,7 +50,8 @@ def main(): sys.exit(0) # Update OBM credentials - expected_username, expected_password = credential_secrets() + expected_username = credential("obm", "username") + expected_password = credential("obm", "password") updates = [ f"driver_info/redfish_username={expected_username}",