From 591db509c600e0326bec21380c27e4a50724d212 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Mon, 29 Jul 2024 08:46:39 +0200 Subject: [PATCH] add trigger-undersync workflow All of the python code lives in the nautobot-update-cf folder to avoid duplication, but the intention is to cleanup and merge those together in a single python container image down the line. --- .../nautobot-update-cf/code/helpers.py | 41 ++++++++++++-- .../nautobot-update-cf/code/nautobot.py | 22 +++++++- .../nautobot-update-cf/code/undersync.py | 44 +++++++++++++++ .../nautobot-update-cf/code/with_ifaces.py | 54 +++++++++++++++++++ .../workflowtemplates/sync.yaml | 41 ++++++++++++++ 5 files changed, 197 insertions(+), 5 deletions(-) create mode 100644 argo-workflows/nautobot-update-cf/code/undersync.py create mode 100644 argo-workflows/nautobot-update-cf/code/with_ifaces.py create mode 100644 argo-workflows/trigger-undersync/workflowtemplates/sync.yaml diff --git a/argo-workflows/nautobot-update-cf/code/helpers.py b/argo-workflows/nautobot-update-cf/code/helpers.py index 716c09a89..ae0bddc77 100644 --- a/argo-workflows/nautobot-update-cf/code/helpers.py +++ b/argo-workflows/nautobot-update-cf/code/helpers.py @@ -20,10 +20,10 @@ def setup_logger(name): def arg_parser(name): parser = argparse.ArgumentParser( - prog=os.path.basename(name), description="Ironic to Nautobot provisioning state sync" + prog=os.path.basename(name), + description="Ironic to Nautobot provisioning state sync", ) - parser.add_argument("--device_uuid", required=True, - help="Nautobot device UUID") + parser.add_argument("--device_uuid", required=True, help="Nautobot device UUID") parser.add_argument("--field-name", required=True) parser.add_argument("--field-value", required=True) parser.add_argument("--nautobot_url", required=False) @@ -32,6 +32,40 @@ def arg_parser(name): return parser +def undersync_parser(name): + parser = argparse.ArgumentParser( + prog=os.path.basename(name), description="Trigger undersync run for a device" + ) + parser.add_argument("--device_uuid", required=True, help="Nautobot device UUID") + parser.add_argument("--network-name", required=True) + parser.add_argument("--nautobot_url", required=False) + parser.add_argument("--nautobot_token", required=False) + 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 __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") + + def exit_with_error(error): logger.error(error) sys.exit(1) @@ -42,4 +76,3 @@ def credential(subpath, item): return open(f"/etc/{subpath}/{item}", "r").read().strip() except FileNotFoundError: exit_with_error(f"{subpath} {item} not found in mounted files") - diff --git a/argo-workflows/nautobot-update-cf/code/nautobot.py b/argo-workflows/nautobot-update-cf/code/nautobot.py index 715aa3ab0..1f53e4f42 100644 --- a/argo-workflows/nautobot-update-cf/code/nautobot.py +++ b/argo-workflows/nautobot-update-cf/code/nautobot.py @@ -31,9 +31,29 @@ def device_by_id(self, device_id: str) -> NautobotDevice: self.exit_with_error(f"Device {device_id} not found in Nautobot") return device + def device_interfaces(self, device_id: str): + return self.session.dcim.interfaces.filter(device_id=device_id) + def update_cf(self, device_id, field_name: str, field_value: str): device = self.device_by_id(device_id) device.custom_fields[field_name] = field_value response = device.save() - print(f"save result: {response}") + self.logger.info(f"save result: {response}") return response + + def uplink_switches(self, device_id) -> list[str]: + interfaces = self.device_interfaces(device_id) + ids = set() + for iface in interfaces: + endpoint = iface.connected_endpoint + if not endpoint: + continue + endpoint.full_details() + self.logger.debug(f"{iface} connected device {iface.connected_endpoint.device} ") + remote_switch = endpoint.device + if not remote_switch: + continue + + ids.add(remote_switch.id) + + return list(ids) diff --git a/argo-workflows/nautobot-update-cf/code/undersync.py b/argo-workflows/nautobot-update-cf/code/undersync.py new file mode 100644 index 000000000..d0f2e349f --- /dev/null +++ b/argo-workflows/nautobot-update-cf/code/undersync.py @@ -0,0 +1,44 @@ +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_hostnames: list[str], force=False, dry_run=False): + if dry_run: + return [self.dry_run(hostname) for hostname in switch_hostnames] + elif force: + return [self.force(hostname) for hostname in switch_hostnames] + else: + return [self.sync(hostname) for hostname in switch_hostnames] + + @cached_property + def client(self): + session = requests.Session() + session.headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {self.token}", + } + return session + + def sync(self, hostname: str) -> requests.Response: + response = self.client.post(f"{self.api_url}/v1/devices/{hostname}/sync") + response.raise_for_status() + return response + + def dry_run(self, hostname: str) -> requests.Response: + response = self.client.post(f"{self.api_url}/v1/devices/{hostname}/dry-run") + response.raise_for_status() + return response + + def force(self, hostname: str) -> requests.Response: + response = self.client.post(f"{self.api_url}/v1/devices/{hostname}/force") + response.raise_for_status() + return response diff --git a/argo-workflows/nautobot-update-cf/code/with_ifaces.py b/argo-workflows/nautobot-update-cf/code/with_ifaces.py new file mode 100644 index 000000000..18bfe925a --- /dev/null +++ b/argo-workflows/nautobot-update-cf/code/with_ifaces.py @@ -0,0 +1,54 @@ +import sys +from nautobot import Nautobot +from helpers import undersync_parser +from helpers import credential +from helpers import setup_logger +from undersync import Undersync + +logger = setup_logger(__name__) + +def update_nautobot(args) -> list[str]: + default_nb_url = "http://nautobot-default.nautobot.svc.cluster.local" + device_uuid = args.device_uuid + field_name = 'connected_to_network' + field_value = args.network_name + nb_url = args.nautobot_url or default_nb_url + + nb_token = args.nautobot_token or credential("nb-token", "token") + nautobot = Nautobot(nb_url, nb_token, logger=logger) + logger.info(f"Updating Device {device_uuid} and moving it to '{field_value}' network.") + nautobot.update_cf(device_uuid, field_name, field_value) + logger.debug(f"Updated Device.{field_name} to {field_value}") + switches = nautobot.uplink_switches(device_uuid) + logger.info(f"Obtained switch IDs: {switches}") + return switches + +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: + return undersync.sync_devices(switches, dry_run=args.dry_run, force=args.force) + except Exception as error: + logger.error(error) + sys.exit(2) + +def main(): + """ + Updates Nautobot Device's 'connected_to_network' field and follows with + request to Undersync service, requesting sync for all of the + uplink_switches that the device is connected to. + """ + parser = undersync_parser(__file__) + args = parser.parse_args() + + switches = update_nautobot(args) + for response in call_undersync(args, switches): + logger.info(f"Undersync returned: {response.json()}") + + +if __name__ == "__main__": + main() diff --git a/argo-workflows/trigger-undersync/workflowtemplates/sync.yaml b/argo-workflows/trigger-undersync/workflowtemplates/sync.yaml new file mode 100644 index 000000000..1d35f4078 --- /dev/null +++ b/argo-workflows/trigger-undersync/workflowtemplates/sync.yaml @@ -0,0 +1,41 @@ +apiVersion: argoproj.io/v1alpha1 +metadata: + name: undersync-device +kind: WorkflowTemplate +spec: + templates: + - name: trigger-undersync + container: + image: ghcr.io/rackerlabs/understack/nautobot-update-cf:latest + command: + - python + - /app/with_ifaces.py + args: + - --device_uuid + - "{{workflow.parameters.device_uuid}}" + - --network-name + - "{{workflow.parameters.network_name}}" + - --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: device_uuid + - name: network_name + - name: force + - name: dry_run + volumes: + - name: nb-token + secret: + secretName: nautobot-token + - name: undersync-token + secret: + secretName: undersync-token