From 8f7488fb752cd3eb8a029c9d8c594e8bf713cf5f Mon Sep 17 00:00:00 2001 From: flosch62 Date: Thu, 2 Oct 2025 16:20:48 +0200 Subject: [PATCH 1/6] cx ftw --- README.md | 214 ++++++++------------------ clab/README.md | 89 +++++++++++ cleanup.sh | 26 ++-- configs/servers/wait-for-ifaces.sh | 17 +++ cx/container-shell | 17 +++ cx/node-ssh | 23 +++ cx/topology/configure-servers.sh | 27 ++++ cx/topology/simtopo.yaml | 18 +++ cx/topology/topo.sh | 82 ++++++++++ cx/topology/topo.yaml | 117 ++++++++++++++ init.sh | 229 +++++++++++++++------------- manifests/0010_netbox_instance.yaml | 7 +- manifests/0020_allocations.yaml | 12 +- manifests/0060_fabric.yaml | 6 +- scripts/configure_netbox.py | 2 +- 15 files changed, 594 insertions(+), 292 deletions(-) create mode 100644 clab/README.md create mode 100755 configs/servers/wait-for-ifaces.sh create mode 100755 cx/container-shell create mode 100755 cx/node-ssh create mode 100755 cx/topology/configure-servers.sh create mode 100644 cx/topology/simtopo.yaml create mode 100755 cx/topology/topo.sh create mode 100644 cx/topology/topo.yaml diff --git a/README.md b/README.md index ba3893f..42a9e05 100644 --- a/README.md +++ b/README.md @@ -1,189 +1,99 @@ -# EDA NetBox Integration Lab +# Nokia EDA NetBox Lab -This lab demonstrates the integration between Nokia EDA and NetBox for IPAM (IP Address Management) synchronization. It shows how EDA can dynamically create allocation pools based on NetBox's IPAM Prefixes and post allocated objects back to NetBox. +[![Discord][discord-svg]][discord-url] -## Overview +[discord-svg]: https://gitlab.com/rdodin/pics/-/wikis/uploads/b822984bc95d77ba92d50109c66c7afe/join-discord-btn.svg +[discord-url]: https://eda.dev/discord -The NetBox app in EDA enables: -- Dynamic creation of allocation pools based on NetBox Prefixes -- Synchronization of allocated resources back to NetBox -- Automated tracking of resource ownership +When IPAM data and automation live in different systems, network provisioning quickly drifts from the intended design. The Nokia EDA NetBox lab demonstrates how the [**Nokia Event Driven Automation**](https://docs.eda.dev/) platform can stay in sync with NetBox: allocation pools are generated straight from tagged prefixes, SR Linux fabrics consume those pools, and every assignment is written back to the source of truth. -## Prerequisites - -- Working EDA installation -- `kubectl` access to the EDA cluster -- `uv` tool (will be installed by init script) -- `helm` v3.x installed - https://helm.sh/docs/intro/install/ +In its default form the lab runs entirely inside the EDA Digital Twin (CX) environment: the topology, NetBox instance, and integration resources are deployed with a single script. A Containerlab option is available for environments running EDA with `Simulate=False`—see the dedicated guide in [`clab/README.md`](./clab/README.md). ## Lab Components -### Topology -- 2 Spine switches (spine1, spine2) -- 2 Leaf switches (leaf1, leaf2) -- 2 Linux servers (server1, server2) -- All devices are Nokia SR Linux based +- **EDA Digital Twin (CX):** Provides simulated SR Linux nodes (2× spines, 2× leafs) and two Linux application servers. +- **NetBox:** Installed via Helm inside Kubernetes; exposes a UI and API secured with secrets consumed by EDA. +- **EDA NetBox Application:** Processes NetBox webhooks, creates allocation pools, and reconciles changes back to NetBox. +- **Fabric Manifests:** Reference NetBox-managed pools so SR Linux provisioning always matches the IPAM intent. +- **Helper Scripts:** `./cx/node-ssh`, `./cx/container-shell`, and `scripts/configure_netbox.py` streamline everyday operations. -### NetBox Integration Features -- Webhook for real-time updates -- Event rules for IPAM synchronization -- Custom fields for EDA tracking -- Pre-configured tags and prefixes +## Requirements -## Quick Start +> [!IMPORTANT] +> **EDA Version:** 25.8.2 or later. Ensure your EDA playground (or production deployment) is installed and healthy before starting the lab. -1. **Initialize the lab**: +1. **Helm** – install from . +2. **kubectl** – verify the EDA engine status: ```bash - ./init.sh + kubectl -n eda-system get engineconfig engine-config \ + -o jsonpath='{.status.run-status}{"\n"}' ``` - This will: - - Install NetBox using Helm ( takes ~10 minutes ) - - Create Kubernetes secrets - - Configure webhook endpoint - - Set up initial prefixes and tags + Expected output: `Started` +3. **Local shell access** to the EDA cluster. No additional tooling is required; the init script installs `uv` and `clab-connector` when needed. -2. **Configure NetBox** (optional - for manual setup): - ```bash - uv run scripts/configure_netbox.py - ``` +## 🚀 Lab Deployment -3. **Deploy Containerlab topology**: - ```bash - containerlab deploy -t eda-nb.clab.yaml - ``` +The `init.sh` script performs the entire CX deployment flow: -4. **Import topology to EDA**: - ```bash - clab-connector integrate \ - --topology-data clab-eda-nb/topology-data.json \ - --eda-url "https://$(cat .eda_api_address)" \ - --skip-edge-intfs - ``` +- Bootstraps the `eda-netbox` namespace (CX only) +- Loads the SR Linux topology into CX and configures the server containers +- Installs NetBox via Helm and waits for the service to become reachable +- Creates Kubernetes secrets for the NetBox API token and webhook signature +- Applies the NetBox integration manifests (`manifests/*.yaml`) +- Runs `scripts/configure_netbox.py` to create tags, prefixes, webhooks, and event rules in NetBox -5. **Apply EDA resources**: - ```bash - # Install NetBox app - kubectl apply -f manifests/0001_netbox_app_install.yaml - - # Wait for the netbox app to be ready - - # Apply remaining resources - kubectl apply -f manifests/ - ``` - -## Accessing Services - -### NetBox UI -- URL: Check `.netbox_url` file or run `kubectl get svc -n netbox` -- Username: `admin` -- Password: `netbox` - -### EDA API -- Stored in `.eda_api_address` file - -## Resource Types - -### NetBox Instance -Defines connection to NetBox: -```yaml -apiVersion: netbox.eda.nokia.com/v1alpha1 -kind: Instance -metadata: - name: netbox - namespace: eda -spec: - url: http://netbox-server.netbox.svc.cluster.local - apiToken: netbox-api-token - signatureKey: netbox-webhook-signature +```bash +./init.sh ``` -### Allocation Resources -Map NetBox prefixes to EDA allocation pools: - -| Type | EDA Pool | Use Case | -|------|----------|----------| -| `ip-address` | IPAllocationPool | System IPs | -| `ip-in-subnet` | IPInSubnetAllocationPool | Management IPs | -| `subnet` | SubnetAllocationPool | ISL links | - -## Example Workflow - -1. **Create Prefix in NetBox**: - - Navigate to IPAM → Prefixes - - Add prefix (e.g., `192.168.100.0/24`) - - Set Status to `Active` (for IP pools) or `Container` (for subnet pools) - - Add appropriate tag (e.g., `eda-systemip-v4`) - -2. **EDA Creates Allocation Pool**: - - NetBox sends webhook to EDA - - EDA creates matching allocation pool - - Pool name matches Allocation resource name - -3. **Use in Fabric**: - ```yaml - apiVersion: fabrics.eda.nokia.com/v1alpha1 - kind: Fabric - metadata: - name: netbox-ebgp-fabric - spec: - systemPoolIPV4: nb-systemip-v4 - interSwitchLinks: - poolIPV4: nb-isl-v4 - ``` - -4. **View Allocations in NetBox**: - - Allocated IPs appear under original prefix - - Custom fields show EDA owner and allocation +> [!NOTE] +> The script detects CX automatically. If CX pods are not present it prepares the environment for the Containerlab workflow—follow the instructions in [`clab/README.md`](./clab/README.md) to continue with that path. -## Pre-configured Resources +### Verify Deployment -### Tags -- `eda-systemip-v4` - IPv4 System IPs -- `eda-systemip-v6` - IPv6 System IPs -- `eda-isl-v4` - IPv4 ISL subnets -- `eda-isl-v6` - IPv6 ISL subnets -- `eda-mgmt-v4` - Management IPs -- `EDAManaged` - Auto-assigned to EDA allocations +1. **NetBox UI:** The URL is printed at the end of `init.sh` and stored in `.netbox_url`. Default credentials: `admin` / `netbox`. +2. **EDA Namespace:** + ```bash + kubectl get toponode -n eda-netbox + kubectl get allocation -n eda-netbox + ``` +3. **Webhook Logs:** + ```bash + kubectl logs -n eda-system -l app=netbox --tail 100 + ``` -### Prefixes -- `192.168.10.0/24` - System IPs (Active) -- `10.0.0.0/16` - ISL subnets (Container) -- `2001:db8::/32` - IPv6 System IPs (Active) -- `2005::/64` - IPv6 ISL subnets (Container) -- `172.16.0.0/16` - Management IPs (Active) +## Accessing Network Elements -## Troubleshooting +- **SR Linux nodes (CX):** `./cx/node-ssh leaf1` +- **Server containers (CX):** `./cx/container-shell server1` +- **NetBox API token:** stored as the `netbox-api-token` secret in the `eda-netbox` namespace +- **EDA API endpoint:** saved locally in `.eda_api_address` for use with `clab-connector` or custom tooling -### Check NetBox connectivity: -```bash -kubectl get instance netbox -n clab-eda-nb -o yaml -``` +## Working with NetBox Integration -### View allocation status: -```bash -kubectl get allocation -n clab-eda-nb -``` +- **Prefixes & Tags:** Examples (e.g., `eda-systemip-v4`, `eda-isl-v4`) are created automatically. Add your own prefixes in NetBox using the same tags to provision additional pools. +- **Allocations:** Every pool is mirrored into EDA as an `Allocation` CR. Watch updates with: + ```bash + kubectl get allocation -n eda-netbox + ``` +- **Fabric:** The sample `Fabric` resource (`manifests/0060_fabric.yaml`) references the NetBox-managed pools and runs EBGP across the spine-leaf topology. -### Check webhook logs: -```bash -kubectl logs -n eda-system -l app=netbox -``` +## Containerlab Variant -### Port forwarding (if LoadBalancer not available): -```bash -kubectl port-forward -n netbox service/netbox-server 8001:80 --address=0.0.0.0 -``` +Running EDA with `Simulate=False` and external SR Linux nodes? After `./init.sh` completes, follow [`clab/README.md`](./clab/README.md) to deploy the Containerlab topology, import it with `clab-connector`, and access the physical or virtual nodes. ## Cleanup -To remove all lab resources: ```bash ./cleanup.sh +# Optional: remove the Containerlab topology if used containerlab destroy -t eda-nb.clab.yaml +# Optional: delete the namespace when testing different scenarios +kubectl delete namespace eda-netbox --wait=false ``` ## Additional Resources -- [EDA NetBox App Detailed Guide](https://docs.eda.dev/25.4/apps/netbox/) - Comprehensive documentation on the NetBox app including configuration examples and troubleshooting +- [EDA NetBox App Guide](https://docs.eda.dev/25.4/apps/netbox/) - [NetBox Documentation](https://docs.netbox.dev/) -- [Containerlab Documentation](https://containerlab.dev/) \ No newline at end of file +- [Containerlab Documentation](https://containerlab.dev/) diff --git a/clab/README.md b/clab/README.md new file mode 100644 index 0000000..e562d5a --- /dev/null +++ b/clab/README.md @@ -0,0 +1,89 @@ +# 📦 Containerlab Deployment + +- **EDA Mode:** `Simulate=False` – integrates external Containerlab SR Linux nodes +- **Namespace:** `eda-netbox` +- **Automation:** `init.sh` installs NetBox, seeds secrets, and applies manifests. Containerlab brings up the fabric. +- **License:** Requires a valid EDA hardware license (25.8+) when running with Simulate=False. +- **Traffic Generation:** Basic nginx workloads on server containers (can be extended with your own tooling). + +> [!IMPORTANT] +> Install EDA in `Simulate=False` mode for Containerlab deployments. Follow the [official instructions][sim-false-doc] and ensure your license is active before starting the lab. + +[sim-false-doc]: https://docs.eda.dev/user-guide/containerlab-integration/#installing-eda + +## Common Requirements + +1. **EDA (25.8.2+)** with Simulate=False mode +2. **Helm** – +3. **kubectl** – verify EDA status: + ```bash + kubectl -n eda-system get engineconfig engine-config \ + -o jsonpath='{.status.run-status}{"\n"}' + ``` + Expected output: `Started` +4. **Containerlab** – install from + +## Step 1: Initialize the Lab + +Run the installer script once. It installs dependencies (`uv`, `clab-connector`), deploys NetBox via Helm, creates the secrets in `eda-netbox`, and applies the integration manifests. + +```bash +./init.sh +``` + +## Step 2: Deploy the Containerlab Topology + +```bash +containerlab deploy -t eda-nb.clab.yaml +``` + +The topology spins up two spines, two leafs, and two Linux servers. Artifacts (inventory, topology-data) land under `./clab-eda-nb/`. + +## Step 3: Integrate Containerlab with EDA + +```bash +clab-connector integrate \ + --topology-data clab-eda-nb/topology-data.json \ + --eda-url "https://$(cat .eda_api_address)" \ + --namespace eda-netbox \ + --skip-edge-intfs +``` + +> [!IMPORTANT] +> `--skip-edge-intfs` is required. The lab attaches server interfaces through manifests, so leave the edge links untouched. + +## Step 4: Validate the Deployment + +1. **NetBox UI:** Open the URL stored in `.netbox_url` (`admin` / `netbox`) +2. **EDA namespace:** + ```bash + kubectl get toponode -n eda-netbox + kubectl get allocation -n eda-netbox + ``` +3. **Webhook logs:** + ```bash + kubectl logs -n eda-system -l app=netbox --tail 50 + ``` + +## Accessing the Nodes + +| Node Type | Access Example | Notes | +|-----------|----------------|-------| +| SR Linux | `ssh admin@clab-eda-nb-leaf1` | Password `NokiaSrl1!` | +| Servers | `ssh admin@clab-eda-nb-server1` | Password `multit00l` | + +## Customising the Lab + +- Add or modify prefixes in NetBox (tagged with `eda-*`) to create new allocation pools on the fly +- Extend the fabric by editing `manifests/0060_fabric.yaml` +- Build additional automation on top of the secrets created in `eda-netbox` + +## Cleanup + +```bash +./cleanup.sh +containerlab destroy -t eda-nb.clab.yaml +``` + +> [!TIP] +> `cleanup.sh` leaves the `eda-netbox` namespace in place. Run `kubectl delete namespace eda-netbox --wait=false` if you want a completely clean slate before redeploying. diff --git a/cleanup.sh b/cleanup.sh index fc499dc..8bc99f4 100755 --- a/cleanup.sh +++ b/cleanup.sh @@ -1,25 +1,23 @@ #!/bin/bash +set -euo pipefail + echo "Cleaning up EDA NetBox lab..." -# Stop any port-forwards echo "Stopping port-forwards..." pkill -f "kubectl port-forward.*netbox" 2>/dev/null || true -# Delete EDA resources echo "Deleting EDA resources..." -kubectl delete -f manifests/0060_fabric.yaml 2>/dev/null || true -kubectl delete -f manifests/0020_allocations.yaml 2>/dev/null || true -kubectl delete -f manifests/0010_netbox_instance.yaml 2>/dev/null || true +if [ -d manifests ]; then + kubectl delete -f manifests --ignore-not-found=true 2>/dev/null || true +fi -# Delete secrets echo "Deleting secrets..." -kubectl delete secret netbox-api-token -n clab-eda-nb 2>/dev/null || true -kubectl delete secret netbox-webhook-signature -n clab-eda-nb 2>/dev/null || true +kubectl delete secret netbox-api-token -n eda-netbox 2>/dev/null || true +kubectl delete secret netbox-webhook-signature -n eda-netbox 2>/dev/null || true -# Uninstall NetBox app from EDA echo "Uninstalling NetBox app..." -cat << EOF | kubectl apply -f - +cat <<'YAML' | kubectl apply -f - >/dev/null apiVersion: core.eda.nokia.com/v1 kind: Workflow metadata: @@ -33,21 +31,17 @@ spec: - app: netbox catalog: eda-catalog-builtin-apps vendor: nokia -EOF +YAML -# Wait for uninstall to complete sleep 10 -# Uninstall NetBox helm release echo "Uninstalling NetBox helm release..." helm uninstall netbox-server -n netbox 2>/dev/null || true -# Delete NetBox namespace echo "Deleting NetBox namespace..." kubectl delete namespace netbox --wait=false 2>/dev/null || true -# Clean up local files echo "Cleaning up local files..." rm -f .netbox_url .eda_api_address -echo "Cleanup completed!" \ No newline at end of file +echo "Cleanup completed!" diff --git a/configs/servers/wait-for-ifaces.sh b/configs/servers/wait-for-ifaces.sh new file mode 100755 index 0000000..9183e42 --- /dev/null +++ b/configs/servers/wait-for-ifaces.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +# Wait up to 120 seconds for eth1 to become available. +timeout=120 +elapsed=0 +while [ $elapsed -lt $timeout ]; do + if ip link show eth1 >/dev/null 2>&1; then + break + fi + sleep 5 + elapsed=$((elapsed + 5)) +done + +if [ $elapsed -ge $timeout ]; then + echo "Error: eth1 interface did not appear within ${timeout}s" + exit 1 +fi diff --git a/cx/container-shell b/cx/container-shell new file mode 100755 index 0000000..1d11376 --- /dev/null +++ b/cx/container-shell @@ -0,0 +1,17 @@ +#!/bin/bash + +CONTAINER_NAME=${1} +CORE_NS=${2:-eda-system} + +if [ -z "${CONTAINER_NAME}" ]; then + echo "Usage: $0 " + exit 1 +fi + +POD=$(kubectl get -n "${CORE_NS}" pods -l "eda.nokia.com/app=sim-${CONTAINER_NAME}" -o jsonpath="{.items[0].metadata.name}" 2>/dev/null) +if [ -z "${POD}" ]; then + echo "Simulation container ${CONTAINER_NAME} not found" + exit 1 +fi + +kubectl -n "${CORE_NS}" exec -it "${POD}" -c "${CONTAINER_NAME}" -- bash diff --git a/cx/node-ssh b/cx/node-ssh new file mode 100755 index 0000000..9046a6b --- /dev/null +++ b/cx/node-ssh @@ -0,0 +1,23 @@ +#!/bin/bash + +NODE_NAME=${1} +USER_NS=${2:-eda-netbox} +CORE_NS=${3:-eda-system} + +if [ -z "${NODE_NAME}" ]; then + echo "Usage: $0 []" + exit 1 +fi + +get_pod() { + kubectl get -n "${CORE_NS}" pods -l eda.nokia.com/app=eda-toolbox -o jsonpath="{.items[0].metadata.name}" +} + +TOPO_NODE_ADDR=$(kubectl -n "${USER_NS}" get targetnode "${NODE_NAME}" -o jsonpath='{.spec.address}' 2>/dev/null) +if [ -z "${TOPO_NODE_ADDR}" ]; then + echo "Node ${NODE_NAME} not found in namespace ${USER_NS}" + exit 1 +fi + +kubectl -n "${CORE_NS}" exec -it "$(get_pod)" -- ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ + admin@"${TOPO_NODE_ADDR}" diff --git a/cx/topology/configure-servers.sh b/cx/topology/configure-servers.sh new file mode 100755 index 0000000..104abee --- /dev/null +++ b/cx/topology/configure-servers.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +set -euo pipefail + +TOPO_NS=${TOPO_NS:-eda-netbox} +CORE_NS=${CORE_NS:-eda-system} + +servers=(server1 server2) + +echo "Waiting for simulation links to be created" +for link in leaf1-ethernet-1-1 leaf2-ethernet-1-1; do + kubectl -n "${TOPO_NS}" wait --for=create simlink "${link}" --timeout=120s +done + +for server in "${servers[@]}"; do + echo "Waiting for interfaces on ${server}" + kubectl -n "${CORE_NS}" exec \ + $(kubectl get -n "${CORE_NS}" pods -l "eda.nokia.com/app=sim-${server}" -o jsonpath="{.items[0].metadata.name}") \ + -c "${server}" -- bash -c "$(cat configs/servers/wait-for-ifaces.sh)" +done + +for server in "${servers[@]}"; do + echo "Configuring ${server}" + kubectl -n "${CORE_NS}" exec \ + $(kubectl get -n "${CORE_NS}" pods -l "eda.nokia.com/app=sim-${server}" -o jsonpath="{.items[0].metadata.name}") \ + -c "${server}" -- bash -c "$(cat configs/servers/${server}.sh)" +done diff --git a/cx/topology/simtopo.yaml b/cx/topology/simtopo.yaml new file mode 100644 index 0000000..6bcbb59 --- /dev/null +++ b/cx/topology/simtopo.yaml @@ -0,0 +1,18 @@ +items: + - spec: + simNodes: + - name: "server1" + type: "Linux" + image: "ghcr.io/srl-labs/network-multitool:v0.4.1" + - name: "server2" + type: "Linux" + image: "ghcr.io/srl-labs/network-multitool:v0.4.1" + topology: + - node: "leaf1" + interface: "ethernet-1-1" + simNode: "server1" + simNodeInterface: "eth1" + - node: "leaf2" + interface: "ethernet-1-1" + simNode: "server2" + simNodeInterface: "eth1" diff --git a/cx/topology/topo.sh b/cx/topology/topo.sh new file mode 100755 index 0000000..7bc1eeb --- /dev/null +++ b/cx/topology/topo.sh @@ -0,0 +1,82 @@ +#!/bin/bash + +# Wrapper around api-server-topo to load or remove CX topologies. + +CMD=${1} +TOPO_YAML=${2} +SIMTOPO_FILE=${3} +TOPO_NS=${TOPO_NS:-eda-netbox} +CORE_NS=${CORE_NS:-eda-system} + +if [[ "${CMD}" == "remove" ]]; then + echo "Removing topology from namespace ${TOPO_NS}" + cat < " + exit 1 + fi + if [ ! -f "${TOPO_YAML}" ] || [ ! -f "${SIMTOPO_FILE}" ]; then + echo "Topology files not found" + exit 1 + fi + + TOOLBOX_POD=$(kubectl -n ${CORE_NS} get pods \ + -l eda.nokia.com/app=eda-toolbox -o jsonpath="{.items[0].metadata.name}") + + if [ -z "${TOOLBOX_POD}" ]; then + echo "Could not find eda-toolbox pod in namespace ${CORE_NS}" + exit 1 + fi + + TOPO_FILENAME=$(basename -- "${TOPO_YAML}") + SIMTOPO_FILENAME=$(basename -- "${SIMTOPO_FILE}") + + echo "Copying topology ${TOPO_YAML} to ${CORE_NS}/${TOOLBOX_POD}:/tmp/${TOPO_FILENAME}" + kubectl -n ${CORE_NS} cp "${TOPO_YAML}" "${TOOLBOX_POD}:/tmp/${TOPO_FILENAME}" || { + echo "kubectl cp failed" + exit 1 + } + + echo "Copying sim topology ${SIMTOPO_FILE} to ${CORE_NS}/${TOOLBOX_POD}:/tmp/${SIMTOPO_FILENAME}" + kubectl -n ${CORE_NS} cp "${SIMTOPO_FILE}" "${TOOLBOX_POD}:/tmp/${SIMTOPO_FILENAME}" || { + echo "kubectl cp failed" + exit 1 + } + + echo "Converting topology YAML to JSON" + kubectl -n ${CORE_NS} exec "${TOOLBOX_POD}" -- sh -c "yq -o json '.' /tmp/${TOPO_FILENAME} > /tmp/topo.json" + kubectl -n ${CORE_NS} exec "${TOOLBOX_POD}" -- sh -c "yq -o json '.' /tmp/${SIMTOPO_FILENAME} > /tmp/simtopo.json" + echo "Loading topology into namespace ${TOPO_NS}" + kubectl -n ${CORE_NS} exec "${TOOLBOX_POD}" -- api-server-topo -n ${TOPO_NS} -f /tmp/topo.json -s /tmp/simtopo.json + exit $? +fi + +echo "Unsupported command ${CMD}" +exit 1 diff --git a/cx/topology/topo.yaml b/cx/topology/topo.yaml new file mode 100644 index 0000000..091831a --- /dev/null +++ b/cx/topology/topo.yaml @@ -0,0 +1,117 @@ +--- +srl_leaf_template: &srl_leaf_template + labels: + eda.nokia.com/security-profile: managed + eda.nokia.com/role: leaf + spec: + operatingSystem: srl + platform: 7220 IXR-D2L + version: 25.7.2 + nodeProfile: srlinux-ghcr-25.7.2 + +srl_spine_template: &srl_spine_template + labels: + eda.nokia.com/security-profile: managed + eda.nokia.com/role: spine + spec: + operatingSystem: srl + platform: 7220 IXR-D3L + version: 25.7.2 + nodeProfile: srlinux-ghcr-25.7.2 + +interswitch_labels: &interswitch_labels + eda.nokia.com/role: interSwitch + +edge_labels: &edge_labels + eda.nokia.com/role: edge + +items: + - spec: + nodes: + - name: leaf1 + <<: *srl_leaf_template + - name: leaf2 + <<: *srl_leaf_template + - name: spine1 + <<: *srl_spine_template + - name: spine2 + <<: *srl_spine_template + + links: + - name: leaf1-spine1 + labels: + <<: *interswitch_labels + spec: + links: + - type: interSwitch + local: + node: leaf1 + interface: ethernet-1-49 + remote: + node: spine1 + interface: ethernet-1-1 + + - name: leaf1-spine2 + labels: + <<: *interswitch_labels + spec: + links: + - type: interSwitch + local: + node: leaf1 + interface: ethernet-1-50 + remote: + node: spine2 + interface: ethernet-1-1 + + - name: leaf2-spine1 + labels: + <<: *interswitch_labels + spec: + links: + - type: interSwitch + local: + node: leaf2 + interface: ethernet-1-49 + remote: + node: spine1 + interface: ethernet-1-2 + + - name: leaf2-spine2 + labels: + <<: *interswitch_labels + spec: + links: + - type: interSwitch + local: + node: leaf2 + interface: ethernet-1-50 + remote: + node: spine2 + interface: ethernet-1-2 + + - name: leaf1-server1 + labels: + <<: *edge_labels + spec: + links: + - type: edge + local: + node: leaf1 + interface: ethernet-1-1 + remote: + node: server1 + interface: eth1 + + - name: leaf2-server2 + labels: + <<: *edge_labels + spec: + links: + - type: edge + local: + node: leaf2 + interface: ethernet-1-1 + remote: + node: server2 + interface: eth1 diff --git a/init.sh b/init.sh index a1c57c3..93344fa 100755 --- a/init.sh +++ b/init.sh @@ -1,28 +1,72 @@ #!/bin/bash -function install-uv { - # error if uv is not in the path - if ! command -v uv &> /dev/null; - then - echo "Installing uv"; +set -euo pipefail + +ensure_uv() { + if ! command -v uv >/dev/null 2>&1; then + echo "Installing uv runtime..." curl -LsSf https://astral.sh/uv/install.sh | sh + export PATH="$HOME/.local/bin:$PATH" fi } -# Install uv and clab-connector -install-uv -uv tool install git+https://github.com/eda-labs/clab-connector.git -uv tool upgrade clab-connector +indent_out() { sed 's/^/ /'; } + +GREEN="\033[0;32m" +RESET="\033[0m" + +ST_STACK_NS=eda-netbox +DEFAULT_USER_NS=eda + +ensure_uv + +CX_DEP=$(kubectl get -A deployment -l eda.nokia.com/app=cx 2>/dev/null | grep eda-cx || true) + +if [[ -n "$CX_DEP" ]]; then + echo -e "${GREEN}--> EDA CX environment detected. Using CX resources.${RESET}" + IS_CX=true + + echo "Labeling SR Linux node profile for bootstrap (if present)..." + kubectl -n ${DEFAULT_USER_NS} label nodeprofile srlinux-ghcr-25.7.2 \ + eda.nokia.com/bootstrap=true --overwrite >/dev/null 2>&1 || true + + edactl() { + kubectl -n eda-system exec \ + $(kubectl -n eda-system get pods -l eda.nokia.com/app=eda-toolbox -o jsonpath="{.items[0].metadata.name}") \ + -- edactl "$@" + } + + echo -e "${GREEN}--> Bootstrapping namespace ${ST_STACK_NS}...${RESET}" + if ! edactl namespace bootstrap ${ST_STACK_NS} | indent_out; then + echo "Warning: namespace bootstrap reported an issue; continuing." >&2 + fi + + echo -e "${GREEN}--> Deploying CX topology...${RESET}" + bash ./cx/topology/topo.sh load cx/topology/topo.yaml cx/topology/simtopo.yaml | indent_out + + echo -e "${GREEN}--> Waiting for CX nodes to reach Synced state...${RESET}" + kubectl -n ${ST_STACK_NS} wait --for=jsonpath='{.status.node-state}'=Synced \ + toponode --all --timeout=300s | indent_out + + echo -e "${GREEN}--> Configuring CX server containers...${RESET}" + bash ./cx/topology/configure-servers.sh | indent_out +else + echo -e "${GREEN}Containerlab environment detected (no CX pods found).${RESET}" + IS_CX=false + + echo "Installing/upgrading clab-connector tooling..." + uv tool install git+https://github.com/eda-labs/clab-connector.git >/dev/null + uv tool upgrade clab-connector >/dev/null + + kubectl get namespace ${ST_STACK_NS} >/dev/null 2>&1 || kubectl create namespace ${ST_STACK_NS} +fi -# Add NetBox helm repo if not already added echo "Adding NetBox helm repository..." helm repo add netbox https://netbox-community.github.io/netbox-chart/ 2>/dev/null || true -helm repo update +helm repo update >/dev/null -# Check if NetBox is already installed if helm list -n netbox | grep -q netbox-server; then echo "NetBox is already installed. Upgrading..." - # Pin database dependencies to the Bitnami legacy namespace after the August 2025 migration. helm upgrade netbox-server netbox/netbox \ --namespace=netbox \ --set postgresql.auth.password=netbox123 \ @@ -38,10 +82,9 @@ if helm list -n netbox | grep -q netbox-server; then --set valkey.image.tag=8.1.3-debian-12-r3 \ --set worker.waitForBackend.image.repository=bitnamilegacy/kubectl \ --set worker.waitForBackend.image.tag=1.33.2-debian-12-r3 \ - --version 6.0.52 + --version 6.0.52 >/dev/null else echo "Installing NetBox helm chart..." - # Pin database dependencies to the Bitnami legacy namespace after the August 2025 migration. helm install netbox-server netbox/netbox \ --create-namespace \ --namespace=netbox \ @@ -58,145 +101,115 @@ else --set valkey.image.tag=8.1.3-debian-12-r3 \ --set worker.waitForBackend.image.repository=bitnamilegacy/kubectl \ --set worker.waitForBackend.image.tag=1.33.2-debian-12-r3 \ - --version 6.0.52 + --version 6.0.52 >/dev/null fi -# Wait for NetBox to be ready echo "Waiting for NetBox pods to be ready..." -echo "Note: First-time deployment may take several minutes while downloading container images." -kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=netbox --field-selector=status.phase!=Succeeded -n netbox --timeout=600s - -# Get NetBox service info -echo "Checking NetBox service type..." -SERVICE_TYPE=$(kubectl get svc netbox-server -n netbox -o jsonpath='{.spec.type}' 2>/dev/null) -echo "Service type: $SERVICE_TYPE" - -if [ "$SERVICE_TYPE" == "LoadBalancer" ]; then - echo "Waiting for NetBox LoadBalancer to get external IP..." - NETBOX_IP="" - RETRY_COUNT=0 - MAX_RETRIES=30 - - while [ -z "$NETBOX_IP" ] && [ $RETRY_COUNT -lt $MAX_RETRIES ]; do - NETBOX_IP=$(kubectl get svc netbox-server -n netbox -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null) - if [ -z "$NETBOX_IP" ]; then - # Also check for hostname (some cloud providers use hostname instead of IP) - NETBOX_IP=$(kubectl get svc netbox-server -n netbox -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' 2>/dev/null) +kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=netbox \ + --field-selector=status.phase!=Succeeded -n netbox --timeout=600s >/dev/null + +SERVICE_TYPE=$(kubectl get svc netbox-server -n netbox -o jsonpath='{.spec.type}') +echo "NetBox service type: $SERVICE_TYPE" + +NETBOX_URL="" +if [[ "$SERVICE_TYPE" == "LoadBalancer" ]]; then + echo "Waiting for NetBox LoadBalancer address..." + for attempt in {1..30}; do + ADDR=$(kubectl get svc netbox-server -n netbox -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null) + if [[ -z "$ADDR" ]]; then + ADDR=$(kubectl get svc netbox-server -n netbox -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' 2>/dev/null) fi - if [ -z "$NETBOX_IP" ]; then - echo "Waiting for external IP... (attempt $((RETRY_COUNT+1))/$MAX_RETRIES)" - sleep 10 - RETRY_COUNT=$((RETRY_COUNT+1)) + if [[ -n "$ADDR" ]]; then + NETBOX_URL="http://${ADDR}" + echo "LoadBalancer reachable at: $NETBOX_URL" + break fi + echo "Waiting for external address... (attempt ${attempt}/30)" + sleep 10 done - - if [ -n "$NETBOX_IP" ]; then - echo "Got NetBox external IP/hostname: $NETBOX_IP" - NETBOX_URL="http://$NETBOX_IP" - else - echo "Warning: LoadBalancer IP not assigned. Falling back to port-forward." - SERVICE_TYPE="ClusterIP" - fi fi -if [ "$SERVICE_TYPE" != "LoadBalancer" ] || [ -z "$NETBOX_IP" ]; then - # Kill any existing port-forwards +if [[ -z "$NETBOX_URL" ]]; then pkill -f "kubectl port-forward.*netbox-server.*8001" 2>/dev/null || true - - # Get the actual service port SERVICE_PORT=$(kubectl get svc -n netbox netbox-server -o jsonpath='{.spec.ports[0].port}' 2>/dev/null || echo "80") - - # Use port-forward for ClusterIP or if LoadBalancer failed - echo "Starting NetBox port-forward on port 8001..." + echo "Starting NetBox port-forward on 8001..." nohup kubectl port-forward -n netbox service/netbox-server 8001:${SERVICE_PORT} --address=0.0.0.0 >/dev/null 2>&1 & PORT_FORWARD_PID=$! sleep 5 - - # Check if port-forward is running - if ps -p $PORT_FORWARD_PID > /dev/null; then - echo "NetBox port-forward started successfully (PID: $PORT_FORWARD_PID)" - NETBOX_URL="http://$(hostname -I | awk '{print $1}'):8001" - echo "NetBox is accessible at: $NETBOX_URL" - echo "Or via: http://localhost:8001 (from this machine)" + if ps -p $PORT_FORWARD_PID >/dev/null; then + HOST_IP=$(hostname -I | awk '{print $1}') + NETBOX_URL="http://${HOST_IP}:8001" + echo "NetBox port-forward active (PID $PORT_FORWARD_PID)" else - echo "Error: Port-forward failed to start. Please run manually:" - echo "kubectl port-forward -n netbox service/netbox-server 8001:${SERVICE_PORT} --address=0.0.0.0" + echo "Port-forward failed to start; please configure manually." NETBOX_URL="http://localhost:8001" fi fi -# Save NetBox URL for later use echo "$NETBOX_URL" > .netbox_url -# Fetch EDA ext domain name from engine config EDA_API=$(uv run ./scripts/get_eda_api.py) - -# Ensure input is not empty if [[ -z "$EDA_API" ]]; then - echo "No EDA API address found. Exiting." - exit 1 + echo "No EDA API address found. Exiting." + exit 1 fi - -# Save EDA API address to a file echo "$EDA_API" > .eda_api_address -# Get NetBox API token -echo "Getting NetBox API token..." NETBOX_API_TOKEN=$(kubectl -n netbox get secret netbox-server-superuser -o jsonpath='{.data.api_token}' | base64 -d) -# Create Kubernetes secrets for NetBox integration -echo "Creating Kubernetes secrets for NetBox integration..." - -# Create namespace if it doesn't exist -kubectl create namespace clab-eda-nb 2>/dev/null || true +echo "Ensuring namespace ${ST_STACK_NS} exists..." +kubectl get namespace ${ST_STACK_NS} >/dev/null 2>&1 || kubectl create namespace ${ST_STACK_NS} -# Create NetBox API token secret -cat << EOF | kubectl apply -f - +token_b64=$(echo -n "$NETBOX_API_TOKEN" | base64) +cat < Applying EDA resources...${RESET}" +kubectl apply -f ./manifests | indent_out -# Configure NetBox automatically -echo "" echo "Configuring NetBox for EDA integration..." -uv run scripts/configure_netbox.py +uv run scripts/configure_netbox.py | indent_out + +echo "" +if [[ "$IS_CX" == "true" ]]; then + echo "CX topology ready. Helpers:" + echo " ./cx/node-ssh " + echo " ./cx/container-shell " +else + echo "Next steps for containerlab deployment:" + echo " 1. containerlab deploy -t eda-nb.clab.yaml" + echo " 2. clab-connector integrate \\ + --topology-data clab-eda-nb/topology-data.json \\ + --eda-url \"https://$(cat .eda_api_address)\" \\ + --namespace ${ST_STACK_NS} \\ + --skip-edge-intfs" +fi echo "" echo "===================================" echo "NetBox installation completed!" echo "===================================" -echo "" -echo "NetBox Access:" -echo " URL: $NETBOX_URL" -echo " Username: admin" -echo " Password: netbox" -echo "" -if [ "$SERVICE_TYPE" != "LoadBalancer" ] || [ -z "$NETBOX_IP" ]; then - echo "Note: Using port-forward to access NetBox" - echo " - For Kind/local clusters, you may need additional port-forwarding from your host" - echo " - To restart port-forward: kubectl port-forward -n netbox service/netbox-server 8001:80 --address=0.0.0.0" -fi -echo "" -echo "Next steps:" -echo "1. Deploy the containerlab topology: sudo containerlab deploy -t eda-nb.clab.yaml" -echo "2. Import topology to EDA: clab-connector import -t eda-nb.clab.yaml" -echo "3. Apply EDA resources: kubectl apply -f manifests/" +echo "NetBox URL: $NETBOX_URL" +echo "Username: admin" +echo "Password: netbox" diff --git a/manifests/0010_netbox_instance.yaml b/manifests/0010_netbox_instance.yaml index d695365..608924d 100644 --- a/manifests/0010_netbox_instance.yaml +++ b/manifests/0010_netbox_instance.yaml @@ -2,11 +2,8 @@ apiVersion: netbox.eda.nokia.com/v1alpha1 kind: Instance metadata: name: netbox - namespace: clab-eda-nb + namespace: eda-netbox spec: - # URL will be updated by init script url: http://netbox-server.netbox.svc.cluster.local - # Reference to the secret containing the API token apiToken: netbox-api-token - # Reference to the secret containing the webhook signature - signatureKey: netbox-webhook-signature \ No newline at end of file + signatureKey: netbox-webhook-signature diff --git a/manifests/0020_allocations.yaml b/manifests/0020_allocations.yaml index 18eec38..5482827 100644 --- a/manifests/0020_allocations.yaml +++ b/manifests/0020_allocations.yaml @@ -4,7 +4,7 @@ apiVersion: netbox.eda.nokia.com/v1alpha1 kind: Allocation metadata: name: nb-systemip-v4 - namespace: clab-eda-nb + namespace: eda-netbox spec: enabled: true instance: netbox @@ -19,7 +19,7 @@ apiVersion: netbox.eda.nokia.com/v1alpha1 kind: Allocation metadata: name: nb-systemip-v6 - namespace: clab-eda-nb + namespace: eda-netbox spec: enabled: true instance: netbox @@ -34,7 +34,7 @@ apiVersion: netbox.eda.nokia.com/v1alpha1 kind: Allocation metadata: name: nb-isl-v4 - namespace: clab-eda-nb + namespace: eda-netbox spec: enabled: true instance: netbox @@ -50,7 +50,7 @@ apiVersion: netbox.eda.nokia.com/v1alpha1 kind: Allocation metadata: name: nb-isl-v6 - namespace: clab-eda-nb + namespace: eda-netbox spec: enabled: true instance: netbox @@ -66,7 +66,7 @@ apiVersion: netbox.eda.nokia.com/v1alpha1 kind: Allocation metadata: name: nb-mgmt-v4 - namespace: clab-eda-nb + namespace: eda-netbox spec: enabled: true instance: netbox @@ -74,4 +74,4 @@ spec: - eda-mgmt-v4 type: ip-in-subnet subnetLength: 32 # Individual IPs with mask - description: "Management IPs for devices" \ No newline at end of file + description: "Management IPs for devices" diff --git a/manifests/0060_fabric.yaml b/manifests/0060_fabric.yaml index c00f3aa..d66f503 100644 --- a/manifests/0060_fabric.yaml +++ b/manifests/0060_fabric.yaml @@ -2,7 +2,7 @@ apiVersion: fabrics.eda.nokia.com/v1alpha1 kind: Fabric metadata: name: netbox-ebgp-fabric - namespace: clab-eda-nb + namespace: eda-netbox spec: leafs: leafNodeSelector: @@ -13,10 +13,8 @@ spec: interSwitchLinks: linkSelector: - eda.nokia.com/role=interSwitch - # Use NetBox-managed allocation pools poolIPV4: nb-isl-v4 poolIPV6: nb-isl-v6 - # Use NetBox-managed system IP pools systemPoolIPV4: nb-systemip-v4 systemPoolIPV6: nb-systemip-v6 underlayProtocol: @@ -25,4 +23,4 @@ spec: protocol: - EBGP overlayProtocol: - protocol: EBGP \ No newline at end of file + protocol: EBGP diff --git a/scripts/configure_netbox.py b/scripts/configure_netbox.py index 824acc5..25a7c40 100755 --- a/scripts/configure_netbox.py +++ b/scripts/configure_netbox.py @@ -86,7 +86,7 @@ def create_webhook(self, eda_api): webhook_data = { "name": "eda", - "payload_url": f"https://{eda_api}/core/httpproxy/v1/netbox/webhook/clab-eda-nb/netbox", + "payload_url": f"https://{eda_api}/core/httpproxy/v1/netbox/webhook/eda-netbox/netbox", "enabled": True, "http_method": "POST", "http_content_type": "application/json", From 20fa44db755bc3bf35f029c98804337c54c0a2b4 Mon Sep 17 00:00:00 2001 From: FloSch62 Date: Thu, 16 Oct 2025 16:00:08 +0200 Subject: [PATCH 2/6] wait for full init --- configs/netbox-values.yaml | 32 ++++++++++++++++++++++++++++++++ init.sh | 36 ++++++++++++++++++++++++++++++------ 2 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 configs/netbox-values.yaml diff --git a/configs/netbox-values.yaml b/configs/netbox-values.yaml new file mode 100644 index 0000000..d66b456 --- /dev/null +++ b/configs/netbox-values.yaml @@ -0,0 +1,32 @@ +worker: + waitForBackend: + image: + repository: bitnamilegacy/kubectl + tag: 1.33.2-debian-12-r3 + command: + - /bin/bash + - -ec + args: + - | + set -euo pipefail + deployment="${DEPLOYMENT_NAME:?deployment name is missing}" + namespace="${POD_NAMESPACE:-netbox}" + max_attempts=${WAIT_MAX_ATTEMPTS:-20} + delay_seconds=${WAIT_DELAY_SECONDS:-30} + rollout_timeout=${WAIT_ROLLOUT_TIMEOUT:-"120s"} + attempt=1 + echo "Waiting for deployment \"${deployment}\" to report a successful rollout..." + while [ "${attempt}" -le "${max_attempts}" ]; do + echo "Attempt ${attempt}/${max_attempts}" + if kubectl rollout status deployment "${deployment}" --namespace="${namespace}" --timeout="${rollout_timeout}"; then + echo "Deployment \"${deployment}\" is ready." + exit 0 + fi + echo "Deployment \"${deployment}\" not ready yet; sleeping ${delay_seconds}s before retry." + attempt=$((attempt + 1)) + sleep "${delay_seconds}" + done + echo "Deployment \"${deployment}\" did not become ready after ${max_attempts} attempts." >&2 + kubectl get deployment "${deployment}" --namespace="${namespace}" || true + kubectl get pods --namespace="${namespace}" || true + exit 1 diff --git a/init.sh b/init.sh index 93344fa..13071e7 100755 --- a/init.sh +++ b/init.sh @@ -22,6 +22,29 @@ ensure_uv CX_DEP=$(kubectl get -A deployment -l eda.nokia.com/app=cx 2>/dev/null | grep eda-cx || true) +ensure_progress_deadline() { + local deployment=$1 + local namespace=${2:-netbox} + local deadline=${3:-1200} + local attempts=12 + local wait_seconds=5 + + for ((i=1; i<=attempts; i++)); do + if kubectl -n "${namespace}" get deployment "${deployment}" >/dev/null 2>&1; then + if kubectl -n "${namespace}" patch deployment "${deployment}" \ + --type merge \ + --patch "{\"spec\":{\"progressDeadlineSeconds\":${deadline}}}" >/dev/null; then + echo "Set progressDeadlineSeconds=${deadline} for deployment ${deployment} in namespace ${namespace}." + return 0 + fi + echo "Warning: unable to patch progressDeadlineSeconds for deployment ${deployment} (attempt ${i}/${attempts})." >&2 + fi + sleep "${wait_seconds}" + done + echo "Warning: failed to patch progressDeadlineSeconds for deployment ${deployment} in namespace ${namespace}." >&2 + return 1 +} + if [[ -n "$CX_DEP" ]]; then echo -e "${GREEN}--> EDA CX environment detected. Using CX resources.${RESET}" IS_CX=true @@ -69,6 +92,7 @@ if helm list -n netbox | grep -q netbox-server; then echo "NetBox is already installed. Upgrading..." helm upgrade netbox-server netbox/netbox \ --namespace=netbox \ + -f configs/netbox-values.yaml \ --set postgresql.auth.password=netbox123 \ --set redis.auth.password=netbox123 \ --set superuser.password=netbox \ @@ -80,14 +104,13 @@ if helm list -n netbox | grep -q netbox-server; then --set postgresql.image.tag=17.5.0-debian-12-r9 \ --set valkey.image.repository=bitnamilegacy/valkey \ --set valkey.image.tag=8.1.3-debian-12-r3 \ - --set worker.waitForBackend.image.repository=bitnamilegacy/kubectl \ - --set worker.waitForBackend.image.tag=1.33.2-debian-12-r3 \ --version 6.0.52 >/dev/null else echo "Installing NetBox helm chart..." helm install netbox-server netbox/netbox \ --create-namespace \ --namespace=netbox \ + -f configs/netbox-values.yaml \ --set postgresql.auth.password=netbox123 \ --set redis.auth.password=netbox123 \ --set superuser.password=netbox \ @@ -99,14 +122,15 @@ else --set postgresql.image.tag=17.5.0-debian-12-r9 \ --set valkey.image.repository=bitnamilegacy/valkey \ --set valkey.image.tag=8.1.3-debian-12-r3 \ - --set worker.waitForBackend.image.repository=bitnamilegacy/kubectl \ - --set worker.waitForBackend.image.tag=1.33.2-debian-12-r3 \ --version 6.0.52 >/dev/null fi -echo "Waiting for NetBox pods to be ready..." +ensure_progress_deadline netbox-server +ensure_progress_deadline netbox-server-worker + +echo "Waiting for NetBox pods to be ready (this can take up to 15 minutes, check kubectl get pods -n netbox)..." kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=netbox \ - --field-selector=status.phase!=Succeeded -n netbox --timeout=600s >/dev/null + --field-selector=status.phase!=Succeeded -n netbox --timeout=900s >/dev/null SERVICE_TYPE=$(kubectl get svc netbox-server -n netbox -o jsonpath='{.spec.type}') echo "NetBox service type: $SERVICE_TYPE" From c0f2e567f6f0899d16d377685177a4d3ff432118 Mon Sep 17 00:00:00 2001 From: FloSch62 Date: Thu, 16 Oct 2025 16:47:38 +0200 Subject: [PATCH 3/6] optional device library input via k8s job --- README.md | 8 ++ init.sh | 35 ++++++ scripts/import_device_types.py | 221 +++++++++++++++++++++++++++++++++ 3 files changed, 264 insertions(+) create mode 100755 scripts/import_device_types.py diff --git a/README.md b/README.md index 42a9e05..ade2ad1 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,14 @@ The `init.sh` script performs the entire CX deployment flow: ./init.sh ``` +Need standard Nokia device definitions preloaded? Add `--import-nokia-device-types` to let the init flow pull them from the community Device Type Library once NetBox is online: + +```bash +./init.sh --import-nokia-device-types +``` + +The importer script is also available on its own and always runs inside Kubernetes: `uv run scripts/import_device_types.py --vendors nokia`. Use `--library-url`, `--library-branch`, or `--importer-image` to point at custom sources if required. + > [!NOTE] > The script detects CX automatically. If CX pods are not present it prepares the environment for the Containerlab workflow—follow the instructions in [`clab/README.md`](./clab/README.md) to continue with that path. diff --git a/init.sh b/init.sh index 13071e7..1bdef17 100755 --- a/init.sh +++ b/init.sh @@ -18,6 +18,36 @@ RESET="\033[0m" ST_STACK_NS=eda-netbox DEFAULT_USER_NS=eda +IMPORT_NOKIA_DEVICE_TYPES=false + +usage() { + cat <&2 + usage >&2 + exit 1 + ;; + esac +done + ensure_uv CX_DEP=$(kubectl get -A deployment -l eda.nokia.com/app=cx 2>/dev/null | grep eda-cx || true) @@ -215,6 +245,11 @@ kubectl apply -f ./manifests | indent_out echo "Configuring NetBox for EDA integration..." uv run scripts/configure_netbox.py | indent_out +if [[ "$IMPORT_NOKIA_DEVICE_TYPES" == "true" ]]; then + echo "Importing Nokia device types from the NetBox Device Type Library..." + uv run scripts/import_device_types.py | indent_out +fi + echo "" if [[ "$IS_CX" == "true" ]]; then echo "CX topology ready. Helpers:" diff --git a/scripts/import_device_types.py b/scripts/import_device_types.py new file mode 100755 index 0000000..58c8774 --- /dev/null +++ b/scripts/import_device_types.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python +# /// script +# dependencies = [ +# "requests", +# ] +# /// +"""Import NetBox device types from the community library using the official importer.""" + +import argparse +import subprocess +import sys +import time +import textwrap +from pathlib import Path +from typing import List + +import requests +from urllib3.exceptions import InsecureRequestWarning + +requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning) + + +DEFAULT_LIBRARY_URL = "https://github.com/netbox-community/devicetype-library.git" +DEFAULT_LIBRARY_BRANCH = "develop" +DEFAULT_K8S_NAMESPACE = "netbox" +DEFAULT_CLUSTER_NETBOX_URL = "http://netbox-server.netbox.svc.cluster.local" + + +def read_netbox_url() -> str: + path = Path(".netbox_url") + if not path.exists(): + raise FileNotFoundError("Missing .netbox_url. Run init.sh to deploy NetBox first.") + return path.read_text().strip() + + +def wait_for_netbox(url: str, retries: int = 30, delay: int = 5) -> None: + """Poll the NetBox API root until it responds with HTTP 200.""" + for attempt in range(1, retries + 1): + try: + response = requests.get(f"{url.rstrip('/')}/api/", timeout=10, verify=False) + if response.status_code == 200: + return + except requests.RequestException: + pass + time.sleep(delay) + raise TimeoutError( + "Timed out waiting for NetBox API to respond. Verify that the deployment is healthy." + ) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Import device types from the NetBox Device Type Library", + ) + parser.add_argument( + "--vendors", + default="nokia", + help="Comma-separated list of vendors to import (default: nokia)", + ) + parser.add_argument( + "--library-url", + default=DEFAULT_LIBRARY_URL, + help="Git URL for the NetBox Device Type Library", + ) + parser.add_argument( + "--library-branch", + default=DEFAULT_LIBRARY_BRANCH, + help="Git branch of the NetBox Device Type Library", + ) + parser.add_argument( + "--k8s-namespace", + default=DEFAULT_K8S_NAMESPACE, + help="Namespace where the importer job should run (default: netbox)", + ) + parser.add_argument( + "--cluster-netbox-url", + default=DEFAULT_CLUSTER_NETBOX_URL, + help=( + "Internal NetBox URL to pass to the Kubernetes job " + "(default: http://netbox-server.netbox.svc.cluster.local)" + ), + ) + parser.add_argument( + "--importer-image", + default="ghcr.io/minitriga/netbox-device-type-library-import:latest", + help="Container image to use for the Kubernetes job", + ) + return parser.parse_args() + + +def render_job_manifest( + job_name: str, + namespace: str, + image: str, + netbox_url: str, + vendors: List[str], + library_url: str, + library_branch: str, +) -> str: + vendor_value = ",".join(vendors) + # Build YAML manually to avoid adding PyYAML dependency for the controller. + lines = [ + "apiVersion: batch/v1", + "kind: Job", + f"metadata:", + f" name: {job_name}", + f" namespace: {namespace}", + "spec:", + " ttlSecondsAfterFinished: 600", + " template:", + " spec:", + " restartPolicy: Never", + " containers:", + " - name: importer", + f" image: {image}", + " env:", + f" - name: NETBOX_URL", + f" value: \"{netbox_url.rstrip('/')}\"", + " - name: VENDORS", + f" value: \"{vendor_value}\"", + " - name: REPO_URL", + f" value: \"{library_url}\"", + " - name: REPO_BRANCH", + f" value: \"{library_branch}\"", + " - name: NETBOX_TOKEN", + " valueFrom:", + " secretKeyRef:", + " name: netbox-server-superuser", + " key: api_token", + ] + return "\n".join(lines) + + +def run_importer_job( + namespace: str, + netbox_url: str, + vendors: List[str], + image: str, + library_url: str, + library_branch: str, + timeout_seconds: int = 900, +) -> None: + job_name = f"netbox-dtl-import-{int(time.time())}" + manifest = render_job_manifest( + job_name, + namespace, + image, + netbox_url, + vendors, + library_url, + library_branch, + ) + + apply = subprocess.run( + ["kubectl", "apply", "-f", "-"], + input=manifest, + text=True, + capture_output=True, + check=False, + ) + if apply.returncode != 0: + raise RuntimeError( + "Failed to create importer job: " + apply.stderr.strip() + ) + + try: + wait_cmd = [ + "kubectl", + "wait", + f"--for=condition=complete", + f"job/{job_name}", + "-n", + namespace, + f"--timeout={timeout_seconds}s", + ] + wait = subprocess.run(wait_cmd, capture_output=True, text=True, check=False) + if wait.returncode != 0: + describe = subprocess.run( + ["kubectl", "describe", f"job/{job_name}", "-n", namespace], + text=True, + capture_output=True, + ) + raise RuntimeError( + "Importer job did not complete successfully:\n" + + wait.stderr.strip() + + ("\n\nJob description:\n" + describe.stdout if describe.stdout else "") + ) + finally: + logs = subprocess.run( + ["kubectl", "logs", f"job/{job_name}", "-n", namespace], + text=True, + capture_output=True, + ) + if logs.stdout: + print(textwrap.indent(logs.stdout.strip(), " ")) + if logs.stderr: + print(textwrap.indent(logs.stderr.strip(), " "), file=sys.stderr) + + + +def main() -> None: + args = parse_args() + vendors = [v.strip() for v in args.vendors.split(",") if v.strip()] + if not vendors: + raise ValueError("At least one vendor must be specified for import.") + + netbox_url = read_netbox_url() + wait_for_netbox(netbox_url) + + run_importer_job( + namespace=args.k8s_namespace, + netbox_url=args.cluster_netbox_url or netbox_url, + vendors=vendors, + image=args.importer_image, + library_url=args.library_url, + library_branch=args.library_branch, + ) + + +if __name__ == "__main__": + main() From f146631094d203bff613221afaf13ceb5bfe9b4a Mon Sep 17 00:00:00 2001 From: FloSch62 Date: Thu, 16 Oct 2025 17:45:34 +0200 Subject: [PATCH 4/6] support for vlan and asns --- manifests/0020_allocations.yaml | 30 +++++ scripts/configure_netbox.py | 216 +++++++++++++++++++++++++++++++- 2 files changed, 241 insertions(+), 5 deletions(-) diff --git a/manifests/0020_allocations.yaml b/manifests/0020_allocations.yaml index 5482827..0125b2f 100644 --- a/manifests/0020_allocations.yaml +++ b/manifests/0020_allocations.yaml @@ -75,3 +75,33 @@ spec: type: ip-in-subnet subnetLength: 32 # Individual IPs with mask description: "Management IPs for devices" + +--- +# VLAN allocation pool +apiVersion: netbox.eda.nokia.com/v1alpha1 +kind: Allocation +metadata: + name: nb-vlans + namespace: eda-netbox +spec: + enabled: true + instance: netbox + tags: + - eda-vlans + type: vlan + description: "VLAN IDs managed via NetBox" + +--- +# ASN allocation pool +apiVersion: netbox.eda.nokia.com/v1alpha1 +kind: Allocation +metadata: + name: nb-asns + namespace: eda-netbox +spec: + enabled: true + instance: netbox + tags: + - eda-asns + type: asn + description: "Private ASNs sourced from NetBox" diff --git a/scripts/configure_netbox.py b/scripts/configure_netbox.py index 25a7c40..ffc0462 100755 --- a/scripts/configure_netbox.py +++ b/scripts/configure_netbox.py @@ -105,20 +105,61 @@ def create_webhook(self, eda_api): return None def create_event_rule(self, webhook_id): - """Create event rule for webhook""" + """Create or update the EDA event rule for the webhook""" print("Creating event rule...") - # Check if event rule already exists + required_object_types = [ + "dcim.site", + "dcim.device", + "dcim.cable", + "dcim.devicetype", + "ipam.ipaddress", + "ipam.prefix", + "ipam.vlangroup", + "ipam.vlan", + "ipam.asn", + "ipam.asnrange", + ] + response = self.session.get( f"{self.netbox_url}/api/extras/event-rules/?name=eda" ) - if response.json()["count"] > 0: - print("Event rule 'eda' already exists") + data = response.json() + if data["count"] > 0: + event_rule = data["results"][0] + event_rule_id = event_rule["id"] + existing_types = set(event_rule.get("object_types", [])) + missing_types = set(required_object_types).difference(existing_types) + updated_types = sorted(existing_types.union(required_object_types)) + needs_update = ( + bool(missing_types) + or event_rule.get("action_object_id") != webhook_id + or not event_rule.get("enabled", False) + ) + if needs_update: + patch_payload = { + "object_types": updated_types, + "action_object_id": webhook_id, + "enabled": True, + } + patch_response = self.session.patch( + f"{self.netbox_url}/api/extras/event-rules/{event_rule_id}/", + json=patch_payload, + ) + if patch_response.status_code == 200: + print("Event rule 'eda' updated successfully") + else: + print( + "Error updating event rule 'eda': " + f"{patch_response.status_code} {patch_response.text}" + ) + else: + print("Event rule 'eda' already up to date") return event_rule_data = { "name": "eda", - "object_types": ["ipam.ipaddress", "ipam.prefix"], + "object_types": required_object_types, "enabled": True, "event_types": ["object_created", "object_updated", "object_deleted"], "action_type": "webhook", @@ -142,6 +183,8 @@ def create_tags(self): {"name": "eda-isl-v4", "slug": "eda-isl-v4", "color": "00cc66"}, {"name": "eda-isl-v6", "slug": "eda-isl-v6", "color": "00cc66"}, {"name": "eda-mgmt-v4", "slug": "eda-mgmt-v4", "color": "cc6600"}, + {"name": "eda-vlans", "slug": "eda-vlans", "color": "ff5722"}, + {"name": "eda-asns", "slug": "eda-asns", "color": "9e9e9e"}, ] print("Creating tags...") @@ -162,6 +205,167 @@ def create_tags(self): else: print(f"Error creating tag '{tag['name']}': {response.text}") + def create_vlan_groups(self): + """Create VLAN groups used for EDA allocations""" + vlan_groups = [ + { + "name": "eda-vlans", + "slug": "eda-vlans", + "description": "EDA managed VLAN IDs", + "vid_ranges": [[1, 300]], + "tags": [{"name": "eda-vlans"}], + } + ] + + print("Creating VLAN groups...") + for group in vlan_groups: + response = self.session.get( + f"{self.netbox_url}/api/ipam/vlan-groups/?name={group['name']}" + ) + if response.json().get("count", 0) > 0: + print(f"VLAN group '{group['name']}' already exists") + continue + + create_response = self.session.post( + f"{self.netbox_url}/api/ipam/vlan-groups/", json=group + ) + if create_response.status_code == 201: + print(f"VLAN group '{group['name']}' created successfully") + else: + print( + f"Error creating VLAN group '{group['name']}': " + f"{create_response.status_code} {create_response.text}" + ) + + def create_rir(self, slug="eda", name="eda"): + """Create or correct the RIR required for ASN allocations""" + response = self.session.get( + f"{self.netbox_url}/api/ipam/rirs/?slug={slug}" + ) + data = response.json() + if data.get("count", 0) > 0: + rir = data["results"][0] + if rir.get("name") != name: + patch_payload = {"name": name} + patch_response = self.session.patch( + f"{self.netbox_url}/api/ipam/rirs/{rir['id']}/", + json=patch_payload, + ) + if patch_response.status_code != 200: + print( + f"Warning: Unable to update RIR '{slug}' name: " + f"{patch_response.status_code} {patch_response.text}" + ) + return rir["id"] + + rir_payload = { + "name": name, + "slug": slug, + "is_private": False, + "description": "For EDA managed resources", + } + create_response = self.session.post( + f"{self.netbox_url}/api/ipam/rirs/", json=rir_payload + ) + if create_response.status_code == 201: + rir = create_response.json() + print(f"Created RIR '{name}' with ID {rir['id']}") + return rir["id"] + + print( + "Error: Unable to create RIR '" + f"{name}' ({create_response.status_code} {create_response.text})" + ) + return None + + def create_asn_ranges(self): + """Create ASN ranges used for EDA allocations""" + rir_id = self.create_rir() + if rir_id is None: + return + + asn_ranges = [ + { + "name": "eda-asns", + "slug": "eda-asns", + "start": 65000, + "end": 65100, + "description": "EDA managed private ASNs", + "rir": rir_id, + "tags": [{"name": "eda-asns"}], + } + ] + + print("Creating ASN ranges...") + for asn_range in asn_ranges: + response = self.session.get( + f"{self.netbox_url}/api/ipam/asn-ranges/?slug={asn_range['slug']}" + ) + data = response.json() + if data.get("count", 0) > 0: + existing = data["results"][0] + patch_payload = { + "name": asn_range["name"], + "start": asn_range["start"], + "end": asn_range["end"], + "description": asn_range["description"], + "rir": rir_id, + "tags": [dict(tag) for tag in asn_range["tags"]], + } + patch_response = self.session.patch( + f"{self.netbox_url}/api/ipam/asn-ranges/{existing['id']}/", + json=patch_payload, + ) + if patch_response.status_code == 200: + print(f"ASN range '{asn_range['name']}' updated successfully") + else: + print( + f"Error updating ASN range '{asn_range['name']}': " + f"{patch_response.status_code} {patch_response.text}" + ) + continue + + legacy_response = self.session.get( + f"{self.netbox_url}/api/ipam/asn-ranges/?slug=eda-ans" + ) + legacy_data = legacy_response.json() + if legacy_data.get("count", 0) > 0: + legacy = legacy_data["results"][0] + patch_payload = { + "name": asn_range["name"], + "slug": asn_range["slug"], + "start": asn_range["start"], + "end": asn_range["end"], + "description": asn_range["description"], + "rir": rir_id, + "tags": [dict(tag) for tag in asn_range["tags"]], + } + patch_response = self.session.patch( + f"{self.netbox_url}/api/ipam/asn-ranges/{legacy['id']}/", + json=patch_payload, + ) + if patch_response.status_code == 200: + print( + "Legacy ASN range 'eda-ans' migrated to 'eda-asns' successfully" + ) + else: + print( + "Error migrating legacy ASN range 'eda-ans': " + f"{patch_response.status_code} {patch_response.text}" + ) + continue + + create_response = self.session.post( + f"{self.netbox_url}/api/ipam/asn-ranges/", json=asn_range + ) + if create_response.status_code == 201: + print(f"ASN range '{asn_range['name']}' created successfully") + else: + print( + f"Error creating ASN range '{asn_range['name']}': " + f"{create_response.status_code} {create_response.text}" + ) + def create_prefixes(self): """Create example prefixes for EDA allocation pools""" prefixes = [ @@ -239,6 +443,8 @@ def main(): if webhook_id: configurator.create_event_rule(webhook_id) configurator.create_prefixes() + configurator.create_vlan_groups() + configurator.create_asn_ranges() print("\nNetBox configuration completed!") print(f"You can now access NetBox at: {netbox_url}") From 163ee925643a9f01f89e93c0e21663da8aa63b98 Mon Sep 17 00:00:00 2001 From: FloSch62 Date: Fri, 17 Oct 2025 08:57:09 +0200 Subject: [PATCH 5/6] fixed branch --- scripts/import_device_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/import_device_types.py b/scripts/import_device_types.py index 58c8774..19730fc 100755 --- a/scripts/import_device_types.py +++ b/scripts/import_device_types.py @@ -21,7 +21,7 @@ DEFAULT_LIBRARY_URL = "https://github.com/netbox-community/devicetype-library.git" -DEFAULT_LIBRARY_BRANCH = "develop" +DEFAULT_LIBRARY_BRANCH = "master" DEFAULT_K8S_NAMESPACE = "netbox" DEFAULT_CLUSTER_NETBOX_URL = "http://netbox-server.netbox.svc.cluster.local" From 7814b3f49b600c68f3fb31359d14503ffca93f2b Mon Sep 17 00:00:00 2001 From: FloSch62 Date: Mon, 20 Oct 2025 13:15:18 +0200 Subject: [PATCH 6/6] correct import image --- scripts/import_device_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/import_device_types.py b/scripts/import_device_types.py index 19730fc..0cc5fb9 100755 --- a/scripts/import_device_types.py +++ b/scripts/import_device_types.py @@ -82,7 +82,7 @@ def parse_args() -> argparse.Namespace: ) parser.add_argument( "--importer-image", - default="ghcr.io/minitriga/netbox-device-type-library-import:latest", + default="kifeo/netbox-device-type-library-import:latest", help="Container image to use for the Kubernetes job", ) return parser.parse_args()