From 9e0dca6807ae12ba09b4450dc4eb518241c967a1 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Thu, 2 Apr 2026 11:53:27 +0000 Subject: [PATCH 01/12] env-setup: netbox and arista --- .gitignore | 1 + Makefile | 52 +++ lab/dev/3-nodes.clab.yaml | 4 + lab/dev/3-nodes.clab.yaml.annotations.json | 35 ++ lab/dev/netbox/initializers/device-roles.yaml | 3 + lab/dev/netbox/initializers/device-types.yaml | 16 + lab/dev/netbox/initializers/devices.yaml | 36 ++ lab/dev/netbox/initializers/interfaces.yaml | 40 ++ lab/dev/netbox/initializers/ip-addresses.yaml | 50 +++ .../netbox/initializers/manufacturers.yaml | 4 + lab/dev/netbox/initializers/sites.yaml | 2 + lab/dev/netbox/netbox-values.yaml | 30 ++ lab/dev/netbox/old_netbox-ressources.yaml | 23 ++ lab/dev/netbox/publish.py | 341 ++++++++++++++++++ lab/dev/resources/pipelines/cpipe1.yaml | 17 + lab/dev/resources/subscriptions/csub1.yaml | 12 + lab/dev/resources/targets/ceos1.yaml | 9 + lab/dev/resources/targets/profile/ccreds.yaml | 8 + .../resources/targets/profile/cprofile.yaml | 15 + lab/dev/temp | 0 20 files changed, 698 insertions(+) create mode 100644 lab/dev/3-nodes.clab.yaml.annotations.json create mode 100644 lab/dev/netbox/initializers/device-roles.yaml create mode 100644 lab/dev/netbox/initializers/device-types.yaml create mode 100644 lab/dev/netbox/initializers/devices.yaml create mode 100644 lab/dev/netbox/initializers/interfaces.yaml create mode 100644 lab/dev/netbox/initializers/ip-addresses.yaml create mode 100644 lab/dev/netbox/initializers/manufacturers.yaml create mode 100644 lab/dev/netbox/initializers/sites.yaml create mode 100644 lab/dev/netbox/netbox-values.yaml create mode 100644 lab/dev/netbox/old_netbox-ressources.yaml create mode 100755 lab/dev/netbox/publish.py create mode 100644 lab/dev/resources/pipelines/cpipe1.yaml create mode 100644 lab/dev/resources/subscriptions/csub1.yaml create mode 100644 lab/dev/resources/targets/ceos1.yaml create mode 100644 lab/dev/resources/targets/profile/ccreds.yaml create mode 100644 lab/dev/resources/targets/profile/cprofile.yaml create mode 100644 lab/dev/temp diff --git a/.gitignore b/.gitignore index dbf0f8c..3d2a7fd 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ Dockerfile.cross *~ private/ lab/dev/clab-* +lab/dev/netbox/secrets design/ notes/ docs/public diff --git a/Makefile b/Makefile index fa96776..b88fff1 100644 --- a/Makefile +++ b/Makefile @@ -291,3 +291,55 @@ apply-clusters-dev-lab: ## Apply the clusters for the development lab cluster delete-clusters-dev-lab: ## Delete the clusters for the development lab cluster kubectl delete -f lab/dev/resources/clusters + +# Add NetBox instance to Kubernetes +##@ NetBox Image Build +NETBOX_CHART ?= netbox/netbox +NETBOX_RELEASE ?= netbox +NETBOX_NAMESPACE ?= netbox +NETBOX_URL ?= http://localhost:8081 +NETBOX_TOKEN ?= $(shell kubectl get secret netbox-superuser -n netbox -o jsonpath='{.data.api_token}' | base64 -d || true) +NETBOX_VALUES ?= lab/dev/netbox/netbox-values.yaml +NETBOX_PASSWORD ?= +NB_INIT ?= lab/dev/netbox/initializers + + +.PHONY: netbox-install +netbox-install: ## Generate NetBox secrets, patch templates, create namespace, and deploy NetBox via Helm +ifndef NETBOX_PASSWORD + $(error NETBOX_PASSWORD is required. Usage: make netbox-install NETBOX_PASSWORD=yourpassword) +endif + mkdir -p lab/dev/netbox/secrets + @echo "Generating NetBox secrets..." + @PEPPER=$$(openssl rand -hex 32); \ + API_TOKEN=$$(openssl rand -hex 32); \ + echo -e '---\napiVersion: v1\nkind: ConfigMap\nmetadata:\n name: netbox-peppers\n namespace: netbox\ndata:\n peppers.yaml: |-\n API_TOKEN_PEPPERS:\n 1: '\''$$PEPPER'\''\n' > lab/dev/netbox/secrets/netbox_peppers.yaml; \ + echo -e '---\napiVersion: v1\nkind: Secret\nmetadata:\n name: netbox-superuser\n namespace: netbox\ntype: Opaque\nstringData:\n username: "admin"\n email: "admin@example.com"\n password: "$(NETBOX_PASSWORD)"\n api_token: "$$API_TOKEN"\n' > lab/dev/netbox/secrets/netbox_secret.yaml; \ + sed -i "s|\$$PEPPER|$${PEPPER}|g" lab/dev/netbox/secrets/netbox_peppers.yaml; \ + sed -i "s|\$$API_TOKEN|$${API_TOKEN}|g" lab/dev/netbox/secrets/netbox_secret.yaml + kubectl create namespace $(NETBOX_NAMESPACE) || true + kubectl apply -f lab/dev/netbox/secrets/ -n $(NETBOX_NAMESPACE) + helm repo add netbox https://netbox-community.github.io/netbox-helm 2>/dev/null || true + helm repo update + helm upgrade --install $(NETBOX_RELEASE) $(NETBOX_CHART) \ + -n $(NETBOX_NAMESPACE) -f $(NETBOX_VALUES) + kubectl wait --for=condition=Ready pod -l app.kubernetes.io/name=netbox -n $(NETBOX_NAMESPACE) --timeout=600s + @echo "Make sure NetBox is reachable before 'make netbox-sync' by running: kubectl port-forward svc/netbox 8081:80 -n netbox --address='0.0.0.0' &" + +.PHONY: netbox-delete +netbox-delete: ## Uninstall NetBox and delete the namespace + helm uninstall netbox -n netbox || true + kubectl delete namespace netbox || true + +##@ NetBox Sync With Initializer Data +.PHONY: netbox-sync +netbox-sync: ## Publish initializers data into NetBox via REST API + @echo "NetBox URL: $(NETBOX_URL)" + @POD=$$(kubectl -n $(NETBOX_NAMESPACE) get pod -l app.kubernetes.io/name=netbox -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true); \ + if [ -z "$$POD" ]; then echo "Error: no NetBox pod found in namespace $(NETBOX_NAMESPACE). Run make netbox-install first."; exit 1; fi; \ + TOKEN_KEY=$$(kubectl exec -n $(NETBOX_NAMESPACE) $$POD -- python manage.py shell -c "from users.models import Token; print(next((t.key for t in Token.objects.filter(user__username='admin')), ''))" 2>/dev/null | tr -d '\r' | grep -E '^[A-Za-z0-9]+$$' | head -n1); \ + if [ -z "$$TOKEN_KEY" ]; then echo "Error: no admin v2 API token found in NetBox. Create one in NetBox admin and retry."; exit 1; fi; \ + echo "NetBox Token Key: $$TOKEN_KEY"; \ + echo "NetBox Token: $(NETBOX_TOKEN)"; \ + python3 lab/dev/netbox/publish.py $(NETBOX_URL) "nbt_$$TOKEN_KEY.$(NETBOX_TOKEN)" $(NB_INIT) --force + @echo "NetBox sync complete!" diff --git a/lab/dev/3-nodes.clab.yaml b/lab/dev/3-nodes.clab.yaml index 89b9191..d656839 100644 --- a/lab/dev/3-nodes.clab.yaml +++ b/lab/dev/3-nodes.clab.yaml @@ -19,7 +19,11 @@ topology: leaf1: leaf2: + ceos1: + kind: arista_ceos + image: ceos:4.35.2F links: - endpoints: ["spine1:e1-1", "leaf1:e1-49"] - endpoints: ["spine1:e1-2", "leaf2:e1-49"] + - endpoints: ["spine1:e1-3", "ceos1:eth1"] diff --git a/lab/dev/3-nodes.clab.yaml.annotations.json b/lab/dev/3-nodes.clab.yaml.annotations.json new file mode 100644 index 0000000..9188d4c --- /dev/null +++ b/lab/dev/3-nodes.clab.yaml.annotations.json @@ -0,0 +1,35 @@ +{ + "freeTextAnnotations": [], + "freeShapeAnnotations": [], + "groupStyleAnnotations": [], + "networkNodeAnnotations": [], + "nodeAnnotations": [ + { + "id": "spine1", + "interfacePattern": "e1-{n}", + "position": { + "x": 360, + "y": 380 + } + }, + { + "id": "leaf1", + "interfacePattern": "e1-{n}", + "position": { + "x": 480, + "y": 260 + } + }, + { + "id": "leaf2", + "interfacePattern": "e1-{n}", + "position": { + "x": 520, + "y": 400 + } + } + ], + "edgeAnnotations": [], + "aliasEndpointAnnotations": [], + "viewerSettings": {} +} \ No newline at end of file diff --git a/lab/dev/netbox/initializers/device-roles.yaml b/lab/dev/netbox/initializers/device-roles.yaml new file mode 100644 index 0000000..9167dab --- /dev/null +++ b/lab/dev/netbox/initializers/device-roles.yaml @@ -0,0 +1,3 @@ +- name: Router + slug: router + color: ff0000 diff --git a/lab/dev/netbox/initializers/device-types.yaml b/lab/dev/netbox/initializers/device-types.yaml new file mode 100644 index 0000000..a6279ed --- /dev/null +++ b/lab/dev/netbox/initializers/device-types.yaml @@ -0,0 +1,16 @@ +- model: ixr-d2l + slug: arista-ixr-d2l + manufacturer: + name: Arista +- model: ixr-d2l + slug: nokia-ixr-d2l + manufacturer: + name: Nokia +- model: ixr-d2l-leaf + slug: nokia-ixr-d2l-leaf + manufacturer: + name: Nokia +- model: ixr-d3l + slug: nokia-ixr-d3l + manufacturer: + name: Nokia diff --git a/lab/dev/netbox/initializers/devices.yaml b/lab/dev/netbox/initializers/devices.yaml new file mode 100644 index 0000000..17ed036 --- /dev/null +++ b/lab/dev/netbox/initializers/devices.yaml @@ -0,0 +1,36 @@ +- name: ceos1 + role: + slug: router + manufacturer: + name: Arista + device_type: + slug: arista-ixr-d2l + site: + name: Lab +- name: leaf1 + role: + slug: router + manufacturer: + name: Nokia + device_type: + slug: nokia-ixr-d2l + site: + name: Lab +- name: leaf2 + role: + slug: router + manufacturer: + name: Nokia + device_type: + slug: nokia-ixr-d2l-leaf + site: + name: Lab +- name: spine1 + role: + slug: router + manufacturer: + name: Nokia + device_type: + slug: nokia-ixr-d3l + site: + name: Lab diff --git a/lab/dev/netbox/initializers/interfaces.yaml b/lab/dev/netbox/initializers/interfaces.yaml new file mode 100644 index 0000000..05e8d24 --- /dev/null +++ b/lab/dev/netbox/initializers/interfaces.yaml @@ -0,0 +1,40 @@ +- device: + name: spine1 + name: e1-1 + type: 1000base-t +- device: + name: leaf1 + name: e1-49 + type: 1000base-t +- device: + name: spine1 + name: e1-2 + type: 1000base-t +- device: + name: leaf2 + name: e1-49 + type: 1000base-t +- device: + name: spine1 + name: e1-3 + type: 1000base-t +- device: + name: ceos1 + name: eth1 + type: 1000base-t +- device: + name: spine1 + name: mgmt0 + type: 1000base-t +- device: + name: leaf1 + name: mgmt0 + type: 1000base-t +- device: + name: leaf2 + name: mgmt0 + type: 1000base-t +- device: + name: ceos1 + name: mgmt0 + type: 1000base-t diff --git a/lab/dev/netbox/initializers/ip-addresses.yaml b/lab/dev/netbox/initializers/ip-addresses.yaml new file mode 100644 index 0000000..c474fa1 --- /dev/null +++ b/lab/dev/netbox/initializers/ip-addresses.yaml @@ -0,0 +1,50 @@ +- address: 10.0.1.1/32 + assigned_object: + device: + name: leaf1 + name: system0 + status: active +- address: 10.0.1.2/32 + assigned_object: + device: + name: leaf2 + name: system0 + status: active +- address: 10.0.2.1/32 + assigned_object: + device: + name: spine1 + name: system0 + status: active +- address: 172.18.0.4/32 + assigned_object: + device: + name: spine1 + name: mgmt0 + status: active + primary: true + dns_name: clab-3-nodes-spine1 +- address: 172.18.0.5/32 + assigned_object: + device: + name: leaf1 + name: mgmt0 + status: active + primary: true + dns_name: clab-3-nodes-leaf1 +- address: 172.18.0.3/32 + assigned_object: + device: + name: leaf2 + name: mgmt0 + status: active + primary: true + dns_name: clab-3-nodes-leaf2 +- address: 172.18.0.6/32 + assigned_object: + device: + name: ceos1 + name: mgmt0 + status: active + primary: true + dns_name: clab-3-nodes-ceos1 diff --git a/lab/dev/netbox/initializers/manufacturers.yaml b/lab/dev/netbox/initializers/manufacturers.yaml new file mode 100644 index 0000000..68627af --- /dev/null +++ b/lab/dev/netbox/initializers/manufacturers.yaml @@ -0,0 +1,4 @@ +- name: Nokia + slug: nokia +- name: Arista + slug: arista diff --git a/lab/dev/netbox/initializers/sites.yaml b/lab/dev/netbox/initializers/sites.yaml new file mode 100644 index 0000000..bc8ed18 --- /dev/null +++ b/lab/dev/netbox/initializers/sites.yaml @@ -0,0 +1,2 @@ +- name: Lab + slug: lab diff --git a/lab/dev/netbox/netbox-values.yaml b/lab/dev/netbox/netbox-values.yaml new file mode 100644 index 0000000..eb89f14 --- /dev/null +++ b/lab/dev/netbox/netbox-values.yaml @@ -0,0 +1,30 @@ +postgresql: + enabled: true + auth: + username: netbox + password: netbox + database: netbox + +redis: + enabled: true + +superuser: + enabled: true + existingSecret: netbox-superuser + +service: + type: LoadBalancer + +persistence: + enabled: true + size: 1Gi + +extraVolumes: + - name: peppers + configMap: + name: netbox-peppers + +extraVolumeMounts: + - name: peppers + mountPath: /run/config/extra/peppers + readOnly: true \ No newline at end of file diff --git a/lab/dev/netbox/old_netbox-ressources.yaml b/lab/dev/netbox/old_netbox-ressources.yaml new file mode 100644 index 0000000..aa79e2d --- /dev/null +++ b/lab/dev/netbox/old_netbox-ressources.yaml @@ -0,0 +1,23 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: netbox-peppers + namespace: netbox +data: + # please change `openssl rand -hex 32` + peppers.yaml: |- + API_TOKEN_PEPPERS: + 1: '4250726b8f643bd059b7cdc65eb411119d9f1e034a7961c80d3ba04eaf1186de' +--- +apiVersion: v1 +kind: Secret +metadata: + name: netbox-superuser + namespace: netbox +type: Opaque +stringData: + username: "admin" + email: "admin@example.com" + password: "netbox" # please change + api_token: "0123456789abcdef0123456789abcdef01234567" \ No newline at end of file diff --git a/lab/dev/netbox/publish.py b/lab/dev/netbox/publish.py new file mode 100755 index 0000000..0922ab4 --- /dev/null +++ b/lab/dev/netbox/publish.py @@ -0,0 +1,341 @@ +#!/usr/bin/env python3 +import argparse +import json +import os +import sys + +try: + import requests + import yaml +except ImportError as e: + print("Missing dependency:", e, file=sys.stderr) + print("Install with: pip install requests pyyaml", file=sys.stderr) + sys.exit(1) + +ENDPOINTS = { + "sites": "dcim/sites", + "manufacturers": "dcim/manufacturers", + "device-roles": "dcim/device-roles", + "device-types": "dcim/device-types", + "devices": "dcim/devices", + "interfaces": "dcim/interfaces", + # "cables": "dcim/cables", + "ip-addresses": "ipam/ip-addresses", +} + + +def netbox_get(base_url, headers, path, params=None): + url = f"{base_url.rstrip('/')}/api/{path}/" + r = requests.get(url, headers=headers, params=params, timeout=60) + r.raise_for_status() + return r.json() + + +def netbox_find_id(base_url, headers, path, field, value): + if value is None: + return None + params = {field: value} + try: + r = netbox_get(base_url, headers, path, params) + except Exception: + return None + results = r.get("results", []) + if results: + return results[0].get("id") + return None + + +def resolve_relation(base_url, headers, path, value): + if value is None: + return None + if isinstance(value, int): + return value + if isinstance(value, dict): + if "id" in value: + return value["id"] + if "pk" in value: + return value["pk"] + if "name" in value: + return netbox_find_id(base_url, headers, path, "name", value["name"]) + if "slug" in value: + return netbox_find_id(base_url, headers, path, "slug", value["slug"]) + return None + + if isinstance(value, str): + if value.isdigit(): + return int(value) + found = netbox_find_id(base_url, headers, path, "name", value) + if found: + return found + return netbox_find_id(base_url, headers, path, "slug", value) + return None + + +def get_interface_id(base_url, headers, device_ref, iface_name): + if device_ref is None or iface_name is None: + return None + device_id = None + if isinstance(device_ref, int): + device_id = device_ref + else: + device_id = resolve_relation(base_url, headers, "dcim/devices", device_ref) + if not device_id: + return None + params = {"device_id": device_id, "name": iface_name} + r = netbox_get(base_url, headers, "dcim/interfaces", params) + results = r.get("results", []) + if results: + return results[0].get("id") + return None + + +def update_device_primary_ip(base_url, headers, interface_id, ip_id): + r = netbox_get(base_url, headers, f"dcim/interfaces/{interface_id}") + device = r.get("device") + if not device: + return + device_id = device.get("id") + if not device_id: + return + url = f"{base_url.rstrip('/')}/api/dcim/devices/{device_id}/" + data = {"primary_ip4": ip_id} + r2 = requests.patch(url, headers=headers, json=data, timeout=60) + if r2.status_code not in (200, 202): + print(f"Warning: failed to update primary_ip4 for device {device_id}: {r2.status_code} {r2.text}") + + +def resolve_ip_address(base_url, headers, address): + r = netbox_get(base_url, headers, "ipam/ip-addresses", {"address": address}) + results = r.get("results", []) + if results: + return results[0].get("id") + return None + + +def ensure_ip_assigned_to_interface(base_url, headers, ip_id, interface_id): + url = f"{base_url.rstrip('/')}/api/ipam/ip-addresses/{ip_id}/" + r = requests.get(url, headers=headers, timeout=60) + if r.status_code not in (200, 201): + print(f"Warning: failed to get IP address {ip_id}: {r.status_code} {r.text}") + return False + + assigned = r.json().get("assigned_object") + if assigned and assigned.get("object_type") == "dcim.interface" and assigned.get("id") == interface_id: + return True + + r2 = requests.patch(url, headers=headers, json={"assigned_object_type": "dcim.interface", "assigned_object_id": interface_id}, timeout=60) + if r2.status_code not in (200, 202): + print(f"Warning: failed to assign IP {ip_id} to interface {interface_id}: {r2.status_code} {r2.text}") + return False + return True + + +def post_items(base_url, token, dirname, force=False): + if not token: + raise ValueError("NETBOX_TOKEN must be provided") + + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Accept": "application/json", + } + + primary_ip_entries = [] + + cache = { + 'sites': {}, + 'manufacturers': {}, + 'device-roles': {}, + 'device-types': {}, + 'devices': {}, + } + + def resolve_cached_id(path_key, value): + path_mapping = { + 'dcim/sites': 'sites', + 'dcim/manufacturers': 'manufacturers', + 'dcim/device-roles': 'device-roles', + 'dcim/device-types': 'device-types', + 'dcim/devices': 'devices', + 'sites': 'sites', + 'manufacturers': 'manufacturers', + 'device-roles': 'device-roles', + 'device-types': 'device-types', + 'devices': 'devices', + } + cache_key = path_mapping.get(path_key, None) + + if isinstance(value, dict): + if 'id' in value: + return value['id'] + if 'name' in value and cache_key and value['name'] in cache.get(cache_key, {}): + return cache[cache_key][value['name']] + if 'slug' in value and cache_key and value['slug'] in cache.get(cache_key, {}): + return cache[cache_key][value['slug']] + if 'name' in value: + return resolve_relation(base_url, headers, path_key, value['name']) + if 'slug' in value: + return resolve_relation(base_url, headers, path_key, value['slug']) + return None + + if isinstance(value, str): + if cache_key and value in cache.get(cache_key, {}): + return cache[cache_key][value] + return resolve_relation(base_url, headers, path_key, value) + + if isinstance(value, int): + return value + + return None + + for name, path in ENDPOINTS.items(): + file_path = os.path.join(dirname, f"{name}.yaml") + if not os.path.isfile(file_path): + print(f"Skipping {name}: file not found: {file_path}") + continue + + with open(file_path, "r", encoding="utf-8") as f: + payload = yaml.safe_load(f) + + if not payload: + print(f"Skipping {name}: empty payload in {file_path}") + continue + + if isinstance(payload, list): + items = payload + elif isinstance(payload, dict): + if name in payload and isinstance(payload[name], list): + items = payload[name] + elif len(payload) == 1 and isinstance(next(iter(payload.values())), list): + items = next(iter(payload.values())) + else: + items = [payload] + else: + print(f"Skipping {name}: invalid payload type {type(payload)} in {file_path}") + continue + + if not items: + print(f"Skipping {name}: no entries") + continue + + print(f"Posting {len(items)} {name} entries to {path}...") + for i, obj in enumerate(items, 1): + data = obj.copy() if isinstance(obj, dict) else obj + + if path == "dcim/device-types": + manufacturer = data.get("manufacturer") + if manufacturer: + mid = resolve_relation(base_url, headers, "dcim/manufacturers", manufacturer) + if mid: + data["manufacturer"] = mid + + if path == "dcim/devices": + data.pop("manufacturer", None) + + for field, endpoint in ( + ("role", "dcim/device-roles"), + ("device_type", "dcim/device-types"), + ("site", "dcim/sites"), + ): + if field in data: + resolved = resolve_relation(base_url, headers, endpoint, data[field]) + if resolved: + data[field] = resolved + + if path == "dcim/interfaces": + device = data.get("device") + if device is not None: + did = resolve_cached_id("dcim/devices", device) + if did: + data["device"] = did + if force: + existing_id = get_interface_id(base_url, headers, did, data.get("name")) + if existing_id: + del_url = f"{base_url.rstrip('/')}/api/dcim/interfaces/{existing_id}/" + requests.delete(del_url, headers=headers, timeout=60) + print(f" {name}[{i}] deleted existing interface") + else: + print(f" {name}[{i}] warning: cannot resolve device {device}") + + primary_ip4 = False + if path == "ipam/ip-addresses": + assigned_object = data.pop("assigned_object", None) + if assigned_object and isinstance(assigned_object, dict): + assigned_device = assigned_object.get("device") + assigned_name = assigned_object.get("name") + device_id = resolve_cached_id("dcim/devices", assigned_device) + if assigned_device and assigned_name: + interface_id = get_interface_id(base_url, headers, device_id if device_id else assigned_device, assigned_name) + if interface_id: + data["assigned_object_type"] = "dcim.interface" + data["assigned_object_id"] = interface_id + # preserve mapping for later primary update + if "primary" in obj and bool(obj.get("primary")): + primary_ip_entries.append({ + "address": data.get("address"), + "device_id": device_id, + "interface": assigned_name, + }) + if "primary" in data: + primary_ip4 = bool(data.pop("primary")) + + url = f"{base_url.rstrip('/')}/api/{path}/" + r = requests.post(url, headers=headers, json=data, timeout=60) + + if r.status_code in (200, 201): + resp = r.json() + print(f" {name}[{i}] created") + if name == 'sites': + cache['sites'][resp.get('name')] = resp.get('id') + cache['sites'][resp.get('slug')] = resp.get('id') + elif name == 'manufacturers': + cache['manufacturers'][resp.get('name')] = resp.get('id') + cache['manufacturers'][resp.get('slug')] = resp.get('id') + elif name == 'device-roles': + cache['device-roles'][resp.get('name')] = resp.get('id') + cache['device-roles'][resp.get('slug')] = resp.get('id') + elif name == 'device-types': + cache['device-types'][resp.get('slug')] = resp.get('id') + elif name == 'devices': + cache['devices'][resp.get('name')] = resp.get('id') + + elif r.status_code in (409, 400): + reason = "already exists" if r.status_code == 409 else f"invalid ({r.text})" + print(f" {name}[{i}] skipped ({reason})") + if name in ('sites', 'manufacturers', 'device-roles', 'device-types', 'devices'): + key = None + if isinstance(obj, dict): + key = obj.get('name') or obj.get('slug') + if not key and isinstance(obj, str): + key = obj + if key: + existing_id = resolve_relation(base_url, headers, path, key) + if existing_id: + cache_key = name + cache[cache_key][key] = existing_id + + else: + print(f" {name}[{i}] failed: {r.status_code} {r.text}") + r.raise_for_status() + + # apply primary IP updates based on YAML intent (and tolerate existing/duplicated records) + for info in primary_ip_entries: + ip_id = resolve_ip_address(base_url, headers, info["address"]) + interface_ref = info.get("device_id") if info.get("device_id") is not None else info.get("device") + interface_id = get_interface_id(base_url, headers, interface_ref, info["interface"]) + if not ip_id or not interface_id: + print(f"Skipping primary assignment for {info}: missing IP/interface") + continue + if ensure_ip_assigned_to_interface(base_url, headers, ip_id, interface_id): + update_device_primary_ip(base_url, headers, interface_id, ip_id) + + +if __name__ == "__main__": + p = argparse.ArgumentParser(description="Publish NetBox initializers via REST API") + p.add_argument("netbox_url", help="Base URL for NetBox (http://host:80)") + p.add_argument("token", help="NetBox API token") + p.add_argument("init_dir", help="directory with initializers *.yaml") + p.add_argument("--force", action="store_true", help="Force recreate interfaces and IPs even if they exist") + args = p.parse_args() + + post_items(args.netbox_url, args.token, args.init_dir, args.force) diff --git a/lab/dev/resources/pipelines/cpipe1.yaml b/lab/dev/resources/pipelines/cpipe1.yaml new file mode 100644 index 0000000..8bfcb54 --- /dev/null +++ b/lab/dev/resources/pipelines/cpipe1.yaml @@ -0,0 +1,17 @@ +apiVersion: operator.gnmic.dev/v1alpha1 +kind: Pipeline +metadata: + name: cpipe1 +spec: + clusterRef: c1 + enabled: true + targetSelectors: + - matchLabels: + vendor: arista_ceos + subscriptionSelectors: + - matchLabels: + type: interfaces + outputs: + outputSelectors: + - matchLabels: + type: metrics diff --git a/lab/dev/resources/subscriptions/csub1.yaml b/lab/dev/resources/subscriptions/csub1.yaml new file mode 100644 index 0000000..b318d9a --- /dev/null +++ b/lab/dev/resources/subscriptions/csub1.yaml @@ -0,0 +1,12 @@ +apiVersion: operator.gnmic.dev/v1alpha1 +kind: Subscription +metadata: + name: csub1 + labels: + type: interfaces +spec: + paths: + - /interfaces + - /network-instances + mode: STREAM/SAMPLE + sampleInterval: 10s diff --git a/lab/dev/resources/targets/ceos1.yaml b/lab/dev/resources/targets/ceos1.yaml new file mode 100644 index 0000000..350b621 --- /dev/null +++ b/lab/dev/resources/targets/ceos1.yaml @@ -0,0 +1,9 @@ +apiVersion: operator.gnmic.dev/v1alpha1 +kind: Target +metadata: + name: ceos1 + labels: + vendor: arista_ceos +spec: + address: clab-3-nodes-ceos1:6030 + profile: eos diff --git a/lab/dev/resources/targets/profile/ccreds.yaml b/lab/dev/resources/targets/profile/ccreds.yaml new file mode 100644 index 0000000..862d677 --- /dev/null +++ b/lab/dev/resources/targets/profile/ccreds.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Secret +metadata: + name: eos-credentials +type: Opaque +data: + username: YWRtaW4= + password: YWRtaW4= \ No newline at end of file diff --git a/lab/dev/resources/targets/profile/cprofile.yaml b/lab/dev/resources/targets/profile/cprofile.yaml new file mode 100644 index 0000000..0b415fb --- /dev/null +++ b/lab/dev/resources/targets/profile/cprofile.yaml @@ -0,0 +1,15 @@ +apiVersion: operator.gnmic.dev/v1alpha1 +kind: TargetProfile +metadata: + name: eos +spec: + credentialsRef: eos-credentials + tls: null + encoding: JSON_IETF + timeout: 10s + tcpKeepAlive: 10s + retryTimer: 5s + grpcKeepAlive: + time: 10s + timeout: 10s + permitWithoutStream: true \ No newline at end of file diff --git a/lab/dev/temp b/lab/dev/temp new file mode 100644 index 0000000..e69de29 From a2efe7c1b876bf89b1fa047b617ddc86a0a41314 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Thu, 2 Apr 2026 11:58:12 +0000 Subject: [PATCH 02/12] remove unused kubernetes ressources for netbox --- lab/dev/netbox/old_netbox-ressources.yaml | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 lab/dev/netbox/old_netbox-ressources.yaml diff --git a/lab/dev/netbox/old_netbox-ressources.yaml b/lab/dev/netbox/old_netbox-ressources.yaml deleted file mode 100644 index aa79e2d..0000000 --- a/lab/dev/netbox/old_netbox-ressources.yaml +++ /dev/null @@ -1,23 +0,0 @@ ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: netbox-peppers - namespace: netbox -data: - # please change `openssl rand -hex 32` - peppers.yaml: |- - API_TOKEN_PEPPERS: - 1: '4250726b8f643bd059b7cdc65eb411119d9f1e034a7961c80d3ba04eaf1186de' ---- -apiVersion: v1 -kind: Secret -metadata: - name: netbox-superuser - namespace: netbox -type: Opaque -stringData: - username: "admin" - email: "admin@example.com" - password: "netbox" # please change - api_token: "0123456789abcdef0123456789abcdef01234567" \ No newline at end of file From 3f4832a06a164d640b6cd781e7f85bd7bd44da3b Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Wed, 8 Apr 2026 15:00:39 +0000 Subject: [PATCH 03/12] init skeleton for target autodiscover implementation --- internal/controller/targetsource/loaders.go | 3 +++ .../controller/targetsource/loaders/http_pull/loader.go | 2 ++ .../targetsource/loaders/http_pull/loader_test.go | 1 + .../controller/targetsource/loaders/http_push/loader.go | 3 +++ .../targetsource/loaders/http_push/loader_test.go | 1 + internal/controller/targetsource/loaders_test.go | 1 + internal/controller/targetsource/mapper.go | 3 +++ internal/controller/targetsource/mapper_test.go | 1 + internal/controller/targetsource_controller.go | 8 ++++++++ 9 files changed, 23 insertions(+) create mode 100644 internal/controller/targetsource/loaders.go create mode 100644 internal/controller/targetsource/loaders/http_pull/loader.go create mode 100644 internal/controller/targetsource/loaders/http_pull/loader_test.go create mode 100644 internal/controller/targetsource/loaders/http_push/loader.go create mode 100644 internal/controller/targetsource/loaders/http_push/loader_test.go create mode 100644 internal/controller/targetsource/loaders_test.go create mode 100644 internal/controller/targetsource/mapper.go create mode 100644 internal/controller/targetsource/mapper_test.go diff --git a/internal/controller/targetsource/loaders.go b/internal/controller/targetsource/loaders.go new file mode 100644 index 0000000..9641cda --- /dev/null +++ b/internal/controller/targetsource/loaders.go @@ -0,0 +1,3 @@ +package targetsource +// This file defines the loader interface +// targets received from loaders are sent via channel to the controller for reconciliation \ No newline at end of file diff --git a/internal/controller/targetsource/loaders/http_pull/loader.go b/internal/controller/targetsource/loaders/http_pull/loader.go new file mode 100644 index 0000000..97beadc --- /dev/null +++ b/internal/controller/targetsource/loaders/http_pull/loader.go @@ -0,0 +1,2 @@ +package http_pull +// this file implements the logic to load targets from HTTP endpoint \ No newline at end of file diff --git a/internal/controller/targetsource/loaders/http_pull/loader_test.go b/internal/controller/targetsource/loaders/http_pull/loader_test.go new file mode 100644 index 0000000..158a576 --- /dev/null +++ b/internal/controller/targetsource/loaders/http_pull/loader_test.go @@ -0,0 +1 @@ +package http_pull \ No newline at end of file diff --git a/internal/controller/targetsource/loaders/http_push/loader.go b/internal/controller/targetsource/loaders/http_push/loader.go new file mode 100644 index 0000000..132457a --- /dev/null +++ b/internal/controller/targetsource/loaders/http_push/loader.go @@ -0,0 +1,3 @@ +package http_push +// this file implements the logic receive target updates via HTTP push +// REST API defined internal/apiserver \ No newline at end of file diff --git a/internal/controller/targetsource/loaders/http_push/loader_test.go b/internal/controller/targetsource/loaders/http_push/loader_test.go new file mode 100644 index 0000000..c75a5a0 --- /dev/null +++ b/internal/controller/targetsource/loaders/http_push/loader_test.go @@ -0,0 +1 @@ +package http_push \ No newline at end of file diff --git a/internal/controller/targetsource/loaders_test.go b/internal/controller/targetsource/loaders_test.go new file mode 100644 index 0000000..07acd25 --- /dev/null +++ b/internal/controller/targetsource/loaders_test.go @@ -0,0 +1 @@ +package targetsource \ No newline at end of file diff --git a/internal/controller/targetsource/mapper.go b/internal/controller/targetsource/mapper.go new file mode 100644 index 0000000..f21b225 --- /dev/null +++ b/internal/controller/targetsource/mapper.go @@ -0,0 +1,3 @@ +package targetsource +// This file makes diff between existing and new targets +// file decides which targets to create/update/delete diff --git a/internal/controller/targetsource/mapper_test.go b/internal/controller/targetsource/mapper_test.go new file mode 100644 index 0000000..07acd25 --- /dev/null +++ b/internal/controller/targetsource/mapper_test.go @@ -0,0 +1 @@ +package targetsource \ No newline at end of file diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 032b103..f6fa1cd 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -57,6 +57,14 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request // - PodSelector: select Kubernetes pods // - ServiceSelector: select Kubernetes services + // TODO: + // 1. Start go routines for loader of target source + // 2. Retrieve list of targets from go channel + // 3. Fetch existing Targets from Kubernetes API + // 4. Compare and determine which Targets to create/update/delete + // 5. Create/update/delete Target CRs accordingly + // 6. Update TargetSource status with sync results + return ctrl.Result{}, nil } From 6650ab39464178228eb6cf428ef98cc990c4479f Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Wed, 8 Apr 2026 15:03:03 +0000 Subject: [PATCH 04/12] add temporary comment for netbox automation --- Makefile | 2 ++ lab/dev/netbox/readme.txt | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 lab/dev/netbox/readme.txt diff --git a/Makefile b/Makefile index b88fff1..4885a72 100644 --- a/Makefile +++ b/Makefile @@ -293,6 +293,8 @@ delete-clusters-dev-lab: ## Delete the clusters for the development lab cluster # Add NetBox instance to Kubernetes +# Only for development and testing purposes +# is generally vibe coded and will be removed after development ##@ NetBox Image Build NETBOX_CHART ?= netbox/netbox NETBOX_RELEASE ?= netbox diff --git a/lab/dev/netbox/readme.txt b/lab/dev/netbox/readme.txt new file mode 100644 index 0000000..813b5f9 --- /dev/null +++ b/lab/dev/netbox/readme.txt @@ -0,0 +1,2 @@ +# Only for development and testing purposes +# is generally vibe coded and will be removed after development \ No newline at end of file From 6d8ec08a1b1fdb199eb44baae772e33d95dac2bd Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Wed, 8 Apr 2026 15:04:30 +0000 Subject: [PATCH 05/12] update temporary comment --- lab/dev/netbox/readme.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lab/dev/netbox/readme.txt b/lab/dev/netbox/readme.txt index 813b5f9..c0edbe1 100644 --- a/lab/dev/netbox/readme.txt +++ b/lab/dev/netbox/readme.txt @@ -1,2 +1,3 @@ -# Only for development and testing purposes +# All files within operator/lab/dev/netbox are +# only for development and testing purposes # is generally vibe coded and will be removed after development \ No newline at end of file From c8c65bfa18fca8acd78c62eb7f6098795330e979 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Thu, 9 Apr 2026 09:44:02 +0000 Subject: [PATCH 06/12] files linted --- internal/controller/targetsource/loaders.go | 3 ++- internal/controller/targetsource/loaders/http_pull/loader.go | 3 ++- .../controller/targetsource/loaders/http_pull/loader_test.go | 2 +- internal/controller/targetsource/loaders/http_push/loader.go | 3 ++- .../controller/targetsource/loaders/http_push/loader_test.go | 2 +- internal/controller/targetsource/loaders_test.go | 2 +- internal/controller/targetsource/mapper.go | 1 + internal/controller/targetsource/mapper_test.go | 2 +- internal/controller/targetsource_controller.go | 2 +- 9 files changed, 12 insertions(+), 8 deletions(-) diff --git a/internal/controller/targetsource/loaders.go b/internal/controller/targetsource/loaders.go index 9641cda..817194d 100644 --- a/internal/controller/targetsource/loaders.go +++ b/internal/controller/targetsource/loaders.go @@ -1,3 +1,4 @@ package targetsource + // This file defines the loader interface -// targets received from loaders are sent via channel to the controller for reconciliation \ No newline at end of file +// targets received from loaders are sent via channel to the controller for reconciliation diff --git a/internal/controller/targetsource/loaders/http_pull/loader.go b/internal/controller/targetsource/loaders/http_pull/loader.go index 97beadc..6185fe6 100644 --- a/internal/controller/targetsource/loaders/http_pull/loader.go +++ b/internal/controller/targetsource/loaders/http_pull/loader.go @@ -1,2 +1,3 @@ package http_pull -// this file implements the logic to load targets from HTTP endpoint \ No newline at end of file + +// this file implements the logic to load targets from HTTP endpoint diff --git a/internal/controller/targetsource/loaders/http_pull/loader_test.go b/internal/controller/targetsource/loaders/http_pull/loader_test.go index 158a576..d606d4d 100644 --- a/internal/controller/targetsource/loaders/http_pull/loader_test.go +++ b/internal/controller/targetsource/loaders/http_pull/loader_test.go @@ -1 +1 @@ -package http_pull \ No newline at end of file +package http_pull diff --git a/internal/controller/targetsource/loaders/http_push/loader.go b/internal/controller/targetsource/loaders/http_push/loader.go index 132457a..95dc1e9 100644 --- a/internal/controller/targetsource/loaders/http_push/loader.go +++ b/internal/controller/targetsource/loaders/http_push/loader.go @@ -1,3 +1,4 @@ package http_push + // this file implements the logic receive target updates via HTTP push -// REST API defined internal/apiserver \ No newline at end of file +// REST API defined internal/apiserver diff --git a/internal/controller/targetsource/loaders/http_push/loader_test.go b/internal/controller/targetsource/loaders/http_push/loader_test.go index c75a5a0..bb7d848 100644 --- a/internal/controller/targetsource/loaders/http_push/loader_test.go +++ b/internal/controller/targetsource/loaders/http_push/loader_test.go @@ -1 +1 @@ -package http_push \ No newline at end of file +package http_push diff --git a/internal/controller/targetsource/loaders_test.go b/internal/controller/targetsource/loaders_test.go index 07acd25..603b690 100644 --- a/internal/controller/targetsource/loaders_test.go +++ b/internal/controller/targetsource/loaders_test.go @@ -1 +1 @@ -package targetsource \ No newline at end of file +package targetsource diff --git a/internal/controller/targetsource/mapper.go b/internal/controller/targetsource/mapper.go index f21b225..fded27d 100644 --- a/internal/controller/targetsource/mapper.go +++ b/internal/controller/targetsource/mapper.go @@ -1,3 +1,4 @@ package targetsource + // This file makes diff between existing and new targets // file decides which targets to create/update/delete diff --git a/internal/controller/targetsource/mapper_test.go b/internal/controller/targetsource/mapper_test.go index 07acd25..603b690 100644 --- a/internal/controller/targetsource/mapper_test.go +++ b/internal/controller/targetsource/mapper_test.go @@ -1 +1 @@ -package targetsource \ No newline at end of file +package targetsource diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index f6fa1cd..06e07ab 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -57,7 +57,7 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request // - PodSelector: select Kubernetes pods // - ServiceSelector: select Kubernetes services - // TODO: + // TODO: // 1. Start go routines for loader of target source // 2. Retrieve list of targets from go channel // 3. Fetch existing Targets from Kubernetes API From 29857ff7d63f59d6a3c97321d7b0ce25d4327522 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 10 Apr 2026 08:36:34 +0000 Subject: [PATCH 07/12] moved netbox to its own makefile --- Makefile | 55 +------------------------------------------------------ netbox.mk | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 54 deletions(-) create mode 100644 netbox.mk diff --git a/Makefile b/Makefile index 5f42fc5..e988a43 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,5 @@ include test.mk +include netbox.mk # Image URL to use all building/pushing image targets IMG ?= controller:latest @@ -310,57 +311,3 @@ run-integration-tests: docker-build undeploy-test-cluster deploy-test-cluster in kubectl get svc --selector=app.kubernetes.io/managed-by=gnmic-operator -o wide kubectl get pods --selector=app.kubernetes.io/managed-by=gnmic-operator -o wide kubectl logs -n gnmic-system deploy/gnmic-controller-manager - -# Add NetBox instance to Kubernetes -# Only for development and testing purposes -# is generally vibe coded and will be removed after development -##@ NetBox Image Build -NETBOX_CHART ?= netbox/netbox -NETBOX_RELEASE ?= netbox -NETBOX_NAMESPACE ?= netbox -NETBOX_URL ?= http://localhost:8081 -NETBOX_TOKEN ?= $(shell kubectl get secret netbox-superuser -n netbox -o jsonpath='{.data.api_token}' | base64 -d || true) -NETBOX_VALUES ?= lab/dev/netbox/netbox-values.yaml -NETBOX_PASSWORD ?= -NB_INIT ?= lab/dev/netbox/initializers - - -.PHONY: netbox-install -netbox-install: ## Generate NetBox secrets, patch templates, create namespace, and deploy NetBox via Helm -ifndef NETBOX_PASSWORD - $(error NETBOX_PASSWORD is required. Usage: make netbox-install NETBOX_PASSWORD=yourpassword) -endif - mkdir -p lab/dev/netbox/secrets - @echo "Generating NetBox secrets..." - @PEPPER=$$(openssl rand -hex 32); \ - API_TOKEN=$$(openssl rand -hex 32); \ - echo -e '---\napiVersion: v1\nkind: ConfigMap\nmetadata:\n name: netbox-peppers\n namespace: netbox\ndata:\n peppers.yaml: |-\n API_TOKEN_PEPPERS:\n 1: '\''$$PEPPER'\''\n' > lab/dev/netbox/secrets/netbox_peppers.yaml; \ - echo -e '---\napiVersion: v1\nkind: Secret\nmetadata:\n name: netbox-superuser\n namespace: netbox\ntype: Opaque\nstringData:\n username: "admin"\n email: "admin@example.com"\n password: "$(NETBOX_PASSWORD)"\n api_token: "$$API_TOKEN"\n' > lab/dev/netbox/secrets/netbox_secret.yaml; \ - sed -i "s|\$$PEPPER|$${PEPPER}|g" lab/dev/netbox/secrets/netbox_peppers.yaml; \ - sed -i "s|\$$API_TOKEN|$${API_TOKEN}|g" lab/dev/netbox/secrets/netbox_secret.yaml - kubectl create namespace $(NETBOX_NAMESPACE) || true - kubectl apply -f lab/dev/netbox/secrets/ -n $(NETBOX_NAMESPACE) - helm repo add netbox https://netbox-community.github.io/netbox-helm 2>/dev/null || true - helm repo update - helm upgrade --install $(NETBOX_RELEASE) $(NETBOX_CHART) \ - -n $(NETBOX_NAMESPACE) -f $(NETBOX_VALUES) - kubectl wait --for=condition=Ready pod -l app.kubernetes.io/name=netbox -n $(NETBOX_NAMESPACE) --timeout=600s - @echo "Make sure NetBox is reachable before 'make netbox-sync' by running: kubectl port-forward svc/netbox 8081:80 -n netbox --address='0.0.0.0' &" - -.PHONY: netbox-delete -netbox-delete: ## Uninstall NetBox and delete the namespace - helm uninstall netbox -n netbox || true - kubectl delete namespace netbox || true - -##@ NetBox Sync With Initializer Data -.PHONY: netbox-sync -netbox-sync: ## Publish initializers data into NetBox via REST API - @echo "NetBox URL: $(NETBOX_URL)" - @POD=$$(kubectl -n $(NETBOX_NAMESPACE) get pod -l app.kubernetes.io/name=netbox -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true); \ - if [ -z "$$POD" ]; then echo "Error: no NetBox pod found in namespace $(NETBOX_NAMESPACE). Run make netbox-install first."; exit 1; fi; \ - TOKEN_KEY=$$(kubectl exec -n $(NETBOX_NAMESPACE) $$POD -- python manage.py shell -c "from users.models import Token; print(next((t.key for t in Token.objects.filter(user__username='admin')), ''))" 2>/dev/null | tr -d '\r' | grep -E '^[A-Za-z0-9]+$$' | head -n1); \ - if [ -z "$$TOKEN_KEY" ]; then echo "Error: no admin v2 API token found in NetBox. Create one in NetBox admin and retry."; exit 1; fi; \ - echo "NetBox Token Key: $$TOKEN_KEY"; \ - echo "NetBox Token: $(NETBOX_TOKEN)"; \ - python3 lab/dev/netbox/publish.py $(NETBOX_URL) "nbt_$$TOKEN_KEY.$(NETBOX_TOKEN)" $(NB_INIT) --force - @echo "NetBox sync complete!" diff --git a/netbox.mk b/netbox.mk new file mode 100644 index 0000000..b48cc45 --- /dev/null +++ b/netbox.mk @@ -0,0 +1,53 @@ + +# Add NetBox instance to Kubernetes +# Only for development and testing purposes +# is generally vibe coded and will be removed after development +##@ NetBox +NETBOX_CHART ?= netbox/netbox +NETBOX_RELEASE ?= netbox +NETBOX_NAMESPACE ?= netbox +NETBOX_URL ?= http://localhost:8081 +NETBOX_TOKEN ?= $(shell kubectl get secret netbox-superuser -n netbox -o jsonpath='{.data.api_token}' | base64 -d || true) +NETBOX_VALUES ?= lab/dev/netbox/netbox-values.yaml +NETBOX_PASSWORD ?= +NB_INIT ?= lab/dev/netbox/initializers + + +.PHONY: netbox-install +netbox-install: ## Generate NetBox secrets, patch templates, create namespace, and deploy NetBox via Helm +ifndef NETBOX_PASSWORD + $(error NETBOX_PASSWORD is required. Usage: make netbox-install NETBOX_PASSWORD=yourpassword) +endif + mkdir -p lab/dev/netbox/secrets + @echo "Generating NetBox secrets..." + @PEPPER=$$(openssl rand -hex 32); \ + API_TOKEN=$$(openssl rand -hex 32); \ + echo -e '---\napiVersion: v1\nkind: ConfigMap\nmetadata:\n name: netbox-peppers\n namespace: netbox\ndata:\n peppers.yaml: |-\n API_TOKEN_PEPPERS:\n 1: '\''$$PEPPER'\''\n' > lab/dev/netbox/secrets/netbox_peppers.yaml; \ + echo -e '---\napiVersion: v1\nkind: Secret\nmetadata:\n name: netbox-superuser\n namespace: netbox\ntype: Opaque\nstringData:\n username: "admin"\n email: "admin@example.com"\n password: "$(NETBOX_PASSWORD)"\n api_token: "$$API_TOKEN"\n' > lab/dev/netbox/secrets/netbox_secret.yaml; \ + sed -i "s|\$$PEPPER|$${PEPPER}|g" lab/dev/netbox/secrets/netbox_peppers.yaml; \ + sed -i "s|\$$API_TOKEN|$${API_TOKEN}|g" lab/dev/netbox/secrets/netbox_secret.yaml + kubectl create namespace $(NETBOX_NAMESPACE) || true + kubectl apply -f lab/dev/netbox/secrets/ -n $(NETBOX_NAMESPACE) + helm repo add netbox https://netbox-community.github.io/netbox-helm 2>/dev/null || true + helm repo update + helm upgrade --install $(NETBOX_RELEASE) $(NETBOX_CHART) \ + -n $(NETBOX_NAMESPACE) -f $(NETBOX_VALUES) + kubectl wait --for=condition=Ready pod -l app.kubernetes.io/name=netbox -n $(NETBOX_NAMESPACE) --timeout=600s + @echo "Make sure NetBox is reachable before 'make netbox-sync' by running: kubectl port-forward svc/netbox 8081:80 -n netbox --address='0.0.0.0' &" + +.PHONY: netbox-delete +netbox-delete: ## Uninstall NetBox and delete the namespace + helm uninstall netbox -n netbox || true + kubectl delete namespace netbox || true + +.PHONY: netbox-sync +netbox-sync: ## Publish initializers data into NetBox via REST API + @echo "NetBox URL: $(NETBOX_URL)" + @POD=$$(kubectl -n $(NETBOX_NAMESPACE) get pod -l app.kubernetes.io/name=netbox -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true); \ + if [ -z "$$POD" ]; then echo "Error: no NetBox pod found in namespace $(NETBOX_NAMESPACE). Run make netbox-install first."; exit 1; fi; \ + TOKEN_KEY=$$(kubectl exec -n $(NETBOX_NAMESPACE) $$POD -- python manage.py shell -c "from users.models import Token; print(next((t.key for t in Token.objects.filter(user__username='admin')), ''))" 2>/dev/null | tr -d '\r' | grep -E '^[A-Za-z0-9]+$$' | head -n1); \ + if [ -z "$$TOKEN_KEY" ]; then echo "Error: no admin v2 API token found in NetBox. Create one in NetBox admin and retry."; exit 1; fi; \ + echo "NetBox Token Key: $$TOKEN_KEY"; \ + echo "NetBox Token: $(NETBOX_TOKEN)"; \ + python3 lab/dev/netbox/publish.py $(NETBOX_URL) "nbt_$$TOKEN_KEY.$(NETBOX_TOKEN)" $(NB_INIT) --force + @echo "NetBox sync complete!" From 37febb1a5a3971a96507fc31946c39520de180f1 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 10 Apr 2026 12:31:53 +0000 Subject: [PATCH 08/12] makefile make netbox install in different kind clusters viable --- netbox.mk | 69 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 49 insertions(+), 20 deletions(-) diff --git a/netbox.mk b/netbox.mk index b48cc45..555c2f1 100644 --- a/netbox.mk +++ b/netbox.mk @@ -3,51 +3,80 @@ # Only for development and testing purposes # is generally vibe coded and will be removed after development ##@ NetBox +NETBOX_CLUSTER_NAME ?= NETBOX_CHART ?= netbox/netbox NETBOX_RELEASE ?= netbox NETBOX_NAMESPACE ?= netbox -NETBOX_URL ?= http://localhost:8081 -NETBOX_TOKEN ?= $(shell kubectl get secret netbox-superuser -n netbox -o jsonpath='{.data.api_token}' | base64 -d || true) +NETBOX_PORT ?= 8081 +NETBOX_URL ?= http://localhost:$(NETBOX_PORT) +NETBOX_TOKEN ?= $(shell kubectl get secret netbox-superuser -n $(NETBOX_NAMESPACE) --context kind-$(NETBOX_CLUSTER_NAME) -o jsonpath='{.data.api_token}' | base64 -d || true) NETBOX_VALUES ?= lab/dev/netbox/netbox-values.yaml NETBOX_PASSWORD ?= -NB_INIT ?= lab/dev/netbox/initializers +NETBOX_INIT ?= lab/dev/netbox/initializers +.PHONY: netbox-setup +netbox-setup: netbox-deploy-cluster netbox-install + +.PHONY: netbox-deploy-cluster +netbox-deploy-cluster: ## Deploy the netbox cluster +ifndef NETBOX_CLUSTER_NAME + $(error NETBOX_CLUSTER_NAME is required. Usage: make netbox-deploy-cluster NETBOX_CLUSTER_NAME=cluster-name) +endif + kind get clusters | grep -q "$(NETBOX_CLUSTER_NAME)" || kind create cluster --name $(NETBOX_CLUSTER_NAME) + kubectl config use-context kind-$(CLUSTER_NAME) + +.PHONY: netbox-undeploy +netbox-undeploy: ## Undeploy the netbox cluster +ifndef NETBOX_CLUSTER_NAME + $(error NETBOX_CLUSTER_NAME is required. This will delete the cluster!!! Usage: make netbox-undeploy NETBOX_CLUSTER_NAME=cluster-name) +endif + kind delete cluster --name $(NETBOX_CLUSTER_NAME) .PHONY: netbox-install netbox-install: ## Generate NetBox secrets, patch templates, create namespace, and deploy NetBox via Helm ifndef NETBOX_PASSWORD $(error NETBOX_PASSWORD is required. Usage: make netbox-install NETBOX_PASSWORD=yourpassword) +endif +ifndef NETBOX_CLUSTER_NAME + $(error NETBOX_CLUSTER_NAME is required. Usage: make netbox-install NETBOX_CLUSTER_NAME=cluster-name) endif mkdir -p lab/dev/netbox/secrets @echo "Generating NetBox secrets..." @PEPPER=$$(openssl rand -hex 32); \ API_TOKEN=$$(openssl rand -hex 32); \ - echo -e '---\napiVersion: v1\nkind: ConfigMap\nmetadata:\n name: netbox-peppers\n namespace: netbox\ndata:\n peppers.yaml: |-\n API_TOKEN_PEPPERS:\n 1: '\''$$PEPPER'\''\n' > lab/dev/netbox/secrets/netbox_peppers.yaml; \ - echo -e '---\napiVersion: v1\nkind: Secret\nmetadata:\n name: netbox-superuser\n namespace: netbox\ntype: Opaque\nstringData:\n username: "admin"\n email: "admin@example.com"\n password: "$(NETBOX_PASSWORD)"\n api_token: "$$API_TOKEN"\n' > lab/dev/netbox/secrets/netbox_secret.yaml; \ + echo -e '---\napiVersion: v1\nkind: ConfigMap\nmetadata:\n name: netbox-peppers\n namespace: $(NETBOX_NAMESPACE)\ndata:\n peppers.yaml: |-\n API_TOKEN_PEPPERS:\n 1: '\''$$PEPPER'\''\n' > lab/dev/netbox/secrets/netbox_peppers.yaml; \ + echo -e '---\napiVersion: v1\nkind: Secret\nmetadata:\n name: netbox-superuser\n namespace: $(NETBOX_NAMESPACE)\ntype: Opaque\nstringData:\n username: "admin"\n email: "admin@example.com"\n password: "$(NETBOX_PASSWORD)"\n api_token: "$$API_TOKEN"\n' > lab/dev/netbox/secrets/netbox_secret.yaml; \ sed -i "s|\$$PEPPER|$${PEPPER}|g" lab/dev/netbox/secrets/netbox_peppers.yaml; \ sed -i "s|\$$API_TOKEN|$${API_TOKEN}|g" lab/dev/netbox/secrets/netbox_secret.yaml - kubectl create namespace $(NETBOX_NAMESPACE) || true - kubectl apply -f lab/dev/netbox/secrets/ -n $(NETBOX_NAMESPACE) - helm repo add netbox https://netbox-community.github.io/netbox-helm 2>/dev/null || true - helm repo update + kubectl create namespace $(NETBOX_NAMESPACE) --context kind-$(NETBOX_CLUSTER_NAME) || true + kubectl apply -f lab/dev/netbox/secrets/ -n $(NETBOX_NAMESPACE) --context kind-$(NETBOX_CLUSTER_NAME) + helm repo add netbox https://netbox-community.github.io/netbox-helm 2>/dev/null --kube-context kind-$(NETBOX_CLUSTER_NAME) || true + helm repo update --kube-context kind-$(NETBOX_CLUSTER_NAME) helm upgrade --install $(NETBOX_RELEASE) $(NETBOX_CHART) \ - -n $(NETBOX_NAMESPACE) -f $(NETBOX_VALUES) - kubectl wait --for=condition=Ready pod -l app.kubernetes.io/name=netbox -n $(NETBOX_NAMESPACE) --timeout=600s - @echo "Make sure NetBox is reachable before 'make netbox-sync' by running: kubectl port-forward svc/netbox 8081:80 -n netbox --address='0.0.0.0' &" + -n $(NETBOX_NAMESPACE) --kube-context kind-$(NETBOX_CLUSTER_NAME) -f $(NETBOX_VALUES) \ + --wait --timeout 10m +# kubectl wait --for=condition=Ready pod -l app.kubernetes.io/name=netbox -n $(NETBOX_NAMESPACE) --context kind-$(NETBOX_CLUSTER_NAME) --timeout=600s + @echo "Make sure NetBox is reachable before 'make netbox-sync-data' by running: kubectl port-forward svc/netbox 8081:80 -n $(NETBOX_NAMESPACE) --context kind-$(NETBOX_CLUSTER_NAME) --address='0.0.0.0' &" .PHONY: netbox-delete -netbox-delete: ## Uninstall NetBox and delete the namespace - helm uninstall netbox -n netbox || true - kubectl delete namespace netbox || true +netbox-delete: ## Uninstall NetBox helm deployment and delete the namespace +ifndef NETBOX_CLUSTER_NAME + $(error NETBOX_CLUSTER_NAME is required. Usage: make netbox-delete NETBOX_CLUSTER_NAME=cluster-name) +endif + helm uninstall netbox -n $(NETBOX_NAMESPACE) --kube-context kind-$(NETBOX_CLUSTER_NAME) || true + kubectl delete namespace $(NETBOX_NAMESPACE) --context kind-$(NETBOX_CLUSTER_NAME) || true -.PHONY: netbox-sync -netbox-sync: ## Publish initializers data into NetBox via REST API +.PHONY: netbox-sync-data +netbox-sync-data: ## Publish initializers data into NetBox via REST API +ifndef NETBOX_CLUSTER_NAME + $(error NETBOX_CLUSTER_NAME is required. Usage: make netbox-sync-data NETBOX_CLUSTER_NAME=cluster-name) +endif @echo "NetBox URL: $(NETBOX_URL)" - @POD=$$(kubectl -n $(NETBOX_NAMESPACE) get pod -l app.kubernetes.io/name=netbox -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true); \ + @POD=$$(kubectl -n $(NETBOX_NAMESPACE) --context kind-$(NETBOX_CLUSTER_NAME) get pod -l app.kubernetes.io/name=netbox -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true); \ if [ -z "$$POD" ]; then echo "Error: no NetBox pod found in namespace $(NETBOX_NAMESPACE). Run make netbox-install first."; exit 1; fi; \ - TOKEN_KEY=$$(kubectl exec -n $(NETBOX_NAMESPACE) $$POD -- python manage.py shell -c "from users.models import Token; print(next((t.key for t in Token.objects.filter(user__username='admin')), ''))" 2>/dev/null | tr -d '\r' | grep -E '^[A-Za-z0-9]+$$' | head -n1); \ + TOKEN_KEY=$$(kubectl exec -n $(NETBOX_NAMESPACE) --context kind-$(NETBOX_CLUSTER_NAME) $$POD -- python manage.py shell -c "from users.models import Token; print(next((t.key for t in Token.objects.filter(user__username='admin')), ''))" 2>/dev/null | tr -d '\r' | grep -E '^[A-Za-z0-9]+$$' | head -n1); \ if [ -z "$$TOKEN_KEY" ]; then echo "Error: no admin v2 API token found in NetBox. Create one in NetBox admin and retry."; exit 1; fi; \ echo "NetBox Token Key: $$TOKEN_KEY"; \ echo "NetBox Token: $(NETBOX_TOKEN)"; \ - python3 lab/dev/netbox/publish.py $(NETBOX_URL) "nbt_$$TOKEN_KEY.$(NETBOX_TOKEN)" $(NB_INIT) --force + python3 lab/dev/netbox/publish.py $(NETBOX_URL) "nbt_$$TOKEN_KEY.$(NETBOX_TOKEN)" $(NETBOX_INIT) --force @echo "NetBox sync complete!" From 0ad97c78a0452fc621105147b3dcf6b5c6ec2792 Mon Sep 17 00:00:00 2001 From: Janooski Date: Mon, 13 Apr 2026 11:18:22 +0000 Subject: [PATCH 09/12] rename folder --- internal/controller/targetsource/loaders.go | 2 +- .../targetsource/loaders/{http_pull => pull}/loader.go | 0 .../targetsource/loaders/{http_pull => pull}/loader_test.go | 0 .../targetsource/loaders/{http_push => push}/loader.go | 0 .../targetsource/loaders/{http_push => push}/loader_test.go | 0 5 files changed, 1 insertion(+), 1 deletion(-) rename internal/controller/targetsource/loaders/{http_pull => pull}/loader.go (100%) rename internal/controller/targetsource/loaders/{http_pull => pull}/loader_test.go (100%) rename internal/controller/targetsource/loaders/{http_push => push}/loader.go (100%) rename internal/controller/targetsource/loaders/{http_push => push}/loader_test.go (100%) diff --git a/internal/controller/targetsource/loaders.go b/internal/controller/targetsource/loaders.go index 817194d..7294725 100644 --- a/internal/controller/targetsource/loaders.go +++ b/internal/controller/targetsource/loaders.go @@ -1,4 +1,4 @@ package targetsource -// This file defines the loader interface +// This file defines the loader interface, pull and push implemented as interfaces as well // targets received from loaders are sent via channel to the controller for reconciliation diff --git a/internal/controller/targetsource/loaders/http_pull/loader.go b/internal/controller/targetsource/loaders/pull/loader.go similarity index 100% rename from internal/controller/targetsource/loaders/http_pull/loader.go rename to internal/controller/targetsource/loaders/pull/loader.go diff --git a/internal/controller/targetsource/loaders/http_pull/loader_test.go b/internal/controller/targetsource/loaders/pull/loader_test.go similarity index 100% rename from internal/controller/targetsource/loaders/http_pull/loader_test.go rename to internal/controller/targetsource/loaders/pull/loader_test.go diff --git a/internal/controller/targetsource/loaders/http_push/loader.go b/internal/controller/targetsource/loaders/push/loader.go similarity index 100% rename from internal/controller/targetsource/loaders/http_push/loader.go rename to internal/controller/targetsource/loaders/push/loader.go diff --git a/internal/controller/targetsource/loaders/http_push/loader_test.go b/internal/controller/targetsource/loaders/push/loader_test.go similarity index 100% rename from internal/controller/targetsource/loaders/http_push/loader_test.go rename to internal/controller/targetsource/loaders/push/loader_test.go From a2b9d254c9044452d4238e0981dd01025c5f4311 Mon Sep 17 00:00:00 2001 From: Janooski Date: Mon, 13 Apr 2026 11:40:49 +0000 Subject: [PATCH 10/12] rename go package --- internal/controller/targetsource/loaders/pull/loader.go | 2 +- internal/controller/targetsource/loaders/pull/loader_test.go | 2 +- internal/controller/targetsource/loaders/push/loader.go | 2 +- internal/controller/targetsource/loaders/push/loader_test.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/controller/targetsource/loaders/pull/loader.go b/internal/controller/targetsource/loaders/pull/loader.go index 6185fe6..48aa27d 100644 --- a/internal/controller/targetsource/loaders/pull/loader.go +++ b/internal/controller/targetsource/loaders/pull/loader.go @@ -1,3 +1,3 @@ -package http_pull +package pull // this file implements the logic to load targets from HTTP endpoint diff --git a/internal/controller/targetsource/loaders/pull/loader_test.go b/internal/controller/targetsource/loaders/pull/loader_test.go index d606d4d..0493bec 100644 --- a/internal/controller/targetsource/loaders/pull/loader_test.go +++ b/internal/controller/targetsource/loaders/pull/loader_test.go @@ -1 +1 @@ -package http_pull +package pull diff --git a/internal/controller/targetsource/loaders/push/loader.go b/internal/controller/targetsource/loaders/push/loader.go index 95dc1e9..92f0ccc 100644 --- a/internal/controller/targetsource/loaders/push/loader.go +++ b/internal/controller/targetsource/loaders/push/loader.go @@ -1,4 +1,4 @@ -package http_push +package push // this file implements the logic receive target updates via HTTP push // REST API defined internal/apiserver diff --git a/internal/controller/targetsource/loaders/push/loader_test.go b/internal/controller/targetsource/loaders/push/loader_test.go index bb7d848..63fdf61 100644 --- a/internal/controller/targetsource/loaders/push/loader_test.go +++ b/internal/controller/targetsource/loaders/push/loader_test.go @@ -1 +1 @@ -package http_push +package push From e199f36f7e381559aa1c0a730aa23980ab822c66 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Mon, 13 Apr 2026 14:49:37 +0000 Subject: [PATCH 11/12] undo targetsource modifications --- internal/controller/targetsource/loaders.go | 4 ---- internal/controller/targetsource/loaders/pull/loader.go | 3 --- .../controller/targetsource/loaders/pull/loader_test.go | 1 - internal/controller/targetsource/loaders/push/loader.go | 4 ---- .../controller/targetsource/loaders/push/loader_test.go | 1 - internal/controller/targetsource/loaders_test.go | 1 - internal/controller/targetsource/mapper.go | 4 ---- internal/controller/targetsource/mapper_test.go | 1 - internal/controller/targetsource_controller.go | 8 -------- 9 files changed, 27 deletions(-) delete mode 100644 internal/controller/targetsource/loaders.go delete mode 100644 internal/controller/targetsource/loaders/pull/loader.go delete mode 100644 internal/controller/targetsource/loaders/pull/loader_test.go delete mode 100644 internal/controller/targetsource/loaders/push/loader.go delete mode 100644 internal/controller/targetsource/loaders/push/loader_test.go delete mode 100644 internal/controller/targetsource/loaders_test.go delete mode 100644 internal/controller/targetsource/mapper.go delete mode 100644 internal/controller/targetsource/mapper_test.go diff --git a/internal/controller/targetsource/loaders.go b/internal/controller/targetsource/loaders.go deleted file mode 100644 index 7294725..0000000 --- a/internal/controller/targetsource/loaders.go +++ /dev/null @@ -1,4 +0,0 @@ -package targetsource - -// This file defines the loader interface, pull and push implemented as interfaces as well -// targets received from loaders are sent via channel to the controller for reconciliation diff --git a/internal/controller/targetsource/loaders/pull/loader.go b/internal/controller/targetsource/loaders/pull/loader.go deleted file mode 100644 index 48aa27d..0000000 --- a/internal/controller/targetsource/loaders/pull/loader.go +++ /dev/null @@ -1,3 +0,0 @@ -package pull - -// this file implements the logic to load targets from HTTP endpoint diff --git a/internal/controller/targetsource/loaders/pull/loader_test.go b/internal/controller/targetsource/loaders/pull/loader_test.go deleted file mode 100644 index 0493bec..0000000 --- a/internal/controller/targetsource/loaders/pull/loader_test.go +++ /dev/null @@ -1 +0,0 @@ -package pull diff --git a/internal/controller/targetsource/loaders/push/loader.go b/internal/controller/targetsource/loaders/push/loader.go deleted file mode 100644 index 92f0ccc..0000000 --- a/internal/controller/targetsource/loaders/push/loader.go +++ /dev/null @@ -1,4 +0,0 @@ -package push - -// this file implements the logic receive target updates via HTTP push -// REST API defined internal/apiserver diff --git a/internal/controller/targetsource/loaders/push/loader_test.go b/internal/controller/targetsource/loaders/push/loader_test.go deleted file mode 100644 index 63fdf61..0000000 --- a/internal/controller/targetsource/loaders/push/loader_test.go +++ /dev/null @@ -1 +0,0 @@ -package push diff --git a/internal/controller/targetsource/loaders_test.go b/internal/controller/targetsource/loaders_test.go deleted file mode 100644 index 603b690..0000000 --- a/internal/controller/targetsource/loaders_test.go +++ /dev/null @@ -1 +0,0 @@ -package targetsource diff --git a/internal/controller/targetsource/mapper.go b/internal/controller/targetsource/mapper.go deleted file mode 100644 index fded27d..0000000 --- a/internal/controller/targetsource/mapper.go +++ /dev/null @@ -1,4 +0,0 @@ -package targetsource - -// This file makes diff between existing and new targets -// file decides which targets to create/update/delete diff --git a/internal/controller/targetsource/mapper_test.go b/internal/controller/targetsource/mapper_test.go deleted file mode 100644 index 603b690..0000000 --- a/internal/controller/targetsource/mapper_test.go +++ /dev/null @@ -1 +0,0 @@ -package targetsource diff --git a/internal/controller/targetsource_controller.go b/internal/controller/targetsource_controller.go index 06e07ab..032b103 100644 --- a/internal/controller/targetsource_controller.go +++ b/internal/controller/targetsource_controller.go @@ -57,14 +57,6 @@ func (r *TargetSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request // - PodSelector: select Kubernetes pods // - ServiceSelector: select Kubernetes services - // TODO: - // 1. Start go routines for loader of target source - // 2. Retrieve list of targets from go channel - // 3. Fetch existing Targets from Kubernetes API - // 4. Compare and determine which Targets to create/update/delete - // 5. Create/update/delete Target CRs accordingly - // 6. Update TargetSource status with sync results - return ctrl.Result{}, nil } From fc7f38ecadcecbeff25e2089a44aa3e91787e623 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Wed, 15 Apr 2026 11:44:38 -0600 Subject: [PATCH 12/12] removed unnecessary files and updated gitignore --- .gitignore | 1 + lab/dev/3-nodes.clab.yaml.annotations.json | 35 ---------------------- lab/dev/netbox/readme.txt | 3 -- lab/dev/temp | 0 4 files changed, 1 insertion(+), 38 deletions(-) delete mode 100644 lab/dev/3-nodes.clab.yaml.annotations.json delete mode 100644 lab/dev/netbox/readme.txt delete mode 100644 lab/dev/temp diff --git a/.gitignore b/.gitignore index c04366f..29d31af 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ Dockerfile.cross *.swo *~ private/ +lab/dev/**/*.annotations.json lab/dev/clab-* lab/dev/netbox/secrets design/ diff --git a/lab/dev/3-nodes.clab.yaml.annotations.json b/lab/dev/3-nodes.clab.yaml.annotations.json deleted file mode 100644 index 9188d4c..0000000 --- a/lab/dev/3-nodes.clab.yaml.annotations.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "freeTextAnnotations": [], - "freeShapeAnnotations": [], - "groupStyleAnnotations": [], - "networkNodeAnnotations": [], - "nodeAnnotations": [ - { - "id": "spine1", - "interfacePattern": "e1-{n}", - "position": { - "x": 360, - "y": 380 - } - }, - { - "id": "leaf1", - "interfacePattern": "e1-{n}", - "position": { - "x": 480, - "y": 260 - } - }, - { - "id": "leaf2", - "interfacePattern": "e1-{n}", - "position": { - "x": 520, - "y": 400 - } - } - ], - "edgeAnnotations": [], - "aliasEndpointAnnotations": [], - "viewerSettings": {} -} \ No newline at end of file diff --git a/lab/dev/netbox/readme.txt b/lab/dev/netbox/readme.txt deleted file mode 100644 index c0edbe1..0000000 --- a/lab/dev/netbox/readme.txt +++ /dev/null @@ -1,3 +0,0 @@ -# All files within operator/lab/dev/netbox are -# only for development and testing purposes -# is generally vibe coded and will be removed after development \ No newline at end of file diff --git a/lab/dev/temp b/lab/dev/temp deleted file mode 100644 index e69de29..0000000