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..97ef54b8 --- /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 == "idrac" 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..4b7eb858 100644 --- a/python/understack-workflows/understack_workflows/main/synchronize_obm_creds.py +++ b/python/understack-workflows/understack_workflows/main/synchronize_obm_creds.py @@ -2,7 +2,9 @@ import sys 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 @@ -11,42 +13,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 - 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 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( @@ -64,42 +30,37 @@ 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"] + STATES_ALLOWING_UPDATES = ["enroll", "manage"] 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) # 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) + expected_username = credential("obm", "username") + expected_password = credential("obm", "password") - 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] - response = client.update_node(node_id, patches) + # 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}") 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..d4e6db5d 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 @@ -11,26 +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 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,47 +28,52 @@ 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"] + STATES_ALLOWING_UPDATES = ["enroll", "manage"] 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) 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] - response = client.update_node(node_id, patches) + # 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}") 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..afb91229 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,39 @@ 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"] + + # 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']}", + 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