From 91e39eba8286a6998d47e492c6955d0563b536e8 Mon Sep 17 00:00:00 2001 From: Doug Goldstein Date: Wed, 18 Sep 2024 11:38:35 -0500 Subject: [PATCH 1/5] feat(workflows): make clouds.yaml available for auth We'll want to use clouds.yaml for authentication across the board in the future so make this available to these workflows. --- .../workflowtemplates/sync-interfaces-to-ironic.yaml | 8 ++++++++ .../argo-events/workflowtemplates/sync-obm-creds.yaml | 6 ++++++ .../workflowtemplates/sync-server-to-ironic.yaml | 6 ++++++ 3 files changed, 20 insertions(+) diff --git a/workflows/argo-events/workflowtemplates/sync-interfaces-to-ironic.yaml b/workflows/argo-events/workflowtemplates/sync-interfaces-to-ironic.yaml index 78952f6c6..0545723a7 100644 --- a/workflows/argo-events/workflowtemplates/sync-interfaces-to-ironic.yaml +++ b/workflows/argo-events/workflowtemplates/sync-interfaces-to-ironic.yaml @@ -24,6 +24,10 @@ spec: - --device-id - "{{inputs.parameters.device_id}}" - --debug + volumeMounts: + - mountPath: /etc/openstack + name: openstack-svc-acct + readOnly: true envFrom: - secretRef: name: production-ironic-for-argo-creds @@ -38,3 +42,7 @@ spec: secretKeyRef: name: nautobot-token key: token + volumes: + - name: openstack-svc-acct + secret: + secretName: openstack-svc-acct diff --git a/workflows/argo-events/workflowtemplates/sync-obm-creds.yaml b/workflows/argo-events/workflowtemplates/sync-obm-creds.yaml index 88a48d25a..86d0a2dc1 100644 --- a/workflows/argo-events/workflowtemplates/sync-obm-creds.yaml +++ b/workflows/argo-events/workflowtemplates/sync-obm-creds.yaml @@ -41,6 +41,9 @@ spec: args: - "{{workflow.parameters.interface_update_event}}" volumeMounts: + - mountPath: /etc/openstack + name: openstack-svc-acct + readOnly: true - mountPath: /etc/ironic-secrets/ name: ironic-secrets readOnly: true @@ -48,6 +51,9 @@ spec: name: obm-secret readOnly: true volumes: + - name: openstack-svc-acct + secret: + secretName: openstack-svc-acct - name: ironic-secrets secret: secretName: production-ironic-for-argo-creds diff --git a/workflows/argo-events/workflowtemplates/sync-server-to-ironic.yaml b/workflows/argo-events/workflowtemplates/sync-server-to-ironic.yaml index 93e7678a9..51f6f31cf 100644 --- a/workflows/argo-events/workflowtemplates/sync-server-to-ironic.yaml +++ b/workflows/argo-events/workflowtemplates/sync-server-to-ironic.yaml @@ -25,10 +25,16 @@ spec: args: - "{{workflow.parameters.interface_update_event}}" volumeMounts: + - mountPath: /etc/openstack + name: openstack-svc-acct + readOnly: true - mountPath: /etc/ironic-secrets/ name: ironic-secrets readOnly: true volumes: + - name: openstack-svc-acct + secret: + secretName: openstack-svc-acct - name: ironic-secrets secret: secretName: production-ironic-for-argo-creds From 92533beebc91856e8ea2ec396a897ea815db25c7 Mon Sep 17 00:00:00 2001 From: Doug Goldstein Date: Wed, 18 Sep 2024 10:59:12 -0500 Subject: [PATCH 2/5] feat(understack-workflow): use clouds.yaml for auth Provided a wrapper so that we can use clouds.yaml for authentication from all of our workflows. Dropped the API version override from Ironic as it conflicted with how negotiation works. Negiotation sets a minimum and a maximum range based on server and client support. 1.82 is older than the client and the server support so it failed. Previous negotiation when done through the manually configured client resulted in a range of 1.1 to 1.90, which is why we needed to pass 1.82 to get new enough micro API. But this client results in 1.88 being used. --- .../understack_workflows/ironic/client.py | 49 ++++------------ .../openstack/__init__.py | 0 .../understack_workflows/openstack/client.py | 56 +++++++++++++++++++ 3 files changed, 67 insertions(+), 38 deletions(-) create mode 100644 python/understack-workflows/understack_workflows/openstack/__init__.py create mode 100644 python/understack-workflows/understack_workflows/openstack/client.py diff --git a/python/understack-workflows/understack_workflows/ironic/client.py b/python/understack-workflows/understack_workflows/ironic/client.py index 536437f18..accdea522 100644 --- a/python/understack-workflows/understack_workflows/ironic/client.py +++ b/python/understack-workflows/understack_workflows/ironic/client.py @@ -1,6 +1,4 @@ -from ironicclient import client as iclient -from keystoneauth1 import session -from keystoneauth1.identity import v3 +from understack_workflows.openstack.client import get_ironic_client class IronicClient: @@ -13,42 +11,16 @@ def __init__( tenant_name: str, ) -> None: """Initialize our ironicclient wrapper.""" - self.svc_url = svc_url - self.username = username - self.password = password - self.auth_url = auth_url - self.tenant_name = tenant_name self.logged_in = False - self.os_ironic_api_version = "1.82" def login(self): - auth = v3.Password( - auth_url=self.auth_url, - username=self.username, - password=self.password, - project_name=self.tenant_name, - project_domain_name="Default", - user_domain_name="Default", - ) - insecure_ssl = True - sess = session.Session( - auth=auth, verify=(not insecure_ssl), app_name="nautobot" - ) - self.client = iclient.Client( - 1, - endpoint_override=self.svc_url, - session=sess, - insecure=insecure_ssl, - ) - self.client.negotiate_api_version() + self.client = get_ironic_client() self.logged_in = True def create_node(self, node_data: dict): self._ensure_logged_in() - return self.client.node.create( - os_ironic_api_version=self.os_ironic_api_version, **node_data - ) + return self.client.node.create(**node_data) def list_nodes(self): self._ensure_logged_in() @@ -59,35 +31,36 @@ def get_node(self, node_ident: str, fields: list[str] | None = None): self._ensure_logged_in() return self.client.node.get( - node_ident, fields, os_ironic_api_version=self.os_ironic_api_version + node_ident, + fields, ) def update_node(self, node_id, patch): self._ensure_logged_in() return self.client.node.update( - node_id, patch, os_ironic_api_version=self.os_ironic_api_version + node_id, + patch, ) def create_port(self, port_data: dict): self._ensure_logged_in() - return self.client.port.create( - os_ironic_api_version=self.os_ironic_api_version, **port_data - ) + return self.client.port.create(**port_data) def update_port(self, port_id: str, patch: list): self._ensure_logged_in() return self.client.port.update( - port_id, patch, os_ironic_api_version=self.os_ironic_api_version + port_id, + patch, ) def delete_port(self, port_id: str): self._ensure_logged_in() return self.client.port.delete( - port_id, os_ironic_api_version=self.os_ironic_api_version + port_id, ) def list_ports(self, node_id: dict): diff --git a/python/understack-workflows/understack_workflows/openstack/__init__.py b/python/understack-workflows/understack_workflows/openstack/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/understack-workflows/understack_workflows/openstack/client.py b/python/understack-workflows/understack_workflows/openstack/client.py new file mode 100644 index 000000000..b755ab062 --- /dev/null +++ b/python/understack-workflows/understack_workflows/openstack/client.py @@ -0,0 +1,56 @@ +"""helper to setup OpenStack clients.""" + +# attempt to prevent re-export +import os as _os +import sys as _sys +from importlib import metadata as _meta + +from ironicclient.client import Client as IronicClient +from ironicclient.client import get_client as _get_ironic_client +from openstack import config as _os_config +from openstack.connection import Connection + +try: + _pkg_ver = _meta.version(__package__.split(".")[0]) +except Exception: + _pkg_ver = "dev" + +try: + _prog_name = _os.path.basename(_sys.argv[0]) or "local" +except Exception: + _prog_name = "local" + + +def _get_os_cloud_region(cloud=None, region_name=""): + """Returns a keystoneauth1 Session based on our clouds.yaml.""" + return _os_config.get_cloud_region( + load_yaml_config=True, + load_envvars=True, + app_name=_prog_name, + app_version=_pkg_ver, + cloud=cloud, + region_name=region_name, + ) + + +def get_openstack_client(cloud=None, region_name="") -> Connection: + """Returns an OpenStackSDK Connection based on our clouds.yaml.""" + cloud_region = _get_os_cloud_region(cloud, region_name) + + return Connection(config=cloud_region) + + +def get_ironic_client(cloud=None, region_name="") -> IronicClient: + """Returns our Ironic Client wrapper configured from our clouds.yaml.""" + cloud_region = _get_os_cloud_region(cloud, region_name) + client = _get_ironic_client( + "1", session=cloud_region.get_session(), os_ironic_api_version="latest" + ) + client.negotiate_api_version() + return client + + +__all__ = [ + "get_openstack_client", + "get_ironic_client", +] From 2a7bd565279fad6adfc5133123b6c38d4a56f66b Mon Sep 17 00:00:00 2001 From: Doug Goldstein Date: Wed, 18 Sep 2024 12:09:27 -0500 Subject: [PATCH 3/5] feat(undercloud-workflows): drop usage of old auth Remove the old authentication and utilize clouds.yaml exclusively. --- .../understack_workflows/ironic/client.py | 5 ----- .../understack_workflows/main/sync_interfaces.py | 8 +------- .../understack_workflows/main/sync_keystone.py | 7 +++---- .../understack_workflows/main/sync_obm_creds.py | 9 +-------- .../understack_workflows/main/sync_server.py | 13 +------------ 5 files changed, 6 insertions(+), 36 deletions(-) diff --git a/python/understack-workflows/understack_workflows/ironic/client.py b/python/understack-workflows/understack_workflows/ironic/client.py index accdea522..ddcc4abf4 100644 --- a/python/understack-workflows/understack_workflows/ironic/client.py +++ b/python/understack-workflows/understack_workflows/ironic/client.py @@ -4,11 +4,6 @@ class IronicClient: def __init__( self, - svc_url: str, - username: str, - password: str, - auth_url: str, - tenant_name: str, ) -> None: """Initialize our ironicclient wrapper.""" self.logged_in = False diff --git a/python/understack-workflows/understack_workflows/main/sync_interfaces.py b/python/understack-workflows/understack_workflows/main/sync_interfaces.py index e41da9b58..7e5a0c7f2 100644 --- a/python/understack-workflows/understack_workflows/main/sync_interfaces.py +++ b/python/understack-workflows/understack_workflows/main/sync_interfaces.py @@ -85,13 +85,7 @@ def main(): nautobot_ports = get_nautobot_interfaces(nautobot, device_id) # get Ironic Ports - client = IronicClient( - svc_url=os.environ["IRONIC_SVC_URL"], - username=os.environ["IRONIC_USERNAME"], - password=os.environ["IRONIC_PASSWORD"], - auth_url=os.environ["IRONIC_AUTH_URL"], - tenant_name=os.environ["IRONIC_TENANT"], - ) + client = IronicClient() logger.info("Fetching Ironic Ports ...") ironic_ports = client.list_ports(device_id) diff --git a/python/understack-workflows/understack_workflows/main/sync_keystone.py b/python/understack-workflows/understack_workflows/main/sync_keystone.py index f1f3b6bac..8f91d455b 100644 --- a/python/understack-workflows/understack_workflows/main/sync_keystone.py +++ b/python/understack-workflows/understack_workflows/main/sync_keystone.py @@ -3,15 +3,14 @@ import uuid from enum import StrEnum -import openstack -from openstack.connection import Connection - from understack_workflows.domain import DefaultDomain from understack_workflows.domain import domain_id from understack_workflows.helpers import credential from understack_workflows.helpers import parser_nautobot_args from understack_workflows.helpers import setup_logger from understack_workflows.nautobot import Nautobot +from understack_workflows.openstack.client import Connection +from understack_workflows.openstack.client import get_openstack_client logger = setup_logger(__name__, level=logging.INFO) @@ -123,7 +122,7 @@ def do_action( def main(): args = argument_parser().parse_args() - conn = openstack.connect(cloud=args.os_cloud) + conn = get_openstack_client(cloud=args.os_cloud) nb_token = args.nautobot_token or credential("nb-token", "token") nautobot = Nautobot(args.nautobot_url, nb_token, logger=logger) diff --git a/python/understack-workflows/understack_workflows/main/sync_obm_creds.py b/python/understack-workflows/understack_workflows/main/sync_obm_creds.py index 0b77004d2..c296552a3 100644 --- a/python/understack-workflows/understack_workflows/main/sync_obm_creds.py +++ b/python/understack-workflows/understack_workflows/main/sync_obm_creds.py @@ -7,7 +7,6 @@ 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 from understack_workflows.node_configuration import IronicNodeConfiguration logger = setup_logger(__name__) @@ -20,13 +19,7 @@ def main(): ) logger.info("Pushing device new node to Ironic.") - client = IronicClient( - svc_url=read_secret("IRONIC_SVC_URL"), - username=read_secret("IRONIC_USERNAME"), - password=read_secret("IRONIC_PASSWORD"), - auth_url=read_secret("IRONIC_AUTH_URL"), - tenant_name=read_secret("IRONIC_TENANT"), - ) + client = IronicClient() interface_update_event = json.loads(sys.argv[1]) logger.debug(f"Received: {interface_update_event}") diff --git a/python/understack-workflows/understack_workflows/main/sync_server.py b/python/understack-workflows/understack_workflows/main/sync_server.py index 8efeab983..dd85d26a5 100644 --- a/python/understack-workflows/understack_workflows/main/sync_server.py +++ b/python/understack-workflows/understack_workflows/main/sync_server.py @@ -6,7 +6,6 @@ from understack_workflows.helpers import setup_logger from understack_workflows.ironic.client import IronicClient -from understack_workflows.ironic.secrets import read_secret from understack_workflows.node_configuration import IronicNodeConfiguration logger = setup_logger(__name__) @@ -26,16 +25,6 @@ def get_args(): return json.loads(sys.argv[1]) -def get_ironic_client(): - return IronicClient( - svc_url=read_secret("IRONIC_SVC_URL"), - username=read_secret("IRONIC_USERNAME"), - password=read_secret("IRONIC_PASSWORD"), - auth_url=read_secret("IRONIC_AUTH_URL"), - tenant_name=read_secret("IRONIC_TENANT"), - ) - - def get_ironic_node(node, ironic_client): logger.debug(f"Checking if node UUID {node.uuid} exists in Ironic.") @@ -82,7 +71,7 @@ def main(): update_data = interface_update_event["data"] logger.info("Pushing device new node to Ironic.") - ironic_client = get_ironic_client() + ironic_client = IronicClient() node = IronicNodeConfiguration.from_event(interface_update_event) ironic_node = get_ironic_node(node, ironic_client) From dc7e2fc7a8c355c62c41bb04260622914dc58c81 Mon Sep 17 00:00:00 2001 From: Doug Goldstein Date: Wed, 18 Sep 2024 12:11:37 -0500 Subject: [PATCH 4/5] feat(workflows): remove old ironic credentials These aren't necessary anymore so remove them. --- workflows/argo-events/kustomization.yaml | 1 - .../secrets/production-ironic-for-argo-creds.yaml | 12 ------------ .../workflowtemplates/sync-interfaces-to-ironic.yaml | 3 --- .../workflowtemplates/sync-obm-creds.yaml | 6 ------ .../workflowtemplates/sync-server-to-ironic.yaml | 6 ------ 5 files changed, 28 deletions(-) delete mode 100644 workflows/argo-events/secrets/production-ironic-for-argo-creds.yaml diff --git a/workflows/argo-events/kustomization.yaml b/workflows/argo-events/kustomization.yaml index b016af527..f2e396da1 100644 --- a/workflows/argo-events/kustomization.yaml +++ b/workflows/argo-events/kustomization.yaml @@ -9,7 +9,6 @@ resources: - secrets/obm-creds.yaml - secrets/placeholder-obm-creds.yaml - secrets/operate-workflow-sa.token.yaml - - secrets/production-ironic-for-argo-creds.yaml - secrets/nautobot-token.yaml - secrets/placeholder-obm-legacy-passwords.yaml - sensors/ironic-node-update.yaml diff --git a/workflows/argo-events/secrets/production-ironic-for-argo-creds.yaml b/workflows/argo-events/secrets/production-ironic-for-argo-creds.yaml deleted file mode 100644 index df766ed41..000000000 --- a/workflows/argo-events/secrets/production-ironic-for-argo-creds.yaml +++ /dev/null @@ -1,12 +0,0 @@ ---- -apiVersion: v1 -data: - IRONIC_AUTH_URL: aHR0cDovL2tleXN0b25lLWFwaS5vcGVuc3RhY2suc3ZjLmNsdXN0ZXIubG9jYWw6NTAwMA== - IRONIC_PASSWORD: YXJnb3dvcmtmbG93 - IRONIC_SVC_URL: aHR0cDovL2lyb25pYy1hcGkub3BlbnN0YWNrLnN2Yy5jbHVzdGVyLmxvY2FsOjYzODU= - IRONIC_TENANT: dW5kZXJjbG91ZA== - IRONIC_USERNAME: YXJnb3dvcmtmbG93 -kind: Secret -metadata: - name: production-ironic-for-argo-creds - namespace: argo diff --git a/workflows/argo-events/workflowtemplates/sync-interfaces-to-ironic.yaml b/workflows/argo-events/workflowtemplates/sync-interfaces-to-ironic.yaml index 0545723a7..cab08fbbc 100644 --- a/workflows/argo-events/workflowtemplates/sync-interfaces-to-ironic.yaml +++ b/workflows/argo-events/workflowtemplates/sync-interfaces-to-ironic.yaml @@ -28,9 +28,6 @@ spec: - mountPath: /etc/openstack name: openstack-svc-acct readOnly: true - envFrom: - - secretRef: - name: production-ironic-for-argo-creds env: - name: NAUTOBOT_API valueFrom: diff --git a/workflows/argo-events/workflowtemplates/sync-obm-creds.yaml b/workflows/argo-events/workflowtemplates/sync-obm-creds.yaml index 86d0a2dc1..8c9eff2f4 100644 --- a/workflows/argo-events/workflowtemplates/sync-obm-creds.yaml +++ b/workflows/argo-events/workflowtemplates/sync-obm-creds.yaml @@ -44,9 +44,6 @@ spec: - mountPath: /etc/openstack name: openstack-svc-acct readOnly: true - - mountPath: /etc/ironic-secrets/ - name: ironic-secrets - readOnly: true - mountPath: /etc/obm name: obm-secret readOnly: true @@ -54,9 +51,6 @@ spec: - name: openstack-svc-acct secret: secretName: openstack-svc-acct - - name: ironic-secrets - secret: - secretName: production-ironic-for-argo-creds - name: obm-secret secret: secretName: "{{ inputs.parameters.obm }}" diff --git a/workflows/argo-events/workflowtemplates/sync-server-to-ironic.yaml b/workflows/argo-events/workflowtemplates/sync-server-to-ironic.yaml index 51f6f31cf..e23dd3128 100644 --- a/workflows/argo-events/workflowtemplates/sync-server-to-ironic.yaml +++ b/workflows/argo-events/workflowtemplates/sync-server-to-ironic.yaml @@ -28,13 +28,7 @@ spec: - mountPath: /etc/openstack name: openstack-svc-acct readOnly: true - - mountPath: /etc/ironic-secrets/ - name: ironic-secrets - readOnly: true volumes: - name: openstack-svc-acct secret: secretName: openstack-svc-acct - - name: ironic-secrets - secret: - secretName: production-ironic-for-argo-creds From f7c647773f6c839c0ff19568a86d638c46e90ca7 Mon Sep 17 00:00:00 2001 From: Doug Goldstein Date: Wed, 18 Sep 2024 12:41:57 -0500 Subject: [PATCH 5/5] chore: remove unused code --- .../understack_workflows/create_node.py | 27 ------------------- .../understack_workflows/ironic/secrets.py | 20 -------------- 2 files changed, 47 deletions(-) delete mode 100644 python/understack-workflows/understack_workflows/create_node.py delete mode 100644 python/understack-workflows/understack_workflows/ironic/secrets.py diff --git a/python/understack-workflows/understack_workflows/create_node.py b/python/understack-workflows/understack_workflows/create_node.py deleted file mode 100644 index 8dff54e90..000000000 --- a/python/understack-workflows/understack_workflows/create_node.py +++ /dev/null @@ -1,27 +0,0 @@ -import json -import logging -import sys - -from understack_workflows.ironic.client import IronicClient -from understack_workflows.ironic.secrets import read_secret - -logger = logging.getLogger(__name__) - - -if len(sys.argv) < 1: - raise ValueError( - "Please provide node configuration in JSON format as first argument." - ) - -logger.info("Pushing device new node to Ironic.") -client = IronicClient( - svc_url=read_secret("IRONIC_SVC_URL"), - username=read_secret("IRONIC_USERNAME"), - password=read_secret("IRONIC_PASSWORD"), - auth_url=read_secret("IRONIC_AUTH_URL"), - tenant_name=read_secret("IRONIC_TENANT"), -) - -node_config = json.loads(sys.argv[1]) -response = client.create_node(node_config) -logger.debug(response) diff --git a/python/understack-workflows/understack_workflows/ironic/secrets.py b/python/understack-workflows/understack_workflows/ironic/secrets.py deleted file mode 100644 index af57d92e8..000000000 --- a/python/understack-workflows/understack_workflows/ironic/secrets.py +++ /dev/null @@ -1,20 +0,0 @@ -import logging -import os -import re - -logger = logging.getLogger(__name__) - - -def read_secret(secret_name: str) -> str: - """Retrieve value of Kubernetes secret.""" - - def normalized(name): - return re.sub(r"[^A-Za-z0-9-_]", "", name) - - base_path = os.environ.get("SECRETS_BASE_PATH", "/etc/ironic-secrets/") - secret_path = os.path.join(base_path, normalized(secret_name)) - try: - return open(secret_path).read() - except FileNotFoundError: - logger.error(f"Secret {secret_name} is not defined.") - return ""