NodeLink storage seam and typed mounts
## 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.
Artifacts
Activity
- Draft
- Ready
- Runner started
- Runner ended (done)