Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# SPDX-License-Identifier: MIT
name: Weekly test against NetBox main
name: Test against live NetBox

on:
schedule:
- cron: '0 6 * * 1' # Every Monday at 06:00 UTC
workflow_dispatch:
pull_request:

permissions:
contents: read
Expand All @@ -17,6 +18,14 @@
integration-test:
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
# Weekly runs track the moving target (main) only; released tags are
# immutable, so the full supported matrix (latest patch of each line)
# runs on pull requests and on demand instead.
netbox_ref: ${{ github.event_name == 'schedule' && fromJSON('["main"]') || fromJSON('["main", "v4.5.10", "v4.4.10", "v4.3.7"]') }}

services:
postgres:
image: postgres:16
Expand Down Expand Up @@ -52,20 +61,23 @@
- name: Install uv
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
enable-cache: true
# No Actions cache in this job: it executes third-party code (the
# NetBox checkout), which must not be able to poison the default
# branch cache from the scheduled/dispatched privileged context.
enable-cache: false

- name: Checkout NetBox main
- name: Checkout NetBox ${{ matrix.netbox_ref }}
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
repository: netbox-community/netbox
path: netbox
ref: main
ref: ${{ matrix.netbox_ref }}

- name: Install NetBox dependencies
run: pip install -r netbox/requirements.txt

- name: Configure NetBox

Check failure

Code scanning / CodeQL

Cache Poisoning via execution of untrusted code High test

