@homeflare/alchemy 0.11.0 → 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 +2 -2
- 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/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.map +14 -14
- 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/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/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 +356 -142
- package/dist/proxmox/index.js.map +59 -56
- package/dist/proxmox/lease-cache.d.ts +4 -4
- package/dist/proxmox/lxc-harness.d.ts +1 -1
- package/dist/proxmox/lxc-props.d.ts +2 -2
- package/dist/proxmox/lxc.d.ts +3 -3
- 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 +1 -1
- package/docs/ownership.md +26 -1
- package/docs/provision-baseline.md +106 -0
- package/docs/proxmox-lxc-adopt.md +2 -2
- package/docs/proxmox-lxc.md +5 -4
- package/docs/proxmox.md +6 -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/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/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 +3 -3
- package/src/proxmox/lxc-create-form.ts +1 -1
- package/src/proxmox/lxc-guards.test.ts +4 -4
- package/src/proxmox/lxc-harness.ts +3 -3
- package/src/proxmox/lxc-judge.test.ts +5 -5
- package/src/proxmox/lxc-lifecycle.ts +1 -1
- 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 +1 -1
- 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 +3 -3
- 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
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* straight to the Forgejo engine's role and nowhere else.
|
|
4
4
|
*
|
|
5
5
|
* ★ TIM'S CALL, 2026-09-14: "Random password, piped", and membership of the HomeFlare Owners team
|
|
6
|
-
* (
|
|
6
|
+
* (<estate>/forgejo, Forgejo.TeamMember). Forgejo has no service-account type and no app.ini setting
|
|
7
7
|
* that creates users — the bot type is database-only (codeberg issue 8136) — so this is an
|
|
8
8
|
* ordinary user made by `forgejo admin user create`.
|
|
9
9
|
*
|
|
@@ -34,12 +34,12 @@ import {
|
|
|
34
34
|
} from './forgejo-bootstrap-parse.ts';
|
|
35
35
|
|
|
36
36
|
export const PROVISION_USER = 'forgejo-provision';
|
|
37
|
-
/** `NO_REPLY_ADDRESS` in
|
|
37
|
+
/** `NO_REPLY_ADDRESS` in the host's Forgejo `app.ini` — a service account's mail goes nowhere. */
|
|
38
38
|
const PROVISION_EMAIL = `${PROVISION_USER}@noreply.lab.example.com`;
|
|
39
39
|
export const ROLE_PATH = 'forgejo/roles/provision';
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
* ★ THE SCOPES ARE WHAT
|
|
42
|
+
* ★ THE SCOPES ARE WHAT <estate>/forgejo WRITES TODAY. Org labels, teams and org secrets need
|
|
43
43
|
* `write:organization`; repositories, webhooks and branch protection need `write:repository`.
|
|
44
44
|
* Membership decides what the tokens can reach, and the scopes cap it.
|
|
45
45
|
* ⚠️ FORGEJO TOKENS NEVER EXPIRE ON THE SERVER. The TTL is OpenBao's lease, and revoking the lease is
|
|
@@ -92,7 +92,7 @@ export const bootstrapProvisionAccount = Effect.gen(function* () {
|
|
|
92
92
|
const binary = runningForgejoBinary(ps.stdout);
|
|
93
93
|
if (binary === undefined) {
|
|
94
94
|
return yield* refuse(
|
|
95
|
-
'no running `forgejo web` found. Run this on the
|
|
95
|
+
'no running `forgejo web` found. Run this on the host where Forgejo runs.',
|
|
96
96
|
);
|
|
97
97
|
}
|
|
98
98
|
const cli = (subcommand: string, ...args: string[]) =>
|
|
@@ -14,16 +14,16 @@ import {
|
|
|
14
14
|
|
|
15
15
|
const POD: BaoKubernetesRoleProps = {
|
|
16
16
|
aliasNameSource: 'serviceaccount_name',
|
|
17
|
-
boundServiceAccountNames: ['
|
|
17
|
+
boundServiceAccountNames: ['llm-gateway'],
|
|
18
18
|
boundServiceAccountNamespaces: ['ai'],
|
|
19
|
-
name: 'ai-
|
|
20
|
-
tokenPolicies: ['ai-
|
|
19
|
+
name: 'ai-gateway',
|
|
20
|
+
tokenPolicies: ['ai-gateway'],
|
|
21
21
|
tokenTtl: '1h',
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
const LIVE: Record<string, unknown> = {
|
|
25
25
|
alias_name_source: 'serviceaccount_name',
|
|
26
|
-
bound_service_account_names: ['
|
|
26
|
+
bound_service_account_names: ['llm-gateway'],
|
|
27
27
|
bound_service_account_namespace_selector: '',
|
|
28
28
|
bound_service_account_namespaces: ['ai'],
|
|
29
29
|
token_bound_cidrs: [],
|
|
@@ -32,7 +32,7 @@ const LIVE: Record<string, unknown> = {
|
|
|
32
32
|
token_no_default_policy: false,
|
|
33
33
|
token_num_uses: 0,
|
|
34
34
|
token_period: 0,
|
|
35
|
-
token_policies: ['ai-
|
|
35
|
+
token_policies: ['ai-gateway'],
|
|
36
36
|
token_ttl: 3600,
|
|
37
37
|
token_type: 'default',
|
|
38
38
|
};
|
package/src/openbao/mount.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* in OpenBao, not Alchemy state.
|
|
7
7
|
*
|
|
8
8
|
* ★ `defaultRemovalPolicy: 'retain'` — disabling a mount destroys every secret under it.
|
|
9
|
-
* Opt in with `.pipe(RemovalPolicy.destroy())`; see resource.ts in
|
|
9
|
+
* Opt in with `.pipe(RemovalPolicy.destroy())`; see resource.ts in <estate>/proxmox.
|
|
10
10
|
*
|
|
11
11
|
* ★ REPLACE SEMANTICS (audited 2026-09-21, see src/openbao/REPLACE.md):
|
|
12
12
|
* · `type` changed → plans `replace`, and the APPLY FAILS, destroying nothing: the new
|
package/src/openbao/pki-role.ts
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
* never a policy one — but neither is a measurement.
|
|
37
37
|
*
|
|
38
38
|
* ★ `defaultRemovalPolicy: 'retain'` — see the delete comment below. Opt in with
|
|
39
|
-
* `.pipe(RemovalPolicy.destroy())`; see resource.ts in
|
|
39
|
+
* `.pipe(RemovalPolicy.destroy())`; see resource.ts in <estate>/proxmox.
|
|
40
40
|
*/
|
|
41
41
|
import { Resource } from 'alchemy';
|
|
42
42
|
import { isResolved } from 'alchemy/Diff';
|
|
@@ -28,11 +28,11 @@ describe('policy wire', () => {
|
|
|
28
28
|
it('round-trips through the stripped newline with equal digests', async () => {
|
|
29
29
|
await withFake(aclPolicies(), async (bao) => {
|
|
30
30
|
const env = { BAO_ADDR: bao.address };
|
|
31
|
-
await run(env, writePolicy('
|
|
31
|
+
await run(env, writePolicy('app-llm', ASSEMBLED));
|
|
32
32
|
assert.equal(bao.seen[0]?.method, 'PUT');
|
|
33
|
-
assert.equal(bao.seen[0]?.path, `${PREFIX}
|
|
33
|
+
assert.equal(bao.seen[0]?.path, `${PREFIX}app-llm`);
|
|
34
34
|
assert.deepEqual(JSON.parse(bao.seen[0]?.body ?? ''), { policy: ASSEMBLED });
|
|
35
|
-
const live = await run(env, readPolicy('
|
|
35
|
+
const live = await run(env, readPolicy('app-llm'));
|
|
36
36
|
assert.notEqual(live, ASSEMBLED, 'the fake strips the newline, as OpenBao does');
|
|
37
37
|
assert.equal(sha256(live), sha256(ASSEMBLED));
|
|
38
38
|
});
|
package/src/openbao/policy.ts
CHANGED
|
@@ -26,8 +26,8 @@ import { guardRename, judgeRename, nameIdentity } from './rename-identity.ts';
|
|
|
26
26
|
* ⛔ THERE IS NO `Bao.Secret`, AND THERE MUST NEVER BE ONE. Alchemy persists attributes to
|
|
27
27
|
* its state store and does NOT encrypt them: StateEncoding.ts unwraps `Redacted` to
|
|
28
28
|
* plaintext under a marker key, and nothing under alchemy/src/State encrypts. This
|
|
29
|
-
* estate's state store is the `alchemy` Postgres database, which
|
|
30
|
-
*
|
|
29
|
+
* estate's state store is the `alchemy` Postgres database, which a nightly job dumps
|
|
30
|
+
* (it enumerates EVERY non-template database), copies to a backup guest, and PBS backs up
|
|
31
31
|
* from there. A KV value here would exist in four places, none of them OpenBao.
|
|
32
32
|
*
|
|
33
33
|
* So this declares the SHAPE AROUND secrets. Every attribute it stores is a name, a
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
* MEASURED 2026-09-13, read-only, against the live server (BAO_ADDR=http://127.0.0.1:8200,
|
|
5
5
|
* BAO_NAMESPACE=homeflare). No write of any kind was issued to a role or a mount:
|
|
6
6
|
*
|
|
7
|
-
* bao read -format=json proxmox-
|
|
7
|
+
* bao read -format=json proxmox-c1/roles/read
|
|
8
8
|
* → { max_ttl: 21600, mint_user: "hf-read@pve", name: "read", ttl: 3600 }
|
|
9
|
-
* bao read -format=json proxmox-
|
|
9
|
+
* bao read -format=json proxmox-c1/roles/provision
|
|
10
10
|
* → { max_ttl: 1800, mint_user: "hf-provision@pve", name: "provision", ttl: 300 }
|
|
11
|
-
* GET /v1/proxmox-
|
|
11
|
+
* GET /v1/proxmox-c1/?help=1 → components.schemas.WriteRolesNameRequest:
|
|
12
12
|
* mint_user string — the ONLY required field, described as "The `user@realm` whose
|
|
13
13
|
* standing ACL is the minted token's entire privilege set. Created
|
|
14
14
|
* out-of-band; this engine never grants ACLs."
|
|
@@ -24,7 +24,7 @@ import { sha256 } from './digest.ts';
|
|
|
24
24
|
import { mountPath, parseDuration, ttlSeconds } from './mount-form.ts';
|
|
25
25
|
|
|
26
26
|
export interface BaoProxmoxRoleProps {
|
|
27
|
-
/** Mount path of the proxmox secrets engine, e.g. `proxmox-
|
|
27
|
+
/** Mount path of the proxmox secrets engine, e.g. `proxmox-c1`. A trailing slash is fine. */
|
|
28
28
|
mount: string;
|
|
29
29
|
/**
|
|
30
30
|
* Role name, as `<mount>/roles/<name>` takes it.
|
|
@@ -105,7 +105,7 @@ const ttlText = (seconds: number | undefined, fallback = '0') => {
|
|
|
105
105
|
return `${String(seconds)}s`;
|
|
106
106
|
};
|
|
107
107
|
|
|
108
|
-
/** `proxmox-
|
|
108
|
+
/** `proxmox-c1/roles/read` — read, write and delete all address the role here. */
|
|
109
109
|
export const rolePath = (mount: string, name: string) => `${mountPath(mount)}/roles/${name}`;
|
|
110
110
|
|
|
111
111
|
export const attributesOf = (
|
|
@@ -3,20 +3,20 @@
|
|
|
3
3
|
* mint user, plus the lease ceilings that consumer's tokens get. METADATA ONLY.
|
|
4
4
|
*
|
|
5
5
|
* ★ WHY THIS FAMILY MATTERS MORE THAN IT LOOKS. Declaring these roles is how the estate
|
|
6
|
-
* replaces HAND-MADE PERMANENT PVE TOKENS with short-lived minted ones. On
|
|
7
|
-
* carry expire=0 and privsep=0 — root@pam!
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* replaces HAND-MADE PERMANENT PVE TOKENS with short-lived minted ones. On C1, SEVEN tokens
|
|
7
|
+
* carry expire=0 and privsep=0 — root@pam!inventory-sync, agent@pve!executor,
|
|
8
|
+
* metrics@pve!exporter, app@pve!app, iac@pve!ro, iac@pve!apply and
|
|
9
|
+
* mint@pve!engine. Six of those are one Bao.ProxmoxRole plus a scoped PVE user away from
|
|
10
10
|
* being a 5-minute lease instead of a forever key.
|
|
11
11
|
*
|
|
12
|
-
* The seventh cannot be.
|
|
12
|
+
* The seventh cannot be. mint@pve!engine is the credential the ENGINE ITSELF
|
|
13
13
|
* authenticates with in order to mint anything at all: the thing that issues short-lived
|
|
14
14
|
* tokens needs a standing one, and no role can resolve that chicken-and-egg. It stays
|
|
15
15
|
* permanent, which makes it the single most valuable token on the cluster and the one whose
|
|
16
16
|
* rotation must be a deliberate, hand-run procedure rather than a plan.
|
|
17
17
|
*
|
|
18
|
-
* MEASURED here: `bao read proxmox-
|
|
19
|
-
* `
|
|
18
|
+
* MEASURED here: `bao read proxmox-c1/config` returns parent_token_id
|
|
19
|
+
* `mint@pve!engine`, parent_secret_set true, parent_secret_tail `…eb01` — the engine's
|
|
20
20
|
* own key, present and, by the schema's own "Write-only: never returned by a read", not
|
|
21
21
|
* retrievable. The seven-token inventory was handed to this change and NOT re-counted against
|
|
22
22
|
* the PVE API in this session.
|
|
@@ -27,13 +27,13 @@
|
|
|
27
27
|
*
|
|
28
28
|
* ⛔ ISSUANCE IS NOT THIS RESOURCE, AND MUST NEVER BECOME ONE. `<mount>/creds/<name>` returns a
|
|
29
29
|
* LIVE PVE TOKEN. Alchemy persists attributes unencrypted (StateEncoding.ts tags Redacted
|
|
30
|
-
* values rather than encrypting them) into the `alchemy` Postgres, which
|
|
31
|
-
*
|
|
30
|
+
* values rather than encrypting them) into the `alchemy` Postgres, which a nightly job dumps
|
|
31
|
+
* to a backup guest, from where PBS backs it up. A minted token declared as a resource would
|
|
32
32
|
* outlive its own lease in at least four places, none of them OpenBao. This resource declares
|
|
33
33
|
* the DOOR; walking through it is a runtime call, not a plan.
|
|
34
34
|
*
|
|
35
35
|
* ★ `defaultRemovalPolicy: 'retain'` — deleting a role breaks every consumer that mints from
|
|
36
|
-
* it. Opt in with `.pipe(RemovalPolicy.destroy())`; see resource.ts in
|
|
36
|
+
* it. Opt in with `.pipe(RemovalPolicy.destroy())`; see resource.ts in <estate>/proxmox.
|
|
37
37
|
*/
|
|
38
38
|
import { Resource } from 'alchemy';
|
|
39
39
|
import { isResolved } from 'alchemy/Diff';
|
|
@@ -80,7 +80,7 @@ export const BaoProxmoxRoleProvider = () =>
|
|
|
80
80
|
BaoProxmoxRole.Provider.of({
|
|
81
81
|
/**
|
|
82
82
|
* ⛔ `bao list <mount>/roles` IS NOT A LIST OF THINGS THIS OWNS. MEASURED today:
|
|
83
|
-
* proxmox-
|
|
83
|
+
* proxmox-c1/roles answers `provision, read` and proxmox-c2/roles answers `read` —
|
|
84
84
|
* four roles created by hand at bringup, none of them declared anywhere yet.
|
|
85
85
|
* Returning them would invite Alchemy to adopt, and then delete, roles it never
|
|
86
86
|
* created, and deleting a role is how every consumer of it stops being able to mint.
|
package/src/openbao/ssh-role.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* ⛔ THE SIGNING KEY IS OUT OF SCOPE AND MUST STAY OUT. `ssh/config/ca` holds (or generates)
|
|
5
5
|
* the CA private key and `ssh/sign/<role>` issues certificates. Neither is declarable
|
|
6
6
|
* here, for the reason policy.ts spells out: Alchemy persists attributes to its state
|
|
7
|
-
* store WITHOUT encrypting them, that store is the `alchemy` Postgres, and
|
|
8
|
-
* dumps it
|
|
7
|
+
* store WITHOUT encrypting them, that store is the `alchemy` Postgres, and a nightly job
|
|
8
|
+
* dumps it to a backup guest. A CA key there would outlive the vault that guards it. This
|
|
9
9
|
* resource declares the SHAPE a signature must satisfy — principals, extensions, TTLs.
|
|
10
10
|
*
|
|
11
11
|
* ⛔ THE ROLE WRITE IS A FULL REPLACE, NOT A PATCH. OpenBao rebuilds the role entry from the
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
*
|
|
30
30
|
* ★ `defaultRemovalPolicy: 'retain'`. Deleting this role locks every operator out of the
|
|
31
31
|
* estate — the memory note "operator SSH is homeflare-bot@<host>" is this CA. Opt in with
|
|
32
|
-
* `.pipe(RemovalPolicy.destroy())`; see resource.ts in
|
|
32
|
+
* `.pipe(RemovalPolicy.destroy())`; see resource.ts in <estate>/proxmox.
|
|
33
33
|
*/
|
|
34
34
|
import { Resource } from 'alchemy';
|
|
35
35
|
import { isResolved } from 'alchemy/Diff';
|
package/src/proxmox/acl.ts
CHANGED
|
@@ -41,14 +41,14 @@ export type AclSubjectType = 'user' | 'group' | 'token';
|
|
|
41
41
|
|
|
42
42
|
export interface AclProps extends WithTarget {
|
|
43
43
|
/**
|
|
44
|
-
* The PVE object path the grant is ON — `/`, `/pool/
|
|
44
|
+
* The PVE object path the grant is ON — `/`, `/pool/lab`, `/vms/101`, `/storage/local-zfs`.
|
|
45
45
|
* ⚠️ NOT THE API PATH: `spec.path` below is the endpoint (`access/acl`). PVE's API overloads the
|
|
46
46
|
* word, so this file does too rather than renaming a field the cluster calls `path`.
|
|
47
47
|
*/
|
|
48
48
|
path: string;
|
|
49
49
|
/** Which kind of subject `ugid` names. Identity: changing it is a different grant. */
|
|
50
50
|
type: AclSubjectType;
|
|
51
|
-
/** `
|
|
51
|
+
/** `alice@pve`, `admins`, `hf-provision@pve!hf-provision-…`. Identity. */
|
|
52
52
|
ugid: string;
|
|
53
53
|
/** The role bound here, e.g. `PVEAuditor`. ⚠️ It must exist — PVE refuses an unknown roleid. */
|
|
54
54
|
roleid: string;
|
|
@@ -85,9 +85,9 @@ export const ProxmoxAcl = Resource<ProxmoxAcl>('Proxmox.Acl');
|
|
|
85
85
|
/**
|
|
86
86
|
* ⚠️ PVE NORMALISES ACL PATHS AND RETURNS THE NORMALISED FORM, a forever-update trap when only one
|
|
87
87
|
* side of the comparison is normalised. `PVE::AccessControl::normalize_path` collapses repeated
|
|
88
|
-
* slashes and strips the trailing one, so a declared `/pool/
|
|
88
|
+
* slashes and strips the trailing one, so a declared `/pool/lab/` reads back as `/pool/lab`
|
|
89
89
|
* and a naive match never fires again. It also refuses a path with no leading slash, so one is
|
|
90
|
-
* added here rather than letting `pool/
|
|
90
|
+
* added here rather than letting `pool/lab` 400.
|
|
91
91
|
*/
|
|
92
92
|
const normalize = (raw: string) => `/${raw.split('/').filter(Boolean).join('/')}`;
|
|
93
93
|
|
|
@@ -32,8 +32,8 @@ type Case = {
|
|
|
32
32
|
|
|
33
33
|
const cases: readonly Case[] = [
|
|
34
34
|
{
|
|
35
|
-
declare: () => ProxmoxPool('
|
|
36
|
-
live: { 'pools/
|
|
35
|
+
declare: () => ProxmoxPool('lab', { comment: 'x', poolid: 'lab', target: FAKE_TARGET }),
|
|
36
|
+
live: { 'pools/lab': { comment: 'x', members: [] } },
|
|
37
37
|
name: 'Proxmox.Pool (pveHandlers: every factory family)',
|
|
38
38
|
provider: ProxmoxPoolProvider as never,
|
|
39
39
|
},
|
|
@@ -98,15 +98,15 @@ describe('adopting a matching object only reads', () => {
|
|
|
98
98
|
});
|
|
99
99
|
|
|
100
100
|
test('and the harness does see a write: a drifted Pool is PUT once', async () => {
|
|
101
|
-
const fake = clusterWith({ 'pools/
|
|
101
|
+
const fake = clusterWith({ 'pools/lab': { comment: 'old', members: [] } });
|
|
102
102
|
await withoutBao(async () => {
|
|
103
103
|
const engine = engineOver(ProxmoxPoolProvider().pipe(Layer.provideMerge(fake.layer)));
|
|
104
104
|
const declared = () =>
|
|
105
|
-
ProxmoxPool('
|
|
105
|
+
ProxmoxPool('lab', { comment: 'x', poolid: 'lab', target: FAKE_TARGET });
|
|
106
106
|
const report = await engine.verify(declared());
|
|
107
107
|
expect(report.rows[0]).toMatchObject({ changed: ['comment'], diff: 'update', ok: false });
|
|
108
|
-
expect(await engine.deploy(declared())).toEqual({
|
|
108
|
+
expect(await engine.deploy(declared())).toEqual({ lab: 'adopted' });
|
|
109
109
|
});
|
|
110
|
-
expect(fake.writes()).toEqual(['PUT pools/
|
|
110
|
+
expect(fake.writes()).toEqual(['PUT pools/lab']);
|
|
111
111
|
});
|
|
112
112
|
});
|
|
@@ -12,7 +12,7 @@ import type { ApiTokenAttributes, ApiTokenProps } from './api-token.ts';
|
|
|
12
12
|
*
|
|
13
13
|
* ⛔ THE WRITE SEMANTICS BELOW ARE READ FROM THE CLUSTER'S OWN PERL RATHER THAN INFERRED FROM THE
|
|
14
14
|
* API SCHEMA — `/usr/share/perl5/PVE/API2/User.pm`, subs `generate_token` and
|
|
15
|
-
* `update_token_info`, read over SSH on
|
|
15
|
+
* `update_token_info`, read over SSH on node-b on 2026-09-13. The two disagree in exactly the place
|
|
16
16
|
* that costs a forever-diff: the schema gives `expire` the default "same as user", and the code
|
|
17
17
|
* never implements it. api-token.ts records what that means.
|
|
18
18
|
*/
|
|
@@ -26,7 +26,7 @@ import { bool, int, text } from './values.ts';
|
|
|
26
26
|
* `ugid`, so reporting it as an attribute is what lets a privilege-separated token be granted
|
|
27
27
|
* anything at all without somebody retyping the join by hand.
|
|
28
28
|
*
|
|
29
|
-
* ⚠️ DERIVED, NOT READ. MEASURED: `GET /access/users/
|
|
29
|
+
* ⚠️ DERIVED, NOT READ. MEASURED: `GET /access/users/metrics@pve/token/exporter` answers
|
|
30
30
|
* `{"expire":0,"privsep":0}` and echoes back neither half of its own identity. So this is built
|
|
31
31
|
* from props, cannot disagree with the path the read used, and is therefore true by
|
|
32
32
|
* construction — which is why it is reported and never compared.
|
|
@@ -106,8 +106,8 @@ export const apiTokenSpec: PveSpec<ApiTokenProps, ApiTokenAttributes> = {
|
|
|
106
106
|
/**
|
|
107
107
|
* ⚠️ EXACTLY THE THREE FIELDS A PUT CAN PUT BACK, WHICH IS ALSO EXACTLY WHAT THE GET REPORTS.
|
|
108
108
|
* MEASURED, by replaying these two functions over the live GET bodies of all four token
|
|
109
|
-
* shapes this cluster has: `
|
|
110
|
-
* `
|
|
109
|
+
* shapes this cluster has: `metrics@pve!exporter` (no comment), `iac@pve!apply` and
|
|
110
|
+
* `app@pve!app` (commented), and a live OpenBao lease under `hf-read@pve` (a real
|
|
111
111
|
* non-zero `expire`, offered as both `1789327175` and `"1789327175"` since `int` must not care
|
|
112
112
|
* which). All four answer noop; flipping `privsep` answers update; an empty body answers
|
|
113
113
|
* absent. ⚠️ THAT IS THE COMPARISON, NOT THE ENGINE — no `alchemy plan` was run against this
|
package/src/proxmox/api-token.ts
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* `Proxmox.ApiToken` — a PVE API token: the thing a service actually authenticates as.
|
|
3
3
|
*
|
|
4
4
|
* ⛔ THE SECRET EXISTS FOR ONE HTTP RESPONSE AND THEN NOWHERE. MEASURED from the cluster's own
|
|
5
|
-
* schema (`/usr/share/pve-docs/api-viewer/apidoc.js` on
|
|
5
|
+
* schema (`/usr/share/pve-docs/api-viewer/apidoc.js` on node-b, 2026-09-13): POST returns
|
|
6
6
|
* `["full-tokenid","info","value"]`; PUT returns `["comment","expire","full-tokenid","privsep",
|
|
7
7
|
* "value"]` with `value` present ONLY when `regenerate` was set; GET returns
|
|
8
8
|
* `["comment","expire","privsep"]` and never the secret. A live token agrees —
|
|
9
|
-
* `GET /access/users/
|
|
9
|
+
* `GET /access/users/metrics@pve/token/exporter` answers `{"expire":0,"privsep":0}`. PVE says
|
|
10
10
|
* it plainly on the POST: the value "needs to be stored as it cannot be retrieved afterwards".
|
|
11
11
|
*
|
|
12
12
|
* ⛔ SO A TOKEN THIS RESOURCE CREATES IS UNUSABLE, AND SAYING SO IS THE POINT OF THIS BLOCK. The
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* ★ SO POINT THIS FAMILY AT TOKENS THAT ALREADY EXIST. `comment`, `expire` and `privsep` are the
|
|
20
20
|
* whole of a token's policy, and declaring them is real work: it is how `expire` stops being
|
|
21
21
|
* whatever somebody typed in 2024. Mint NEW tokens where the secret can be caught — OpenBao's
|
|
22
|
-
* `proxmox-
|
|
22
|
+
* `proxmox-c1` mount, or a human at `pveum user token add`.
|
|
23
23
|
* ⚠️ THE CREATE PATH IS LEFT REACHABLE RATHER THAN STUBBED. A create that silently did nothing
|
|
24
24
|
* would be the same lie as the stubbed delete resource.ts's ★ refuses; the honest arrangement
|
|
25
25
|
* is a create that works and a header that says what it produces.
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
* that owns the lease, or to a human who is watching.
|
|
32
32
|
*
|
|
33
33
|
* ⚠️ THERE IS A SECOND WRITER TO THESE OBJECTS AND IT IS THE ONE THIS PROVIDER RUNS ON. OpenBao's
|
|
34
|
-
* `proxmox-
|
|
35
|
-
*
|
|
34
|
+
* `proxmox-c1` engine mints PVE tokens under `hf-read@pve` and `hf-provision@pve` — read from
|
|
35
|
+
* `<estate>/platform/secrets/vault/plugin-proxmox/proxmox/`: `privsep=0` (client.go), `expire` set
|
|
36
36
|
* to the lease deadline, and an id the plugin CHOOSES,
|
|
37
37
|
* `hf-<role>-<actor>-<entity6>-<stamp>-<nonce>` (tokenname.go). Revocation and WAL rollback look
|
|
38
38
|
* up that one exact id (wal.go); neither sweeps a prefix. So the two systems cannot collide on
|
|
@@ -62,7 +62,7 @@ import { type PveRequirements, type WithTarget, pveHandlers, pveOperations } fro
|
|
|
62
62
|
|
|
63
63
|
export interface ApiTokenProps extends WithTarget {
|
|
64
64
|
/**
|
|
65
|
-
* The account that owns the token, realm-qualified: `
|
|
65
|
+
* The account that owns the token, realm-qualified: `iac@pve`, `root@pam`. Identity.
|
|
66
66
|
*
|
|
67
67
|
* ⚠️ THE ACCOUNT MUST ALREADY EXIST. Every method here runs `check_user_exist` first, so a
|
|
68
68
|
* token named under a missing user fails the READ (folded to "absent") and then fails the
|
|
@@ -120,7 +120,7 @@ export interface ApiTokenProps extends WithTarget {
|
|
|
120
120
|
* — `$privsep = $privsep ? 1 : 0` (AccessControl.pm:1622), so there is no unset state to
|
|
121
121
|
* preserve. Were this optional, adopting a live token without mentioning `privsep` would plan
|
|
122
122
|
* an update to 1 and STRIP a working credential of every privilege it has, silently: PVE
|
|
123
|
-
* returns 401/403 to the service, nothing errors here, and `
|
|
123
|
+
* returns 401/403 to the service, nothing errors here, and `iac@pve!apply` simply stops
|
|
124
124
|
* working. Required, so adopting a token is a sentence that states what it is.
|
|
125
125
|
* ⚠️ AND `false` IS NOT A SHRUG. It is a token with its owner's whole privilege set; under a
|
|
126
126
|
* provisioning account that is the account's full authority with a separate secret.
|
|
@@ -156,9 +156,9 @@ export interface ProxmoxApiToken extends Resource<
|
|
|
156
156
|
* ★ `retain` BY DEFAULT, AND THIS IS THE PLAINEST CASE FOR IT IN THE PACKAGE. A token's contents
|
|
157
157
|
* are one irreplaceable secret: delete it and the value is gone, a replacement is a DIFFERENT
|
|
158
158
|
* value, and every holder loses access the instant `cfs_write_file` returns — with no error
|
|
159
|
-
* raised anywhere near them. The live cluster's tokens are `
|
|
160
|
-
* exporter feeding VictoriaMetrics), `
|
|
161
|
-
* `
|
|
159
|
+
* raised anywhere near them. The live cluster's tokens are `metrics@pve!exporter` (the PVE
|
|
160
|
+
* exporter feeding VictoriaMetrics), `agent@pve!executor`, `iac@pve!apply`, `iac@pve!ro`,
|
|
161
|
+
* `app@pve!app` and `mint@pve!engine` — the last being the parent credential the
|
|
162
162
|
* OpenBao mount itself authenticates with, so orphaning that one would stop every plan in this
|
|
163
163
|
* package, this resource included. `delete` is FULLY IMPLEMENTED (DELETE is a real method on
|
|
164
164
|
* this path, measured) and runs the moment a caller opts in with `.pipe(RemovalPolicy.destroy())`.
|
|
@@ -184,7 +184,7 @@ const ops = pveOperations(apiTokenSpec);
|
|
|
184
184
|
* `reconcile` refuses by name and says what to do instead.
|
|
185
185
|
*
|
|
186
186
|
* ★ EVERYTHING ELSE STILL WORKS, AND IT IS THE HALF WORTH HAVING. Adopting an existing token and
|
|
187
|
-
* converging its `comment`, `expire` and `privsep` are real operations:
|
|
187
|
+
* converging its `comment`, `expire` and `privsep` are real operations: C1 carries SEVEN tokens
|
|
188
188
|
* with `expire=0` and `privsep=0` made by clicks nobody recorded. Declaring those freezes the
|
|
189
189
|
* set, and an eighth appearing shows up as drift.
|
|
190
190
|
*
|
|
@@ -203,7 +203,7 @@ const handlers = {
|
|
|
203
203
|
'the secret only in the create response and it cannot be stored, so a token made here ' +
|
|
204
204
|
'would be a live credential nobody holds. Create it with `pveum user token add` and ' +
|
|
205
205
|
'capture the value, or mint a short-lived one from the OpenBao proxmox mount ' +
|
|
206
|
-
'(`bao read proxmox-
|
|
206
|
+
'(`bao read proxmox-c1/creds/<role>`) -- then declare it here to manage it.',
|
|
207
207
|
),
|
|
208
208
|
);
|
|
209
209
|
}
|
|
@@ -213,7 +213,7 @@ const handlers = {
|
|
|
213
213
|
|
|
214
214
|
/**
|
|
215
215
|
* ⛔ `list` IS EMPTY, AND FOR THIS FAMILY THAT MATTERS MORE THAN THE GENERIC ARGUMENT IN
|
|
216
|
-
* resource.ts. A token index handed to Alchemy would offer up `
|
|
216
|
+
* resource.ts. A token index handed to Alchemy would offer up `mint@pve!engine` and every
|
|
217
217
|
* live OpenBao lease for adoption — and adoption is what makes a later plan willing to delete.
|
|
218
218
|
* The mount's leases in particular appear and vanish on their own; anything that adopted one
|
|
219
219
|
* would report drift against a credential that was never its to hold.
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* here decides a diff; `findRow` picks a row out of an answer its caller already fetched.
|
|
10
10
|
*
|
|
11
11
|
* ⛔ THE TWO PATHS ARE DIFFERENT AND THAT IS THE WHOLE REASON THIS FILE HAS TWO OF THEM. MEASURED
|
|
12
|
-
* on
|
|
12
|
+
* on node-b, 2026-09-13: there is no GET on the id path — `pvesh get /nodes/node-b/ceph/mon/node-b` answers
|
|
13
13
|
* "No 'get' handler defined" — while POST and DELETE are registered ONLY there. So the read uses
|
|
14
14
|
* `collectionPath` and the two writes use `daemonPath`, and swapping either one is a silent
|
|
15
15
|
* 501: a create that never lands, or a destroy that reports failure while the daemon runs on.
|
|
@@ -41,8 +41,8 @@ export const daemonId = (props: CephDaemonProps) => props.name ?? props.node;
|
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
43
|
* The read path. ⚠️ `{node}` HERE IS ONLY THE NODE BEING ASKED, not a filter. MEASURED:
|
|
44
|
-
* `GET /nodes/
|
|
45
|
-
* three IN A DIFFERENT ORDER — [
|
|
44
|
+
* `GET /nodes/node-b/ceph/mon` returns node-b, node-c AND node-d, and `GET /nodes/node-c/ceph/mon` returns the same
|
|
45
|
+
* three IN A DIFFERENT ORDER — [node-d,node-b,node-c] from node-b against [node-c,node-b,node-d] from node-c. A caller that took
|
|
46
46
|
* row zero, or assumed the list was this node's daemons, would be wrong on both counts.
|
|
47
47
|
*/
|
|
48
48
|
export const collectionPath = (props: CephDaemonProps) => `nodes/${props.node}/ceph/${props.kind}`;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* `Proxmox.CephDaemon` — a Ceph monitor, manager or metadata server on one node.
|
|
3
3
|
*
|
|
4
4
|
* ★ ONE RESOURCE FOR THREE KINDS, AND THE SCHEMAS WERE CHECKED BEFORE THAT WAS DECIDED. Parsed
|
|
5
|
-
* from `/usr/share/pve-docs/api-viewer/apidoc.js` on
|
|
5
|
+
* from `/usr/share/pve-docs/api-viewer/apidoc.js` on node-b, 2026-09-13: mon, mgr and mds each
|
|
6
6
|
* expose GET on the collection and POST + DELETE on the id below it, each returns
|
|
7
7
|
* name/host/state/addr/ceph_version*, and the only divergence is the create parameter
|
|
8
8
|
* (`mon-address` / none / `hotstandby`) and the read-only extras (mon: quorum, rank; mds: rank,
|
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
* times over and let them drift — the argument resource.ts already makes for the factory itself.
|
|
11
11
|
*
|
|
12
12
|
* ⛔ THE ONLY GET IS THE COLLECTION'S, AND IT ANSWERS FOR THE WHOLE CLUSTER. MEASURED:
|
|
13
|
-
* `GET /nodes/
|
|
14
|
-
* three IN A DIFFERENT ORDER — [
|
|
13
|
+
* `GET /nodes/node-b/ceph/mon` returns node-b, node-c AND node-d, and `GET /nodes/node-c/ceph/mon` returns the same
|
|
14
|
+
* three IN A DIFFERENT ORDER — [node-d,node-b,node-c] from node-b, [node-c,node-b,node-d] from node-c — so a row is found by
|
|
15
15
|
* `name` and NEVER by position, and the `{node}` in the path is only the node being ASKED.
|
|
16
|
-
* MEASURED too: there is no GET on the id path at all; `pvesh get /nodes/
|
|
17
|
-
* "No 'get' handler defined for '/nodes/
|
|
16
|
+
* MEASURED too: there is no GET on the id path at all; `pvesh get /nodes/node-b/ceph/mon/node-b` answers
|
|
17
|
+
* "No 'get' handler defined for '/nodes/node-b/ceph/mon/node-b'", and mgr and mds answer the same.
|
|
18
18
|
* That is why `path()` below is the COLLECTION, and that in turn is why `delete` is the one
|
|
19
19
|
* handler this file writes by hand: `pveOperations.destroy` sends its DELETE to `spec.path`,
|
|
20
20
|
* which here would be the collection — a 501 reported as a failed destroy, with the daemon still
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
* strips its section from ceph.conf. THREE MONS TOLERATE ONE LOSS; TWO TOLERATE NONE. Removing
|
|
28
28
|
* two in one deploy — or removing the mon on the node whose API is serving the call — loses
|
|
29
29
|
* quorum, and a cluster without mon quorum BLOCKS every RBD and CephFS I/O rather than erroring
|
|
30
|
-
* it, so on this cluster every guest on `
|
|
30
|
+
* it, so on this cluster every guest on `rbd-c1` hangs.
|
|
31
31
|
* `mds`: destroying a STANDBY is a non-event; destroying the ACTIVE one fails CephFS over,
|
|
32
|
-
* and with no standby left `cephfs-
|
|
32
|
+
* and with no standby left `cephfs-c1` goes unavailable. `mgr`: no guest I/O depends on it, but
|
|
33
33
|
* the last one takes the PG autoscaler, the dashboard and PVE's own Ceph status with it.
|
|
34
|
-
* Live on 2026-09-13: mon, mgr AND mds on each of
|
|
34
|
+
* Live on 2026-09-13: mon, mgr AND mds on each of node-b, node-c, node-d.
|
|
35
35
|
*
|
|
36
36
|
* ★ AND THE ONLY WAY TO REACH THAT DESTROY IS TO DELETE THE DECLARATION. `matches` is deliberately
|
|
37
37
|
* total (see its ⛔), so this resource CANNOT plan a `replace` — which is the one action that
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
* `Sys.Audit`/`Datastore.Audit` on `/`, and PVEAuditor holds both (MEASURED:
|
|
42
42
|
* `GET /access/roles/PVEAuditor`), so the mount's `read` role can see this family — unlike
|
|
43
43
|
* `Proxmox.Storage`, which 403s under that lease. POST and DELETE both check `Sys.Modify` on
|
|
44
|
-
* `/`, and MEASURED on this cluster
|
|
44
|
+
* `/`, and MEASURED on this cluster the provision role already held it (it is in `PROVISION_PRIVILEGES`), so the provision lane
|
|
45
45
|
* needs no widening for once. ⚠️ `Sys.Modify` on the ROOT path is broad — it also buys
|
|
46
46
|
* datacenter options and every other cluster-wide config write — so it is worth knowing that
|
|
47
47
|
* this credential already has it rather than discovering it the next time something is scoped.
|
|
@@ -86,9 +86,9 @@ export interface CephDaemonProps extends WithTarget {
|
|
|
86
86
|
name?: string;
|
|
87
87
|
/**
|
|
88
88
|
* mon only. Overrides the autodetected monitor IP; must sit in Ceph's public network — on this
|
|
89
|
-
* cluster that is
|
|
89
|
+
* cluster that is 198.51.100.0/24, carried by `vmbr1.42`.
|
|
90
90
|
* ⛔ CREATE-ONLY AND NEVER COMPARED. PVE takes a bare ip-list on write and hands back `addr` as
|
|
91
|
-
* `
|
|
91
|
+
* `198.51.100.12:6789/0` — an address, a port and a nonce. The two are not the same string and
|
|
92
92
|
* never will be, so diffing them would report a mismatch on every plan; with no PUT on this
|
|
93
93
|
* family that mismatch becomes a REPLACE, and a replace of a mon is the quorum loss in the
|
|
94
94
|
* header. Left out of `matches` on purpose.
|
|
@@ -98,7 +98,7 @@ export interface CephDaemonProps extends WithTarget {
|
|
|
98
98
|
* mds only. Makes this standby replay the active MDS's journal for a faster failover.
|
|
99
99
|
* ⛔ CREATE-ONLY AND NEVER COMPARED, AND THE FIELD THAT LOOKS LIKE ITS READBACK IS NOT ONE.
|
|
100
100
|
* `standby_replay` in the list is the daemon's CURRENT state, not its configuration: MEASURED
|
|
101
|
-
* 2026-09-13, all three mds report `standby_replay: false`,
|
|
101
|
+
* 2026-09-13, all three mds report `standby_replay: false`, node-c included — and node-c is the ACTIVE
|
|
102
102
|
* mds, which is not a standby at all and so can never report true however it was created.
|
|
103
103
|
* Comparing `hotstandby` to it would plan a replace against whichever mds Ceph happens to have
|
|
104
104
|
* elected, i.e. against a value no declaration controls.
|
|
@@ -128,22 +128,22 @@ export interface CephDaemonAttributes {
|
|
|
128
128
|
host: string;
|
|
129
129
|
/**
|
|
130
130
|
* mon `running`/`stopped`/`unknown`, mgr `active`/`standby`, mds `up:active`/`up:standby`/…
|
|
131
|
-
* ⛔ CEPH ELECTS THIS AND REWRITES IT WITHOUT ANYONE DECLARING ANYTHING. MEASURED: mgr
|
|
132
|
-
* `active` while
|
|
131
|
+
* ⛔ CEPH ELECTS THIS AND REWRITES IT WITHOUT ANYONE DECLARING ANYTHING. MEASURED: mgr node-b is
|
|
132
|
+
* `active` while node-c and node-d are `standby`; mds node-c is `up:active` while node-b and node-d are
|
|
133
133
|
* `up:standby`. Restart a daemon and the roles move. It is reported so a plan can show what is
|
|
134
134
|
* live and kept out of `matches` so that an election is never a diff.
|
|
135
135
|
* ⚠️ IT ALSO MEANS THIS RESOURCE DOES NOT MANAGE WHETHER THE DAEMON IS RUNNING. A configured but
|
|
136
136
|
* stopped daemon is still a row in the list, so it reads as present and plans `noop`.
|
|
137
137
|
*/
|
|
138
138
|
state: string;
|
|
139
|
-
/** Ceph-formatted, e.g. `
|
|
139
|
+
/** Ceph-formatted, e.g. `198.51.100.12:6789/0`. Reported; see `mon-address` for why never compared. */
|
|
140
140
|
addr: string;
|
|
141
141
|
/** `ceph_version_short`, e.g. `20.2.2`. Reported: a straggler after an upgrade is worth seeing. */
|
|
142
142
|
version: string;
|
|
143
143
|
/**
|
|
144
144
|
* mon rank in the monmap, mds rank in the filesystem, `-1` when the daemon has neither.
|
|
145
|
-
* ⛔ CEPH ASSIGNS IT AND NOTHING DECLARES IT. MEASURED: the mons are ranked
|
|
146
|
-
* monmap order, not declaration order — and the mds ranks are 0 for the active
|
|
145
|
+
* ⛔ CEPH ASSIGNS IT AND NOTHING DECLARES IT. MEASURED: the mons are ranked node-d=0, node-c=1, node-b=2 —
|
|
146
|
+
* monmap order, not declaration order — and the mds ranks are 0 for the active node-c and -1 for
|
|
147
147
|
* the two standbys. Rank moves when a daemon is added, removed or restarted.
|
|
148
148
|
*/
|
|
149
149
|
rank: number;
|
|
@@ -152,8 +152,8 @@ export interface CephDaemonAttributes {
|
|
|
152
152
|
/** mds only, and NOT the readback of `hotstandby` — see the ⛔ on that prop. */
|
|
153
153
|
standbyReplay: boolean;
|
|
154
154
|
/**
|
|
155
|
-
* mds only: the CephFS this daemon currently serves — `cephfs-
|
|
156
|
-
* ⛔ PRESENT ONLY ON THE ACTIVE MDS. MEASURED:
|
|
155
|
+
* mds only: the CephFS this daemon currently serves — `cephfs-c1` here.
|
|
156
|
+
* ⛔ PRESENT ONLY ON THE ACTIVE MDS. MEASURED: node-c carries `fs_name`, node-b and node-d have no such key
|
|
157
157
|
* at all. So it is empty for two of three identical declarations, and comparing it would
|
|
158
158
|
* report work on exactly the daemons that are healthy standbys.
|
|
159
159
|
*/
|
|
@@ -234,7 +234,7 @@ export const ProxmoxCephDaemonProvider = () =>
|
|
|
234
234
|
* problem. Everything else here is the factory's.
|
|
235
235
|
* ⚠️ AND IT IS THE DANGEROUS ONE. Re-read the destroy ⛔ in the header before letting a
|
|
236
236
|
* plan that removes a mon run: two gone at once is a cluster with no quorum and every
|
|
237
|
-
* guest on `
|
|
237
|
+
* guest on `rbd-c1` blocked on I/O.
|
|
238
238
|
*/
|
|
239
239
|
delete: ({ olds }: { olds: CephDaemonProps }) =>
|
|
240
240
|
pve(olds.target, 'provision', 'DELETE', daemonPath(olds)),
|
package/src/proxmox/ceph-flag.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* `Proxmox.CephFlag` — one cluster-wide Ceph OSD flag, declared: `noout`, `pause`, `norebalance`.
|
|
3
3
|
*
|
|
4
4
|
* ★ ONE RESOURCE PER FLAG, NOT ONE CARRYING ALL ELEVEN, AND THE CLUSTER'S OWN API DECIDED IT.
|
|
5
|
-
* MEASURED on
|
|
5
|
+
* MEASURED on C1 2026-09-13 (pve-manager 9.2.11, ceph tentacle 20.2.2) by reading
|
|
6
6
|
* `/usr/share/perl5/PVE/API2/Cluster/Ceph.pm` and the published schema in
|
|
7
7
|
* `/usr/share/pve-docs/api-viewer/apidoc.js`:
|
|
8
8
|
* · `PUT /cluster/ceph/flags` ends in `fork_worker('cephsetflags', ...)` and returns a STRING
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* defaulting to false would make `CephFlag('noout', { flag: 'noout', target })` mean "clear
|
|
31
31
|
* it" — the dangerous direction, chosen by OMISSION. Here the dangerous sentence has to be
|
|
32
32
|
* typed out by a person. Do not add a default, and do not declare a flag merely to document
|
|
33
|
-
* that it is off: MEASURED 2026-09-13, all eleven flags on
|
|
33
|
+
* that it is off: MEASURED 2026-09-13, all eleven flags on C1 read 0, so eleven
|
|
34
34
|
* `value: false` lines would plan green forever and do nothing except take the brake off
|
|
35
35
|
* whenever somebody happens to apply them.
|
|
36
36
|
*
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
* and plans `noop` straight past it. It cannot express a half-pause, so never use it to prove
|
|
59
59
|
* one is gone; `ceph osd dump | head -1` can.
|
|
60
60
|
* ⛔ `pause` AND `noup` ARE ALSO THE TWO THAT TAKE THE CLUSTER DOWN FROM A TYPO. `pause: true`
|
|
61
|
-
* stops all reads and writes, so every guest on `
|
|
62
|
-
* MEASURED 2026-09-13 — freezes on its root disk, and so does `cephfs-
|
|
61
|
+
* stops all reads and writes, so every guest on `rbd-c1` — the cluster's only rbd pool,
|
|
62
|
+
* MEASURED 2026-09-13 — freezes on its root disk, and so does `cephfs-c1`. `noup: true`
|
|
63
63
|
* stops a rebooted OSD from ever rejoining. Neither is undone by deleting the line.
|
|
64
64
|
*
|
|
65
65
|
* ⚠️ PRIVILEGES, AND THE WRITE ONE IS A BIG ASK. Both GETs check `Sys.Audit` on `/`; both PUTs
|
|
@@ -83,7 +83,7 @@ import { bool, flag } from './values.ts';
|
|
|
83
83
|
*
|
|
84
84
|
* ⚠️ THE ENUM IS CLOSED AND A TYPO IS A 400, WHICH IS WHY THIS IS A UNION AND NOT `string`. Both
|
|
85
85
|
* the GET and the PUT declare `additionalProperties => 0` over exactly this list.
|
|
86
|
-
* ⚠️ CEPH HAS FLAGS PVE DOES NOT MODEL, and four of them are always on. MEASURED on
|
|
86
|
+
* ⚠️ CEPH HAS FLAGS PVE DOES NOT MODEL, and four of them are always on. MEASURED on C1:
|
|
87
87
|
* `ceph osd dump` reports `flags sortbitwise,recovery_deletes,purged_snapdirs,pglog_hardlimit`.
|
|
88
88
|
* PVE reads that same string and answers only about its own eleven, so the others can neither
|
|
89
89
|
* leak in here nor be set from here — `noautoscale` and `nosnaptrim` included.
|
|
@@ -167,7 +167,7 @@ const handlers = pveHandlers<CephFlagProps, CephFlagAttributes>({
|
|
|
167
167
|
*
|
|
168
168
|
* ⛔ IT NEVER ANSWERS undefined, WHICH MAKES THE CREATE BRANCH UNREACHABLE, AND ITS ERROR WILL
|
|
169
169
|
* MISLEAD YOU. All eleven flags always exist; there is nothing for "absent" to mean. So a POST
|
|
170
|
-
* to `collection` means the READ failed — an expired 300s lease,
|
|
170
|
+
* to `collection` means the READ failed — an expired 300s lease, node-b down, or ceph simply not
|
|
171
171
|
* configured on the cluster (every one of these four handlers opens with
|
|
172
172
|
* `check_ceph_configured()`, which dies). Read the resulting "Method 'POST /cluster/ceph/flags'
|
|
173
173
|
* not implemented" as "the read failed" and go and look at the credential, not at ceph. Same
|
|
@@ -187,7 +187,7 @@ const handlers = pveHandlers<CephFlagProps, CephFlagAttributes>({
|
|
|
187
187
|
* and `name` exist only on the bulk GET, which this file does not read, so neither can be
|
|
188
188
|
* compared by accident. Nothing PVE returns here is rewritten, re-ordered or re-typed by the
|
|
189
189
|
* cluster, because all PVE returns here is one boolean.
|
|
190
|
-
* ★ MEASURED: on
|
|
190
|
+
* ★ MEASURED: on C1, `GET /cluster/ceph/flags` answers value 0 for all eleven flags, so a
|
|
191
191
|
* declaration of `value: false` reads back false and plans `noop`.
|
|
192
192
|
*/
|
|
193
193
|
matches: (attributes, props) => attributes.value === props.value,
|