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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions changelog.d/20260817_124358_undersync_token_removal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
### Deprecations and removals

The `undersync-switch` workflow now authenticates to Undersync with the
OpenStack credentials in the `baremetal-manage` Secret instead of a static
bearer token, matching how the Neutron mechanism driver already calls Undersync.
Nothing in UnderStack reads the `undersync-token` Secret any more, so you can
remove it from your deploy repo.

The workflow also no longer mounts `nautobot-token`, which it never used.
1 change: 0 additions & 1 deletion docs/deploy-guide/components/argo-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ Required or commonly required items:
- `core-creds` Secret: Provide `username` and `password` keys for any shared automation account used by event-driven jobs.
- `bmc-master` Secret: Provide a `key` value when hardware-management workflows need a master credential.
- `bmc-legacy-passwords` Secret: Provide a `passwords` key when workflows still need a flat password bundle.
- `undersync-token` Secret: Provide a `token` key if workflows call the undersync API.
- `deploy-repo-auth` Secret: Provide `ssh-privatekey` and `known_hosts` so workflows can clone or update deployment content.
- `dockerconfigjson-github-com` Secret: Provide `.dockerconfigjson` when workflow images are pulled from a private registry.

Expand Down
73 changes: 73 additions & 0 deletions python/understack-workflows/tests/test_undersync_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from unittest.mock import Mock

import pytest

from understack_workflows.undersync.client import Undersync

API_URL = "http://undersync.example.com"


@pytest.fixture
def session():
session = Mock()
session.post.return_value.status_code = 200
return session


@pytest.fixture
def undersync(session):
return Undersync(session, api_url=API_URL)


@pytest.mark.parametrize(
("method", "action"),
[("sync", "sync"), ("dry_run", "dry-run"), ("force", "force")],
)
def test_posts_to_action_endpoint(undersync, session, method, action):
response = getattr(undersync, method)("a1-1-network")

session.post.assert_called_once_with(
f"{API_URL}/v1/vlan-group/a1-1-network/{action}", timeout=undersync.timeout
)
assert response is session.post.return_value


@pytest.mark.parametrize(
("kwargs", "action"),
[
({}, "sync"),
({"force": True}, "force"),
({"dry_run": True}, "dry-run"),
# dry_run wins so that a preview never pushes to the switches
({"force": True, "dry_run": True}, "dry-run"),
],
)
def test_sync_devices_dispatch(undersync, session, kwargs, action):
undersync.sync_devices("a1-1-network", **kwargs)

session.post.assert_called_once_with(
f"{API_URL}/v1/vlan-group/a1-1-network/{action}", timeout=undersync.timeout
)


def test_physical_network_is_escaped(undersync, session):
undersync.sync("weird/name space")

session.post.assert_called_once_with(
f"{API_URL}/v1/vlan-group/weird%2Fname%20space/sync", timeout=undersync.timeout
)


def test_raises_for_status(undersync, session):
session.post.return_value.raise_for_status.side_effect = RuntimeError("boom")

with pytest.raises(RuntimeError):
undersync.sync("a1-1-network")


def test_each_request_goes_through_the_session(undersync, session):
"""keystoneauth1 handles token refresh, so we must not cache a token."""
undersync.sync("a1-1-network")
undersync.sync("a1-2-network")

assert session.post.call_count == 2
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,13 @@
import sys

from understack_workflows.helpers import boolean_args
from understack_workflows.helpers import credential
from understack_workflows.helpers import setup_logger
from understack_workflows.openstack.client import get_session
from understack_workflows.undersync.client import Undersync


def call_undersync(args):
undersync_token = credential("undersync", "token")
if not undersync_token:
logger.error("Please provide auth token for Undersync.")
sys.exit(1)

undersync = Undersync(undersync_token)
undersync = Undersync(get_session())

try:
logger.debug(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from ironicclient.client import Client as IronicClient
from ironicclient.client import get_client as _get_ironic_client
from keystoneauth1.session import Session
from openstack import config as _os_config
from openstack.connection import Connection

Expand All @@ -33,6 +34,11 @@ def _get_os_cloud_region(cloud=None, region_name=""):
)


def get_session(cloud=None, region_name="") -> Session:
"""Returns a keystoneauth1 Session based on our clouds.yaml."""
return _get_os_cloud_region(cloud, region_name).get_session()


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)
Expand All @@ -56,4 +62,5 @@ def get_ironic_client(cloud=None, region_name="") -> IronicClient: # type: igno
__all__ = [
"get_ironic_client",
"get_openstack_client",
"get_session",
]
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
from functools import cached_property
from urllib.parse import quote

import requests
from keystoneauth1.session import Session


class Undersync:
def __init__(
self,
auth_token: str,
session: Session,
api_url="http://undersync.undersync.svc.cluster.local:8080",
timeout: int = 90,
) -> None:
"""Simple client for Undersync."""
self.token = auth_token
"""Simple client for Undersync.

Authenticates with the OpenStack credentials from the supplied
keystoneauth1 session, which handles token refresh transparently.
"""
self.session = session
self.api_url = api_url
self.timeout = timeout

def sync_devices(self, physical_network: str, force=False, dry_run=False):
if dry_run:
Expand All @@ -22,29 +28,19 @@ def sync_devices(self, physical_network: str, force=False, dry_run=False):
else:
return self.sync(physical_network)

@cached_property
def client(self):
session = requests.Session()
session.headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.token}",
}
return session

def sync(self, physical_network: str) -> requests.Response:
def _post(self, action: str, physical_network: str) -> requests.Response:
physnet = quote(physical_network, safe="")
response = self.client.post(f"{self.api_url}/v1/vlan-group/{physnet}/sync")
response = self.session.post(
f"{self.api_url}/v1/vlan-group/{physnet}/{action}", timeout=self.timeout
)
response.raise_for_status()
return response

def sync(self, physical_network: str) -> requests.Response:
return self._post("sync", physical_network)

def dry_run(self, physical_network: str) -> requests.Response:
physnet = quote(physical_network, safe="")
response = self.client.post(f"{self.api_url}/v1/vlan-group/{physnet}/dry-run")
response.raise_for_status()
return response
return self._post("dry-run", physical_network)

def force(self, physical_network: str) -> requests.Response:
physnet = quote(physical_network, safe="")
response = self.client.post(f"{self.api_url}/v1/vlan-group/{physnet}/force")
response.raise_for_status()
return response
return self._post("force", physical_network)
24 changes: 9 additions & 15 deletions workflows/argo-events/workflowtemplates/undersync-switch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,22 +63,16 @@ spec:
- --force
- "{{inputs.parameters.force}}"
volumeMounts:
- mountPath: /etc/nb-token/
name: nb-token
readOnly: true
- mountPath: /etc/undersync/
name: undersync-token
- mountPath: /etc/openstack
name: baremetal-manage
readOnly: true
env:
- name: NAUTOBOT_URL
valueFrom:
secretKeyRef:
name: nautobot-token
key: url
- name: OS_CLOUD
value: understack
volumes:
- name: nb-token
secret:
secretName: nautobot-token
- name: undersync-token
- name: baremetal-manage
secret:
secretName: undersync-token
secretName: baremetal-manage
items:
- key: clouds.yaml
path: clouds.yaml
Loading