Skip to content

Development

Requires uv and Docker. PowerShell (pwsh) is optional: with it, the unit tests also run the real scripts against stub cmdlets.

git clone https://github.com/thomaschristory/netbox-windows-dhcp-plugin
cd netbox-windows-dhcp-plugin
uv sync

Make targets

Target Does
make lint ruff check and ruff format --check (line length 120)
make format Fix what ruff can and format
make test Unit tests (tests/): the Windows layer, the diff engine, the docs. No NetBox needed
make dev Start the dev stack: NetBox 4.7 with netbox-plugin-dhcp, netbox-branching and netbox-change-control, at http://localhost:8000
make test-netbox NetBox integration tests (tests_netbox/) inside the dev stack
make restart Restart NetBox and the worker after Python changes (the source is mounted into the containers)
make logs Follow the NetBox log
make down Stop the dev stack
make dist Build the wheel and sdist and check them
make docs Build this site into site/ with Zensical (strict)
make docs-serve Serve the site with live reload
make test-lab Opt-in lab tests against a real Windows server, see below
make lab-e2e Opt-in end-to-end run on the dev stack against that server

The dev stack runs in branching mode. Its first start migrates a fresh database and takes several minutes. Log in as admin with the SUPERUSER_PASSWORD from dev/env/netbox.env.

Code layout

netbox_windows_dhcp_plugin/
  windows/      pure Python: PowerShell scripts, transports (WinRM, SSH), the client (one function per cmdlet),
                normalisation, and an in-memory fake server for tests. No Django imports
  sync/         diff engine, Windows and NetBox records, push planning and writing, import
  models/ api/ graphql/ views.py forms.py ...   the NetBox side
  connection.py builds a client from a Windows server and its credential profile
  jobs.py       every job; all Windows I/O happens here
  integrations/branching.py   the only module that imports netbox-branching and netbox-change-control

The design explains the decisions; the roadmap what is done.

Rules worth knowing before a change:

  • Values reach PowerShell only through the $p JSON literal built by windows/psscript.py; never format a value into a script.
  • Never log or store credentials.
  • A new cmdlet gets an implementation in windows/fake.py, a JSON fixture under tests/windows/testdata/ and a pwsh stub test; mark it # unverified: until it has run against a real server.
  • Model changes need a migration and refreshed query-count baselines in both standalone and branching modes.
  • Update CHANGELOG.md under Unreleased with every user-visible change, and these pages when behaviour changes.

Testing tiers

  1. Unit (make test): script rendering and envelope parsing, normalisation, the client against recorded fixtures and the fake server, the diff engine, and checks on these docs.
  2. PowerShell (part of make test, skipped without pwsh; the PWSH variable picks the binary): the real scripts run against stub cmdlets in tests/windows/testdata/stubs.ps1.
  3. NetBox (make test-netbox): models, views, API, import, push and leases against the fake server. CI runs it in standalone and branching modes.
  4. Lab (opt-in): a real Windows DHCP server, see below.

CI runs lint, unit tests on Python 3.12 to 3.14, the build, the docs build, and the NetBox tests in both modes.

Lab tests

The lab tier checks the client and the whole sync against a real Windows DHCP server. CI never runs it.

Use a lab server

The tests only create objects named nbwd-test-... in 198.18.0.0/15 and 2001:db8:4e42::/64 (MAC addresses 02-4e-42-...), check name and range before every delete, and clean up even when a test fails. Still, never point them at a production server.

Preparing a lab VM

Any Windows Server 2016 or later VM will do; an evaluation edition on a host-only network is enough, since the tests never serve real clients. In an elevated PowerShell on the VM, run the setup script from the repository:

Set-ExecutionPolicy -Scope Process Bypass
.\dev\scripts\lab-server-setup.ps1 -AllowFrom 192.168.56.0/24

It installs the DHCP role, creates a local account nbwd-lab in DHCP Administrators and Remote Management Users (asking for its password), sets up WinRM over HTTPS on 5986 with a self-signed certificate, opens the firewall (optionally only to the given network), and prints the settings for the next step. It is safe to run again.

Running them

Put the connection settings in dev/env/netbox.env (never committed) or the environment:

Variable Meaning
WINDOWS_DHCP_LAB_HOST Host name or address of the lab server
WINDOWS_DHCP_USERNAME, WINDOWS_DHCP_PASSWORD The account
WINDOWS_DHCP_LAB_PORT Optional, default 5986
WINDOWS_DHCP_LAB_AUTH Optional: ntlm (default), basic or kerberos
WINDOWS_DHCP_LAB_REALM Optional, Kerberos realm
WINDOWS_DHCP_LAB_INSECURE 1 skips the certificate check (self-signed lab certificate)
WINDOWS_DHCP_LAB_RECORD 1 saves every raw PowerShell answer under .lab/record/, to turn into fixtures after scrubbing

Then:

make test-lab        # the client over WinRM, one round trip per unverified cmdlet
make dev             # recreate the dev stack once, so it mounts the lab helpers
make lab-e2e         # import, push creates, updates and deletes, leases, on the dev stack

make test-lab writes property dumps to .lab/probe/. make lab-e2e prints PASSED at the end and exits non-zero on failure. Both leave the server as they found it. Everything under .lab/ can identify the lab and is ignored by git: scrub host names and addresses before turning a recording into a fixture.

Documentation

These pages live in docs/ and are built with Zensical from mkdocs.yml:

make docs-serve      # http://localhost:8001, reloads on save
make docs            # strict build into site/

make test checks the docs too: no em dashes, the configuration table matches the plugin's defaults, every page is in the navigation, and the PLUGINS_CONFIG examples are valid. The site is published to GitHub Pages when a release is tagged.

Releasing

  1. Update netbox_windows_dhcp_plugin/version.py and move the Unreleased section of CHANGELOG.md under the new version and date.
  2. make dist, merge to main.
  3. Tag vX.Y.Z on main and push the tag. The release workflow checks the tag is on main, builds, and publishes to PyPI through trusted publishing after a maintainer approves the pypi environment; the docs workflow publishes the site.