@homeflare/alchemy 0.10.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/caddy/admin-guard.d.ts +1 -1
- package/dist/caddy/index.js.map +4 -4
- package/dist/cloudflare/client.d.ts +1 -1
- package/dist/cloudflare/index.js.map +2 -2
- package/dist/cloudflare/lock-rules.d.ts +1 -1
- package/dist/forgejo/branch-protection-form.d.ts +1 -1
- package/dist/forgejo/branch-protection.d.ts +1 -1
- package/dist/forgejo/index.js.map +10 -10
- package/dist/forgejo/org-actions-secrets.d.ts +1 -1
- package/dist/forgejo/org-label.d.ts +1 -1
- package/dist/forgejo/org-team.d.ts +1 -1
- package/dist/forgejo/repo-webhook.d.ts +1 -1
- package/dist/forgejo/repository.d.ts +2 -2
- package/dist/forgejo/repository.d.ts.map +1 -1
- package/dist/forgejo/resource.d.ts +2 -2
- package/dist/forgejo/team-member.d.ts +1 -1
- package/dist/launchd/index.js.map +2 -2
- package/dist/openbao/auth-role.d.ts +1 -1
- package/dist/openbao/bao-http.d.ts +4 -4
- package/dist/openbao/cloudflare-permission-groups.d.ts +1 -1
- package/dist/openbao/cloudflare-role-form.d.ts +2 -2
- package/dist/openbao/cloudflare-role-form.d.ts.map +1 -1
- package/dist/openbao/cloudflare-roles-expand.d.ts +1 -1
- package/dist/openbao/digest.d.ts +1 -1
- package/dist/openbao/forgejo-bootstrap-parse.d.ts +2 -2
- package/dist/openbao/forgejo-bootstrap-parse.d.ts.map +1 -1
- package/dist/openbao/forgejo-bootstrap.d.ts +1 -1
- package/dist/openbao/index.js +10 -8
- package/dist/openbao/index.js.map +18 -18
- package/dist/openbao/mount.d.ts +1 -1
- package/dist/openbao/pki-role.d.ts +1 -1
- package/dist/openbao/policy.d.ts +2 -2
- package/dist/openbao/proxmox-role-form.d.ts +2 -2
- package/dist/openbao/proxmox-role-form.d.ts.map +1 -1
- package/dist/openbao/proxmox-role.d.ts +10 -10
- package/dist/openbao/ssh-role.d.ts +3 -3
- package/dist/ownership/adopting.d.ts +34 -0
- package/dist/ownership/adopting.d.ts.map +1 -0
- package/dist/ownership/probe.d.ts +15 -5
- package/dist/ownership/probe.d.ts.map +1 -1
- package/dist/ownership/rows.d.ts +12 -0
- package/dist/ownership/rows.d.ts.map +1 -1
- package/dist/ownership/store-fixture.d.ts +10 -0
- package/dist/ownership/store-fixture.d.ts.map +1 -0
- package/dist/proxmox/acl.d.ts +2 -2
- package/dist/proxmox/acl.d.ts.map +1 -1
- package/dist/proxmox/api-token-form.d.ts +2 -2
- package/dist/proxmox/api-token.d.ts +11 -11
- package/dist/proxmox/ceph-daemon-form.d.ts +3 -3
- package/dist/proxmox/ceph-daemon.d.ts +19 -19
- package/dist/proxmox/ceph-daemon.d.ts.map +1 -1
- package/dist/proxmox/ceph-flag.d.ts +5 -5
- package/dist/proxmox/ceph-fs-wire.d.ts +5 -5
- package/dist/proxmox/ceph-fs.d.ts +16 -16
- package/dist/proxmox/ceph-osd-tree.d.ts +16 -16
- package/dist/proxmox/ceph-osd.d.ts +5 -5
- package/dist/proxmox/ceph-pool-form.d.ts +13 -13
- package/dist/proxmox/ceph-pool-form.d.ts.map +1 -1
- package/dist/proxmox/ceph-pool.d.ts +3 -3
- package/dist/proxmox/client.d.ts +2 -2
- package/dist/proxmox/credentials.d.ts +8 -8
- package/dist/proxmox/credentials.d.ts.map +1 -1
- package/dist/proxmox/fake-pve-lxc.d.ts +8 -0
- package/dist/proxmox/fake-pve-lxc.d.ts.map +1 -1
- package/dist/proxmox/firewall-alias-form.d.ts +1 -1
- package/dist/proxmox/firewall-alias.d.ts +1 -1
- package/dist/proxmox/generated/pbs.d.ts +1 -1
- package/dist/proxmox/generated/pve.d.ts +1 -1
- package/dist/proxmox/group.d.ts +8 -8
- package/dist/proxmox/ha-resource.d.ts +3 -2
- package/dist/proxmox/ha-resource.d.ts.map +1 -1
- package/dist/proxmox/ha-rule-form.d.ts +5 -5
- package/dist/proxmox/ha-rule.d.ts +3 -3
- package/dist/proxmox/index.d.ts +6 -0
- package/dist/proxmox/index.d.ts.map +1 -1
- package/dist/proxmox/index.js +465 -217
- package/dist/proxmox/index.js.map +63 -58
- package/dist/proxmox/lease-cache.d.ts +4 -4
- package/dist/proxmox/lxc-adoption.d.ts +32 -0
- package/dist/proxmox/lxc-adoption.d.ts.map +1 -0
- package/dist/proxmox/lxc-harness.d.ts +12 -0
- package/dist/proxmox/lxc-harness.d.ts.map +1 -1
- package/dist/proxmox/lxc-props.d.ts +2 -2
- package/dist/proxmox/lxc.d.ts +8 -8
- package/dist/proxmox/lxc.d.ts.map +1 -1
- package/dist/proxmox/members.d.ts +3 -3
- package/dist/proxmox/network-apply-read.d.ts +5 -5
- package/dist/proxmox/network-apply.d.ts +9 -9
- package/dist/proxmox/node-network-form.d.ts +6 -6
- package/dist/proxmox/node-network.d.ts +17 -17
- package/dist/proxmox/node-network.d.ts.map +1 -1
- package/dist/proxmox/notification-target.d.ts +3 -2
- package/dist/proxmox/notification-target.d.ts.map +1 -1
- package/dist/proxmox/pbs-datastore-guard.d.ts +1 -1
- package/dist/proxmox/pbs-datastore.d.ts +10 -10
- package/dist/proxmox/pbs-prune-job.d.ts +2 -2
- package/dist/proxmox/pbs-sync-job.d.ts +4 -4
- package/dist/proxmox/pbs-verify-job.d.ts +1 -1
- package/dist/proxmox/pool.d.ts +4 -4
- package/dist/proxmox/provision-baseline.d.ts +74 -0
- package/dist/proxmox/provision-baseline.d.ts.map +1 -0
- package/dist/proxmox/provision-bootstrap.d.ts +33 -0
- package/dist/proxmox/provision-bootstrap.d.ts.map +1 -0
- package/dist/proxmox/provision-cli-fake.d.ts +38 -0
- package/dist/proxmox/provision-cli-fake.d.ts.map +1 -0
- package/dist/proxmox/provision-declare.d.ts +25 -0
- package/dist/proxmox/provision-declare.d.ts.map +1 -0
- package/dist/proxmox/replication-job.d.ts +9 -8
- package/dist/proxmox/replication-job.d.ts.map +1 -1
- package/dist/proxmox/resource.d.ts +2 -2
- package/dist/proxmox/resource.d.ts.map +1 -1
- package/dist/proxmox/sdn-apply-read.d.ts +7 -7
- package/dist/proxmox/sdn-apply.d.ts +1 -1
- package/dist/proxmox/sdn-subnet-form.d.ts +1 -1
- package/dist/proxmox/sdn-subnet.d.ts +5 -5
- package/dist/proxmox/sdn-vnet.d.ts +5 -6
- package/dist/proxmox/sdn-vnet.d.ts.map +1 -1
- package/dist/proxmox/storage.d.ts +4 -3
- package/dist/proxmox/storage.d.ts.map +1 -1
- package/dist/proxmox/user.d.ts +3 -2
- package/dist/proxmox/user.d.ts.map +1 -1
- package/dist/proxmox/values.d.ts +1 -1
- package/dist/proxmox/zfs-pool-write.d.ts +1 -1
- package/dist/proxmox/zfs-pool.d.ts +5 -5
- package/dist/talos/credentials.d.ts +1 -1
- package/dist/talos/credentials.d.ts.map +1 -1
- package/dist/talos/index.js.map +1 -1
- package/docs/adopt-verify.md +21 -5
- package/docs/adopted-deploys.md +5 -3
- package/docs/ownership.md +27 -2
- package/docs/provision-baseline.md +106 -0
- package/docs/proxmox-lxc-adopt.md +83 -0
- package/docs/proxmox-lxc.md +12 -49
- package/docs/proxmox.md +8 -0
- package/package.json +1 -1
- package/src/caddy/admin-guard.ts +1 -1
- package/src/caddy/local-admin.ts +1 -1
- package/src/cloudflare/client.ts +1 -1
- package/src/cloudflare/lock-rules.ts +1 -1
- package/src/forgejo/branch-protection-form.ts +1 -1
- package/src/forgejo/branch-protection.ts +1 -1
- package/src/forgejo/client.ts +1 -1
- package/src/forgejo/org-actions-secrets.ts +1 -1
- package/src/forgejo/org-label.ts +1 -1
- package/src/forgejo/org-team.ts +1 -1
- package/src/forgejo/repo-webhook.ts +1 -1
- package/src/forgejo/repository.ts +2 -2
- package/src/forgejo/resource.ts +2 -2
- package/src/forgejo/team-member.ts +1 -1
- package/src/openbao/README.md +4 -4
- package/src/openbao/auth-role.ts +1 -1
- package/src/openbao/bao-http.test.ts +2 -2
- package/src/openbao/bao-http.ts +6 -6
- package/src/openbao/bao-status.test.ts +1 -1
- package/src/openbao/bao-status.ts +1 -1
- package/src/openbao/cloudflare-group-scope.test.ts +1 -1
- package/src/openbao/cloudflare-permission-groups.test.ts +1 -1
- package/src/openbao/cloudflare-permission-groups.ts +3 -3
- package/src/openbao/cloudflare-policy.ts +1 -1
- package/src/openbao/cloudflare-role-form.ts +2 -2
- package/src/openbao/cloudflare-roles-expand.ts +1 -1
- package/src/openbao/digest.ts +1 -1
- package/src/openbao/forgejo-bootstrap-parse.test.ts +3 -3
- package/src/openbao/forgejo-bootstrap-parse.ts +2 -2
- package/src/openbao/forgejo-bootstrap.ts +4 -4
- package/src/openbao/kubernetes-role-form.test.ts +5 -5
- package/src/openbao/mount.ts +1 -1
- package/src/openbao/pki-role.ts +1 -1
- package/src/openbao/policy-wire.test.ts +3 -3
- package/src/openbao/policy.ts +2 -2
- package/src/openbao/proxmox-role-form.ts +5 -5
- package/src/openbao/proxmox-role.ts +11 -11
- package/src/openbao/ssh-role.ts +3 -3
- package/src/ownership/adopting.test.ts +63 -0
- package/src/ownership/adopting.ts +46 -0
- package/src/ownership/ownership.test.ts +1 -34
- package/src/ownership/probe.ts +27 -14
- package/src/ownership/rows.ts +8 -2
- package/src/ownership/store-fixture.ts +50 -0
- package/src/proxmox/acl.ts +4 -4
- package/src/proxmox/adopt-noop.test.ts +6 -6
- package/src/proxmox/api-token-form.ts +4 -4
- package/src/proxmox/api-token.ts +13 -13
- package/src/proxmox/ceph-daemon-form.ts +3 -3
- package/src/proxmox/ceph-daemon.ts +20 -20
- package/src/proxmox/ceph-flag.ts +7 -7
- package/src/proxmox/ceph-fs-wire.ts +7 -7
- package/src/proxmox/ceph-fs.ts +18 -18
- package/src/proxmox/ceph-osd-tree.ts +16 -16
- package/src/proxmox/ceph-osd.ts +6 -6
- package/src/proxmox/ceph-pool-adopt.test.ts +14 -14
- package/src/proxmox/ceph-pool-form.ts +13 -13
- package/src/proxmox/ceph-pool.ts +6 -6
- package/src/proxmox/client.test.ts +3 -3
- package/src/proxmox/client.ts +3 -3
- package/src/proxmox/credentials.test.ts +6 -8
- package/src/proxmox/credentials.ts +8 -8
- package/src/proxmox/fake-pve-lxc.ts +13 -1
- package/src/proxmox/firewall-alias-form.ts +1 -1
- package/src/proxmox/firewall-alias.ts +1 -1
- package/src/proxmox/generated/pbs.ts +1 -1
- package/src/proxmox/generated/pve.ts +1 -1
- package/src/proxmox/group.ts +10 -10
- package/src/proxmox/ha-resource.ts +3 -2
- package/src/proxmox/ha-rule-form.ts +5 -5
- package/src/proxmox/ha-rule.ts +3 -3
- package/src/proxmox/index.test.ts +7 -0
- package/src/proxmox/index.ts +14 -0
- package/src/proxmox/lease-cache.test.ts +19 -19
- package/src/proxmox/lease-cache.ts +4 -4
- package/src/proxmox/lxc-adopt.test.ts +16 -23
- package/src/proxmox/lxc-adoption.ts +50 -0
- package/src/proxmox/lxc-create-form.ts +1 -1
- package/src/proxmox/lxc-guards.test.ts +10 -17
- package/src/proxmox/lxc-harness.ts +27 -4
- package/src/proxmox/lxc-judge.test.ts +5 -5
- package/src/proxmox/lxc-lifecycle.ts +1 -1
- package/src/proxmox/lxc-ownership.test.ts +19 -14
- package/src/proxmox/lxc-props.ts +2 -2
- package/src/proxmox/lxc-read.test.ts +3 -3
- package/src/proxmox/lxc-strict-adopt.test.ts +104 -0
- package/src/proxmox/lxc-volume.test.ts +9 -9
- package/src/proxmox/lxc-volume.ts +1 -1
- package/src/proxmox/lxc-wire.ts +1 -1
- package/src/proxmox/lxc.ts +19 -15
- package/src/proxmox/members.test.ts +1 -1
- package/src/proxmox/members.ts +3 -3
- package/src/proxmox/metric-server-otel.ts +2 -2
- package/src/proxmox/network-apply-read.ts +10 -10
- package/src/proxmox/network-apply.ts +10 -10
- package/src/proxmox/node-network-form.ts +7 -7
- package/src/proxmox/node-network.ts +18 -18
- package/src/proxmox/notification-target.ts +3 -2
- package/src/proxmox/pbs-datastore-guard.ts +1 -1
- package/src/proxmox/pbs-datastore.ts +10 -10
- package/src/proxmox/pbs-prune-job.ts +2 -2
- package/src/proxmox/pbs-sync-job.ts +4 -4
- package/src/proxmox/pbs-verify-job.ts +1 -1
- package/src/proxmox/pool.test.ts +2 -2
- package/src/proxmox/pool.ts +4 -4
- package/src/proxmox/provision-baseline.test.ts +87 -0
- package/src/proxmox/provision-baseline.ts +162 -0
- package/src/proxmox/provision-bootstrap.test.ts +132 -0
- package/src/proxmox/provision-bootstrap.ts +135 -0
- package/src/proxmox/provision-cli-fake.ts +217 -0
- package/src/proxmox/provision-declare.test.ts +111 -0
- package/src/proxmox/provision-declare.ts +103 -0
- package/src/proxmox/replication-job.ts +9 -8
- package/src/proxmox/resource.ts +3 -3
- package/src/proxmox/sdn-apply-read.test.ts +16 -16
- package/src/proxmox/sdn-apply-read.ts +7 -7
- package/src/proxmox/sdn-apply.ts +3 -3
- package/src/proxmox/sdn-subnet-form.ts +1 -1
- package/src/proxmox/sdn-subnet.ts +5 -5
- package/src/proxmox/sdn-vnet.ts +5 -6
- package/src/proxmox/storage.ts +4 -3
- package/src/proxmox/user.ts +3 -2
- package/src/proxmox/values.test.ts +2 -2
- package/src/proxmox/values.ts +1 -1
- package/src/proxmox/zfs-pool-write.ts +1 -1
- package/src/proxmox/zfs-pool.ts +6 -6
- package/src/talos/credentials.ts +3 -3
- package/src/talos/values.test.ts +9 -9
- package/src/verify/cli.test.ts +1 -1
package/src/proxmox/ceph-fs.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* `Proxmox.CephFs` — a CephFS on the cluster's own Ceph. The filesystem a `cephfs` storage mounts.
|
|
3
3
|
*
|
|
4
4
|
* ⛔ A REPLACE OF THIS RESOURCE DESTROYS A LIVE FILESYSTEM, AND THERE IS NO PUT TO SAVE YOU FROM
|
|
5
|
-
* IT. MEASURED from the published schema on
|
|
5
|
+
* IT. MEASURED from the published schema on node-b, 2026-09-13: `/nodes/{node}/ceph/fs/{name}` has
|
|
6
6
|
* exactly two methods, POST and DELETE. A CephFS cannot be edited in place, so `updateForm` is
|
|
7
7
|
* left undefined and anything the factory can see as changed becomes a REPLACE — and Alchemy's
|
|
8
8
|
* replace is create-then-delete, where the delete is `destroyfs`: the entry leaves the MDS map,
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
* one it is a live filesystem destroyed on every deploy, forever, because a background daemon
|
|
13
13
|
* moved a number nobody declared.
|
|
14
14
|
*
|
|
15
|
-
* ⛔ THERE IS NO SINGLE-OBJECT READ, WHICH INVERTS `path` AND `collection`. MEASURED on
|
|
16
|
-
* pvesh get /nodes/
|
|
17
|
-
* -> No 'get' handler defined for '/nodes/
|
|
15
|
+
* ⛔ THERE IS NO SINGLE-OBJECT READ, WHICH INVERTS `path` AND `collection`. MEASURED on node-b:
|
|
16
|
+
* pvesh get /nodes/node-b/ceph/fs/cephfs-c1
|
|
17
|
+
* -> No 'get' handler defined for '/nodes/node-b/ceph/fs/cephfs-c1'
|
|
18
18
|
* `GET /nodes/{node}/ceph/fs` is a directory index and is the only read there is, so `path()`
|
|
19
19
|
* below returns the INDEX and `attributes` picks this filesystem out of the array client-side —
|
|
20
20
|
* the shape acl.ts uses for the one flat `GET /access/acl`. The POST goes to the `{name}` path,
|
|
@@ -28,9 +28,9 @@
|
|
|
28
28
|
*
|
|
29
29
|
* ⛔ CREATING A CephFS CREATES TWO CEPH POOLS, AND A `Proxmox.CephPool` MUST NOT ALSO DECLARE THEM.
|
|
30
30
|
* `createfs` builds `<name>_data` and `<name>_metadata` itself and refuses outright if either
|
|
31
|
-
* exists ("ceph pools '…_data' and/or '…_metadata' already exist"). MEASURED on
|
|
32
|
-
* `cephfs-
|
|
33
|
-
* `GET /nodes/
|
|
31
|
+
* exists ("ceph pools '…_data' and/or '…_metadata' already exist"). MEASURED on C1: the live
|
|
32
|
+
* `cephfs-c1` owns `cephfs-c1_data` (id 6) and `cephfs-c1_metadata` (id 7), both of which
|
|
33
|
+
* `GET /nodes/node-b/ceph/pool` lists like any other pool — and `/nodes/{node}/ceph/pool/{name}` DOES
|
|
34
34
|
* have GET, PUT and DELETE, so a pool resource would be a clean `pveHandlers` fit and would
|
|
35
35
|
* adopt them without noticing. Declaring both is double management of one object: whichever
|
|
36
36
|
* reconciles first wins, and the loser either fails its create or PUTs `size`/`pg_num` onto pools
|
|
@@ -38,16 +38,16 @@
|
|
|
38
38
|
*
|
|
39
39
|
* ⚠️ `add-storage` IS A SECOND SIDE EFFECT AND IT LANDS IN storage.cfg. Set, `createfs` also calls
|
|
40
40
|
* `PVE::API2::Storage::Config->create` with type `cephfs`, content `backup,iso,vztmpl` and
|
|
41
|
-
* `fs-name <name>`. MEASURED: /etc/pve/storage.cfg on
|
|
42
|
-
* cephfs-
|
|
43
|
-
* declaring `cephfs-
|
|
41
|
+
* `fs-name <name>`. MEASURED: /etc/pve/storage.cfg on node-b holds exactly that — `cephfs:
|
|
42
|
+
* cephfs-c1` / `content backup,vztmpl,iso` / `fs-name cephfs-c1`. So a `Proxmox.Storage`
|
|
43
|
+
* declaring `cephfs-c1` would be managing an entry this resource created. Declare ONE of them:
|
|
44
44
|
* either `add-storage` here and no storage resource, or `add-storage` off and a
|
|
45
45
|
* `Proxmox.Storage` that reads this resource's `name` attribute — which also gets the ordering
|
|
46
46
|
* right for free, because Alchemy orders by data flow.
|
|
47
47
|
*
|
|
48
48
|
* ⚠️ `node` IS A ROUTE, NOT IDENTITY, AND MUST NEVER BE DIFFED. A CephFS is cluster-wide; the node
|
|
49
|
-
* in the path only says which node PVE proxies the call to. MEASURED: `GET /nodes/
|
|
50
|
-
* `…/
|
|
49
|
+
* in the path only says which node PVE proxies the call to. MEASURED: `GET /nodes/node-b/ceph/fs`,
|
|
50
|
+
* `…/node-c/…` and `…/node-d/…` returned the byte-identical array. So changing `node` plans `noop`, and
|
|
51
51
|
* two resources naming the same `name` on different nodes are the SAME filesystem — the vmid
|
|
52
52
|
* hazard from lxc.ts, with a filesystem on the end of it instead of a container.
|
|
53
53
|
*
|
|
@@ -84,11 +84,11 @@ export interface CephFsProps extends WithTarget {
|
|
|
84
84
|
* this is under 32. PVE's default is 128.
|
|
85
85
|
*
|
|
86
86
|
* ⛔ CREATE-TIME ONLY, UNREADABLE, AND OWNED BY THE AUTOSCALER FROM THE SECOND IT LANDS. It is
|
|
87
|
-
* never compared — see the ⛔ on `matches` — and this is not caution, it is measured.
|
|
88
|
-
* `cephfs-
|
|
89
|
-
* sits at 32, which is 128/4), and `cephfs-
|
|
87
|
+
* never compared — see the ⛔ on `matches` — and this is not caution, it is measured. C1's
|
|
88
|
+
* `cephfs-c1` was created with the default 128 (the arithmetic proves it: `cephfs-c1_metadata`
|
|
89
|
+
* sits at 32, which is 128/4), and `cephfs-c1_data` reads pg_num 32 TODAY. The autoscaler moved
|
|
90
90
|
* it by a factor of four, and it is not finished: `pg_autoscale_mode` is `on` for every pool on
|
|
91
|
-
* this cluster and `cephfs-
|
|
91
|
+
* this cluster and `cephfs-c1_metadata` already reports `pg_num_final: 16` against its live 32.
|
|
92
92
|
*/
|
|
93
93
|
pg_num?: number;
|
|
94
94
|
/**
|
|
@@ -155,7 +155,7 @@ const ops = pveOperations<CephFsProps, CephFsAttributes>({
|
|
|
155
155
|
* only honest comparison is none at all.
|
|
156
156
|
* ⚠️ THE TEMPTING ADDITION IS `pg_num`, AND IT WOULD BE THE WORST BUG THIS PACKAGE HAS SHIPPED.
|
|
157
157
|
* It is not in the index at all, so it can only be read from the pool endpoint — where the
|
|
158
|
-
* autoscaler owns it. MEASURED on
|
|
158
|
+
* autoscaler owns it. MEASURED on C1 today: created at 128, live at 32, with the metadata
|
|
159
159
|
* pool already scheduled down to 16. Compared, that is an eternal mismatch; and because
|
|
160
160
|
* `updateForm` is undefined, the action it produces is REPLACE. The plan would destroy and
|
|
161
161
|
* rebuild a live filesystem on every deploy, triggered by a daemon on its own schedule.
|
|
@@ -176,7 +176,7 @@ export const ProxmoxCephFsProvider = () =>
|
|
|
176
176
|
ProxmoxCephFs.Provider.of({
|
|
177
177
|
/**
|
|
178
178
|
* ⛔ EMPTY, LIKE EVERY OTHER RESOURCE HERE. The index answers with every filesystem on the
|
|
179
|
-
* cluster, `cephfs-
|
|
179
|
+
* cluster, `cephfs-c1` included — the one holding this estate's ISOs and templates.
|
|
180
180
|
* Adopting it would put Alchemy one `destroy` away from it. Adoption stays explicit.
|
|
181
181
|
*/
|
|
182
182
|
list: () => Effect.succeed([]),
|
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
* `CephOsdProps` is a cycle on paper only: it is erased before anything runs.
|
|
12
12
|
*
|
|
13
13
|
* ⛔ `GET /nodes/{node}/ceph/osd` ANSWERS A TREE, NOT A LIST, AND IT IS THE WHOLE CLUSTER'S TREE.
|
|
14
|
-
* MEASURED 2026-09-13: asked of
|
|
15
|
-
* leaves include osd.0 and osd.5 on
|
|
14
|
+
* MEASURED 2026-09-13: asked of node-b it answers `{"flags":…,"root":{"children":[…]}}` whose
|
|
15
|
+
* leaves include osd.0 and osd.5 on node-d; asked of node-c it answers the identical six leaves. The
|
|
16
16
|
* `{node}` in the path chooses WHO ANSWERS, not what is listed. A provider that read it as "the
|
|
17
|
-
* OSDs on this node" would find osd.0 through
|
|
17
|
+
* OSDs on this node" would find osd.0 through node-b and then report it as living there.
|
|
18
18
|
*
|
|
19
19
|
* ⛔ AND THE SINGLE-OBJECT PATH CANNOT BE USED INSTEAD — THIS WALK EXISTS BECAUSE IT LIES.
|
|
20
|
-
* MEASURED: `GET /nodes/
|
|
20
|
+
* MEASURED: `GET /nodes/node-b/ceph/osd/99`, an id with no OSD behind it, answers HTTP 200 with
|
|
21
21
|
* `[{"name":"metadata"},{"name":"lv-info"}]` — byte for byte what `…/osd/2` answers for a real
|
|
22
22
|
* one. It is a directory index (its schema says `permissions: {"user":"all"}`), not a read of
|
|
23
23
|
* the OSD. Point `PveSpec.path` at it and every declared OSD is "present": `diff` reports
|
|
@@ -25,18 +25,18 @@
|
|
|
25
25
|
* stops this package recording objects that do not exist — passes on an OSD that was never
|
|
26
26
|
* built. The only endpoint that can tell present from absent is the tree.
|
|
27
27
|
*
|
|
28
|
-
* ★ MEASURED ON
|
|
28
|
+
* ★ MEASURED ON C1 ON 2026-09-13, `GET /nodes/node-b/ceph/osd`. Six leaves, all `up` and `in`, Ceph
|
|
29
29
|
* 20.2.2 tentacle:
|
|
30
30
|
*
|
|
31
|
-
* id name host
|
|
32
|
-
* 0 osd.0
|
|
33
|
-
* 5 osd.5
|
|
34
|
-
* 1 osd.1
|
|
35
|
-
* 4 osd.4
|
|
36
|
-
* 2 osd.2
|
|
37
|
-
* 3 osd.3
|
|
31
|
+
* id name host device_class crush_weight reweight pgs status
|
|
32
|
+
* 0 osd.0 node-d ssd 1.86299133300781 1 99 up
|
|
33
|
+
* 5 osd.5 node-d ssd 1.86299133300781 1 94 up
|
|
34
|
+
* 1 osd.1 node-c ssd 1.86299133300781 1 96 up
|
|
35
|
+
* 4 osd.4 node-c ssd 1.81939697265625 1 97 up
|
|
36
|
+
* 2 osd.2 node-b ssd 1.81939697265625 1 88 up
|
|
37
|
+
* 3 osd.3 node-b ssd 1.81939697265625 1 105 up
|
|
38
38
|
*
|
|
39
|
-
* `{ node: '
|
|
39
|
+
* `{ node: 'node-b', osdid: 2, host: 'node-b', device_class: 'ssd' }` plans `noop` against that row.
|
|
40
40
|
* That is the acceptance test this file was written to pass.
|
|
41
41
|
*
|
|
42
42
|
* ⚠️ EVERY OTHER COLUMN IS REPORTED AND NEVER COMPARED, FOR FOUR DIFFERENT REASONS. `pgs` is the
|
|
@@ -72,8 +72,8 @@ const branches = (node: CrushNode): CrushNode[] =>
|
|
|
72
72
|
/**
|
|
73
73
|
* Every OSD leaf under the tree root, at any depth.
|
|
74
74
|
*
|
|
75
|
-
* ⚠️ RECURSIVE BECAUSE THE TREE IS NOT THREE LEVELS DEEP BY LAW.
|
|
76
|
-
* root(`default`) -> host(
|
|
75
|
+
* ⚠️ RECURSIVE BECAUSE THE TREE IS NOT THREE LEVELS DEEP BY LAW. C1 measures as
|
|
76
|
+
* root(`default`) -> host(node-b|node-c|node-d) -> osd, but CRUSH admits datacenter, rack and chassis
|
|
77
77
|
* buckets between them, and an OSD created outside a host bucket hangs off the root. Reaching
|
|
78
78
|
* in as `root.children[].children[]` would read this cluster correctly and answer "absent" —
|
|
79
79
|
* i.e. "the replica is gone" — on the first cluster that has a rack in it.
|
|
@@ -149,7 +149,7 @@ export interface CephOsdAttributes {
|
|
|
149
149
|
* ⚠️ THE TELEMETRY IS DELIBERATELY DROPPED, NOT FORGOTTEN. The leaf also carries `bytes_used`,
|
|
150
150
|
* `percent_used`, `apply_latency_ms` and `commit_latency_ms`. MEASURED, AND THE MEASUREMENT SAYS
|
|
151
151
|
* THE OPPOSITE OF WHAT YOU WOULD EXPECT: across two reads minutes apart, all four were identical
|
|
152
|
-
* on every OSD, and both latencies read 0 —
|
|
152
|
+
* on every OSD, and both latencies read 0 — C1 was idle, so nothing was caught in the act. That
|
|
153
153
|
* these move is therefore REASONED, from what they count, not observed. Alchemy persists
|
|
154
154
|
* attributes; on a cluster doing work, keeping them would rewrite all six resources' state on
|
|
155
155
|
* every deploy and record a number that is stale by the time anybody reads it. What is kept is
|
package/src/proxmox/ceph-osd.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* A PVE method with no permissions block is refused to every identity except `root@pam`, so an
|
|
13
13
|
* OpenBao-minted `hf-provision@pve!…` token is answered 403 however wide its role — widening
|
|
14
|
-
*
|
|
14
|
+
* the provision role cannot fix it. Compare the sibling families, which DO carry a check and
|
|
15
15
|
* therefore do work with a token: `ceph/fs`, `ceph/mon` and `disks/zfs` all want `Sys.Modify`.
|
|
16
16
|
*
|
|
17
17
|
* ⚠️ SO THE WRITE PATH IS HONEST RATHER THAN USEFUL. It is written out properly in
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* operator running as root@pam. Through this package's credential it will 403, and a 403 is a
|
|
20
20
|
* better answer than a `delete` that silently does nothing while the plan claims otherwise.
|
|
21
21
|
*
|
|
22
|
-
* ★ WHAT IT IS ACTUALLY FOR, THEN: ASSERTION. It turns "
|
|
22
|
+
* ★ WHAT IT IS ACTUALLY FOR, THEN: ASSERTION. It turns "node-b, node-c and node-d each hold two ssd OSDs"
|
|
23
23
|
* into a line a plan checks, and it gives Ceph-backed resources something real to depend on —
|
|
24
24
|
* read `osd.osdid` into an `rbd` storage's props and Alchemy orders that storage after the
|
|
25
25
|
* assertion, so a stack can no longer report a healthy Ceph storage on a cluster whose OSDs are
|
|
@@ -53,8 +53,8 @@ export type { CephOsdAttributes };
|
|
|
53
53
|
|
|
54
54
|
export interface CephOsdProps extends WithTarget {
|
|
55
55
|
/**
|
|
56
|
-
* Which node ANSWERS the read — not where the OSD lives. MEASURED: the tree
|
|
57
|
-
* tree
|
|
56
|
+
* Which node ANSWERS the read — not where the OSD lives. MEASURED: the tree node-b returns and the
|
|
57
|
+
* tree node-c returns are the same six leaves, node-d's included. Point this at any node that is up;
|
|
58
58
|
* `host` is the field that says where the disk is.
|
|
59
59
|
*/
|
|
60
60
|
node: string;
|
|
@@ -63,7 +63,7 @@ export interface CephOsdProps extends WithTarget {
|
|
|
63
63
|
/**
|
|
64
64
|
* The CRUSH host bucket this OSD must sit in. Undeclared means unmanaged, as in storage.ts.
|
|
65
65
|
*
|
|
66
|
-
* ⚠️ DECLARING IT IS AN ASSERTION WITH NO REPAIR. If osd.2 turns up under
|
|
66
|
+
* ⚠️ DECLARING IT IS AN ASSERTION WITH NO REPAIR. If osd.2 turns up under node-c, the plan reports
|
|
67
67
|
* work every time and the deploy fails, because moving an OSD between failure domains is a
|
|
68
68
|
* `ceph osd crush move` this provider will not perform. That is the intent: a replica in the
|
|
69
69
|
* wrong failure domain is not something to converge quietly past.
|
|
@@ -125,7 +125,7 @@ export const ProxmoxCephOsd = Resource<ProxmoxCephOsd>('Proxmox.CephOsd', {
|
|
|
125
125
|
* node, an expired lease, a role without `Sys.Audit`, or a cluster with no Ceph at all. Folding
|
|
126
126
|
* those into "absent" would turn every one of them into the sentence "osd.2 is missing", which
|
|
127
127
|
* is the most alarming thing this provider can say and would be a lie in all four cases. Left
|
|
128
|
-
* to fail, the plan shows `PVE GET nodes/
|
|
128
|
+
* to fail, the plan shows `PVE GET nodes/node-b/ceph/osd -> 500: …` and names the real problem.
|
|
129
129
|
*/
|
|
130
130
|
|
|
131
131
|
/**
|
|
@@ -14,16 +14,16 @@ import { engineOver } from '../verify/fake-engine.ts';
|
|
|
14
14
|
import { ProxmoxCephPool, ProxmoxCephPoolProvider } from './ceph-pool.ts';
|
|
15
15
|
import { FAKE_TARGET, type PveCall, fakePve, withoutBao } from './fake-pve.ts';
|
|
16
16
|
|
|
17
|
-
const STATUS = 'nodes/
|
|
18
|
-
const OBJECT = 'nodes/
|
|
17
|
+
const STATUS = 'nodes/node-b/ceph/pool/rbd-c1/status?verbose=1';
|
|
18
|
+
const OBJECT = 'nodes/node-b/ceph/pool/rbd-c1';
|
|
19
19
|
|
|
20
|
-
/**
|
|
20
|
+
/** C1's rbd pool as `getpool` answers it — the fields ceph-pool.ts reads. */
|
|
21
21
|
const livePool = (size: number): Record<string, unknown> => ({
|
|
22
22
|
application_list: ['rbd'],
|
|
23
23
|
crush_rule: 'replicated_rule',
|
|
24
24
|
id: 2,
|
|
25
25
|
min_size: 2,
|
|
26
|
-
name: '
|
|
26
|
+
name: 'rbd-c1',
|
|
27
27
|
pg_autoscale_mode: 'on',
|
|
28
28
|
pg_num: 128,
|
|
29
29
|
size,
|
|
@@ -38,18 +38,18 @@ const cluster = (size: number) => {
|
|
|
38
38
|
for (const [key, value] of Object.entries(call.form)) {
|
|
39
39
|
pool[key] = /^\d+$/.test(value) ? Number(value) : value;
|
|
40
40
|
}
|
|
41
|
-
return 'UPID:
|
|
41
|
+
return 'UPID:node-b:fake:setpool';
|
|
42
42
|
}
|
|
43
43
|
return undefined;
|
|
44
44
|
});
|
|
45
45
|
};
|
|
46
46
|
|
|
47
47
|
const declared = () =>
|
|
48
|
-
ProxmoxCephPool('
|
|
48
|
+
ProxmoxCephPool('rbd-c1', {
|
|
49
49
|
crush_rule: 'replicated_rule',
|
|
50
50
|
min_size: 2,
|
|
51
|
-
name: '
|
|
52
|
-
node: '
|
|
51
|
+
name: 'rbd-c1',
|
|
52
|
+
node: 'node-b',
|
|
53
53
|
pg_autoscale_mode: 'on',
|
|
54
54
|
size: 3,
|
|
55
55
|
target: FAKE_TARGET,
|
|
@@ -67,7 +67,7 @@ describe('adopting a Ceph pool', () => {
|
|
|
67
67
|
expect(report.rows).toEqual([
|
|
68
68
|
expect.objectContaining({ diff: 'noop', ok: true, planned: 'adopted', read: 'found' }),
|
|
69
69
|
]);
|
|
70
|
-
expect(await engine.deploy(declared())).toEqual({
|
|
70
|
+
expect(await engine.deploy(declared())).toEqual({ 'rbd-c1': 'adopted' });
|
|
71
71
|
});
|
|
72
72
|
expect(fake.writes()).toEqual([]);
|
|
73
73
|
expect(fake.calls.every((call) => call.path === STATUS)).toBe(true);
|
|
@@ -82,10 +82,10 @@ describe('adopting a Ceph pool', () => {
|
|
|
82
82
|
test('declaring a field it does not manage: rechecked, still noop, and the deploy only reads', async () => {
|
|
83
83
|
const fake = cluster(3);
|
|
84
84
|
const birth = () =>
|
|
85
|
-
ProxmoxCephPool('
|
|
85
|
+
ProxmoxCephPool('rbd-c1', {
|
|
86
86
|
min_size: 2,
|
|
87
|
-
name: '
|
|
88
|
-
node: '
|
|
87
|
+
name: 'rbd-c1',
|
|
88
|
+
node: 'node-b',
|
|
89
89
|
pg_num: 32,
|
|
90
90
|
size: 3,
|
|
91
91
|
target: FAKE_TARGET,
|
|
@@ -100,7 +100,7 @@ describe('adopting a Ceph pool', () => {
|
|
|
100
100
|
recheck: 'noop',
|
|
101
101
|
});
|
|
102
102
|
expect(report.rows[0]?.why).toContain('does not manage');
|
|
103
|
-
expect(await engine.deploy(birth())).toEqual({
|
|
103
|
+
expect(await engine.deploy(birth())).toEqual({ 'rbd-c1': 'adopted' });
|
|
104
104
|
});
|
|
105
105
|
expect(fake.writes()).toEqual([]);
|
|
106
106
|
});
|
|
@@ -112,7 +112,7 @@ describe('adopting a Ceph pool', () => {
|
|
|
112
112
|
const report = await engine.verify(declared());
|
|
113
113
|
expect(report.rows[0]).toMatchObject({ changed: ['size'], diff: 'update', ok: false });
|
|
114
114
|
expect(fake.writes()).toEqual([]);
|
|
115
|
-
expect(await engine.deploy(declared())).toEqual({
|
|
115
|
+
expect(await engine.deploy(declared())).toEqual({ 'rbd-c1': 'adopted' });
|
|
116
116
|
});
|
|
117
117
|
expect(fake.writes()).toEqual([`PUT ${OBJECT}`]);
|
|
118
118
|
expect(fake.calls.find((call) => call.method === 'PUT')?.form['size']).toBe('3');
|
|
@@ -4,25 +4,25 @@
|
|
|
4
4
|
* ★ SPLIT OUT OF ceph-pool.ts TO KEEP BOTH FILES UNDER THE 250-LINE CAP, and the seam is
|
|
5
5
|
* "does this touch the cluster" — the same one metric-server-form.ts draws, just with more on
|
|
6
6
|
* this side because this family needs it. Here: the declared shape, the reported shape, and the
|
|
7
|
-
* coercions that turn one into the other. There: reading
|
|
7
|
+
* coercions that turn one into the other. There: reading C1, deciding a diff, and the four
|
|
8
8
|
* handlers. Nothing in this file makes a call or decides an action.
|
|
9
9
|
* ⚠️ THE PROPS LIVE HERE RATHER THAN IN THE RESOURCE FILE, which is the one place this pair
|
|
10
10
|
* differs from metric-server / notification-target. There is no `import type` cycle back as a
|
|
11
11
|
* result; ceph-pool.ts re-exports both interfaces so the public surface is unchanged.
|
|
12
12
|
*
|
|
13
13
|
* ⛔ POST AND PUT DO NOT TAKE THE SAME PARAMETERS, AND BOTH EXTRAS ARE DESTRUCTIVE. MEASURED from
|
|
14
|
-
* the cluster's own schema (/usr/share/pve-docs/api-viewer/apidoc.js on
|
|
14
|
+
* the cluster's own schema (/usr/share/pve-docs/api-viewer/apidoc.js on node-b, pve-manager 9.2.11,
|
|
15
15
|
* 2026-09-13): POST adds `add_storages` and `erasure-coding` to the shared set, PUT takes
|
|
16
16
|
* neither. Both are refused below, so what is left between the two bodies is `name`, which PUT
|
|
17
17
|
* reads from the path, and `pg_num`.
|
|
18
18
|
*
|
|
19
19
|
* ⛔ `pg_num` IS IN THE CREATE BODY AND NOT IN THE UPDATE BODY, AND THAT IS THE MOST IMPORTANT
|
|
20
|
-
* LINE IN THIS FILE. MEASURED in PVE::API2::Ceph::Pool::createpool on
|
|
20
|
+
* LINE IN THIS FILE. MEASURED in PVE::API2::Ceph::Pool::createpool on node-b: a POST carrying no
|
|
21
21
|
* `pg_num` gets 128 substituted (`$param->{pg_num} //= 128`), and PVE::Ceph::Tools::create_pool
|
|
22
22
|
* then runs `osd pool create` — which Ceph answers with SUCCESS for a pool that already exists,
|
|
23
23
|
* before applying the rest of the body to it. So a create fired at a live pool SETS its pg_num,
|
|
24
24
|
* and lowering pg_num starts a PG MERGE: hours of backfill across every OSD, on a cluster whose
|
|
25
|
-
* Ceph traffic shares vmbr1.
|
|
25
|
+
* Ceph traffic shares vmbr1.42 with everything else. ceph-pool.ts guards the create for exactly
|
|
26
26
|
* that reason; this half simply never offers pg_num to an update.
|
|
27
27
|
*/
|
|
28
28
|
import type { WithTarget } from './resource.ts';
|
|
@@ -30,10 +30,10 @@ import { csv } from './values.ts';
|
|
|
30
30
|
|
|
31
31
|
export interface CephPoolProps extends WithTarget {
|
|
32
32
|
/**
|
|
33
|
-
* ⚠️ WHICH NODE THE CALL GOES THROUGH, NOT WHERE THE POOL LIVES. A pool is cluster-wide:
|
|
34
|
-
*
|
|
33
|
+
* ⚠️ WHICH NODE THE CALL GOES THROUGH, NOT WHERE THE POOL LIVES. A pool is cluster-wide: node-b and
|
|
34
|
+
* node-c answer byte-identically for `rbd-c1` (measured). The endpoint is node-scoped only
|
|
35
35
|
* because PVE talks to the local RADOS socket, so this is a door, not a location — it is never
|
|
36
|
-
* compared, and moving a declaration from
|
|
36
|
+
* compared, and moving a declaration from node-b to node-c plans noop. Point it at a node you expect
|
|
37
37
|
* to be up: a node that is down makes the read fail, and a failed read reads as "absent".
|
|
38
38
|
*/
|
|
39
39
|
node: string;
|
|
@@ -51,7 +51,7 @@ export interface CephPoolProps extends WithTarget {
|
|
|
51
51
|
min_size?: number;
|
|
52
52
|
/**
|
|
53
53
|
* ⛔ CREATE-TIME ONLY, AND NEVER COMPARED — the single biggest noop hazard in this family. With
|
|
54
|
-
* `pg_autoscale_mode` on or warn the autoscaler owns this number: MEASURED on
|
|
54
|
+
* `pg_autoscale_mode` on or warn the autoscaler owns this number: MEASURED on C1, `rbd-c1`
|
|
55
55
|
* sits at pg_num 128 while the autoscaler's own `pg_num_final` is 256, so a declaration
|
|
56
56
|
* diffing it reports an update the moment Ceph decides to act. A split is gradual even with
|
|
57
57
|
* the autoscaler off, and `nopgchange` can refuse the write outright — which kills the WHOLE
|
|
@@ -61,7 +61,7 @@ export interface CephPoolProps extends WithTarget {
|
|
|
61
61
|
pg_num?: number;
|
|
62
62
|
/** The floor the autoscaler may not go below. Operator-owned and stable, so it IS compared. */
|
|
63
63
|
pg_num_min?: number;
|
|
64
|
-
/** ⚠️ PVE's create default is `warn`; Ceph's own is `on`, which is what all four
|
|
64
|
+
/** ⚠️ PVE's create default is `warn`; Ceph's own is `on`, which is what all four C1 pools have. */
|
|
65
65
|
pg_autoscale_mode?: 'off' | 'on' | 'warn';
|
|
66
66
|
/**
|
|
67
67
|
* The CRUSH rule BY NAME, e.g. `replicated_rule`.
|
|
@@ -108,7 +108,7 @@ export interface CephPoolProps extends WithTarget {
|
|
|
108
108
|
* size or its PG shape, and `set_pool` dies on the whole PUT when any one parameter will not
|
|
109
109
|
* apply (Tools.pm:310). `matches` refuses to diff the fields they freeze, so these attributes
|
|
110
110
|
* are the only place a plan can show WHY a declared size is not being enforced. All three read
|
|
111
|
-
* false on all four
|
|
111
|
+
* false on all four C1 pools.
|
|
112
112
|
* ⚠️ `pg_num`, `id` AND `applications` ARE REPORTED AND NEVER COMPARED — see the props above.
|
|
113
113
|
* ⛔ NO `statistics` AND NO `autoscale_status`, though the verbose read returns both. They change
|
|
114
114
|
* every few seconds; persisting them would rewrite this resource's state on every deploy and
|
|
@@ -170,7 +170,7 @@ export const same = <T>(declared: T | undefined, live: T, frozen = false) =>
|
|
|
170
170
|
* A declared autoscaler hint, or nothing when it is zero.
|
|
171
171
|
*
|
|
172
172
|
* ⚠️ A DECLARED ZERO IS "NO FLOOR, NO HINT", AND IT IS DELIBERATELY LEFT UNCOMPARED. REASONED, NOT
|
|
173
|
-
* MEASURED: all four
|
|
173
|
+
* MEASURED: all four C1 pools carry null for both `pg_num_min` and `target_size`, and finding
|
|
174
174
|
* out for certain would mean writing to the cluster. Ceph CLEARS both when they are set to 0 and
|
|
175
175
|
* `osd pool get all` then omits the key, which PVE reports as null and this pair reads as UNSET
|
|
176
176
|
* — so a declared 0 would compare 0 against -1 on every plan, forever. Mapping 0 to UNSET
|
|
@@ -203,7 +203,7 @@ const numeric = (value: number | undefined) => (value === undefined ? undefined
|
|
|
203
203
|
* ⚠️ AN UNDECLARED FIELD IS NEITHER SENT NOR COMPARED — storage.ts's rule, and the opposite of
|
|
204
204
|
* Proxmox.Pool's. Undeclared means UNMANAGED here, because a pool has no one set of defaults to
|
|
205
205
|
* fall back on: PVE's POST fills in size 3, min_size 2, application rbd and pg_autoscale_mode
|
|
206
|
-
* 'warn' (MEASURED in createpool), while Ceph's own defaults — which is what every pool on
|
|
206
|
+
* 'warn' (MEASURED in createpool), while Ceph's own defaults — which is what every pool on C1
|
|
207
207
|
* was actually born with — put pg_autoscale_mode at 'on'. Guessing either set would rewrite a
|
|
208
208
|
* pool somebody tuned by hand.
|
|
209
209
|
*
|
|
@@ -216,7 +216,7 @@ const numeric = (value: number | undefined) => (value === undefined ? undefined
|
|
|
216
216
|
* NOT PVE's `1T` STRING. MEASURED: the API parses the string with PVE::JSONSchema::parse_size
|
|
217
217
|
* and stores Ceph's `target_size_bytes` (Pool.pm:474-477 for POST, 683-686 for PUT), and
|
|
218
218
|
* `GET .../status` hands that integer back under the name `target_size` (Pool.pm:878). Run on
|
|
219
|
-
*
|
|
219
|
+
* node-b, `parse_size('1099511627776')` returns 1099511627776 — bare digits are bytes, no
|
|
220
220
|
* multiplier — so a byte count survives the round trip exactly, while a declared `1T` would read
|
|
221
221
|
* back as 1099511627776 and diff against itself on every plan, forever.
|
|
222
222
|
*/
|
package/src/proxmox/ceph-pool.ts
CHANGED
|
@@ -18,10 +18,10 @@
|
|
|
18
18
|
* string, so HTTP 200 means "the task started" — an immediate read-back can see a pool that does
|
|
19
19
|
* not exist yet, or values that have not landed. `settle` waits for the cluster to agree.
|
|
20
20
|
*
|
|
21
|
-
* ⛔ THE FOUR POOLS ON
|
|
21
|
+
* ⛔ THE FOUR POOLS ON C1 INCLUDE CEPH'S OWN, AND NONE OF THEM MAY BE ADOPTED. Measured
|
|
22
22
|
* 2026-09-13: `.mgr` (application `mgr`, created and owned by the manager daemons),
|
|
23
|
-
* `cephfs-
|
|
24
|
-
* destroying either destroys the filesystem), and `
|
|
23
|
+
* `cephfs-c1_data` and `cephfs-c1_metadata` (the two halves of the `cephfs-c1` filesystem —
|
|
24
|
+
* destroying either destroys the filesystem), and `rbd-c1`, the rbd pool the guests sit on.
|
|
25
25
|
* `list` answers empty like every resource in this package, so adoption stays an explicit act;
|
|
26
26
|
* the `applications` attribute is reported so that a plan SHOWS which of those a declaration hit.
|
|
27
27
|
*
|
|
@@ -107,9 +107,9 @@ const spec: PveSpec<CephPoolProps, CephPoolAttributes> = {
|
|
|
107
107
|
* write only ever ADDS to a list), `target_size_ratio` (float equality), `id`, `applications`
|
|
108
108
|
* and the three `no*` flags (PVE returns them and accepts none of them on write), `node` and
|
|
109
109
|
* `name` (the address the read was made at — true by construction, never a diff). In, and each
|
|
110
|
-
* measured to round-trip unchanged against
|
|
110
|
+
* measured to round-trip unchanged against C1's four pools on 2026-09-13: size 3, min_size 2,
|
|
111
111
|
* pg_autoscale_mode `on`, crush_rule `replicated_rule`, pg_num_min where it is set (16 on
|
|
112
|
-
* cephfs-
|
|
112
|
+
* cephfs-c1_metadata, absent on rbd-c1), target_size in bytes — and each of the last two
|
|
113
113
|
* through `hint`, which drops a declared zero for the reason given on it.
|
|
114
114
|
*/
|
|
115
115
|
matches: (attributes, props) =>
|
|
@@ -197,7 +197,7 @@ export const ProxmoxCephPoolProvider = () =>
|
|
|
197
197
|
* to remove deliberately.
|
|
198
198
|
* ⚠️ AND THE REFUSAL CAN ARRIVE AFTER THE RESPONSE, WHICH IS WHY THIS WAITS. The DELETE
|
|
199
199
|
* forks a worker and answers 200 with a UPID, so mons running with
|
|
200
|
-
* `mon_allow_pool_delete` false fail once the call has already returned. MEASURED on
|
|
200
|
+
* `mon_allow_pool_delete` false fail once the call has already returned. MEASURED on C1
|
|
201
201
|
* 2026-09-13: the config-db value is false while the running mons report true, i.e. it is
|
|
202
202
|
* set in ceph.conf and the two sources disagree by design.
|
|
203
203
|
*/
|
|
@@ -25,7 +25,7 @@ const CRED: PveCredential = {
|
|
|
25
25
|
|
|
26
26
|
const target = (members: readonly string[]): PveTarget => ({
|
|
27
27
|
members,
|
|
28
|
-
mount: 'proxmox-
|
|
28
|
+
mount: 'proxmox-c1-test',
|
|
29
29
|
scheme: 'pve',
|
|
30
30
|
});
|
|
31
31
|
|
|
@@ -148,7 +148,7 @@ describe('cluster member failover', () => {
|
|
|
148
148
|
const backup = counting('UPID:1');
|
|
149
149
|
try {
|
|
150
150
|
await run(
|
|
151
|
-
pveWith(target([A, B]), CRED, 'PUT', 'nodes/
|
|
151
|
+
pveWith(target([A, B]), CRED, 'PUT', 'nodes/node-c/network').pipe(
|
|
152
152
|
Effect.timeout('50 millis'),
|
|
153
153
|
Effect.flip,
|
|
154
154
|
),
|
|
@@ -169,7 +169,7 @@ describe('cluster member failover', () => {
|
|
|
169
169
|
const backup = counting('UPID:2');
|
|
170
170
|
try {
|
|
171
171
|
// Loopback port 1 is closed: Bun answers ConnectionRefused before a byte is written.
|
|
172
|
-
const data = await run(pveWith(target([A, B]), CRED, 'PUT', 'nodes/
|
|
172
|
+
const data = await run(pveWith(target([A, B]), CRED, 'PUT', 'nodes/node-c/network'), {
|
|
173
173
|
[A]: 1,
|
|
174
174
|
[B]: backup.port,
|
|
175
175
|
});
|
package/src/proxmox/client.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* ★ PLAIN `fetch`, NO TLS ESCAPE HATCH, AND THAT IS A MEASURED CHOICE RATHER THAN AN OMISSION.
|
|
5
5
|
* Proxmox is usually met with `curl -k` because a fresh install serves its own self-signed
|
|
6
6
|
* certificate — I reached for `-k` myself before checking. This cluster serves a real Let's
|
|
7
|
-
* Encrypt certificate (`CN=cluster-
|
|
7
|
+
* Encrypt certificate (`CN=cluster-c1.example.com`), and strict TLS answers 401, i.e. the
|
|
8
8
|
* handshake succeeds and only the credential is missing. So there is no `rejectUnauthorized`
|
|
9
9
|
* option here to be left switched on by accident in somebody else's estate.
|
|
10
10
|
*
|
|
@@ -93,7 +93,7 @@ const buildRequest = (
|
|
|
93
93
|
* package can perform was broken from the day the client was written.
|
|
94
94
|
* ⛔ NOTHING CAUGHT IT BECAUSE NOTHING HAD EVER WRITTEN. Ninety-eight resources were adopted
|
|
95
95
|
* against the live cluster and every one matched, so `reconcile` never reached a PUT. The
|
|
96
|
-
* identical bug in
|
|
96
|
+
* identical bug in <estate>/forgejo/src/client.ts surfaced the moment a write token existed
|
|
97
97
|
* and answered `422 Unsupported Content-Type`; this one is the same line, found by looking.
|
|
98
98
|
* ★ A PLAN THAT SAYS `noop` PROVES THE READ PATH AND NOTHING ELSE. That is the lesson worth
|
|
99
99
|
* more than the fix.
|
|
@@ -213,7 +213,7 @@ export const pveEnvelopeWith = (
|
|
|
213
213
|
* Make one call on a credential for `role`, REUSING a still-valid lease rather than minting one.
|
|
214
214
|
*
|
|
215
215
|
* 🔴 IT USED TO MINT PER CALL, AND AT 98 RESOURCES THAT STOPPED BEING FREE. One plan plus deploy
|
|
216
|
-
* of the
|
|
216
|
+
* of the C1 stack left 760 token entries in `/etc/pve/user.cfg` — read, diff, reconcile and the
|
|
217
217
|
* read-back each minted their own, and `/etc/pve` is a replicated cluster filesystem every node
|
|
218
218
|
* has to agree on. src/lease-cache.ts carries the measurement and the safety argument.
|
|
219
219
|
*
|
|
@@ -29,9 +29,9 @@ const { Bun } = globalThis as unknown as {
|
|
|
29
29
|
|
|
30
30
|
const TOKEN = 'hvs.FAKE-sentinel-never-a-real-token';
|
|
31
31
|
const SECRET = 'fake-pve-secret-0000';
|
|
32
|
-
const
|
|
32
|
+
const C1: PveTarget = {
|
|
33
33
|
members: ['pve.example'],
|
|
34
|
-
mount: 'proxmox-
|
|
34
|
+
mount: 'proxmox-c1',
|
|
35
35
|
scheme: 'pve',
|
|
36
36
|
};
|
|
37
37
|
|
|
@@ -59,11 +59,9 @@ const withFake = async (
|
|
|
59
59
|
};
|
|
60
60
|
|
|
61
61
|
const minted = (env: BaoEnvironment) =>
|
|
62
|
-
Effect.runPromise(mint(
|
|
62
|
+
Effect.runPromise(mint(C1, 'read', env).pipe(Effect.provide(FetchHttpClient.layer)));
|
|
63
63
|
const refused = (env: BaoEnvironment) =>
|
|
64
|
-
Effect.runPromise(
|
|
65
|
-
mint(TB4, 'read', env).pipe(Effect.flip, Effect.provide(FetchHttpClient.layer)),
|
|
66
|
-
);
|
|
64
|
+
Effect.runPromise(mint(C1, 'read', env).pipe(Effect.flip, Effect.provide(FetchHttpClient.layer)));
|
|
67
65
|
|
|
68
66
|
const CREDENTIAL = {
|
|
69
67
|
json: { data: { secret: SECRET, token_id: 'hf-read@pve!hf-read-1' }, lease_duration: 3600 },
|
|
@@ -79,7 +77,7 @@ describe('mint', () => {
|
|
|
79
77
|
secret: SECRET,
|
|
80
78
|
tokenId: 'hf-read@pve!hf-read-1',
|
|
81
79
|
});
|
|
82
|
-
assert.equal(seen[0]?.path, '/v1/proxmox-
|
|
80
|
+
assert.equal(seen[0]?.path, '/v1/proxmox-c1/creds/read');
|
|
83
81
|
assert.equal(seen[0]?.headers.get('x-vault-token'), TOKEN);
|
|
84
82
|
assert.equal(seen[0]?.headers.get('x-vault-namespace'), 'homeflare');
|
|
85
83
|
assert.equal(seen[0]?.headers.get('x-vault-request'), 'true');
|
|
@@ -135,7 +133,7 @@ describe('mint', () => {
|
|
|
135
133
|
CREDENTIAL,
|
|
136
134
|
async (address, seen) => {
|
|
137
135
|
assert.equal((await minted({ BAO_ADDR: address })).leaseSeconds, 3600);
|
|
138
|
-
assert.equal(seen[0]?.path, '/v1/proxmox-
|
|
136
|
+
assert.equal(seen[0]?.path, '/v1/proxmox-c1/creds/read');
|
|
139
137
|
},
|
|
140
138
|
socket,
|
|
141
139
|
);
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
* The PVE credential a reconcile runs with: minted from OpenBao, alive for five minutes.
|
|
3
3
|
*
|
|
4
4
|
* ⛔ THERE IS NO STATIC WRITE TOKEN IN THIS ESTATE, AND THAT IS DELIBERATE. Every PVE token stored
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* on the estate's static KV shelf is read-only — PVEAuditor on each cluster, Audit on PBS, all
|
|
6
|
+
* as one metrics user. The MCP proxmox client says why in its own header: the auditor token bounds
|
|
7
7
|
* what a BUG can do, independently of what the code bounds. Adding a long-lived write token to
|
|
8
8
|
* that shelf would delete the outer lock for every reader of the shelf, not just for this
|
|
9
9
|
* provider.
|
|
10
10
|
*
|
|
11
|
-
* ★ SO WRITES COME FROM A DYNAMIC MOUNT INSTEAD, which the estate already built: `proxmox-
|
|
11
|
+
* ★ SO WRITES COME FROM A DYNAMIC MOUNT INSTEAD, which the estate already built: `proxmox-c1/`
|
|
12
12
|
* exposes two roles — `read` (ttl 3600s) and `provision` (ttl 300s, max 1800s, NOT renewable).
|
|
13
13
|
* A mint returns a PVE API token of its own, `hf-provision@pve!hf-provision-…-<timestamp>-<id>`,
|
|
14
14
|
* which expires on its own whether or not anything cleans up. Same shape as
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
*
|
|
25
25
|
* ⛔ THE SECRET NEVER TOUCHES DISK, A LOG, OR ALCHEMY STATE. Alchemy persists resource attributes
|
|
26
26
|
* WITHOUT encryption — StateEncoding.ts writes `Redacted` as `{"@redacted": <plaintext>}` — and
|
|
27
|
-
* this estate's state store is the `alchemy` Postgres, which
|
|
27
|
+
* this estate's state store is the `alchemy` Postgres, which a nightly job dumps. A PVE
|
|
28
28
|
* secret that reached an attribute would outlive its 300s lease by months, in four places.
|
|
29
29
|
* Nothing here returns it to a resource; it is used to build a header and then dropped.
|
|
30
30
|
*/
|
|
@@ -60,10 +60,10 @@ export type PveRole = 'read' | 'provision';
|
|
|
60
60
|
* use this file unchanged; that is the whole difference between a provider and a script.
|
|
61
61
|
*/
|
|
62
62
|
export type PveTarget = {
|
|
63
|
-
/** OpenBao mount that vends API tokens for this host, e.g. `proxmox-
|
|
63
|
+
/** OpenBao mount that vends API tokens for this host, e.g. `proxmox-c1`. */
|
|
64
64
|
readonly mount: string;
|
|
65
65
|
/**
|
|
66
|
-
* mgmt hostnames for cluster members, e.g. `
|
|
66
|
+
* mgmt hostnames for cluster members, e.g. `node-b.mgmt.example.com`. Any member's :8006 API
|
|
67
67
|
* manages the whole cluster; `client.ts` failovers across them.
|
|
68
68
|
*/
|
|
69
69
|
readonly members: readonly string[];
|
|
@@ -133,14 +133,14 @@ const baoVariable = (env: BaoEnvironment, name: string) =>
|
|
|
133
133
|
* Mint one credential for `role`: `GET /v1/<mount>/creds/<role>` on OpenBao's HTTP API.
|
|
134
134
|
*
|
|
135
135
|
* 🔴 IT SHELLED OUT TO `bao read -format=json` UNTIL 2026-09-14. The case for the CLI was that one
|
|
136
|
-
* client could not disagree with `bao kv get` about what this machine may do.
|
|
136
|
+
* client could not disagree with `bao kv get` about what this machine may do. <estate>/openbao then
|
|
137
137
|
* learned twice what deciding by stderr costs — every failure read as absent, and two commands
|
|
138
138
|
* said "absent" in different words — so this reads STATUS CODES: 2xx is a credential, and 403
|
|
139
139
|
* (denied), 404 (no such role or mount — a mint has no "absent"), 503 (sealed) all fail.
|
|
140
140
|
* ★ THE ENVIRONMENT CONTRACT IS STILL THE CLI's, READ THE CLI's WAY, which keeps the case above
|
|
141
141
|
* true (openbao v2.6.2 api/client.go:34-66, 370, 503-507, 761-762, 776-785): BAO_AGENT_ADDR over
|
|
142
142
|
* BAO_ADDR over https://127.0.0.1:8200; BAO_NAMESPACE as X-Vault-Namespace; BAO_TOKEN as
|
|
143
|
-
* X-Vault-Token; X-Vault-Request always.
|
|
143
|
+
* X-Vault-Token; X-Vault-Request always. <estate>/openbao/src/bao-address.ts has the long form.
|
|
144
144
|
* ★ AGENT MODE IS NO BAO_TOKEN AND NO TOKEN HEADER. An Agent or Proxy listener with
|
|
145
145
|
* `api_proxy { use_auto_auth_token = true }` (or the older `cache { … }`) substitutes its
|
|
146
146
|
* auto-auth token exactly when a request carries none — command/agentproxyshared/cache/handler.go
|
|
@@ -34,6 +34,11 @@ export type FakePve = {
|
|
|
34
34
|
* keyed `node/vmid` — a guest that appears between a plan and its deploy.
|
|
35
35
|
*/
|
|
36
36
|
readonly vanish: Map<string, number>;
|
|
37
|
+
/**
|
|
38
|
+
* Hand edits that land mid-deploy, keyed `node/vmid`: once that guest's config has been read
|
|
39
|
+
* `after` more times, `set` is merged into it — a change made between a plan and its apply.
|
|
40
|
+
*/
|
|
41
|
+
readonly edits: Map<string, { after: number; readonly set: Record<string, unknown> }>;
|
|
37
42
|
/** Config keys a `PUT …/config` accepts with 200 and then does not store. */
|
|
38
43
|
readonly ignore: Set<string>;
|
|
39
44
|
/** How every task ends, and whether a task-starting call answers with no UPID at all. */
|
|
@@ -90,6 +95,7 @@ export const fakePve = (): FakePve => {
|
|
|
90
95
|
const others: { vmid: number | string; node: string; type: string }[] = [];
|
|
91
96
|
const configErrors = new Map<string, { status: number; body: unknown }>();
|
|
92
97
|
const vanish = new Map<string, number>();
|
|
98
|
+
const edits: FakePve['edits'] = new Map();
|
|
93
99
|
const ignore = new Set<string>();
|
|
94
100
|
const task = { exit: 'OK', noUpid: false };
|
|
95
101
|
const seen: Seen[] = [];
|
|
@@ -148,7 +154,12 @@ export const fakePve = (): FakePve => {
|
|
|
148
154
|
const message = `Configuration file 'nodes/${node}/lxc/${vmid}.conf' does not exist\n`;
|
|
149
155
|
return json({ data: null, message }, 500);
|
|
150
156
|
}
|
|
151
|
-
if (tail === '/config' && request.method === 'GET')
|
|
157
|
+
if (tail === '/config' && request.method === 'GET') {
|
|
158
|
+
const answer = json({ data: config });
|
|
159
|
+
const edit = edits.get(`${node}/${vmid}`);
|
|
160
|
+
if (edit !== undefined && --edit.after === 0) Object.assign(config, edit.set);
|
|
161
|
+
return answer;
|
|
162
|
+
}
|
|
152
163
|
if (tail === '/config' && request.method === 'PUT') {
|
|
153
164
|
const digest = form.get('digest');
|
|
154
165
|
if (digest !== null && digest !== config['digest']) {
|
|
@@ -192,6 +203,7 @@ export const fakePve = (): FakePve => {
|
|
|
192
203
|
};
|
|
193
204
|
return {
|
|
194
205
|
configErrors,
|
|
206
|
+
edits,
|
|
195
207
|
fetch: Object.assign(remapped, { preconnect: globalThis.fetch.preconnect }),
|
|
196
208
|
guests,
|
|
197
209
|
ignore,
|
|
@@ -36,7 +36,7 @@ export const fold = (name: string) => name.trim().toLowerCase();
|
|
|
36
36
|
/**
|
|
37
37
|
* A CIDR reduced to the one form PVE will hand back.
|
|
38
38
|
*
|
|
39
|
-
* ⛔ MEASURED by running the cluster's own `PVE::Firewall::parse_alias` on
|
|
39
|
+
* ⛔ MEASURED by running the cluster's own `PVE::Firewall::parse_alias` on node-b (pve-manager 9.2.11,
|
|
40
40
|
* 2026-09-13) — a pure function over a string, no config read and none written:
|
|
41
41
|
* `10.1.1.5/32` -> `10.1.1.5` a /32 host suffix is STRIPPED
|
|
42
42
|
* `2001:0DB8::1/128`-> `2001:0DB8::1` a /128 is stripped, and the case is NOT folded
|