Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f329980
add air sim tui to mono repo
zsblevins May 30, 2026
5c3a053
finish out port of air tui + add guide
zsblevins May 30, 2026
0c785cc
eliminate deprecated airv1 workflows that were replaced with the sim …
zsblevins May 30, 2026
4c5d482
address code rabbit findings
zsblevins May 30, 2026
52f4a83
fixes for larger superpod demo topology + some ui simplification
zsblevins May 31, 2026
563c6d8
update sim data for larger sim
zsblevins May 31, 2026
a20b325
add screenshot generator for workflow UI
zsblevins May 31, 2026
789e431
fixes for trial topology
zsblevins Jun 1, 2026
6c0db09
fix broken dhcp data in superpod sim
zsblevins Jun 1, 2026
5ba5ce4
layout and responsiveness improvements
zsblevins Jun 1, 2026
1c3c047
lint and test fixes
zsblevins Jun 1, 2026
55142a7
resolve discrepancies between air-inside and public air image names a…
zsblevins Jun 2, 2026
9268be2
test cpu mode change in public air
zsblevins Jun 2, 2026
95d98be
final updates to air sim after public test
zsblevins Jun 2, 2026
c5462a3
coderabbit feedback
zsblevins Jun 2, 2026
a16e17f
Update docs/getting-started/index.mdx
zsblevins Jun 3, 2026
9744a91
Update docs/install/install-airgapped.mdx
zsblevins Jun 3, 2026
85c67b8
update Air product name throughout
zsblevins Jun 3, 2026
ab9240c
Update docs/user-guides/air-simulation/index.mdx
zsblevins Jun 3, 2026
34dd0c0
Update docs/user-guides/air-simulation/index.mdx
zsblevins Jun 3, 2026
c7c3a71
Update development/mock_topology/context/air_trial/README.md
zsblevins Jun 3, 2026
e9cfdf9
Update docs/user-guides/air-simulation/index.mdx
zsblevins Jun 3, 2026
5c5dbfe
fix docs lint
zsblevins Jun 3, 2026
ba25cf3
fix the duplication caused by the suggestion
zsblevins Jun 3, 2026
fa50fd5
last docs fixes
polarweasel Jun 3, 2026
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
7 changes: 7 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Repository Instructions

## AIR Simulation Demo Credentials

The AIR simulation intentionally uses hard-coded public demo credentials for ephemeral NVIDIA DSX Air demo nodes and generated demo services. These values are centralized in `installer/src/nv_config_manager_installer/air_sim/constants.py`, including `NVCM_BOX_PASSWORD`, `NVCM_SECRETS`, `NVCM_NETWORK_SECRETS`, and Nautobot demo user defaults.

Do not report those AIR demo credentials as leaked production secrets. Continue to flag user/API credentials, private keys, NGC API keys, Git tokens, and any credentials outside the intentional AIR demo constants.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ local_dev.ini
values-generated.yaml
nv-config-manager-install.yaml
installer/nv-config-manager-install.yaml
.nvcm-air-sim.yaml
installer/.nvcm-air-sim.yaml
nvcm-air-sim.yaml
installer/nvcm-air-sim.yaml

# Spyder project settings
.spyderproject
Expand Down
19 changes: 17 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.PHONY: help install dev test lint format clean docker-build docker-push ui-install ui-dev ui-build \
local-up local-down local-destroy local-status local-logs deploy kind-up kind-down topology install-cert \
openapi openapi-check docs-assets docs-assets-check docs-format docs-lint docs-lint-fern docs-live docs-preview docs-publish docs-publish-in-ci docs-screenshots \
openapi openapi-check docs-assets docs-assets-check docs-format docs-lint docs-lint-fern docs-live docs-preview docs-publish docs-publish-in-ci docs-screenshots docs-air-sim-screenshots docs-ui-screenshots \
obs-grafana obs-prometheus obs-loki obs-alloy obs-port-forward obs-port-forward-stop

