From 618a1e9c51fd8d87cd3d929d06258834ce13da41 Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Mon, 25 Nov 2024 18:19:47 +0000 Subject: [PATCH 1/8] add undersync token volumes to neutron --- components/neutron/aio-values.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/components/neutron/aio-values.yaml b/components/neutron/aio-values.yaml index 30a779805..80996b09e 100644 --- a/components/neutron/aio-values.yaml +++ b/components/neutron/aio-values.yaml @@ -74,20 +74,32 @@ pod: - mountPath: /etc/nb-token/ name: nb-token readOnly: true + - mountPath: /etc/undersync/ + name: undersync-token + readOnly: true volumes: - name: nb-token secret: secretName: nautobot-token + - name: undersync-token + secret: + secretName: undersync-token neutron_rpc_server: neutron_rpc_server: volumeMounts: - mountPath: /etc/nb-token/ name: nb-token readOnly: true + - mountPath: /etc/undersync/ + name: undersync-token + readOnly: true volumes: - name: nb-token secret: secretName: nautobot-token + - name: undersync-token + secret: + secretName: undersync-token # (nicholas.kuechler) updating the jobs list to remove the 'neutron-rabbit-init' job. dependencies: dynamic: From 1cd3116a8d9cee728a7007288402978373970390 Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Mon, 25 Nov 2024 18:21:29 +0000 Subject: [PATCH 2/8] change neutron config options to support undersync --- .../neutron_understack/config.py | 38 ++++++++----------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/python/neutron-understack/neutron_understack/config.py b/python/neutron-understack/neutron_understack/config.py index 03085c384..f48264c9f 100644 --- a/python/neutron-understack/neutron_understack/config.py +++ b/python/neutron-understack/neutron_understack/config.py @@ -4,30 +4,8 @@ cfg.StrOpt( "provisioning_network", help="provisioning_network ID as configured in ironic.conf", + default="change_me", ), - cfg.StrOpt( - "argo_workflow_sa", - default="workflow", - help="ServiceAccount to submit Workflow as", - ), - cfg.StrOpt( - "argo_api_url", - default="https://argo-server.argo.svc.cluster.local:2746", - help="URL of the Argo Server API", - ), - cfg.StrOpt( - "argo_namespace", - default="argo-events", - help="Namespace to submit the Workflows to", - ), - cfg.IntOpt( - "argo_max_attempts", - default=15, - help="Number of tries to retrieve the Workflow run result. " - "Sleeps 5 seconds between attempts.", - ), - cfg.BoolOpt("argo_dry_run", default=True, help="Call Undersync with dry-run mode"), - cfg.BoolOpt("argo_force", default=False, help="Call Undersync with force mode"), ] mech_understack_opts = [ @@ -43,6 +21,20 @@ "ucvni_group", help="hack", ), + cfg.StrOpt( + "undersync_url", + help="Undersync URL", + ), + cfg.StrOpt( + "undersync_token", + help=( + "Undersync API token. If not provided, " + "the '/etc/undersync/token' will be read instead." + ), + ), + cfg.BoolOpt( + "undersync_dry_run", default=True, help="Call Undersync with dry-run mode" + ), ] From b0ead712e95acfa49ae0b7bc059ea87efdf6509c Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Mon, 25 Nov 2024 21:15:41 +0000 Subject: [PATCH 3/8] add undersync client --- .../neutron_understack/undersync.py | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 python/neutron-understack/neutron_understack/undersync.py diff --git a/python/neutron-understack/neutron_understack/undersync.py b/python/neutron-understack/neutron_understack/undersync.py new file mode 100644 index 000000000..c00d5aab1 --- /dev/null +++ b/python/neutron-understack/neutron_understack/undersync.py @@ -0,0 +1,80 @@ +import pathlib +from functools import cached_property + +import requests +from oslo_log import log +from requests.models import HTTPError + +LOG = log.getLogger(__name__) + + +class UndersyncError(Exception): + pass + + +class Undersync: + def __init__( + self, + auth_token: str | None = None, + api_url: str | None = None, + timeout: int = 90, + ) -> None: + """Simple client for Undersync.""" + self.token = auth_token or self._fetch_undersync_token() + self.url = "http://undersync-service.undersync.svc.cluster.local:8080" + self.api_url = api_url or self.url + self.timeout = timeout + + def _fetch_undersync_token(self) -> str: + file = pathlib.Path("/etc/undersync/token") + with file.open() as f: + return f.read().strip() + + def _log_and_raise_for_status(self, response: requests.Response): + try: + response.raise_for_status() + except HTTPError as error: + LOG.error("Undersync error: %(error)s", {"error": error}) + raise UndersyncError() from error + + def sync_devices( + self, vlan_group_uuids: str | list[str], force=False, dry_run=False + ) -> requests.Response: + if isinstance(vlan_group_uuids, list): + vlan_group_uuids = ",".join(vlan_group_uuids) + + if dry_run: + return self.dry_run(vlan_group_uuids) + elif force: + return self.force(vlan_group_uuids) + else: + return self.sync(vlan_group_uuids) + + @cached_property + def client(self): + session = requests.Session() + session.headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {self.token}", + } + return session + + def _undersync_post(self, action: str, uuids: str) -> requests.Response: + response = self.client.post( + f"{self.api_url}/v1/vlan-group/{uuids}/{action}", timeout=self.timeout + ) + LOG.debug( + "undersync %(action)s resp: %(resp)s", + {"resp": response.json(), "action": action}, + ) + self._log_and_raise_for_status(response) + return response + + def sync(self, uuids: str) -> requests.Response: + return self._undersync_post("sync", uuids) + + def dry_run(self, uuids: str) -> requests.Response: + return self._undersync_post("dry-run", uuids) + + def force(self, uuids: str) -> requests.Response: + return self._undersync_post("force", uuids) From 823166a292f20c746fe3ac05f30b31b4d5628b6d Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Mon, 25 Nov 2024 21:16:14 +0000 Subject: [PATCH 4/8] refactor nautobot client and add additional methods --- .../neutron_understack/nautobot.py | 114 ++++++++++++------ 1 file changed, 77 insertions(+), 37 deletions(-) diff --git a/python/neutron-understack/neutron_understack/nautobot.py b/python/neutron-understack/neutron_understack/nautobot.py index 163c72379..ebb1833a0 100644 --- a/python/neutron-understack/neutron_understack/nautobot.py +++ b/python/neutron-understack/neutron_understack/nautobot.py @@ -1,25 +1,65 @@ +import inspect import pathlib from urllib.parse import urljoin import requests from oslo_log import log +from requests.models import HTTPError LOG = log.getLogger(__name__) +class NautobotError(Exception): + pass + + class Nautobot: + CALLER_FRAME = 1 + def __init__(self, nb_url: str | None = None, nb_token: str | None = None): """Basic Nautobot wrapper because pynautobot doesn't expose plugin APIs.""" self.base_url = nb_url or "http://nautobot-default.nautobot.svc.cluster.local" - self.token = nb_token or self.fetch_nb_token() + self.token = nb_token or self._fetch_nb_token() self.s = requests.Session() self.s.headers.update({"Authorization": f"Token {self.token}"}) - def fetch_nb_token(self): + def _fetch_nb_token(self): file = pathlib.Path("/etc/nb-token/token") with file.open() as f: return f.read().strip() + def _log_and_raise_for_status(self, response): + try: + response.raise_for_status() + except HTTPError as error: + LOG.error("Nautobot error: %(error)s", {"error": error}) + raise NautobotError() from error + + def make_api_request( + self, url: str, method: str, payload: dict | None = None + ) -> dict: + endpoint_url = urljoin(self.base_url, url) + caller_function = inspect.stack()[self.CALLER_FRAME].function + http_method = method.upper() + + LOG.debug( + "%(caller_function)s payload: %(payload)s", + {"payload": payload, "caller_function": caller_function}, + ) + resp = self.s.request(http_method, endpoint_url, timeout=10, json=payload) + + if resp.content: + resp_data = resp.json() + else: + resp_data = {"status_code": resp.status_code} + + LOG.debug( + "%(caller_function)s resp: %(resp)s", + {"resp": resp_data, "caller_function": caller_function}, + ) + self._log_and_raise_for_status(resp) + return resp_data + def ucvni_create( self, network_id: str, @@ -38,52 +78,52 @@ def ucvni_create( payload["ucvni_id"] = segment_id payload["ucvni_type"] = "INFRA" - url = urljoin(self.base_url, "/api/plugins/undercloud-vni/ucvnis/") - LOG.debug("ucvni_create payload: %(payload)s", {"payload": payload}) - resp = self.s.post(url, json=payload, timeout=10) - LOG.debug("ucvni_create resp: %(resp)s", {"resp": resp.json()}) - resp.raise_for_status() + url = "/api/plugins/undercloud-vni/ucvnis/" + resp_data = self.make_api_request(url, "post", payload) + return resp_data def ucvni_delete(self, network_id): - payload = {"id": network_id} + url = f"/api/plugins/undercloud-vni/ucvnis/{network_id}/" + return self.make_api_request(url, "delete") - ucvni_url = f"/api/plugins/undercloud-vni/ucvnis/{network_id}/" - url = urljoin(self.base_url, ucvni_url) - LOG.debug("ucvni_delete payload: %(payload)s", {"payload": payload}) - resp = self.s.delete(url, json=payload, timeout=10) - LOG.debug("ucvni_delete resp: %(resp)s", {"resp": resp.status_code}) - resp.raise_for_status() + def prep_switch_interface( + self, connected_interface_id: str, ucvni_uuid: str + ) -> str: + """Runs a Nautobot Job to update a switch interface for tenant mode. - def detach_port(self, network_id, mac_address): + The nautobot job will assign vlans as required and set the interface + into the correct mode for "normal" tenant operation. + + The vlan group ID is returned. + """ + url = "/api/plugins/undercloud-vni/prep_switch_interface" payload = { - "ucvni_uuid": network_id, - "server_interface_mac": mac_address, + "ucvni_id": str(ucvni_uuid), + "connected_interface_id": str(connected_interface_id), } + resp_data = self.make_api_request(url, "post", payload) - url = urljoin(self.base_url, "/api/plugins/undercloud-vni/detach_port") - LOG.debug("detach_port payload: %(payload)s", {"payload": payload}) - resp = self.s.post(url, json=payload, timeout=10) - resp_data = resp.json() - LOG.debug("detach_port resp: %(resp)s", {"resp": resp_data}) - resp.raise_for_status() return resp_data["vlan_group_id"] - def reset_port_status(self, mac_address): - intf_url = urljoin( - self.base_url, f"/api/dcim/interfaces/?mac_address={mac_address}" - ) - intf_resp = self.s.get(intf_url, timeout=10) - intf_resp_data = intf_resp.json() - - LOG.debug("reset_port interface resp: %(resp)s", {"resp": intf_resp_data}) - intf_resp.raise_for_status() + def configure_port_status(self, interface_uuid: str, status: str) -> dict: + url = f"/api/dcim/interfaces/{interface_uuid}/" + payload = {"status": {"name": status}} + resp_data = self.make_api_request(url, "patch", payload) + return resp_data - conn_intf_url = intf_resp_data["results"][0]["connected_endpoint"]["url"] + def fetch_vlan_group_uuid(self, device_uuid: str) -> str: + url = f"/api/dcim/devices/{device_uuid}/?include=relationships" - payload = {"status": {"name": "Active"}} - resp = self.s.patch(conn_intf_url, json=payload, timeout=10) + resp_data = self.make_api_request(url, "get") + try: + vlan_group_uuid = resp_data["relationships"]["vlan_group_to_devices"][ + "source" + ]["objects"][0]["id"] + except (KeyError, IndexError, TypeError) as error: + LOG.error("vlan_group_uuid_error: %(error)s", {"error": error}) + raise NautobotError() from error LOG.debug( - "reset_port connected interface resp: %(resp)s", {"resp": resp.json()} + "vlan_group_uuid: %(vlan_group_uuid)s", {"vlan_group_uuid": vlan_group_uuid} ) - resp.raise_for_status() + return vlan_group_uuid From 60c927a190b6deb2551fe4daf3bb7472db8a20e1 Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Mon, 25 Nov 2024 21:16:45 +0000 Subject: [PATCH 5/8] adjust neutron mech driver to use undersync client instead of argo --- .../neutron_understack_mech.py | 135 ++++++++---------- 1 file changed, 56 insertions(+), 79 deletions(-) diff --git a/python/neutron-understack/neutron_understack/neutron_understack_mech.py b/python/neutron-understack/neutron_understack/neutron_understack_mech.py index 9d253b287..a71bd3d84 100644 --- a/python/neutron-understack/neutron_understack/neutron_understack_mech.py +++ b/python/neutron-understack/neutron_understack/neutron_understack_mech.py @@ -16,8 +16,8 @@ from oslo_config import cfg from neutron_understack import config -from neutron_understack.argo.workflows import ArgoClient from neutron_understack.nautobot import Nautobot +from neutron_understack.undersync import Undersync LOG = logging.getLogger(__name__) @@ -112,11 +112,7 @@ class UnderstackDriver(MechanismDriver): def initialize(self): conf = cfg.CONF.ml2_understack self.nb = Nautobot(conf.nb_url, conf.nb_token) - self.argo_client = ArgoClient( - logger=LOG, - api_url=cfg.CONF.ml2_type_understack.argo_api_url, - namespace=cfg.CONF.ml2_type_understack.argo_namespace, - ) + self.undersync = Undersync(conf.undersync_token, conf.undersync_url) def create_network_precommit(self, context): log_call("create_network_precommit", context) @@ -214,48 +210,25 @@ def update_port_precommit(self, context): def update_port_postcommit(self, context): log_call("update_port_postcommit", context) - self._move_to_network( - vif_type=context.current["binding:vif_type"], - mac_address=context.current["mac_address"], - device_uuid=context.current["binding:host_id"], - network_id=context.current["network_id"], - argo_client=self.argo_client, - ) - - def delete_port_precommit(self, context): - log_call("delete_port_precommit", context) - - def delete_port_postcommit(self, context): - log_call("delete_port_postcommit", context) - vif_type = context.current["binding:vif_type"] - mac_address = context.current["mac_address"] - network_id = context.current["network_id"] if vif_type != portbindings.VIF_TYPE_OTHER: return - LOG.debug( - f"Detaching network with ID: {network_id} from port " - f"with MAC address {mac_address}" - ) + network_id = context.current["network_id"] + connected_interface_uuid = self.fetch_connected_interface_uuid(context) + nb_vlan_group_id = self.update_nautobot(network_id, connected_interface_uuid) - if network_id == cfg.CONF.ml2_type_understack.provisioning_network: - return self.nb.reset_port_status(mac_address) - - nb_vlan_group_id = self.nb.detach_port(network_id, mac_address) - - result = self.argo_client.submit( - template_name="undersync-switch", - entrypoint="undersync-switch", - parameters={ - "switch_uuids": [str(nb_vlan_group_id)], - "dry_run": cfg.CONF.ml2_type_understack.argo_dry_run, - "force": cfg.CONF.ml2_type_understack.argo_force, - }, - service_account=cfg.CONF.ml2_type_understack.argo_workflow_sa, + self.undersync.sync_devices( + vlan_group_uuids=str(nb_vlan_group_id), + dry_run=cfg.CONF.ml2_understack.undersync_dry_run, ) - LOG.info(f"Undersync workflow submitted: {result}") + + def delete_port_precommit(self, context): + log_call("delete_port_precommit", context) + + def delete_port_postcommit(self, context): + log_call("delete_port_postcommit", context) def bind_port(self, context): log_call("bind_port", context) @@ -300,45 +273,49 @@ def check_segment(self, segment): def check_vlan_transparency(self, context): log_call("check_vlan_transparency", context) - def _move_to_network( - self, - vif_type: str, - mac_address: str, - device_uuid: UUID, - network_id: str, - argo_client: ArgoClient, - ): - """Triggers Argo "trigger-undersync" workflow. + def fetch_connected_interface_uuid(self, context: PortContext) -> str: + """Fetches the connected interface UUID from the port context. - This has the effect of connecting our server to the given networks: either - "provisioning" (for PXE booting) or "tenant" (normal access to customer's - networks). The choice of network is based on the network ID. - - This only happens when vif_type is VIF_TYPE_OTHER. - - argo_client is injected by the caller to make testing easier + :param context: The context of the port. + :return: The connected interface UUID. + """ + connected_interface_uuid = ( + context.current["binding:profile"] + .get("local_link_information")[0] + .get("port_id") + ) + try: + UUID(str(connected_interface_uuid)) + except ValueError: + LOG.debug( + "Local link information port_id is not a valid UUID type" + " port_id: %(connected_interface_uuid)s", + {"connected_interface_uuid": connected_interface_uuid}, + ) + raise + return connected_interface_uuid + + def update_nautobot(self, network_id: str, connected_interface_uuid: str) -> UUID: + """Updates Nautobot with the new network ID and connected interface UUID. + + If the network ID is a provisioning network, sets the interface status to + "Provisioning-Interface" and configures Nautobot for provisioning mode. + If the network ID is a tenant network, sets the interface status to a tenant + status and triggers a Nautobot Job to update the switch interface for tenant + mode. In either case, retrieves and returns the VLAN Group UUID for the + specified network and interface. + :param network_id: The ID of the network. + :param connected_interface_uuid: The UUID of the connected interface. + :return: The VLAN group UUID. """ - if vif_type != portbindings.VIF_TYPE_OTHER: - return - if network_id == cfg.CONF.ml2_type_understack.provisioning_network: - network_name = "provisioning" + port_status = "Provisioning-Interface" + configure_port_status_data = self.nb.configure_port_status( + connected_interface_uuid, port_status + ) + switch_uuid = configure_port_status_data.get("device", {}).get("id") + return UUID(self.nb.fetch_vlan_group_uuid(switch_uuid)) else: - network_name = "tenant" - - LOG.debug(f"Selected {network_name=} for {device_uuid=} {mac_address=}") - - result = argo_client.submit( - template_name="undersync-device", - entrypoint="trigger-undersync", - parameters={ - "interface_mac": mac_address, - "device_uuid": device_uuid, - "network_name": network_name, - "network_id": network_id, - "dry_run": cfg.CONF.ml2_type_understack.argo_dry_run, - "force": cfg.CONF.ml2_type_understack.argo_force, - }, - service_account=cfg.CONF.ml2_type_understack.argo_workflow_sa, - ) - LOG.info(f"Binding workflow submitted: {result}") + return UUID( + self.nb.prep_switch_interface(connected_interface_uuid, network_id) + ) From b08c7c1e6d9b4989f7d2cc983ccc0550bba61f24 Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Mon, 25 Nov 2024 21:17:25 +0000 Subject: [PATCH 6/8] add neutron understack mechanism tests --- .../neutron_update_port_postcommit.json | 50 +++++--- .../tests/test_neutron_understack_mech.py | 118 ++++++++++++++---- 2 files changed, 122 insertions(+), 46 deletions(-) diff --git a/python/neutron-understack/neutron_understack/tests/fixtures/neutron_update_port_postcommit.json b/python/neutron-understack/neutron_understack/tests/fixtures/neutron_update_port_postcommit.json index b824ffcf9..a74e619fd 100644 --- a/python/neutron-understack/neutron_understack/tests/fixtures/neutron_update_port_postcommit.json +++ b/python/neutron-understack/neutron_understack/tests/fixtures/neutron_update_port_postcommit.json @@ -1,35 +1,47 @@ { "admin_state_up": true, "allowed_address_pairs": [], - "binding:host_id": "", - "binding:profile": {}, - "binding:vif_details": {}, - "binding:vif_type": "unbound", - "binding:vnic_type": "normal", - "created_at": "2024-07-24T13:42:24Z", + "binding:host_id": "4c2e4c6e-bdbe-4d93-857f-a6003884e8cc", + "binding:profile": { + "local_link_information": [ + { + "port_id": "03921f8d-b4de-412e-a733-f8eade4c6268", + "switch_id": "11:22:33:44:55:66" + } + ] + }, + "binding:vif_details": { + "bound_drivers": { + "0": "understack" + } + }, + "binding:vif_type": "other", + "binding:vnic_type": "baremetal", + "created_at": "2024-11-13T10:40:50Z", "description": "", - "device_id": "41d18c6a-5548-4ee9-926f-4e3ebf43153f", + "device_id": "e5e28c6a-abea-4974-9f25-748ee149ab38", "device_owner": "compute:nova", "extra_dhcp_opts": [], "fixed_ips": [ { - "ip_address": "192.168.17.17", - "subnet_id": "7f2f436c-fd65-44b0-9265-6b642cecbec5" + "ip_address": "172.16.0.197", + "subnet_id": "304bd384-338a-4365-9394-0c356ec698ed" } ], - "id": "e5d5cd73-ca9a-4b74-9d52-43188d0cdcaa", - "mac_address": "fa:16:3e:35:1c:3d", + "id": "9238973b-57d7-43f3-b425-8114e225913c", + "ip_allocation": "immediate", + "mac_address": "d4:04:e6:4f:87:84", "name": "", - "network_id": "c2702769-5592-4555-8ae6-e670db82c31e", + "network_id": "0c9f8f79-66d3-41c8-99c2-83bc0354e02b", "port_security_enabled": true, - "project_id": "ebc5b22e420d4dfc9e385a63b4583623", - "revision_number": 2, + "project_id": "d3c2c85bdbf24ff5843f323524b63768", + "revision_number": 6, "security_groups": [ - "684dcc25-f419-44a5-b4e7-fdcad4335ecd" + "73d7e451-fa23-4a98-8897-c9ef1b1d531b" ], - "standard_attr_id": 56, - "status": "DOWN", + "standard_attr_id": 508, + "status": "ACTIVE", "tags": [], - "tenant_id": "ebc5b22e420d4dfc9e385a63b4583623", - "updated_at": "2024-07-24T13:42:25Z" + "tenant_id": "d3c2c85bdbf24ff5843f323524b63768", + "updated_at": "2024-11-13T10:40:58Z" } diff --git a/python/neutron-understack/neutron_understack/tests/test_neutron_understack_mech.py b/python/neutron-understack/neutron_understack/tests/test_neutron_understack_mech.py index a88768b7e..965bc507d 100644 --- a/python/neutron-understack/neutron_understack/tests/test_neutron_understack_mech.py +++ b/python/neutron-understack/neutron_understack/tests/test_neutron_understack_mech.py @@ -1,36 +1,100 @@ -from unittest.mock import MagicMock +import json +from unittest.mock import MagicMock, patch import pytest -from neutron_understack.argo.workflows import ArgoClient +from neutron_understack.nautobot import Nautobot from neutron_understack.neutron_understack_mech import UnderstackDriver +from neutron_understack.undersync import Undersync @pytest.fixture -def argo_client() -> ArgoClient: - return MagicMock(spec_set=ArgoClient) - - -def test_move_to_network__provisioning(argo_client, device_id, network_id, mac_address): - driver = UnderstackDriver() - driver._move_to_network( - vif_type="other", - mac_address=mac_address, - device_uuid=str(device_id), - network_id=str(network_id), - argo_client=argo_client, - ) +def current_context() -> dict: + file_path = "neutron_understack/tests/fixtures/neutron_update_port_postcommit.json" + with open(file_path) as context_file: + return json.load(context_file) + + +@pytest.fixture +def context(current_context) -> MagicMock: + return MagicMock(current=current_context) + + +@pytest.fixture +def nautobot_client() -> Nautobot: + return MagicMock(spec_set=Nautobot) + + +@pytest.fixture +def undersync_client() -> Undersync: + return MagicMock(spec_set=Undersync) + + +driver = UnderstackDriver() + + +def test_fetch_connected_interface_uuid(context): + result = driver.fetch_connected_interface_uuid(context) + assert result == "03921f8d-b4de-412e-a733-f8eade4c6268" - argo_client.submit.assert_called_once_with( - template_name="undersync-device", - entrypoint="trigger-undersync", - parameters={ - "interface_mac": mac_address, - "device_uuid": str(device_id), - "network_name": "tenant", - "network_id": str(network_id), - "dry_run": True, - "force": False, - }, - service_account="workflow", + +def test_fail_fetch_connected_interface_uuid(context): + context.current["binding:profile"]["local_link_information"][0]["port_id"] = 11 + with pytest.raises(ValueError): + driver.fetch_connected_interface_uuid(context) + + +def test_update_nautobot_for_tenant_network(nautobot_client): + driver.nb = nautobot_client + attrs = { + "prep_switch_interface.return_value": "304bd384-338a-4365-9394-0c356ec698ed" + } + nautobot_client.configure_mock(**attrs) + driver.update_nautobot("111", "222") + + nautobot_client.prep_switch_interface.assert_called_once_with("222", "111") + + +def test_update_nautobot_for_provisioning_network(nautobot_client): + attrs = { + "configure_port_status.return_value": {"device": {"id": "444"}}, + "fetch_vlan_group_uuid.return_value": "304bd384-338a-4365-9394-0c356ec698ed", + } + nautobot_client.configure_mock(**attrs) + driver.nb = nautobot_client + driver.update_nautobot("change_me", "333") + + nautobot_client.configure_port_status.assert_called_once_with( + "333", "Provisioning-Interface" ) + nautobot_client.fetch_vlan_group_uuid.assert_called_once_with("444") + + +@patch("neutron_understack.neutron_understack_mech.UnderstackDriver.update_nautobot") +@patch( + "neutron_understack.neutron_understack_mech.UnderstackDriver.fetch_connected_interface_uuid" +) +def test_success_update_port_post_commit( + mocked_update_nautobot, + mocked_fetch_connected_interface_uuid, + context, + undersync_client, +): + driver.undersync = undersync_client + driver.update_port_postcommit(context) + + mocked_fetch_connected_interface_uuid.assert_called_once() + mocked_update_nautobot.assert_called_once() + undersync_client.sync_devices.assert_called_once() + + +@patch( + "neutron_understack.neutron_understack_mech.UnderstackDriver.fetch_connected_interface_uuid" +) +def test_wrong_vif_type_update_port_post_commit( + mocked_fetch_connected_interface_uuid, context +): + context.current["binding:vif_type"] = "unbound" + driver.update_port_postcommit(context) + + mocked_fetch_connected_interface_uuid.assert_not_called() From bfb1e4c7c905fbf97de6231416b8584ad6d91ff6 Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Mon, 25 Nov 2024 21:17:54 +0000 Subject: [PATCH 7/8] clean up unused modules --- .../neutron_understack/argo/__init__.py | 0 .../neutron_understack/argo/workflows.py | 95 ------------------- 2 files changed, 95 deletions(-) delete mode 100644 python/neutron-understack/neutron_understack/argo/__init__.py delete mode 100644 python/neutron-understack/neutron_understack/argo/workflows.py diff --git a/python/neutron-understack/neutron_understack/argo/__init__.py b/python/neutron-understack/neutron_understack/argo/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/python/neutron-understack/neutron_understack/argo/workflows.py b/python/neutron-understack/neutron_understack/argo/workflows.py deleted file mode 100644 index 6f5a3c787..000000000 --- a/python/neutron-understack/neutron_understack/argo/workflows.py +++ /dev/null @@ -1,95 +0,0 @@ -import time - -import requests -import urllib3 - -urllib3.disable_warnings() - - -DEFAULT_TOKEN_FILENAME = "/run/secrets/kubernetes.io/serviceaccount/token" # noqa: S105 - - -class ArgoClient: - def __init__( - self, - token: str | None = None, - namespace="default", - api_url="https://argo-server.argo.svc.cluster.local:2746", - logger=None, - ): - """Simple Argo Workflows Client.""" - if token is None: - with open(DEFAULT_TOKEN_FILENAME) as token_file: - token = token_file.read() - self.token = token - self.namespace = namespace - self.api_url = api_url - self.headers = {"Authorization": f"Bearer {self.token}"} - self.logger = logger - - def submit( - self, - template_name: str, - entrypoint: str, - parameters: dict, - service_account="default", - ): - json_body = self.__request_body( - template_name, entrypoint, parameters, service_account - ) - - response = requests.post( - f"{self.api_url}/api/v1/workflows/{self.namespace}/submit", - headers=self.headers, - json=json_body, - verify=False, # noqa: S501 we should revisit this - timeout=30, - ) - response.raise_for_status() - if self.logger: - self.logger.debug(f"Response: {response.json()}") - return response - - def submit_wait(self, *args, **kwargs): - max_attempts = kwargs.pop("max_attempts", 20) - response = self.submit(*args, **kwargs) - workflow_name = response.json()["metadata"]["name"] - result = None - for i in range(1, max_attempts + 1): - if self.logger: - self.logger.debug(f"Workflow: {workflow_name} retry {i}/{max_attempts}") - time.sleep(5) - result = self.check_status(workflow_name) - if result in ["Succeeded", "Failed", "Error"]: - break - return result - - def check_status(self, name: str): - response = requests.get( - f"{self.api_url}/api/v1/workflows/{self.namespace}/{name}", - headers=self.headers, - json={"fields": "status.phase"}, - verify=False, # noqa: S501 we should revisit this - timeout=30, - ) - response.raise_for_status() - return response.json()["status"]["phase"] - - def __request_body( - self, - template_name: str, - entrypoint: str, - parameters: dict, - service_account: str, - ): - return { - "resourceKind": "WorkflowTemplate", - "namespace": self.namespace, - "resourceName": template_name, - "submitOptions": { - "labels": f"workflows.argoproj.io/workflow-template={template_name}", - "parameters": [f"{k}={v}" for k, v in parameters.items()], - "entryPoint": entrypoint, - "serviceAccount": service_account, - }, - } From 72892978efdb205a2e9784cadc902a76a80d0d2b Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Mon, 25 Nov 2024 21:18:11 +0000 Subject: [PATCH 8/8] add S101 ruff lint exception for test files --- python/neutron-understack/pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/python/neutron-understack/pyproject.toml b/python/neutron-understack/pyproject.toml index 6f007b1a3..abb4ce518 100644 --- a/python/neutron-understack/pyproject.toml +++ b/python/neutron-understack/pyproject.toml @@ -73,6 +73,7 @@ convention = "google" [tool.ruff.lint.per-file-ignores] "neutron_understack/tests/*.py" = [ "S311", # allow non-cryptographic secure bits for test data + "S101", ] [tool.poetry.plugins."neutron.ml2.mechanism_drivers"]