NodeLink storage seam and typed mounts

Task 8776 · Done · Coder · created · updated

## Requirement Original requirement (epic, verbatim): "Add support for GCP and Azure with the same functionality as AWS: - Your Cloud (former Your AWS) - NodeLink may work on GCP and Azure - What else we have AWS specific Out of Scope: Jaah Hosted (still remains AWS only)" Acceptance this child owns (PRD R6, verbatim): - AC6.2 A NodeLink on a GCP storage attaches to a GCP node (Azure → Azure node) live, mounts fail closed and the node reports its real mount state; tokens shared through `{jaah-home}/auth/.shared` work across two nodes of that provider, exactly as `docs/policies/nodelink.md` states for EFS. → verify: attach to two nodes, log an account in on one, observe it usable on the other. (This child: the node-side mount half and the wire; the GCP/Azure CP adapters are the next NodeLink child.) - AC6.4 Attaching a storage of provider X to a node of provider Y is refused with a readable reason (cross-cloud is out of scope). → verify: try it on dev CP. - AC8.3 (storage doors) Every server door (connection create/update, launch, lifecycle actions, storage create) enforces the provider switch. → verify: API test per door returns 403 with the switch off. (This child: NodeLink attach and EFS storage create/adopt.) - AC9.1 (storage) The provider-specific part of each capability (… storage …) sits behind one provider interface … ## Scope (Architecture §2.4, D6, D8, D11, risk 4) 1. `apps/api/app/clouds/storage.py`: `SharedStorageProvider` protocol — `create_storage`, `adopt_storage`, `delete_storage`, `create_link`, `adopt_link`, `delete_link`, `mount_spec(link) -> dict`, `min_size_note() -> str` — and a registry keyed by provider. Refactor EFS (`apps/api/app/org_shared_store.py`, `apps/api/app/nodelinks.py` access-point create `:1343`) onto it with NO behaviour change (existing NodeLink/EFS tests pass unmodified). 2. Node wire v2 entry (`nodelinks.py:2015-2066`) gains one key `mount`; existing EFS keys stay for old daemons: ```json {"path": "...", "authoritative": true, "mount": {"type": "efs", "efs_id": "...", "access_point_id": "...", "region": "...", "mount_ip": "..."}} {"mount": {"type": "nfs", "server": "10.x.x.x", "export": "/share1/nodelinks/<id>", "root_export": "/share1"}} {"mount": {"type": "aznfs", "server": "<share-host>", "export": "/<share-path>"}} ``` `apps/jaahd/vnodehelper/protocol.py` validates `type`; an unknown type fails closed (no mount, reported unmounted). 3. Node side: `apps/jaahd/nodelink_mount.py` + `packaging/jaah-shared-mount` typed mounts `efs` (unchanged), `nfs` (NFSv4.1, no TLS; for `root_export` mount briefly to `mkdir -m 0755 /nodelinks/<id>` + `chown 3000:3000`, then mount the subdir) and `aznfs` (TLS via stunnel). NFS/aznfs mount at a staging path; a `bindfs` view at the NodeLink path with `--force-user=3000 --force-group=3000 --create-for-user=3000 --create-for-group=3000` and `noexec,nosuid,nodev` (D8). `apps/jaahd/shared_storage.py` / `apps/api/app/shared_storage.py` fstype set gains `fuse.bindfs`; `ismount` and `nodelink_converge` fail-closed checks verify BOTH layers (bindfs over nfs4). 4. Packaging deps (`packaging/nfpm.yaml:127-150`): `nfs-common`, `bindfs` from Ubuntu; `aznfs` via the same apt-channel route as `amazon-efs-utils`; installed only where needed. Windows nodes via the WSL2 engine distro path (Task 8737) — verify FUSE works in that distro. 5. Cross-cloud refusal in the attach path: `store.provider != node.infra.provider` → 409 "This storage is in <X>; this node runs in <Y>." Self-hosted nodes keep today's rule (EFS reachable or not). `require_provider` on NodeLink attach and on EFS storage create/adopt (AWS switch). 6. `docs/policies/nodelink.md` (single home): the directive changes from "always tls" to "tls where the storage offers it (EFS, Azure)"; describe typed mounts and the bindfs uid layer. Provider-specific GCP/Azure sections are written by the next NodeLink child. Recon: `apps/api/app/models/nodelinks.py:61-147`; `apps/api/app/nodelinks.py:1343, 2015-2066`; `packaging/jaah-shared-mount:166`; `apps/jaahd/nodelink_mount.py:607`. Credentials & reach: Unit/integration tests | none beyond the worktree (`./dc check quick`/`full`; jaahd tests with fake mount commands) | **verified**: nothing external. Live two-node GCP/Azure token-sharing oracle | `jaah/test/gcp`, `jaah/test/azure` | **deferred**: not on the roster; operator BA D6 — runs in the epic's "Dev oracle run" child. Out of scope: Do NOT touch Jaah Cloud / Jaah Hosted (N1) or Jaah's platform AWS usage (N2). No cross-cloud NodeLink (GCP/Azure node mounting AWS EFS or vice versa) (N6). No change to how AWS EFS NodeLinks behave for the user (N7). No migration (Foundation child owns `provider`/`provider_state`). Do NOT build the Filestore/Azure Files CP adapters, StorageSourceFields choices or min-size notes (next NodeLink child). MicroVM sessions still see no NodeLink mount. Done means: - EFS refactor no-op — verify: existing NodeLink/EFS suites (`tests/test_org_shared_store.py` with `FakeAws`, nodelink wire/converge tests) pass unmodified; the v2 wire for an EFS link carries both the old keys and `mount.type == "efs"`. - AC6.2 node half — verify: jaahd tests: an `nfs` spec runs the root-export mkdir/chown then subdir mount then bindfs with the exact flags; `aznfs` uses the TLS mount type; an unknown `type` mounts nothing and reports unmounted; `ismount` reports unmounted when either layer is missing (fail closed); a file created through the bindfs view is owned 3000:3000. Live two-node token share (deferred — runs in "Dev oracle run"). - AC6.4 — verify: API test attaching an `aws` store to a node with `infra.provider = gcp` returns 409 with the exact text; self-hosted node attach unchanged. - AC8.3 storage doors — verify: with AWS switch OFF, EFS storage create/adopt and NodeLink attach return 403 (one test each). - AC9.1 — verify: `grep -nE 'provider *(==|!=) *["\x27]|== *"(aws|gcp|azure)"'` over the NodeLink route and attach/converge modules returns nothing except the single cross-cloud comparison `store.provider != node.infra.provider`. - `docs/policies/nodelink.md` states the tls-where-offered directive and typed mounts; `python3 tools/check_context.py` green. - `./dc check quick` green; merge lane `./dc check full` green; Jaah Deploy succeeds; `.deb` builds with the new deps.

Outcome

Shipped NodeLink storage-provider seam (EFS onto it, no behaviour change), typed v2 `mount` wire, cross-cloud 409 + Your Cloud switch 403s, and node-side nfs/aznfs mounts via a 3000:3000 bindfs view. Quick gate green; lane full gate merged #6553 (6517e09); its deploy was superseded by later merges. Live GCP/Azure oracle deferred; draft #8792 filed. Memory: none applied.

Pull request

Artifacts

Activity

  • Draft
  • Ready
  • Runner started
  • Runner ended (done)