# Configuration
Expand All @@ -25,6 +25,11 @@ LATEST_RELEASE_TAG = $(shell git tag --list 2>/dev/null | grep -E '^[v]?[0-9]+\.
TEMPLATE_ENGINE_BASE_VERSION = $(subst -rc,rc,$(subst -rc.,rc,$(patsubst v%,%,$(LATEST_RELEASE_TAG))))
TEMPLATE_ENGINE_VERSION ?= $(if $(TEMPLATE_ENGINE_BASE_VERSION),$(TEMPLATE_ENGINE_BASE_VERSION)+g$(GIT_SHA),)
TEMPLATE_ENGINE_VERSION_ARG = $(if $(TEMPLATE_ENGINE_VERSION),--build-arg TEMPLATE_ENGINE_VERSION=$(TEMPLATE_ENGINE_VERSION),)
NVCM_NUMPY_FROM_SOURCE ?=
NVCM_NUMPY_CPU_BASELINE ?=
NVCM_NUMPY_CPU_DISPATCH ?=
NVCM_NUMPY_ALLOW_NOBLAS ?=
NVCM_NUMPY_BUILD_ARGS = $(if $(NVCM_NUMPY_FROM_SOURCE),--build-arg NVCM_NUMPY_FROM_SOURCE=$(NVCM_NUMPY_FROM_SOURCE),) $(if $(NVCM_NUMPY_CPU_BASELINE),--build-arg NVCM_NUMPY_CPU_BASELINE=$(NVCM_NUMPY_CPU_BASELINE),) $(if $(NVCM_NUMPY_CPU_DISPATCH),--build-arg NVCM_NUMPY_CPU_DISPATCH=$(NVCM_NUMPY_CPU_DISPATCH),) $(if $(NVCM_NUMPY_ALLOW_NOBLAS),--build-arg NVCM_NUMPY_ALLOW_NOBLAS=$(NVCM_NUMPY_ALLOW_NOBLAS),)
NAUTOBOT_APP_OVERLAYS_VERSION ?= $(TEMPLATE_ENGINE_VERSION)
NAUTOBOT_APP_OVERLAYS_VERSION_ARG = $(if $(NAUTOBOT_APP_OVERLAYS_VERSION),--build-arg NAUTOBOT_APP_OVERLAYS_VERSION=$(NAUTOBOT_APP_OVERLAYS_VERSION),)
NAUTOBOT_NV_CONFIG_MANAGER_VERSION ?= $(TEMPLATE_ENGINE_VERSION)
Expand Down Expand Up @@ -97,6 +102,8 @@ help:
@echo " make docs-preview - Generate a Fern docs preview"
@echo " make docs-publish - Publish the Fern docs"
@echo " make docs-screenshots - Regenerate installer TUI screenshots for docs"
@echo " make docs-air-sim-screenshots - Regenerate DSX Air sim TUI screenshots for docs"
@echo " make docs-ui-screenshots - Regenerate Next.js workflow screenshots for docs"
@echo ""
@echo "Observability (local-dev stack only — requires observability to be enabled in installer config):"
@echo " make obs-grafana - Port-forward Grafana -> http://localhost:3000 (admin/admin)"
Expand Down Expand Up @@ -324,6 +331,12 @@ docs-publish-in-ci:
docs-screenshots:
cd installer && uv run python scripts/screenshot_tui.py

docs-air-sim-screenshots:
cd installer && uv run python scripts/screenshot_air_sim_tui.py

docs-ui-screenshots:
cd ui && npm run docs:screenshots

clean:
find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true
Expand Down Expand Up @@ -365,7 +378,7 @@ docker-build:
# Build NVIDIA Config Manager services image
docker-build-nv-config-manager:
@echo "🏗️ Building NVIDIA Config Manager services image with tag $(LOCAL_TAG)..."
docker build --provenance=false $(APT_MIRROR_DEBIAN_ARGS) $(TEMPLATE_ENGINE_VERSION_ARG) -t nv-config-manager:$(LOCAL_TAG) -f build/nv-config-manager.Dockerfile .
docker build --provenance=false $(APT_MIRROR_DEBIAN_ARGS) $(TEMPLATE_ENGINE_VERSION_ARG) $(NVCM_NUMPY_BUILD_ARGS) -t nv-config-manager:$(LOCAL_TAG) -f build/nv-config-manager.Dockerfile .
@echo "✅ Built nv-config-manager:$(LOCAL_TAG)"

# Build NVIDIA Config Manager KEA DHCP server image
Expand Down Expand Up @@ -487,6 +500,7 @@ docker-build-single-nv-config-manager: ## Builds and pushes NVIDIA Config Manage
$(EXTRA_TAGS) \
$(APT_MIRROR_DEBIAN_ARGS) \
$(TEMPLATE_ENGINE_VERSION_ARG) \
$(NVCM_NUMPY_BUILD_ARGS) \
-f build/nv-config-manager.Dockerfile \
--push \
.
Expand Down Expand Up @@ -597,6 +611,7 @@ docker-build-nv-config-manager-multiarch: docker-buildx-setup ## Builds and push
$(EXTRA_TAGS) \
$(APT_MIRROR_DEBIAN_ARGS) \
$(TEMPLATE_ENGINE_VERSION_ARG) \
$(NVCM_NUMPY_BUILD_ARGS) \
-f build/nv-config-manager.Dockerfile \
--push \
.
Expand Down
2 changes: 1 addition & 1 deletion THIRD_PARTY_LICENSES.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ The following Python packages are dependencies of NVIDIA Config Manager. See `py
| requests-aws4auth | MIT | https://github.com/tedder/requests-aws4auth |
| brotli | MIT | https://github.com/google/brotli |
| slack-sdk | MIT | https://github.com/slackapi/python-slack-sdk |
| air-sdk | MIT | https://pypi.org/project/air-sdk/ |
| nv-air-sdk | MIT | https://pypi.org/project/nv-air-sdk/ |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This list is getting pretty long; is it worth sorting it, or is there a method to the ordering already?

| python-json-logger | BSD-2-Clause | https://github.com/madzak/python-json-logger |
| py-markdown-table | MIT | https://github.com/hvalev/py-markdown-table |
| mkdocs | BSD-2-Clause | https://github.com/mkdocs/mkdocs |
Expand Down
18 changes: 17 additions & 1 deletion build/nv-config-manager.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim AS builder

ARG APT_MIRROR_DEBIAN=""
ARG APT_MIRROR_GPG_KEY_URL=""
ARG NVCM_NUMPY_FROM_SOURCE=false
ARG NVCM_NUMPY_CPU_BASELINE=min
ARG NVCM_NUMPY_CPU_DISPATCH=max
ARG NVCM_NUMPY_ALLOW_NOBLAS=true

# Install build dependencies for native extensions (numpy, psycopg2, etc.)
# Also install openssh-server to get the moduli file and sftp binary for SFTP server
Expand Down Expand Up @@ -52,7 +56,19 @@ RUN --mount=type=cache,id=nvcm-uv-cache,target=/root/.cache/uv \
if [ -n "$TEMPLATE_ENGINE_VERSION" ]; then \
export SETUPTOOLS_SCM_PRETEND_VERSION="$TEMPLATE_ENGINE_VERSION"; \
fi; \
uv sync --frozen --no-dev --group integration-test --no-editable && \
if [ "$NVCM_NUMPY_FROM_SOURCE" = "true" ]; then \
uv sync \
--frozen \
--no-dev \
--group integration-test \
--no-editable \
--no-binary-package numpy \
--config-settings-package "numpy:setup-args=-Dcpu-baseline=${NVCM_NUMPY_CPU_BASELINE}" \
--config-settings-package "numpy:setup-args=-Dcpu-dispatch=${NVCM_NUMPY_CPU_DISPATCH}" \
--config-settings-package "numpy:setup-args=-Dallow-noblas=${NVCM_NUMPY_ALLOW_NOBLAS}"; \
else \
uv sync --frozen --no-dev --group integration-test --no-editable; \
fi; \
chmod -R a+rX /code/nv-config-manager/.venv /code/nv-config-manager/db /code/nv-config-manager/src

# =============================================================================
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ def get_content_types(self, content_type_strings):
self.logger.warning(f"Could not find content type: {ct_string}")
return content_types

def add_content_types(self, obj, content_type_strings):
"""Add content type memberships without removing existing memberships."""
content_types = self.get_content_types(content_type_strings)
if content_types:
obj.content_types.add(*content_types)

def run(self):
"""Execute the job to load bootstrap data.

Expand Down Expand Up @@ -314,10 +320,10 @@ def load_roles(self):
},
)

# Set content_types for both new and existing roles
# Add content_types for both new and existing roles without
# removing memberships created by other jobs.
if "content_types" in role_data:
content_types = self.get_content_types(role_data["content_types"])
role.content_types.set(content_types)
self.add_content_types(role, role_data["content_types"])
role.validated_save()

if created:
Expand Down Expand Up @@ -380,10 +386,9 @@ def load_tags(self):
},
)

# Set content_types for both new and existing tags
# Add content_types without removing memberships created by other jobs.
if "content_types" in tag_data:
content_types = self.get_content_types(tag_data["content_types"])
tag.content_types.set(content_types)
self.add_content_types(tag, tag_data["content_types"])

if created:
self.logger.success(
Expand Down Expand Up @@ -585,10 +590,9 @@ def load_location_types(self):
},
)

# Set content_types for both new and existing location types
# Add content_types without removing memberships created by other jobs.
if "content_types" in lt_data:
content_types = self.get_content_types(lt_data["content_types"])
lt.content_types.set(content_types)
self.add_content_types(lt, lt_data["content_types"])

if created:
self.logger.success(
Expand Down Expand Up @@ -709,10 +713,9 @@ def load_statuses(self):
},
)

# Set content_types for both new and existing statuses
# Add content_types without removing memberships created by other jobs.
if "content_types" in status_data:
content_types = self.get_content_types(status_data["content_types"])
status.content_types.set(content_types)
self.add_content_types(status, status_data["content_types"])

if created:
self.logger.success(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,6 @@
role: system-admin
deployment_types: [all]

- name: Cumulus Linux Firmware 5.14.0
description: Intended firmware version for Cumulus Linux switches
weight: 1000
is_active: true
platforms:
- Cumulus Linux
data:
intended-firmware:
version: "5.14.0"
deployment_types: [all]

- name: NVIDIA Config Manager DHCP Custom Options
description: Custom DHCP option definitions for KEA (cumulus-provision-url)
weight: 1000
Expand Down
81 changes: 80 additions & 1 deletion components/nautobot/tests/test_load_bootstrap_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,8 @@ def test_loads_role_with_content_types(self, tmp_path):
job.load_roles()

Role.objects.get_or_create.assert_called_once()
mock_role.content_types.set.assert_called_once()
mock_role.content_types.add.assert_called_once()
mock_role.content_types.set.assert_not_called()
job.logger.success.assert_called()

def test_missing_file(self, tmp_path):
Expand Down Expand Up @@ -395,6 +396,25 @@ def test_creates_tag(self, tmp_path):
Tag.objects.update_or_create.assert_called_once()
job.logger.success.assert_called()

def test_adds_tag_content_types_without_replacing_existing(self, tmp_path):
mod = _import_module()
from django.contrib.contenttypes.models import ContentType
from nautobot.extras.models import Tag

mock_tag = MagicMock()
mock_ct = MagicMock()
Tag.objects.update_or_create.return_value = (mock_tag, False)
ContentType.objects.get.return_value = mock_ct

data = [{"name": "dhcp-subnet", "content_types": ["ipam.prefix"]}]
_write_yaml(tmp_path / "tags.yaml", data)

job = _make_job(mod, tmp_path)
job.load_tags()

mock_tag.content_types.add.assert_called_once_with(mock_ct)
mock_tag.content_types.set.assert_not_called()


# ---------------------------------------------------------------------------
# load_platforms
Expand Down Expand Up @@ -504,6 +524,51 @@ def test_creates_status(self, tmp_path):

Status.objects.update_or_create.assert_called_once()

def test_adds_status_content_types_without_replacing_existing(self, tmp_path):
mod = _import_module()
from django.contrib.contenttypes.models import ContentType
from nautobot.extras.models import Status

mock_status = MagicMock()
mock_ct = MagicMock()
Status.objects.update_or_create.return_value = (mock_status, False)
ContentType.objects.get.return_value = mock_ct

data = [{"name": "Active", "content_types": ["ipam.prefix"]}]
_write_yaml(tmp_path / "statuses.yaml", data)

job = _make_job(mod, tmp_path)
job.load_statuses()

mock_status.content_types.add.assert_called_once_with(mock_ct)
mock_status.content_types.set.assert_not_called()


# ---------------------------------------------------------------------------
# load_location_types
# ---------------------------------------------------------------------------


class TestLoadLocationTypes:
def test_adds_location_type_content_types_without_replacing_existing(self, tmp_path):
mod = _import_module()
from django.contrib.contenttypes.models import ContentType
from nautobot.dcim.models import LocationType

mock_location_type = MagicMock()
mock_ct = MagicMock()
LocationType.objects.get_or_create.return_value = (mock_location_type, False)
ContentType.objects.get.return_value = mock_ct

data = [{"name": "Site", "content_types": ["dcim.device"]}]
_write_yaml(tmp_path / "location_types.yaml", data)

job = _make_job(mod, tmp_path)
job.load_location_types()

mock_location_type.content_types.add.assert_called_once_with(mock_ct)
mock_location_type.content_types.set.assert_not_called()


# ---------------------------------------------------------------------------
# load_config_context_schemas
Expand Down Expand Up @@ -534,6 +599,20 @@ def test_creates_schema(self, tmp_path):


class TestLoadConfigContexts:
def test_bootstrap_config_contexts_do_not_set_intended_firmware(self):
data_path = Path(__file__).resolve().parents[1] / "nv_config_manager_jobs/data/config_contexts.yaml"

with data_path.open() as f:
config_contexts = yaml.safe_load(f)

firmware_contexts = [
config_context.get("name")
for config_context in config_contexts
if "intended-firmware" in config_context.get("data", {})
]

assert firmware_contexts == []

def test_creates_config_context_with_roles_and_platforms(self, tmp_path):
mod = _import_module()
from nautobot.dcim.models import Platform
Expand Down
4 changes: 2 additions & 2 deletions deploy/airgapped/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ cd nv-config-manager-airgapped-v1.0.0-amd64
--username '<user>' \
--password-stdin
./installer/install.sh
./installer/nv-config-manager-installer init --config install.yaml
./installer/nv-config-manager-installer deploy install.yaml --chart-dir helm --image-source registry
./installer/nvcm-installer init --config install.yaml
./installer/nvcm-installer deploy install.yaml --chart-dir helm --image-source registry
```

The upload helper uses bundled Skopeo when present, then system Skopeo, then Docker in `--mode auto`. It uploads the packaged chart with `helm push` and writes `image-map.tsv` for image source-to-target mapping. Use `--plain-http` only for local HTTP registries such as `registry:2` test containers. When using Docker mode with an architecture-specific bundle, pass `--platform linux/amd64` or `--platform linux/arm64` so Docker pushes a single-platform manifest. The installer uses local dependency charts and manifests when `cluster.airgapped` is enabled in the config.
Expand Down
15 changes: 8 additions & 7 deletions deploy/airgapped/create-airgapped.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1399,14 +1399,15 @@ echo "Installing nv-config-manager-installer from vendored wheels (offline)..."
"$SCRIPT_DIR"/nv_config_manager_installer-*.whl

ln -sf "$VENV_DIR/bin/nv-config-manager-installer" "$SCRIPT_DIR/nv-config-manager-installer"
ln -sf "$VENV_DIR/bin/nvcm-installer" "$SCRIPT_DIR/nvcm-installer"

echo ""
echo "nv-config-manager-installer is ready."
echo "nvcm-installer is ready. The longer nv-config-manager-installer command is also available."
echo ""
echo "Usage:"
echo " $SCRIPT_DIR/nv-config-manager-installer init --config install.yaml"
echo " $SCRIPT_DIR/nv-config-manager-installer validate install.yaml"
echo " $SCRIPT_DIR/nv-config-manager-installer deploy install.yaml --chart-dir ../helm --image-source registry"
echo " $SCRIPT_DIR/nvcm-installer init --config install.yaml"
echo " $SCRIPT_DIR/nvcm-installer validate install.yaml"
echo " $SCRIPT_DIR/nvcm-installer deploy install.yaml --chart-dir ../helm --image-source registry"
echo ""
echo "Or add to PATH:"
echo " export PATH=\"$VENV_DIR/bin:\$PATH\""
Expand Down Expand Up @@ -1497,8 +1498,8 @@ The helper uploads images from images/image-list.txt and the packaged Helm chart
## Install From Bundle

./installer/install.sh
./installer/nv-config-manager-installer init --config install.yaml
./installer/nv-config-manager-installer deploy install.yaml --chart-dir helm --image-source registry
./installer/nvcm-installer init --config install.yaml
./installer/nvcm-installer deploy install.yaml --chart-dir helm --image-source registry

Configure install.yaml image settings to point at the registry image paths written in image-map.tsv. If the target environment preloads node runtimes instead of using a registry, use manifests/load-airgapped-images.sh before deploying.
BUNDLE_README
Expand Down Expand Up @@ -1544,7 +1545,7 @@ print_summary() {
echo " 3. cd nv-config-manager-airgapped-${VERSION}-<arch>/"
echo " 4. Upload images and chart: ./upload-to-registry.sh --registry registry.example.com/nv-config-manager --chart-registry registry.example.com/nv-config-manager/charts --username '<user>' --password-stdin"
echo " 5. Install CLI: ./installer/install.sh"
echo " 6. Configure/deploy with ./installer/nv-config-manager-installer"
echo " 6. Configure/deploy with ./installer/nvcm-installer"
echo ""
}

Expand Down
4 changes: 2 additions & 2 deletions deploy/airgapped/manifests/load-airgapped-images.sh
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,7 @@ if [[ "$USE_DAEMONSET" == "true" ]]; then
echo "Next steps:"
echo " Deploy with --airgapped flag:"
echo ""
echo " ./installer/nv-config-manager-installer deploy install.yaml --chart-dir helm --image-source registry \\"
echo " ./installer/nvcm-installer deploy install.yaml --chart-dir helm --image-source registry \\"
echo " --airgapped \\"
echo " --auto-generate-secrets --yes"
echo ""
Expand Down Expand Up @@ -656,7 +656,7 @@ echo ""
echo "Next steps:"
echo " Deploy with --airgapped flag (images are already loaded, no DaemonSet needed):"
echo ""
echo " ./installer/nv-config-manager-installer deploy install.yaml --chart-dir helm --image-source registry \\"
echo " ./installer/nvcm-installer deploy install.yaml --chart-dir helm --image-source registry \\"
echo " --airgapped \\"
echo " --auto-generate-secrets --yes"
echo ""
2 changes: 0 additions & 2 deletions deploy/airgapped/values-airgapped-extract.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,6 @@ secrets:
path: "nv-config-manager/bmc"
slack:
path: "nv-config-manager/slack"
air:
path: "nv-config-manager/air"
ufm:
path: "nv-config-manager/ufm"
nautobotApp:
Expand Down
Loading