Potential cache poisoning in the context of the default branch due to privilege checkout of untrusted code from
matrix.netbox_ref
. (
schedule
).
Potential cache poisoning in the context of the default branch due to privilege checkout of untrusted code from
matrix.netbox_ref
. (
workflow_dispatch
).
run: |
python3 -c "
import textwrap, pathlib
Expand All @@ -92,7 +104,7 @@
# (June 2026) enforces object-level 'view' permission when serving
# /media/devicetype-images/ and /media/image-attachments/ via MediaView, and that
# view's TokenConditionalLoginRequiredMixin only authenticates the API token when
# LOGIN_REQUIRED is True. With it True, the integration test's Bearer-token session
# LOGIN_REQUIRED is True. With it True, the integration test's token-authenticated session
# resolves to the admin superuser, restrict() grants access, and the image is
# served (a genuinely missing file still 404s through django.views.static.serve).
# NOTE: this also gates the REST API, so the readiness curl below must send the token.
Expand All @@ -100,11 +112,11 @@
''').lstrip(), encoding='utf-8')
"

- name: Run NetBox migrations
working-directory: netbox/netbox
run: python manage.py migrate --no-input

- name: Create admin user and API token

Check failure

Code scanning / CodeQL

Cache Poisoning via execution of untrusted code High test

Potential cache poisoning in the context of the default branch due to privilege checkout of untrusted code from
matrix.netbox_ref
. (
schedule
).
Potential cache poisoning in the context of the default branch due to privilege checkout of untrusted code from
matrix.netbox_ref
. (
workflow_dispatch
).
working-directory: netbox/netbox
run: |
DJANGO_SUPERUSER_PASSWORD=admin python manage.py createsuperuser \
Expand All @@ -115,33 +127,40 @@
user = get_user_model().objects.get(username='admin')
t = Token(user=user)
t.save()
# v2 token credential: nbt_<key>.<plaintext> (plaintext only available right after save)
print(f'nbt_{t.key}.{t.token}')
# v2 credential nbt_<key>.<plaintext> (NetBox >= 4.5, plaintext only available
# right after save) or the classic plaintext key on 4.3/4.4.
token = getattr(t, 'token', None)
print(f'nbt_{t.key}.{token}' if token else t.key)
" 2>/dev/null | tail -1)
echo "NETBOX_TOKEN=$TOKEN" >> "$GITHUB_ENV"
# The importer picks the scheme the same way: Bearer for nbt_, else Token.
case "$TOKEN" in
nbt_*) echo "NETBOX_AUTH_SCHEME=Bearer" >> "$GITHUB_ENV" ;;
*) echo "NETBOX_AUTH_SCHEME=Token" >> "$GITHUB_ENV" ;;
esac
echo "Created API token"

- name: Start NetBox dev server
working-directory: netbox/netbox
run: |
rm -f /tmp/netbox.log && touch /tmp/netbox.log
python manage.py runserver 0.0.0.0:8000 >> /tmp/netbox.log 2>&1 &
echo $! > /tmp/netbox.pid
# LOGIN_REQUIRED=True gates the REST API, so the readiness probe must authenticate
# with the token created above (exposed as $NETBOX_TOKEN via GITHUB_ENV); an
# anonymous curl would 403 and never report ready.
# Wait up to 60 s for NetBox to respond
for i in $(seq 1 30); do
if curl -sf -H "Authorization: Bearer $NETBOX_TOKEN" http://localhost:8000/api/ > /dev/null 2>&1; then
if curl -sf -H "Authorization: $NETBOX_AUTH_SCHEME $NETBOX_TOKEN" http://localhost:8000/api/ > /dev/null 2>&1; then
echo "NetBox is ready (attempt $i)"
break
fi
echo "Waiting for NetBox... ($i/30)"
sleep 2
done
curl -sf -H "Authorization: Bearer $NETBOX_TOKEN" http://localhost:8000/api/ > /dev/null || { echo "NetBox did not start"; exit 1; }
curl -sf -H "Authorization: $NETBOX_AUTH_SCHEME $NETBOX_TOKEN" http://localhost:8000/api/ > /dev/null || { echo "NetBox did not start"; exit 1; }

- name: Install importer dependencies

Check failure

Code scanning / CodeQL

Cache Poisoning via execution of untrusted code High test

Potential cache poisoning in the context of the default branch due to privilege checkout of untrusted code from
matrix.netbox_ref
. (
schedule
).
Potential cache poisoning in the context of the default branch due to privilege checkout of untrusted code from
matrix.netbox_ref
. (
workflow_dispatch
).
working-directory: importer
run: uv sync

Expand Down Expand Up @@ -172,9 +191,9 @@
REPO_BRANCH: main
run: uv run pytest tests/integration/ -m integration -x -v --timeout=600

- name: Print NetBox version on failure
if: failure()
run: |
curl -s -H "Authorization: Bearer $NETBOX_TOKEN" http://localhost:8000/api/status/ | python3 -m json.tool || true
curl -s -H "Authorization: $NETBOX_AUTH_SCHEME $NETBOX_TOKEN" http://localhost:8000/api/status/ | python3 -m json.tool || true
echo "--- NetBox server log ---"
cat /tmp/netbox.log 2>/dev/null | tail -50 || true

Check failure

Code scanning / CodeQL

Cache Poisoning via execution of untrusted code High test

Potential cache poisoning in the context of the default branch due to privilege checkout of untrusted code from
matrix.netbox_ref
. (
schedule
).
Potential cache poisoning in the context of the default branch due to privilege checkout of untrusted code from
matrix.netbox_ref
. (
workflow_dispatch
).
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# NetBox Device Type Import

[![Tests](https://github.com/marcinpsk/Device-Type-Library-Import/actions/workflows/tests.yml/badge.svg)](https://github.com/marcinpsk/Device-Type-Library-Import/actions/workflows/tests.yml)
[![NetBox main](https://github.com/marcinpsk/Device-Type-Library-Import/actions/workflows/test-netbox-main.yaml/badge.svg)](https://github.com/marcinpsk/Device-Type-Library-Import/actions/workflows/test-netbox-main.yaml)
[![NetBox](https://img.shields.io/badge/NetBox-3.2%2B_through_4.5%2B-blue)](https://netbox.dev)
[![Live NetBox](https://github.com/marcinpsk/Device-Type-Library-Import/actions/workflows/test-netbox.yaml/badge.svg)](https://github.com/marcinpsk/Device-Type-Library-Import/actions/workflows/test-netbox.yaml)
[![NetBox](https://img.shields.io/badge/NetBox-4.3%2B-blue)](https://netbox.dev)
[![Python](https://img.shields.io/badge/python-3.12%2B-blue)](https://www.python.org)
[![Container image](https://img.shields.io/badge/ghcr.io-device--type--library--import-2496ED?logo=docker&logoColor=white)](https://github.com/marcinpsk/Device-Type-Library-Import/pkgs/container/device-type-library-import)

This library is intended to be your friend and help you import all the device-types defined within
the [NetBox Device Type Library Repository](https://github.com/netbox-community/devicetype-library).

> **Tested working with NetBox 3.2+ through 4.5+** (weekly CI run against NetBox `main`)
> **Requires NetBox 4.3 or later.** Every pull request and manual CI run exercises the full import pipeline against the latest 4.3, 4.4, and 4.5 patch releases plus NetBox `main`; a weekly scheduled run tracks NetBox `main`.

## Description

Expand Down
34 changes: 24 additions & 10 deletions tests/integration/test_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,20 @@ def _test_images(fd: dict, fd_id: int) -> None:
# ──────────────────────────────────────────────────────────────────────────────


def front_port_mappings(fp: dict) -> list:
"""Normalize a front-port template's rear-port linkage across NetBox versions.

NetBox >= 4.5 serves an M2M ``rear_ports`` list of PortMapping entries;
older releases serve a scalar ``rear_port`` object plus ``rear_port_position``.
"""
if "rear_ports" in fp:
return fp.get("rear_ports") or []
rear_port = fp.get("rear_port")
if not rear_port:
return []
return [{"rear_port": rear_port["id"], "rear_port_position": fp.get("rear_port_position")}]


def test_front_port_multiposition() -> None:
print("\n=== Scenario E: Front-port multi-position linkage ===")
fd = get_one("/dcim/device-types/", slug="testvendor-full-device")
Expand All @@ -299,28 +313,28 @@ def test_front_port_multiposition() -> None:

for name, expected_pos in [("FP1", 1), ("FP2", 2)]:
fp = fps[name]
mapping = fp.get("rear_ports", [])
mapping = front_port_mappings(fp)
if not mapping:
fail(f"{name}: rear_ports is empty — M2M linkage not created")
fail(f"{name}: rear-port linkage is empty — mapping not created")
pos = mapping[0].get("rear_port_position")
if pos != expected_pos:
fail(f"{name}: rear_port_position = {pos!r}, expected {expected_pos}")
ok(f"{name}: rear_port_position = {pos}")

# Both front ports should point to the same rear port
rp1_id = fps["FP1"]["rear_ports"][0]["rear_port"]
rp2_id = fps["FP2"]["rear_ports"][0]["rear_port"]
rp1_id = front_port_mappings(fps["FP1"])[0]["rear_port"]
rp2_id = front_port_mappings(fps["FP2"])[0]["rear_port"]
if rp1_id != rp2_id:
fail(f"FP1 and FP2 point to different rear ports ({rp1_id} vs {rp2_id}), expected same RP1")
ok("FP1 and FP2 both map to the same rear port (RP1)")

# Also check patch-panel front ports
pp = get_one("/dcim/device-types/", slug="testvendor-patch-panel-4")
pp_fps = api("/dcim/front-port-templates/", device_type_id=pp["id"])["results"]
broken = [fp["name"] for fp in pp_fps if not fp.get("rear_ports")]
broken = [fp["name"] for fp in pp_fps if not front_port_mappings(fp)]
if broken:
fail(f"patch-panel-4: {len(broken)} front ports have empty rear_ports: {broken}")
ok(f"patch-panel-4: all {len(pp_fps)} front ports have rear_ports mapping")
fail(f"patch-panel-4: {len(broken)} front ports have empty rear-port linkage: {broken}")
ok(f"patch-panel-4: all {len(pp_fps)} front ports have a rear-port mapping")


# ──────────────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -354,10 +368,10 @@ def test_module_types() -> None:

# Front port linkage for module type
mt_fps = api("/dcim/front-port-templates/", module_type_id=mt_id)["results"]
broken = [fp["name"] for fp in mt_fps if not fp.get("rear_ports")]
broken = [fp["name"] for fp in mt_fps if not front_port_mappings(fp)]
if broken:
fail(f"full-module: {len(broken)} front ports have empty rear_ports: {broken}")
ok("full-module: front port rear_ports mapping set correctly")
fail(f"full-module: {len(broken)} front ports have empty rear-port linkage: {broken}")
ok("full-module: front port rear-port mapping set correctly")


# ──────────────────────────────────────────────────────────────────────────────
Expand Down