diff --git a/argo-workflows/trigger-undersync/workflowtemplates/sync.yaml b/argo-workflows/trigger-undersync/workflowtemplates/undersync-device.yaml similarity index 100% rename from argo-workflows/trigger-undersync/workflowtemplates/sync.yaml rename to argo-workflows/trigger-undersync/workflowtemplates/undersync-device.yaml diff --git a/argo-workflows/trigger-undersync/workflowtemplates/undersync-switch.yaml b/argo-workflows/trigger-undersync/workflowtemplates/undersync-switch.yaml new file mode 100644 index 00000000..4e6fb5dd --- /dev/null +++ b/argo-workflows/trigger-undersync/workflowtemplates/undersync-switch.yaml @@ -0,0 +1,39 @@ +apiVersion: argoproj.io/v1alpha1 +metadata: + name: undersync-switch +kind: WorkflowTemplate +spec: + entrypoint: undersync-switch + serviceAccountName: workflow + templates: + - name: undersync-switch + container: + image: ghcr.io/rackerlabs/understack/ironic-nautobot-client:latest + command: + - undersync-switch + args: + - --switch_uuids + - "{{workflow.parameters.switch_uuids}}" + - --dry-run + - "{{workflow.parameters.dry_run}}" + - --force + - "{{workflow.parameters.force}}" + volumeMounts: + - mountPath: /etc/nb-token/ + name: nb-token + readOnly: true + - mountPath: /etc/undersync/ + name: undersync-token + readOnly: true + inputs: + parameters: + - name: switch_uuids + - name: force + - name: dry_run + volumes: + - name: nb-token + secret: + secretName: nautobot-token + - name: undersync-token + secret: + secretName: undersync-token diff --git a/python/understack-workflows/pyproject.toml b/python/understack-workflows/pyproject.toml index 933349ec..3d382b2d 100644 --- a/python/understack-workflows/pyproject.toml +++ b/python/understack-workflows/pyproject.toml @@ -43,6 +43,7 @@ synchronize-interfaces = "understack_workflows.main.synchronize_interfaces:main" synchronize-obm-creds = "understack_workflows.main.synchronize_obm_creds:main" synchronize-server = "understack_workflows.main.synchronize_server:main" sync-nautobot-interfaces = "understack_workflows.main.sync_nautobot_interfaces:main" +undersync-switch = "understack_workflows.main.undersync_switch:main" [tool.setuptools.packages.find] # avoid packaging up our tests diff --git a/python/understack-workflows/understack_workflows/helpers.py b/python/understack-workflows/understack_workflows/helpers.py index d6f7be1e..2d5f78b5 100644 --- a/python/understack-workflows/understack_workflows/helpers.py +++ b/python/understack-workflows/understack_workflows/helpers.py @@ -2,6 +2,7 @@ import logging import os import pathlib +from functools import partial import sushy @@ -35,6 +36,19 @@ def arg_parser(name): return parser +def boolean_args(val): + normalised = str(val).upper() + if normalised in ["YES", "TRUE", "T", "1"]: + return True + elif normalised in ["NO", "FALSE", "F", "N", "0"]: + return False + else: + raise argparse.ArgumentTypeError("boolean expected") + + +comma_list_args = partial(str.split, sep=",") + + def credential(subpath, item): ref = pathlib.Path("/etc").joinpath(subpath).joinpath(item) with ref.open() as f: diff --git a/python/understack-workflows/understack_workflows/main/undersync_switch.py b/python/understack-workflows/understack_workflows/main/undersync_switch.py new file mode 100644 index 00000000..daabc87a --- /dev/null +++ b/python/understack-workflows/understack_workflows/main/undersync_switch.py @@ -0,0 +1,64 @@ +import argparse +import os +import sys + +from understack_workflows.helpers import boolean_args +from understack_workflows.helpers import comma_list_args +from understack_workflows.helpers import credential +from understack_workflows.helpers import setup_logger +from understack_workflows.undersync.client import Undersync + + +def call_undersync(args, switches): + undersync_token = credential("undersync", "token") + if not undersync_token: + logger.error("Please provide auth token for Undersync.") + sys.exit(1) + undersync = Undersync(undersync_token) + + try: + logger.debug(f"Syncing switches: {switches} {args.dry_run=} {args.force=}") + return undersync.sync_devices(switches, dry_run=args.dry_run, force=args.force) + except Exception as error: + logger.error(error) + sys.exit(2) + + +def argument_parser(): + parser = argparse.ArgumentParser( + prog=os.path.basename(__file__), + description="Trigger undersync run for a set of switches.", + ) + parser.add_argument( + "--switch_uuids", + type=comma_list_args, + required=True, + help="Comma separated list of UUIDs of the switches to Undersync", + ) + parser.add_argument( + "--force", + type=boolean_args, + help="Call Undersync's force endpoint", + required=False, + ) + parser.add_argument( + "--dry-run", + type=boolean_args, + help="Call Undersync's dry-run endpoint", + required=False, + ) + + return parser + + +def main(): + """Requests an Undersync run on a pair of switches.""" + args = argument_parser().parse_args() + + response = call_undersync(args, args.switch_uuids) + logger.info(f"Undersync returned: {response.json()}") + + +logger = setup_logger(__name__) +if __name__ == "__main__": + main() diff --git a/python/understack-workflows/understack_workflows/undersync/__init__.py b/python/understack-workflows/understack_workflows/undersync/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/understack-workflows/understack_workflows/undersync/client.py b/python/understack-workflows/understack_workflows/undersync/client.py new file mode 100644 index 00000000..5561d7ea --- /dev/null +++ b/python/understack-workflows/understack_workflows/undersync/client.py @@ -0,0 +1,48 @@ +from functools import cached_property + +import requests + + +class Undersync: + def __init__( + self, + auth_token: str, + api_url="http://undersync-service.undersync.svc.cluster.local:8080", + ) -> None: + self.token = auth_token + self.api_url = api_url + + def sync_devices(self, switch_uuids: str | list[str], force=False, dry_run=False): + if isinstance(switch_uuids, list): + switch_uuids = ",".join(switch_uuids) + + if dry_run: + return self.dry_run(switch_uuids) + elif force: + return self.force(switch_uuids) + else: + return self.sync(switch_uuids) + + @cached_property + def client(self): + session = requests.Session() + session.headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {self.token}", + } + return session + + def sync(self, uuids: str) -> requests.Response: + response = self.client.post(f"{self.api_url}/v1/devices/{uuids}/sync") + response.raise_for_status() + return response + + def dry_run(self, uuids: str) -> requests.Response: + response = self.client.post(f"{self.api_url}/v1/devices/{uuids}/dry-run") + response.raise_for_status() + return response + + def force(self, uuids: str) -> requests.Response: + response = self.client.post(f"{self.api_url}/v1/devices/{uuids}/force") + response.raise_for_status() + return response