Skip to content

Commit 0682000

Browse files
authored
feat(bind-mount): add validated bind mount for pre-provisioned local NVMe (#89)
## Description **TL;DR.** New standalone package `bind-mount`: it exposes one existing host mount at a second stable path through a persistent, owner-marked systemd bind mount, with a fail-closed safety contract on every destructive path. It does no device discovery, no RAID assembly, and no mkfs; it only aliases a mount that platform bootstrap already created, and it survives reboots (which a raw `mount --bind` in user data does not). ### What the package does - Validates the source in the host mount namespace: it must be an exact, read-write mount point, with optional fstype (`expected-source-fstype`) and device-prefix (`expected-source-device-prefix`) assertions. - Renders and enables a systemd `.mount` unit (bind) tagged with an owner marker, then relies on the package's controlled interrupt (reboot) to activate it; the post-interrupt check proves the active mount binds the desired source before the node returns to service. - Persists root-only receipts under `/var/lib/nodewright-packages/bind-mount/` before any systemd operation, so uninstall works even when the operator runs uninstall without the original ConfigMap. - All 10 lifecycle modes are wired in `config.json` and idempotent; `make validate-standalone PACKAGE=bind-mount` passes against the agent v1 schemas. ### Fail-closed guarantees - Refuses a non-empty target directory (never hides existing data), a symlinked target, a target owned by `/etc/fstab`, and any pre-existing unit file with different content (foreign units are never overwritten or removed; removal requires the owner marker). - Refuses source/target overlap, a read-only source or target mount, and a config change while the old target is still mounted (explicit remediation message instead of silent re-pointing). - Uninstall never unmounts a live target: it removes persistence only and tells the operator to recycle the node, so a running workload is never yanked out from under. - A unit rejected by `systemd-analyze verify` is rolled back; a failed `systemctl enable` retains the receipt so cleanup still works. ### Included hardening (folded in after an adversarial review of the initial draft) 1. **ConfigMap-less uninstall of a never-installed package is now a clean no-op.** Previously, if install had never succeeded (no receipts) and the operator sent an uninstall without the package ConfigMap (both the legacy stub-uninstall lineage and the current faux-package path do this), `load_desired_config` hard-failed on the missing `source` key and package removal wedged on that node forever. Now that state degrades to a best-effort sweep: remove any owner-marked `.mount` units, succeed, write nothing. Covered by a new lifecycle test (`test_uninstall_of_never_installed_package_is_clean_noop`): failed install (wrong source fstype), ConfigMaps deleted, then `uninstall` and `uninstall-check` both succeed. 2. **Kubelet ordering parity in the rendered unit.** The unit now renders `Before=local-fs.target kubelet.service snap.kubelet-eks.daemon.service`, matching the equivalent launch-template user-data variant, so on reboot the mount orders before kubelet on both EKS AMI flavors. systemd ignores `Before=` entries for units that do not exist on the node, so this is inert on non-Kubernetes hosts. This is ordering hygiene, not a success gate; the post-interrupt check is the gate. ### Consumer context First consumer is DGXC AV TestStudio (Skyhook/NodeWright on EKS, internal tracker NKX-12808): moving BuildBarn worker file pools from the root EBS volume onto pre-provisioned local NVMe (`/mnt/k8s-disks/0` RAID0 from the EKS AMI's `setup-local-disks`, exposed at `/mnt/data`). Publication of `ghcr.io/nvidia/nodewright-packages/bind-mount:0.1.0` unblocks that fleet rollout; the consumer-side Skyhook config is already staged behind a default-off flag pending this image. ### Testing - `make test-package PACKAGE=bind-mount`: 32 passed (16 test functions x ubuntu:22.04/24.04), including the new never-installed uninstall test. - `make validate-standalone PACKAGE=bind-mount`: schema, step files, and executable-bit validation all pass against `ghcr.io/nvidia/skyhook/agent:latest`. - `shellcheck` (stable) is clean on `bind_mount.sh`. - Lifecycle tests run the real script in containers against fakes for `findmnt`/`nsenter`/`stat`/`systemctl`/`systemd-analyze`/`systemd-escape`; no real systemd or mounts are exercised. On-node validation on an EKS GPU node is planned on the consumer side before any fleet enablement. ## Checklist - [x] I am familiar with the [Contributing Guidelines](https://github.com/NVIDIA/nodewright-packages/blob/HEAD/CONTRIBUTING.md). - [x] New or existing tests cover these changes. - [x] The documentation is up to date with these changes. Signed-off-by: Gregory Giecold <ggiecold@nvidia.com>
1 parent cd1ee10 commit 0682000

28 files changed

Lines changed: 2669 additions & 5 deletions

.github/ISSUE_TEMPLATE/bug_report_form.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ body:
3939
attributes:
4040
label: Which package does this relate to?
4141
options:
42+
- bind-mount
4243
- shellscript
4344
- tuning
4445
- tuned

.github/ISSUE_TEMPLATE/feature_request_form.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ body:
5353
attributes:
5454
label: Which package does this relate to?
5555
options:
56+
- bind-mount
5657
- shellscript
5758
- tuning
5859
- tuned

.github/labeler.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ package/copy-fail:
2727
- any-glob-to-any-file:
2828
- 'copy-fail/**'
2929

30+
package/bind-mount:
31+
- changed-files:
32+
- any-glob-to-any-file:
33+
- 'bind-mount/**'
34+
3035
package/kdump:
3136
- changed-files:
3237
- any-glob-to-any-file:

.github/labels.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,10 @@
130130
color: "1d76db"
131131
description: "Relates to the copy-fail package"
132132

133+
- name: "package/bind-mount"
134+
color: "1d76db"
135+
description: "Relates to the bind-mount package"
136+
133137
- name: "package/kdump"
134138
color: "1d76db"
135139
description: "Relates to the kdump package"

.github/workflows/triage.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ jobs:
7676
'nvidia-setup',
7777
'copy-fail',
7878
'shellscript',
79+
'bind-mount',
7980
'kdump',
8081
'tuned',
8182
'tuning',

AGENTS.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ This repo holds the pre-built **packages** consumed by the [NVIDIA NodeWright op
1515

1616
This repo does **not** contain the operator, the agent, or any Go code. It is shell scripts (could be python, or any executable, but currently is all shell), `Dockerfile`s, package config, and Python-based tests/tooling.
1717

18-
**Rename status (Skyhook to NodeWright):** the project is being renamed from Skyhook to NodeWright. Many public names still use `skyhook` on purpose: the published image path (`ghcr.io/nvidia/skyhook-packages/...`), the CRD group (`skyhook.nvidia.com/v1alpha1`), the on-host `SKYHOOK_DIR`/`skyhook_dir` names, and the CLI. The GitHub repo itself is now `nodewright-packages` (with `skyhook-packages` redirecting). Do not "fix" `skyhook` to `nodewright` or vice versa; match whatever the surrounding code already uses.
18+
**Rename status (Skyhook to NodeWright):** the project is being renamed from Skyhook to NodeWright. New package releases publish at `ghcr.io/nvidia/nodewright-packages/...`; older package versions may still exist only under the legacy `ghcr.io/nvidia/skyhook-packages/...` namespace. The CRD group (`skyhook.nvidia.com/v1alpha1`), the on-host `SKYHOOK_DIR`/`skyhook_dir` names, and the CLI still use `skyhook` on purpose. The GitHub repo itself is now `nodewright-packages` (with `skyhook-packages` redirecting). Do not rename remaining `skyhook` references without checking their specific compatibility contract.
1919

2020
## Repository layout
2121

22-
- **Each top-level directory that contains a `Dockerfile` is a package.** Current packages: `shellscript`, `tuning`, `tuned`, `kdump`, `nvidia-setup`, `nvidia-tuning-gke`, `nvidia-tuned`, `copy-fail`.
22+
- **Each top-level directory that contains a `Dockerfile` is a package.** Current packages: `shellscript`, `tuning`, `tuned`, `kdump`, `nvidia-setup`, `nvidia-tuning-gke`, `nvidia-tuned`, `copy-fail`, `bind-mount`.
2323
- `scripts/`: repo tooling (`validate.py`, `sync_labels.py`).
2424
- `tests/integration/`: Docker-based pytest suites, one subdir per package that has tests (with hyphens converted to underscores, e.g. `nvidia-setup` to `nvidia_setup`; not every package has a subdir).
2525
- `.github/`: workflows, the `build-package` composite action, issue/PR templates, `labels.yml`, `CODEOWNERS`.
@@ -105,7 +105,7 @@ Almost all package logic is bash. The existing scripts are not yet consistent (s
105105

106106
- **On PR (`pr_build.yaml`):** detects which package directories changed, then for each changed package runs validate, test, and a dev build tagged `<version>-dev.sha<short>`. Changed-package detection keys on a directory having a `Dockerfile`; `scripts/` and hidden dirs are skipped.
107107
- **On tag push `<package>/<version>`:** two independent workflows run. `build_container.yaml` builds and pushes the multi-arch image (`linux/amd64,linux/arm64`) with a build-provenance attestation; `release.yml` creates the GitHub Release with `git-cliff` notes scoped to that package. The image build (here and in `pr_build.yaml`) goes through the `.github/actions/build-package` composite action. (`build_package.yml` is a reusable `workflow_call` workflow that exists but is not currently wired up.)
108-
- **Published image path:** `ghcr.io/nvidia/skyhook-packages/<package>:<version>` (the `skyhook-packages` segment is retained pending the rename).
108+
- **Published image path for new releases:** `ghcr.io/nvidia/nodewright-packages/<package>:<version>`. Verify legacy versions before changing an existing consumer because some older tags remain only under `ghcr.io/nvidia/skyhook-packages/`.
109109
- An optional `<package>/preprocess.sh` is run before the image build and can emit `BUILD_ARGS`.
110110

111111
## Code review (CodeRabbit)

README.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,19 @@ A temporary mitigation package for **CVE-2026-31431 ("Copy Fail")** that disable
118118

119119
**Key features:**
120120
- Distro-agnostic (works anywhere `kmod`/`modprobe` is honored)
121-
- No reboot, no configmap, no interrupts minimal surface area
121+
- No reboot, no configmap, no interrupts: minimal surface area
122122
- Designed to be retired once distros ship a fixed kernel
123123

124+
### 8. Bind Mount Package (`bind-mount/`)
125+
126+
A fail-closed package that exposes an existing host mount at a stable second path through a persistent systemd bind mount.
127+
128+
**Capabilities:**
129+
- Validates the exact source mount, filesystem type, device prefix, and read-write state
130+
- Refuses to hide nonempty target data or replace foreign mount definitions
131+
- Activates through a controlled NodeWright interrupt and verifies the host mount afterward
132+
- Removes persistence on uninstall without live-unmounting active workloads
133+
124134
## Package Structure
125135

126136
Each package follows the standard NodeWright package structure:
@@ -261,6 +271,7 @@ This validation step is crucial as the agent uses JSON schema validation to ensu
261271

262272
1. **Choose a package** that fits your use case:
263273
- `shellscript` for custom scripts and automation
274+
- `bind-mount` for a persistent alias to an existing host filesystem
264275
- `tuning` for system-level configuration management
265276
- `tuned` for automated performance tuning with the tuned daemon
266277
- `kdump` for kernel crash dump collection and debugging
@@ -278,6 +289,7 @@ This validation step is crucial as the agent uses JSON schema validation to ensu
278289
- [Kdump Package](./kdump/README.md) - Usage guide for the kdump package
279290
- [NVIDIA Setup Package](./nvidia-setup/README.md) - Node setup (EFA, Lustre, chrony, local disks) per service/accelerator
280291
- [Copy-Fail Package](./copy-fail/README.md) - CVE-2026-31431 modprobe-blacklist mitigation
292+
- [Bind Mount Package](./bind-mount/README.md) - Persistent, fail-closed bind mounts for existing host storage
281293
- [NVIDIA NodeWright Documentation](https://github.com/NVIDIA/skyhook) - Main NodeWright operator documentation
282294

283295
## Contributing
@@ -286,4 +298,4 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines on contributing to this
286298

287299
## License
288300

289-
This project is licensed under the terms specified in [LICENSE](./LICENSE).
301+
This project is licensed under the terms specified in [LICENSE](./LICENSE).

bind-mount/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Changelog
2+
3+
All notable changes to this package will be documented in this file.
4+
5+
## [0.1.0]
6+
7+
### New Features
8+
9+
- Initial release of a fail-closed, systemd-managed bind mount package.

bind-mount/Dockerfile

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
FROM busybox:latest
18+
19+
RUN mkdir -p /skyhook-package/skyhook_dir
20+
COPY . /skyhook-package

bind-mount/README.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Bind Mount Package
2+
3+
The `bind-mount` package exposes one existing host mount at a second stable path through a persistent systemd bind mount. It does not discover, format, assemble, or delete storage.
4+
5+
The package is intended for nodes where platform bootstrap owns the source filesystem and a workload needs a stable path. It validates the source in the host mount namespace, writes and enables a systemd mount unit, and relies on a NodeWright interrupt to activate the unit before workloads resume.
6+
7+
## Configuration
8+
9+
```yaml
10+
apiVersion: skyhook.nvidia.com/v1alpha1
11+
kind: Skyhook
12+
metadata:
13+
name: local-data-bind
14+
spec:
15+
runtimeRequired: true
16+
interruptionBudget:
17+
count: 1
18+
packages:
19+
bind-mount:
20+
version: 0.1.0
21+
image: ghcr.io/nvidia/nodewright-packages/bind-mount
22+
interrupt:
23+
type: reboot
24+
configMap:
25+
source: /mnt/k8s-disks/0
26+
target: /mnt/data
27+
expected-source-fstype: xfs
28+
expected-source-device-prefix: /dev/md
29+
```
30+
31+
`source` and `target` are required. `expected-source-fstype` and `expected-source-device-prefix` are optional fail-closed assertions.
32+
33+
The image may be referenced under multiple package map keys on the same node to
34+
manage multiple bind mounts. Each instance has its own ownership receipt and
35+
owner marker, scoped to the Skyhook resource UID and package map key. Targets
36+
must be distinct: two instances that name the same systemd mount unit fail
37+
closed rather than sharing or replacing it.
38+
39+
## Safety contract
40+
41+
- The source must already be an exact, read-write mount point.
42+
- The target must be empty, or already be the same bind mount backed by this package instance's owned unit; otherwise the package fails.
43+
- The systemd unit rechecks target emptiness at activation and fails if the target is already non-empty, reducing the risk of silently hiding newly written data.
44+
- Existing symlinks, foreign systemd units, and `/etc/fstab` ownership are rejected.
45+
- Source and target paths must be disjoint; neither may contain the other.
46+
- Prepared and active checks reject a read-only target mount.
47+
- A source or target configuration change fails while that instance's old target remains mounted. Remove the package and recycle the node before changing paths.
48+
- Uninstall disables and removes the persistent unit but deliberately does not unmount a live target. Recycle the node to complete rollback safely.
49+
- A root-only ownership receipt supports ConfigMap-less uninstall on operators older than v0.16.0 and preserves cleanup after an interrupted install on newer operators. The receipt is removed after `uninstall-check` confirms that no owned unit remains.
50+
- Uninstall of a package instance that never installed succeeds as a clean no-op, even when the uninstall runs without a ConfigMap. It only sweeps units carrying that exact Skyhook-resource/package-key owner marker and cannot remove another instance's unit.
51+
- The mount unit orders before `local-fs.target` and the kubelet units (`kubelet.service`, `snap.kubelet-eks.daemon.service`) so a rebooting node mounts the target before workloads start. Ordering is hygiene, not a success gate; the post-interrupt check is the gate.
52+
53+
The apply and config checks validate the prepared unit before an interrupt. The post-interrupt check validates the active host mount. Consumers should use `runtimeRequired: true` and a bounded interruption budget.

0 commit comments

Comments
 (0)