Skip to content
Draft
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
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 Credential

The AIR simulation intentionally uses `NVCM_BOX_PASSWORD` as a hard-coded public demo VM password for ephemeral NVIDIA DSX Air demo nodes. It appears in AIR simulation code, TUI tests, and generated AIR screenshot SVGs.

Do not report that specific AIR demo credential as a leaked production secret. Continue to flag any other hard-coded credentials, API tokens, private keys, or passwords.
6 changes: 5 additions & 1 deletion 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 \
obs-grafana obs-prometheus obs-loki obs-alloy obs-port-forward obs-port-forward-stop

# Configuration
Expand Down Expand Up @@ -97,6 +97,7 @@ 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 AIR sim TUI 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 +325,9 @@ 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

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
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/ |
| 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
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 ""
7 changes: 0 additions & 7 deletions deploy/helm/sample-eso-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -124,13 +124,6 @@ secrets:
keys:
password: "password"

# AIR service credentials
air:
path: "prod/air"
keys:
ssaClientId: "ssa_client_id"
ssaClientSecret: "ssa_client_secret"

# Jira service account credentials (used by DiagnosticsWorkflow)
jira:
path: "prod/jira"
Expand Down
8 changes: 0 additions & 8 deletions deploy/helm/sample-nv-config-manager.ini
Original file line number Diff line number Diff line change
Expand Up @@ -122,21 +122,13 @@ local = false
server = https://elasticsearch.example.com
domain = nv-config-manager

[temporal.air]
ssa_client_id = <secret>
ssa_client_secret = <secret>
org_id = org-12345
air_api_url = https://air.nvidia.com/api
air_node_user = nv-config-manager

# -----------------------------------------------------------------
# Temporal API Configuration (REST API for workflow operations)
# -----------------------------------------------------------------
[temporal.api]
# CORS origins allowed to make cross-origin requests with credentials
# Comma-separated list of allowed origins (e.g., "https://config-manager.example.com")
cors_origins = https://config-manager.example.com
air_node_password = <secret>

# =============================================================================
# DEVICE / NETWORK CREDENTIALS
Expand Down
14 changes: 0 additions & 14 deletions deploy/helm/templates/_vault-agent.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -219,10 +219,6 @@ Consul-template prelude: declare $secret vars for nv-config-manager.ini (same KV
{{- printf "{{- $network := secret %q -}}\n" (printf "%s/data/%s" $m $np) -}}
{{- $rf := include "nv-config-manager.vault.secretPath" (dict "root" $root "secret" "redfish") -}}
{{- printf "{{- $redfish := secret %q -}}\n" (printf "%s/data/%s" $m $rf) -}}
{{- if $root.Values.temporal.air.orgId -}}
{{- $ap := include "nv-config-manager.vault.secretPath" (dict "root" $root "secret" "air") -}}
{{- printf "{{- $air := secret %q -}}\n" (printf "%s/data/%s" $m $ap) -}}
{{- end -}}
{{- end -}}
{{- if $root.Values.networkDhcp.enabled -}}
{{- printf "{{- $leasedb := secret %q -}}\n" (printf "%s/data/%s" $m $pg) -}}
Expand Down Expand Up @@ -372,16 +368,6 @@ nv-config-manager.ini body (consul-template): must stay in sync with vault-secre
domain = {{ $root.Values.externalServices.elasticsearch.domain }}
{{- end }}

{{- if $root.Values.temporal.air.orgId }}
[temporal.air]
ssa_client_id = {{ include "nv-config-manager.vaultAgent.ctKv2Key" (dict "var" "air" "key" (include "nv-config-manager.vault.keyName" (dict "root" $root "secret" "air" "key" "ssaClientId"))) }}
ssa_client_secret = {{ include "nv-config-manager.vaultAgent.ctKv2Key" (dict "var" "air" "key" (include "nv-config-manager.vault.keyName" (dict "root" $root "secret" "air" "key" "ssaClientSecret"))) }}
org_id = {{ $root.Values.temporal.air.orgId }}
air_api_url = {{ $root.Values.temporal.air.airApiUrl }}
air_node_user = {{ $root.Values.temporal.air.airNodeUser }}
air_node_password = {{ $root.Values.temporal.air.airNodePassword }}
{{- end }}

# -----------------------------------------------------------------
# Temporal API Configuration (REST API for workflow operations)
# -----------------------------------------------------------------
Expand Down
Loading
Loading