Skip to content

Commit

Permalink
add trigger-undersync workflow
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
skrobul committed Jul 29, 2024
1 parent 08e3744 commit 591db50
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 5 deletions.
41 changes: 37 additions & 4 deletions argo-workflows/nautobot-update-cf/code/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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")

22 changes: 21 additions & 1 deletion argo-workflows/nautobot-update-cf/code/nautobot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
44 changes: 44 additions & 0 deletions argo-workflows/nautobot-update-cf/code/undersync.py
Original file line number Diff line number Diff line change
@@ -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
54 changes: 54 additions & 0 deletions argo-workflows/nautobot-update-cf/code/with_ifaces.py
Original file line number Diff line number Diff line change
@@ -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()
41 changes: 41 additions & 0 deletions argo-workflows/trigger-undersync/workflowtemplates/sync.yaml
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 591db50

Please sign in to comment.