@homeflare/alchemy 0.11.0 → 0.13.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/alertmanager-body.d.ts +54 -0
- package/dist/proxmox/alertmanager-body.d.ts.map +1 -0
- 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/credential-literals.d.ts +36 -0
- package/dist/proxmox/credential-literals.d.ts.map +1 -0
- package/dist/proxmox/credentials.d.ts +8 -8
- package/dist/proxmox/credentials.d.ts.map +1 -1
- package/dist/proxmox/fake-pbs-notify.d.ts +23 -0
- package/dist/proxmox/fake-pbs-notify.d.ts.map +1 -0
- package/dist/proxmox/fake-pve.d.ts +5 -0
- package/dist/proxmox/fake-pve.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 +279 -1
- package/dist/proxmox/generated/pbs.d.ts.map +1 -1
- package/dist/proxmox/generated/pve.d.ts +58 -1
- package/dist/proxmox/generated/pve.d.ts.map +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 +14 -0
- package/dist/proxmox/index.d.ts.map +1 -1
- package/dist/proxmox/index.js +935 -223
- package/dist/proxmox/index.js.map +69 -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-matcher-form.d.ts +100 -0
- package/dist/proxmox/notification-matcher-form.d.ts.map +1 -0
- package/dist/proxmox/notification-matcher.d.ts +46 -0
- package/dist/proxmox/notification-matcher.d.ts.map +1 -0
- 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-notification-matcher.d.ts +53 -0
- package/dist/proxmox/pbs-notification-matcher.d.ts.map +1 -0
- package/dist/proxmox/pbs-notification-target-form.d.ts +63 -0
- package/dist/proxmox/pbs-notification-target-form.d.ts.map +1 -0
- package/dist/proxmox/pbs-notification-target-lifecycle.d.ts +100 -0
- package/dist/proxmox/pbs-notification-target-lifecycle.d.ts.map +1 -0
- package/dist/proxmox/pbs-notification-target-wire.d.ts +30 -0
- package/dist/proxmox/pbs-notification-target-wire.d.ts.map +1 -0
- package/dist/proxmox/pbs-notification-target.d.ts +123 -0
- package/dist/proxmox/pbs-notification-target.d.ts.map +1 -0
- 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 +6 -5
- 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/write-only.d.ts +35 -0
- package/dist/proxmox/write-only.d.ts.map +1 -0
- 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/dist/verify/fake-engine.d.ts +5 -0
- package/dist/verify/fake-engine.d.ts.map +1 -1
- package/docs/adopt-verify.md +21 -5
- package/docs/adopted-deploys.md +9 -1
- package/docs/ownership.md +26 -1
- package/docs/pbs-alertmanager-body.md +73 -0
- package/docs/pbs-notifications.md +163 -0
- 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 +9 -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/alertmanager-body.ts +125 -0
- 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/credential-literals.test.ts +148 -0
- package/src/proxmox/credential-literals.ts +74 -0
- package/src/proxmox/credentials.test.ts +6 -8
- package/src/proxmox/credentials.ts +8 -8
- package/src/proxmox/fake-pbs-notify.ts +108 -0
- package/src/proxmox/fake-pve.ts +12 -2
- package/src/proxmox/firewall-alias-form.ts +1 -1
- package/src/proxmox/firewall-alias.ts +1 -1
- package/src/proxmox/generated/pbs.ts +330 -1
- package/src/proxmox/generated/pve.ts +63 -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 +10 -0
- package/src/proxmox/index.ts +32 -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-matcher-form.ts +221 -0
- package/src/proxmox/notification-matcher.test.ts +234 -0
- package/src/proxmox/notification-matcher.ts +76 -0
- 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-notification-matcher.ts +81 -0
- package/src/proxmox/pbs-notification-target-drift.test.ts +83 -0
- package/src/proxmox/pbs-notification-target-form.ts +229 -0
- package/src/proxmox/pbs-notification-target-lifecycle.ts +177 -0
- package/src/proxmox/pbs-notification-target-state.test.ts +191 -0
- package/src/proxmox/pbs-notification-target-wire.ts +112 -0
- package/src/proxmox/pbs-notification-target.test.ts +226 -0
- package/src/proxmox/pbs-notification-target.ts +139 -0
- package/src/proxmox/pbs-notifications-stack.test.ts +94 -0
- 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 +9 -9
- 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/write-only.test.ts +75 -0
- package/src/proxmox/write-only.ts +106 -0
- 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/verify/fake-engine.ts +8 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Generated PVE API types — DO NOT EDIT BY HAND.
|
|
3
3
|
*
|
|
4
4
|
* Run: bun codegen/generate.ts
|
|
5
|
-
* Source:
|
|
5
|
+
* Source: the cluster's own `apidoc.js` (pve-docs), extracted outside this repo.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
/** GET /cluster/replication — `data` payload after client unwrap. */
|
|
@@ -375,6 +375,68 @@ export type ClusterNotificationsEndpointsWebhookNamePutReturn = null;
|
|
|
375
375
|
/** DELETE /cluster/notifications/endpoints/webhook/{name} — `data` payload after client unwrap. */
|
|
376
376
|
export type ClusterNotificationsEndpointsWebhookNameDeleteReturn = null;
|
|
377
377
|
|
|
378
|
+
/** GET /cluster/notifications/matchers — `data` payload after client unwrap. */
|
|
379
|
+
export type ClusterNotificationsMatchersGetReturn = readonly ({
|
|
380
|
+
comment?: string;
|
|
381
|
+
disable?: boolean | 0 | 1;
|
|
382
|
+
'invert-match'?: boolean | 0 | 1;
|
|
383
|
+
'match-calendar'?: readonly string[];
|
|
384
|
+
'match-field'?: readonly string[];
|
|
385
|
+
'match-severity'?: readonly string[];
|
|
386
|
+
mode?: 'all' | 'any';
|
|
387
|
+
name: string;
|
|
388
|
+
origin: 'user-created' | 'builtin' | 'modified-builtin';
|
|
389
|
+
target?: readonly string[];
|
|
390
|
+
} & Record<string, unknown>)[];
|
|
391
|
+
|
|
392
|
+
/** POST /cluster/notifications/matchers — form/query parameters (path segments omitted). */
|
|
393
|
+
export type ClusterNotificationsMatchersPostParams = {
|
|
394
|
+
comment?: string;
|
|
395
|
+
disable?: '0' | '1';
|
|
396
|
+
'invert-match'?: '0' | '1';
|
|
397
|
+
'match-calendar'?: readonly string[];
|
|
398
|
+
'match-field'?: readonly string[];
|
|
399
|
+
'match-severity'?: readonly string[];
|
|
400
|
+
mode?: 'all' | 'any';
|
|
401
|
+
name: string;
|
|
402
|
+
target?: readonly string[];
|
|
403
|
+
};
|
|
404
|
+
/** POST /cluster/notifications/matchers — `data` payload after client unwrap. */
|
|
405
|
+
export type ClusterNotificationsMatchersPostReturn = null;
|
|
406
|
+
|
|
407
|
+
/** GET /cluster/notifications/matchers/{name} — `data` payload after client unwrap. */
|
|
408
|
+
export type ClusterNotificationsMatchersNameGetReturn = {
|
|
409
|
+
comment?: string;
|
|
410
|
+
digest?: string;
|
|
411
|
+
disable?: boolean | 0 | 1;
|
|
412
|
+
'invert-match'?: boolean | 0 | 1;
|
|
413
|
+
'match-calendar'?: readonly string[];
|
|
414
|
+
'match-field'?: readonly string[];
|
|
415
|
+
'match-severity'?: readonly string[];
|
|
416
|
+
mode?: 'all' | 'any';
|
|
417
|
+
name: string;
|
|
418
|
+
target?: readonly string[];
|
|
419
|
+
} & Record<string, unknown>;
|
|
420
|
+
|
|
421
|
+
/** PUT /cluster/notifications/matchers/{name} — form/query parameters (path segments omitted). */
|
|
422
|
+
export type ClusterNotificationsMatchersNamePutParams = {
|
|
423
|
+
comment?: string;
|
|
424
|
+
delete?: readonly string[];
|
|
425
|
+
digest?: string;
|
|
426
|
+
disable?: '0' | '1';
|
|
427
|
+
'invert-match'?: '0' | '1';
|
|
428
|
+
'match-calendar'?: readonly string[];
|
|
429
|
+
'match-field'?: readonly string[];
|
|
430
|
+
'match-severity'?: readonly string[];
|
|
431
|
+
mode?: 'all' | 'any';
|
|
432
|
+
target?: readonly string[];
|
|
433
|
+
};
|
|
434
|
+
/** PUT /cluster/notifications/matchers/{name} — `data` payload after client unwrap. */
|
|
435
|
+
export type ClusterNotificationsMatchersNamePutReturn = null;
|
|
436
|
+
|
|
437
|
+
/** DELETE /cluster/notifications/matchers/{name} — `data` payload after client unwrap. */
|
|
438
|
+
export type ClusterNotificationsMatchersNameDeleteReturn = null;
|
|
439
|
+
|
|
378
440
|
/** GET /cluster/firewall/aliases — `data` payload after client unwrap. */
|
|
379
441
|
export type ClusterFirewallAliasesGetReturn = readonly ({
|
|
380
442
|
cidr: string;
|
package/src/proxmox/group.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `Proxmox.Group` — a PVE group: a name, a comment, and the set of users PVE hangs off it.
|
|
3
3
|
*
|
|
4
|
-
* ★ DECLARING `hf-mint` IS THE WHOLE POINT OF THIS FAMILY. `
|
|
4
|
+
* ★ DECLARING `hf-mint` IS THE WHOLE POINT OF THIS FAMILY. `mint@pve` holds the `MintTokens` role
|
|
5
5
|
* ON THE PATH `/access/groups/hf-mint` — measured from `GET /access/acl`, 2026-09-13 — which is
|
|
6
6
|
* what confines the OpenBao proxmox engine to minting inside that one group. The group is the
|
|
7
7
|
* fence around the credential every other resource in this package runs on, and until now it was
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* in this file could ever settle.
|
|
18
18
|
*
|
|
19
19
|
* ⛔ THE INDEX AND THE ITEM DISAGREE ABOUT BOTH THE NAME AND THE TYPE, and this file reads only the
|
|
20
|
-
* item. MEASURED against
|
|
20
|
+
* item. MEASURED against node-b, 2026-09-13:
|
|
21
21
|
* GET /access/groups -> [{"groupid":"hf-mint","comment":"…",
|
|
22
22
|
* "users":"hf-provision@pve,hf-read@pve"}, …]
|
|
23
23
|
* GET /access/groups/hf-mint -> {"comment":"…","members":["hf-provision@pve","hf-read@pve"]}
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
*
|
|
28
28
|
* ⛔ AND THE ITEM'S ORDER IS NOT STABLE BETWEEN TWO CONSECUTIVE CALLS. MEASURED, seconds apart, on
|
|
29
29
|
* the same endpoint:
|
|
30
|
-
* ["root@pam","
|
|
31
|
-
* ["
|
|
30
|
+
* ["root@pam","alice@pve","alice@pam","alice@example.com@corp"]
|
|
31
|
+
* ["alice@pam","alice@pve","alice@example.com@corp","root@pam"]
|
|
32
32
|
* `read_group` builds it as `[keys %{ $data->{users} }]` — a bare Perl hash key list, whose order
|
|
33
33
|
* is randomised per process — while the INDEX sorts (`join(',', sort keys …)`). So the array is
|
|
34
34
|
* sorted on the way into `attributes` as well as being kept out of `matches`: unsorted, Alchemy's
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
* So `readRole` is left at the default `read` — unlike storage/sdn-zone/sdn-vnet, whose ITEM
|
|
46
46
|
* reads PVE gates on an allocate privilege. See the ⛔ on `readRole` in `resource.ts`.
|
|
47
47
|
* · POST, PUT and DELETE each check `Group.Allocate` on `/access/groups`, and
|
|
48
|
-
* `hf-provision@pve` holds it via `
|
|
48
|
+
* `hf-provision@pve` holds it via the provision role (`PROVISION_PRIVILEGES`).
|
|
49
49
|
*
|
|
50
50
|
* ⛔ `retain` BY DEFAULT, BECAUSE DELETING A GROUP DESTROYS TWO THINGS THIS FILE CANNOT PUT BACK.
|
|
51
51
|
* Measured in `PVE::AccessControl` on the node:
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
* <comment>:`) and DERIVES each user's `groups` map from it at parse time. Deleting the group
|
|
54
54
|
* deletes the list, and this resource has no `members` prop to restore it from.
|
|
55
55
|
* · `delete_group_acl` then walks the whole ACL tree and drops every grant where the group is
|
|
56
|
-
* the SUBJECT. On this cluster that is `
|
|
57
|
-
* `
|
|
56
|
+
* the SUBJECT. On this cluster that is `admins -> Administrator on /` and
|
|
57
|
+
* `automation -> PVEAuditor on /`: whole populations of access, gone in one call, with no
|
|
58
58
|
* confirmation and nothing in the plan to suggest it.
|
|
59
59
|
* `delete` is FULLY IMPLEMENTED — `DELETE /access/groups/{groupid}` exists, unlike the ACL
|
|
60
60
|
* family's — so `.pipe(RemovalPolicy.destroy())` really removes the group. See the ★ in
|
|
@@ -120,7 +120,7 @@ export const ProxmoxGroup = Resource<ProxmoxGroup>('Proxmox.Group', {
|
|
|
120
120
|
* back as absent. Left alone, `matches` would be false on every plan and every deploy would
|
|
121
121
|
* rewrite the same value into the same hole. Normalising BOTH the comparison and the form is how
|
|
122
122
|
* `values.ts` already treats `''` as the absence of a boolean rather than as `false`.
|
|
123
|
-
* ⚠️ MEASURED IN THE SOURCE ON
|
|
123
|
+
* ⚠️ MEASURED IN THE SOURCE ON node-b, NOT ON THE WIRE — writing `'0'` to the cluster to watch it
|
|
124
124
|
* vanish would have been a write, and this agent had read access only.
|
|
125
125
|
* ⚠️ `pool.ts` HAS THE SAME LATENT HOLE — `pool:$pool:$comment:…` is written by that same
|
|
126
126
|
* truthiness test and is not guarded. Not fixed from here; flagged so it is fixed on purpose.
|
|
@@ -160,7 +160,7 @@ const handlers = pveHandlers<GroupProps, GroupAttributes>({
|
|
|
160
160
|
* without one answers `{"members":[…]}` and nothing else. THAT ONE IS READ OFF THE HANDLER'S
|
|
161
161
|
* SOURCE, not off the wire — every group on this cluster happens to carry a comment, so the
|
|
162
162
|
* commentless shape was not there to measure. The empty-MEMBERS shape WAS measured —
|
|
163
|
-
* `
|
|
163
|
+
* `automation` answers `{"comment":"…","members":[]}`. `''` is what an undeclared
|
|
164
164
|
* `comment` prop normalises to, so the two sides meet either way.
|
|
165
165
|
*/
|
|
166
166
|
comment: text(live['comment']),
|
|
@@ -190,7 +190,7 @@ const handlers = pveHandlers<GroupProps, GroupAttributes>({
|
|
|
190
190
|
|
|
191
191
|
/**
|
|
192
192
|
* ⛔ THE EMPTY `list` IS INHERITED FROM `pveHandlers` AND IT MATTERS HERE. `GET /access/groups`
|
|
193
|
-
* answers with every group on the cluster — on this one that is `
|
|
193
|
+
* answers with every group on the cluster — on this one that is `admins`, which grants
|
|
194
194
|
* `Administrator` on `/` to every human who logs in. Handing it to Alchemy would invite adoption,
|
|
195
195
|
* and therefore one day a delete that takes the whole admin group's access with it. Adoption
|
|
196
196
|
* stays an explicit act, here as everywhere else in this package.
|
|
@@ -35,10 +35,11 @@
|
|
|
35
35
|
* (node-affinity and resource-affinity) and that is the next family to add; do not port the old
|
|
36
36
|
* groups endpoint back just because the docs still describe it.
|
|
37
37
|
*
|
|
38
|
-
* ⚠️ RECONCILE NEEDS `Sys.Console` ON `/`, WHICH
|
|
38
|
+
* ⚠️ RECONCILE NEEDS `Sys.Console` ON `/`, WHICH THE PROVISION ROLE LACKED WHEN THIS WAS WRITTEN
|
|
39
|
+
* (`PROVISION_PRIVILEGES` carries it now — ha-rule.ts has the measurement). Read and diff are
|
|
39
40
|
* fine — `GET /cluster/ha/resources/{sid}` checks `Sys.Audit`, which the role already has — while
|
|
40
41
|
* POST, PUT and DELETE all check `Sys.Console` on `/` and will answer "Permission check failed
|
|
41
|
-
* (/, Sys.Console)"
|
|
42
|
+
* (/, Sys.Console)" on a role without it. Widen it knowingly: `Sys.Console` is also what
|
|
42
43
|
* opens a root shell on every node (`/nodes/{node}/vncshell`, `termproxy`), so granting it to the
|
|
43
44
|
* provision credential buys HA membership at the price of node console access. A separate role
|
|
44
45
|
* for HA writes is the narrower answer if that trade is not wanted.
|
|
@@ -16,21 +16,21 @@ import { csv } from './values.ts';
|
|
|
16
16
|
* A `<node>[:<pri>]` list, spelled the way PVE gives it back.
|
|
17
17
|
*
|
|
18
18
|
* ⛔ THIS IS NOT `csv`, AND THE DIFFERENCE IS A FOREVER-DIFF. MEASURED by reading
|
|
19
|
-
* `/usr/share/perl5/PVE/HA/Rules/NodeAffinity.pm` on
|
|
19
|
+
* `/usr/share/perl5/PVE/HA/Rules/NodeAffinity.pm` on node-b (2026-09-13): `decode_plugin_value`
|
|
20
20
|
* turns the list into a HASH keyed by node name, and `encode_plugin_value` rebuilds it with
|
|
21
21
|
* `for my $node (sort keys %$value)` — sorted by NODE NAME, never by the token — emitting the
|
|
22
22
|
* bare name whenever the priority is falsy, because `PVE::HA::Tools::parse_node_priority` reads
|
|
23
|
-
* a missing priority as 0. So a declared `
|
|
23
|
+
* a missing priority as 0. So a declared `node-c:1,node-b:0` is handed back as `node-b,node-c:1`: re-ordered AND
|
|
24
24
|
* re-spelled. Compare the raw strings and every plan reports an update, forever.
|
|
25
25
|
*
|
|
26
26
|
* ⚠️ SORTED BY NAME, NOT BY TOKEN, AND THE TWO GENUINELY DIVERGE. `csv` sorts whole tokens. Node
|
|
27
27
|
* names may contain `-` — `parse_node_priority`'s own regex is
|
|
28
|
-
* `[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?` — and `-` (0x2D) sorts before `:` (0x3A), so `
|
|
29
|
-
* and `
|
|
28
|
+
* `[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?` — and `-` (0x2D) sorts before `:` (0x3A), so `node-b-a`
|
|
29
|
+
* and `node-b:5` come out in one order by token and the other by name. PVE sorts by the name, so
|
|
30
30
|
* this does too.
|
|
31
31
|
*
|
|
32
32
|
* ⚠️ LAST ENTRY WINS FOR A REPEATED NODE, because PVE's decode is a hash assignment and a hash
|
|
33
|
-
* keeps one value per key. A declaration saying `
|
|
33
|
+
* keeps one value per key. A declaration saying `node-b:1,node-b:5` is stored as `node-b:5`; matching that
|
|
34
34
|
* is what a Map gives for free.
|
|
35
35
|
*/
|
|
36
36
|
export const nodeList = (value: readonly string[] | string | undefined) => {
|
package/src/proxmox/ha-rule.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `Proxmox.HaRule` — where the CRM may place a guest, and which guests must not share a node.
|
|
3
3
|
*
|
|
4
|
-
* ★ THIS IS THE PVE 9 SUCCESSOR TO HA GROUPS, AND THE GROUPS ENDPOINT IS GONE. MEASURED on
|
|
4
|
+
* ★ THIS IS THE PVE 9 SUCCESSOR TO HA GROUPS, AND THE GROUPS ENDPOINT IS GONE. MEASURED on node-b
|
|
5
5
|
* (pve-manager/9.2.11, 2026-09-13): `pvesh get /cluster/ha/groups` answers `cannot index groups:
|
|
6
6
|
* ha groups have been migrated to rules`, and `/etc/pve/ha/` holds `rules.cfg` with no
|
|
7
7
|
* `groups.cfg` beside it. The published schema still documents groups and still offers a `group`
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
* ⚠️ BOTH LANES ALREADY HAVE THE PRIVILEGES, WHICH IS NEW. MEASURED 2026-09-13:
|
|
49
49
|
* GET (collection and item) checks `Sys.Audit` on `/` and the built-in `PVEAuditor` holds it, so
|
|
50
50
|
* `readRole` stays the default `read` lease — unlike storage.ts and the SDN families. POST, PUT
|
|
51
|
-
* and DELETE all check `Sys.Console` on `/`, and
|
|
52
|
-
*
|
|
51
|
+
* and DELETE all check `Sys.Console` on `/`, and the provision role now returns `Sys.Console` among
|
|
52
|
+
* its 27 privileges (`PROVISION_PRIVILEGES`, provision-baseline.ts). The warning in ha-resource.ts's header — that
|
|
53
53
|
* `Sys.Console` is missing — is stale; the role was widened, and the price it named still stands:
|
|
54
54
|
* `Sys.Console` is also what opens a root shell on every node.
|
|
55
55
|
*
|
|
@@ -12,6 +12,8 @@ const src = await Bun.file(new URL('./index.ts', import.meta.url)).text();
|
|
|
12
12
|
|
|
13
13
|
const resourceExports = [
|
|
14
14
|
'PbsDatastore',
|
|
15
|
+
'PbsNotificationMatcher',
|
|
16
|
+
'PbsNotificationTarget',
|
|
15
17
|
'PbsPruneJob',
|
|
16
18
|
'PbsSyncJob',
|
|
17
19
|
'PbsVerifyJob',
|
|
@@ -22,6 +24,7 @@ const resourceExports = [
|
|
|
22
24
|
'ProxmoxHaRule',
|
|
23
25
|
'ProxmoxLxc',
|
|
24
26
|
'ProxmoxMetricServer',
|
|
27
|
+
'ProxmoxNotificationMatcher',
|
|
25
28
|
'ProxmoxNotificationTarget',
|
|
26
29
|
'ProxmoxRole',
|
|
27
30
|
'ProxmoxSdnApply',
|
|
@@ -40,6 +43,13 @@ test('control-plane Resource constructors are on the public barrel', () => {
|
|
|
40
43
|
}
|
|
41
44
|
});
|
|
42
45
|
|
|
46
|
+
test('the provisioning baseline is on the barrel: the list, the declaration, the bootstrap', async () => {
|
|
47
|
+
const barrel = await import('./index.ts');
|
|
48
|
+
expect(barrel.PROVISION_PRIVILEGES).toHaveLength(27);
|
|
49
|
+
expect(typeof barrel.declareProvisionBaseline).toBe('function');
|
|
50
|
+
expect(barrel.provisionBootstrap()).toStartWith('#!/bin/sh\n');
|
|
51
|
+
});
|
|
52
|
+
|
|
43
53
|
test('QEMU and NIC apply stay Provider-only', () => {
|
|
44
54
|
expect(src).not.toMatch(/export \{ ProxmoxVm[, }]/);
|
|
45
55
|
expect(src).not.toMatch(/export \{ ProxmoxNodeNetwork[, }]/);
|
package/src/proxmox/index.ts
CHANGED
|
@@ -12,10 +12,17 @@
|
|
|
12
12
|
* The provider-only version diffed four fields and would have planned a create over any read
|
|
13
13
|
* failure. The Resource now adopts from the live config, refuses every change PVE cannot make
|
|
14
14
|
* in place, and retains on destroy (lxc.ts).
|
|
15
|
+
* ★ THE PROVISIONING BASELINE JOINED 2026-09-21: one privilege list (`PROVISION_PRIVILEGES`), the
|
|
16
|
+
* helper that declares it (`declareProvisionBaseline`) and the pure generator of the one-time
|
|
17
|
+
* root commands that bootstrap it (`provisionBootstrap`) — docs/provision-baseline.md.
|
|
18
|
+
* ★ THE NOTIFICATION FAMILIES JOINED 2026-09-22: PBS targets and matchers, the PVE matcher, the
|
|
19
|
+
* `FromEnv` shape their write-only values take, and the Alertmanager body template
|
|
20
|
+
* (docs/pbs-notifications.md; the template alone: docs/pbs-alertmanager-body.md).
|
|
15
21
|
* ★ Anything else unlisted is still reachable by path if you genuinely need
|
|
16
22
|
* it — that is a deliberate, visible act rather than an accident of barrelling.
|
|
17
23
|
*/
|
|
18
24
|
export { ProxmoxAcl, ProxmoxAclProvider } from './acl.ts';
|
|
25
|
+
export { type AlertmanagerBodyOptions, alertmanagerAlertBody } from './alertmanager-body.ts';
|
|
19
26
|
export { ProxmoxApiTokenProvider } from './api-token.ts';
|
|
20
27
|
export { ProxmoxBackupJob, ProxmoxBackupJobProvider } from './backup-job.ts';
|
|
21
28
|
export { ProxmoxCephDaemonProvider } from './ceph-daemon.ts';
|
|
@@ -31,15 +38,39 @@ export { type LxcAttributes, type LxcProps, ProxmoxLxc, ProxmoxLxcProvider } fro
|
|
|
31
38
|
export { ProxmoxMetricServer, ProxmoxMetricServerProvider } from './metric-server.ts';
|
|
32
39
|
export { ProxmoxNetworkApplyProvider } from './network-apply.ts';
|
|
33
40
|
export { ProxmoxNodeNetworkProvider } from './node-network.ts';
|
|
41
|
+
export {
|
|
42
|
+
ProxmoxNotificationMatcher,
|
|
43
|
+
ProxmoxNotificationMatcherProvider,
|
|
44
|
+
} from './notification-matcher.ts';
|
|
34
45
|
export {
|
|
35
46
|
ProxmoxNotificationTarget,
|
|
36
47
|
ProxmoxNotificationTargetProvider,
|
|
37
48
|
} from './notification-target.ts';
|
|
38
49
|
export { PbsDatastore, PbsDatastoreProvider } from './pbs-datastore.ts';
|
|
50
|
+
export {
|
|
51
|
+
PbsNotificationMatcher,
|
|
52
|
+
PbsNotificationMatcherProvider,
|
|
53
|
+
} from './pbs-notification-matcher.ts';
|
|
54
|
+
export {
|
|
55
|
+
PbsNotificationTarget,
|
|
56
|
+
PbsNotificationTargetProvider,
|
|
57
|
+
type PbsNotificationTargetProps,
|
|
58
|
+
} from './pbs-notification-target.ts';
|
|
39
59
|
export { PbsPruneJob, PbsPruneJobProvider } from './pbs-prune-job.ts';
|
|
40
60
|
export { PbsSyncJob, PbsSyncJobProvider } from './pbs-sync-job.ts';
|
|
41
61
|
export { PbsVerifyJob, PbsVerifyJobProvider } from './pbs-verify-job.ts';
|
|
42
62
|
export { ProxmoxPool, ProxmoxPoolProvider } from './pool.ts';
|
|
63
|
+
export {
|
|
64
|
+
PROVISION_DEFAULTS,
|
|
65
|
+
PROVISION_PRIVILEGES,
|
|
66
|
+
type ProvisionBaseline,
|
|
67
|
+
type ProvisionLane,
|
|
68
|
+
type ProvisionNames,
|
|
69
|
+
type ResolvedProvisionNames,
|
|
70
|
+
provisionBaseline,
|
|
71
|
+
} from './provision-baseline.ts';
|
|
72
|
+
export { provisionBootstrap } from './provision-bootstrap.ts';
|
|
73
|
+
export { type DeclareProvisionOptions, declareProvisionBaseline } from './provision-declare.ts';
|
|
43
74
|
export { ProxmoxVmProvider } from './qemu.ts';
|
|
44
75
|
export { ProxmoxReplicationJobProvider } from './replication-job.ts';
|
|
45
76
|
export { ProxmoxRole, ProxmoxRoleProvider } from './role.ts';
|
|
@@ -49,4 +80,5 @@ export { ProxmoxSdnVnet, ProxmoxSdnVnetProvider } from './sdn-vnet.ts';
|
|
|
49
80
|
export { ProxmoxSdnZone, ProxmoxSdnZoneProvider } from './sdn-zone.ts';
|
|
50
81
|
export { ProxmoxStorage, ProxmoxStorageProvider } from './storage.ts';
|
|
51
82
|
export { ProxmoxUser, ProxmoxUserProvider } from './user.ts';
|
|
83
|
+
export { type FromEnv } from './write-only.ts';
|
|
52
84
|
export { ProxmoxZfsPoolProvider } from './zfs-pool.ts';
|
|
@@ -17,13 +17,13 @@ import * as Exit from 'effect/Exit';
|
|
|
17
17
|
import type { PveCredential } from './credentials.ts';
|
|
18
18
|
import { type LeaseKey, makeLeases, timeToLive } from './lease-cache.ts';
|
|
19
19
|
|
|
20
|
-
const
|
|
21
|
-
mount: 'proxmox-
|
|
20
|
+
const C1: LeaseKey = {
|
|
21
|
+
mount: 'proxmox-c1',
|
|
22
22
|
role: 'read',
|
|
23
23
|
scheme: 'pve',
|
|
24
24
|
};
|
|
25
|
-
/** ⚠️ Same role as
|
|
26
|
-
const
|
|
25
|
+
/** ⚠️ Same role as C1, DIFFERENT mount — the collision the key has to survive. */
|
|
26
|
+
const C2: LeaseKey = { ...C1, mount: 'proxmox-c2' };
|
|
27
27
|
|
|
28
28
|
const cred = (tokenId: string, leaseSeconds: number): PveCredential => ({
|
|
29
29
|
leaseSeconds,
|
|
@@ -85,10 +85,10 @@ describe('lease cache under concurrency', () => {
|
|
|
85
85
|
await Effect.runPromise(
|
|
86
86
|
Effect.gen(function* () {
|
|
87
87
|
const leases = yield* makeLeases(mint.mintFor);
|
|
88
|
-
const exits = yield* concurrently(20, Cache.get(leases,
|
|
88
|
+
const exits = yield* concurrently(20, Cache.get(leases, C1));
|
|
89
89
|
assert.equal(exits.filter(Exit.isSuccess).length, 20);
|
|
90
90
|
// An equal but separately built key is the same entry.
|
|
91
|
-
yield* Cache.get(leases, { ...
|
|
91
|
+
yield* Cache.get(leases, { ...C1 });
|
|
92
92
|
}),
|
|
93
93
|
);
|
|
94
94
|
assert.equal(mint.calls(), 1);
|
|
@@ -102,9 +102,9 @@ describe('lease cache under concurrency', () => {
|
|
|
102
102
|
await Effect.runPromise(
|
|
103
103
|
Effect.gen(function* () {
|
|
104
104
|
const leases = yield* makeLeases(mint.mintFor);
|
|
105
|
-
const first = yield* concurrently(20, Cache.get(leases,
|
|
105
|
+
const first = yield* concurrently(20, Cache.get(leases, C1));
|
|
106
106
|
assert.equal(first.filter(Exit.isFailure).length, 20, 'the waiters share the failure');
|
|
107
|
-
const second = yield* concurrently(20, Cache.get(leases,
|
|
107
|
+
const second = yield* concurrently(20, Cache.get(leases, C1));
|
|
108
108
|
assert.equal(second.filter(Exit.isSuccess).length, 20);
|
|
109
109
|
}),
|
|
110
110
|
);
|
|
@@ -116,8 +116,8 @@ describe('lease cache under concurrency', () => {
|
|
|
116
116
|
await Effect.runPromise(
|
|
117
117
|
Effect.gen(function* () {
|
|
118
118
|
const leases = yield* makeLeases(mint.mintFor);
|
|
119
|
-
yield* concurrently(20, Cache.get(leases,
|
|
120
|
-
yield* Cache.get(leases,
|
|
119
|
+
yield* concurrently(20, Cache.get(leases, C1));
|
|
120
|
+
yield* Cache.get(leases, C1);
|
|
121
121
|
}),
|
|
122
122
|
);
|
|
123
123
|
assert.equal(mint.calls(), 2);
|
|
@@ -125,25 +125,25 @@ describe('lease cache under concurrency', () => {
|
|
|
125
125
|
|
|
126
126
|
it('does not serve one cluster the credential of another with the same mount and role', async () => {
|
|
127
127
|
const mint = countingMint((call) => cred(`hf-read@pve!${String(call)}`, 3600));
|
|
128
|
-
const [
|
|
128
|
+
const [c1, c2] = await Effect.runPromise(
|
|
129
129
|
Effect.gen(function* () {
|
|
130
130
|
const leases = yield* makeLeases(mint.mintFor);
|
|
131
|
-
return [yield* Cache.get(leases,
|
|
131
|
+
return [yield* Cache.get(leases, C1), yield* Cache.get(leases, C2)] as const;
|
|
132
132
|
}),
|
|
133
133
|
);
|
|
134
|
-
assert.notEqual(
|
|
134
|
+
assert.notEqual(c1.tokenId, c2.tokenId);
|
|
135
135
|
assert.equal(mint.calls(), 2);
|
|
136
136
|
});
|
|
137
137
|
|
|
138
138
|
it('shares one lease across two members of the same cluster mount', async () => {
|
|
139
139
|
const mint = countingMint((call) => cred(`hf-read@pve!${String(call)}`, 3600));
|
|
140
|
-
const
|
|
141
|
-
const
|
|
140
|
+
const NODE_B: LeaseKey = { mount: 'proxmox-c1', role: 'read', scheme: 'pve' };
|
|
141
|
+
const NODE_C: LeaseKey = { mount: 'proxmox-c1', role: 'read', scheme: 'pve' };
|
|
142
142
|
await Effect.runPromise(
|
|
143
143
|
Effect.gen(function* () {
|
|
144
144
|
const leases = yield* makeLeases(mint.mintFor);
|
|
145
|
-
yield* Cache.get(leases,
|
|
146
|
-
yield* Cache.get(leases,
|
|
145
|
+
yield* Cache.get(leases, NODE_B);
|
|
146
|
+
yield* Cache.get(leases, NODE_C);
|
|
147
147
|
}),
|
|
148
148
|
);
|
|
149
149
|
assert.equal(mint.calls(), 1);
|
|
@@ -151,11 +151,11 @@ describe('lease cache under concurrency', () => {
|
|
|
151
151
|
|
|
152
152
|
it('keeps read and provision apart', async () => {
|
|
153
153
|
const mint = countingMint((call) => cred(`hf@pve!${String(call)}`, 3600));
|
|
154
|
-
const PROVISION: LeaseKey = { ...
|
|
154
|
+
const PROVISION: LeaseKey = { ...C1, role: 'provision' };
|
|
155
155
|
const [read, provision] = await Effect.runPromise(
|
|
156
156
|
Effect.gen(function* () {
|
|
157
157
|
const leases = yield* makeLeases(mint.mintFor);
|
|
158
|
-
return [yield* Cache.get(leases,
|
|
158
|
+
return [yield* Cache.get(leases, C1), yield* Cache.get(leases, PROVISION)] as const;
|
|
159
159
|
}),
|
|
160
160
|
);
|
|
161
161
|
assert.notEqual(read.tokenId, provision.tokenId);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* One credential per (target, role) for the life of a run, instead of one per API call.
|
|
3
3
|
*
|
|
4
|
-
* 🔴 THE MEASUREMENT THAT MADE THIS NECESSARY. A single `alchemy plan` plus `deploy` of the
|
|
5
|
-
* stack — 98 resources — left **760 API token entries in `/etc/pve/user.cfg`**, counted on
|
|
4
|
+
* 🔴 THE MEASUREMENT THAT MADE THIS NECESSARY. A single `alchemy plan` plus `deploy` of the C1
|
|
5
|
+
* stack — 98 resources — left **760 API token entries in `/etc/pve/user.cfg`**, counted on node-b
|
|
6
6
|
* immediately afterwards. `mint()` asked OpenBao for a new credential on EVERY call, and each
|
|
7
7
|
* mint creates a real Proxmox token: read, diff, reconcile and read-back each got their own. They
|
|
8
8
|
* drain as the leases expire (760 down to 712 over five minutes), so nothing was broken — but
|
|
@@ -66,9 +66,9 @@ const REMINT_MARGIN_SECONDS = 60;
|
|
|
66
66
|
* What a credential is cached under.
|
|
67
67
|
*
|
|
68
68
|
* ⛔ KEYED ON THE OPENBAO MOUNT AND ROLE, NOT ON A MEMBER HOSTNAME. A PVE API token is
|
|
69
|
-
* cluster-wide — `user.cfg` is replicated — so
|
|
69
|
+
* cluster-wide — `user.cfg` is replicated — so node-b, node-c and node-d share one credential. Keying on
|
|
70
70
|
* `api` would mint three identical tokens when failover rotates members.
|
|
71
|
-
* ★ THE MOUNT CANNOT COLLIDE ACROSS CLUSTERS: `proxmox-
|
|
71
|
+
* ★ THE MOUNT CANNOT COLLIDE ACROSS CLUSTERS: `proxmox-c1`, `proxmox-c2` and `pbs-c1` are
|
|
72
72
|
* distinct OpenBao mounts with distinct policies; two estates never share a mount name.
|
|
73
73
|
* ★ A PLAIN OBJECT IS A SAFE KEY BECAUSE EFFECT COMPARES KEYS STRUCTURALLY — two separately built
|
|
74
74
|
* literals with the same fields are one entry, which the probe above measured.
|
|
@@ -100,7 +100,7 @@ describe('drift after the adoption is named at plan and written once', () => {
|
|
|
100
100
|
test('a larger rootfs is a resize after the config, never a new volume', async () => {
|
|
101
101
|
const pve = live();
|
|
102
102
|
const stack = await adoptedAsIs(pve);
|
|
103
|
-
const props: LxcProps = { ...pasted(), rootfs: 'local-zfs:subvol-
|
|
103
|
+
const props: LxcProps = { ...pasted(), rootfs: 'local-zfs:subvol-900-disk-0,size=64G' };
|
|
104
104
|
expect((await stack.deploy(ct(props))).failure).toBe('');
|
|
105
105
|
expect(writesOf(pve)).toEqual([`PUT nodes/${NODE}/lxc/${String(VMID)}/resize`]);
|
|
106
106
|
expect(pve.guests.get(`${NODE}/${String(VMID)}`)?.['rootfs']).toContain('size=64G');
|
|
@@ -109,10 +109,10 @@ describe('drift after the adoption is named at plan and written once', () => {
|
|
|
109
109
|
|
|
110
110
|
describe('changes PVE cannot make in place fail the plan and write nothing', () => {
|
|
111
111
|
test.each([
|
|
112
|
-
['a smaller mount point', { mp0: 'tank:subvol-
|
|
112
|
+
['a smaller mount point', { mp0: 'tank:subvol-900-disk-0,mp=/data,size=100G' }, /shrink/],
|
|
113
113
|
['a storage move', { rootfs: 'other:40' }, /pct move-volume/],
|
|
114
114
|
['an unprivileged flip', { unprivileged: 0 as const }, /read-only option/],
|
|
115
|
-
['a device passthrough change', { dev1: '/dev/fuse' }, /pct set
|
|
115
|
+
['a device passthrough change', { dev1: '/dev/fuse' }, /pct set 900 --dev1/],
|
|
116
116
|
['a feature beyond nesting', { features: 'nesting=1,keyctl=1' }, /root@pam/],
|
|
117
117
|
['detaching a mount point', { mp0: '' }, /unusedN/],
|
|
118
118
|
])('%s', async (_, change, reason) => {
|
|
@@ -23,7 +23,7 @@ import { bool } from './values.ts';
|
|
|
23
23
|
/**
|
|
24
24
|
* ⛔ A CREATE ALLOCATES NEW VOLUMES ONLY. pve-container's `create_disks` allocates `storage:GiB`
|
|
25
25
|
* and takes anything else as "use specified/existing volid", then `restore_archive` unpacks the
|
|
26
|
-
* template into the mounted rootfs. So a create naming `local-zfs:subvol-
|
|
26
|
+
* template into the mounted rootfs. So a create naming `local-zfs:subvol-900-disk-0` — a pasted
|
|
27
27
|
* adoption, re-created after its config vanished while the volume survived (a `pct destroy`
|
|
28
28
|
* only WARNS when a volume will not delete) — unpacks a template over the data on it.
|
|
29
29
|
*/
|
|
@@ -63,8 +63,8 @@ describe('a guest the cluster no longer lists, with state still held', () => {
|
|
|
63
63
|
expect((await stack.deploy(declared())).failure).toBe('');
|
|
64
64
|
pve.guests.delete(KEY);
|
|
65
65
|
const run = await stack.deploy(declared());
|
|
66
|
-
expect(run.failure).toContain('rootfs: local-zfs:subvol-
|
|
67
|
-
expect(run.failure).toContain('mp0: tank:subvol-
|
|
66
|
+
expect(run.failure).toContain('rootfs: local-zfs:subvol-900-disk-0,size=40G names an existing');
|
|
67
|
+
expect(run.failure).toContain('mp0: tank:subvol-900-disk-0');
|
|
68
68
|
expect(writesOf(pve)).toEqual([]);
|
|
69
69
|
});
|
|
70
70
|
|
|
@@ -130,13 +130,13 @@ describe('identity and volumes', () => {
|
|
|
130
130
|
const pve = cluster();
|
|
131
131
|
seed(pve, NODE, VMID);
|
|
132
132
|
const stack = await adoptedAsIs(pve);
|
|
133
|
-
const mp0 = 'tank:subvol-
|
|
133
|
+
const mp0 = 'tank:subvol-900-disk-0,mp=/data,backup=1,size=300G';
|
|
134
134
|
const run = await stack.deploy(ct(pasted({ mp0 })));
|
|
135
135
|
expect(run.failure).toBe('');
|
|
136
136
|
const path = `nodes/${NODE}/lxc/${String(VMID)}`;
|
|
137
137
|
expect(writesOf(pve)).toEqual([`PUT ${path}/config`, `PUT ${path}/resize`]);
|
|
138
138
|
const put = pve.seen.find((call) => call.path.endsWith('/config') && call.method === 'PUT');
|
|
139
|
-
expect(put?.form.get('mp0')).toBe('tank:subvol-
|
|
139
|
+
expect(put?.form.get('mp0')).toBe('tank:subvol-900-disk-0,mp=/data,backup=1,size=200G');
|
|
140
140
|
expect(pve.guests.get(KEY)?.['mp0']).toContain('size=300G');
|
|
141
141
|
});
|
|
142
142
|
|
|
@@ -54,7 +54,7 @@ export const LIVE: Readonly<Record<string, unknown>> = {
|
|
|
54
54
|
hostname: 'ct-example',
|
|
55
55
|
lxc: [['lxc.prlimit.memlock', 'unlimited']],
|
|
56
56
|
memory: 8192,
|
|
57
|
-
mp0: 'tank:subvol-
|
|
57
|
+
mp0: 'tank:subvol-900-disk-0,mp=/data,backup=0,size=200G',
|
|
58
58
|
nameserver: '192.0.2.254',
|
|
59
59
|
net0:
|
|
60
60
|
'name=eth0,bridge=vmbr0,firewall=0,gw=192.0.2.1,hwaddr=00:00:5E:00:53:01,' +
|
|
@@ -62,7 +62,7 @@ export const LIVE: Readonly<Record<string, unknown>> = {
|
|
|
62
62
|
net1: 'name=eth1,bridge=vmbr1,hwaddr=00:00:5E:00:53:02,ip=198.51.100.4/24,mtu=1500,type=veth',
|
|
63
63
|
onboot: 1,
|
|
64
64
|
ostype: 'debian',
|
|
65
|
-
rootfs: 'local-zfs:subvol-
|
|
65
|
+
rootfs: 'local-zfs:subvol-900-disk-0,size=40G',
|
|
66
66
|
searchdomain: 'example.com',
|
|
67
67
|
swap: 0,
|
|
68
68
|
tags: 'web',
|
|
@@ -71,7 +71,7 @@ export const LIVE: Readonly<Record<string, unknown>> = {
|
|
|
71
71
|
|
|
72
72
|
/** Where `seed` puts the production-shaped guest by default, and its `node/vmid` key. */
|
|
73
73
|
export const NODE = 'pve1';
|
|
74
|
-
export const VMID =
|
|
74
|
+
export const VMID = 900;
|
|
75
75
|
export const KEY = `${NODE}/${String(VMID)}`;
|
|
76
76
|
|
|
77
77
|
/** The live config as a declaration: every key, minus what pvesh adds that is not a prop. */
|
|
@@ -13,7 +13,7 @@ import { judgeVolume, parseVolume, sizeBytes } from './lxc-volume.ts';
|
|
|
13
13
|
const props = (over: Partial<LxcProps>): LxcProps => ({
|
|
14
14
|
node: 'pve1',
|
|
15
15
|
target: TARGET,
|
|
16
|
-
vmid:
|
|
16
|
+
vmid: 900,
|
|
17
17
|
...over,
|
|
18
18
|
});
|
|
19
19
|
const live = (config: Record<string, unknown>) => ({ unprivileged: 1, ...config });
|
|
@@ -36,7 +36,7 @@ describe('sizes and volumes, as pve-container parses them', () => {
|
|
|
36
36
|
|
|
37
37
|
test('an options change on a volume declared as new is written with the LIVE volume id', () => {
|
|
38
38
|
const verdict = judgeVolume('mp0', 'tank:200,mp=/data,backup=1', LIVE_MP);
|
|
39
|
-
expect(verdict).toEqual({ put: 'tank:subvol-
|
|
39
|
+
expect(verdict).toEqual({ put: 'tank:subvol-900-disk-0,mp=/data,backup=1,size=200G' });
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
test('a fractional new-disk size grows by the same GiB', () => {
|
|
@@ -44,7 +44,7 @@ describe('sizes and volumes, as pve-container parses them', () => {
|
|
|
44
44
|
});
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
-
const LIVE_MP = 'tank:subvol-
|
|
47
|
+
const LIVE_MP = 'tank:subvol-900-disk-0,mp=/data,backup=0,size=200G';
|
|
48
48
|
|
|
49
49
|
describe('scalars compare the way PVE stores them', () => {
|
|
50
50
|
test.each([
|
|
@@ -106,7 +106,7 @@ describe('features follow check_ct_modify_config_perm', () => {
|
|
|
106
106
|
|
|
107
107
|
test('clearing features that hold more than nesting is root@pam only, as PVE counts keys', () => {
|
|
108
108
|
const change = judge(props({ features: '' }), live({ features: 'nesting=1,keyctl=0' }));
|
|
109
|
-
expect(change.refuse[0]).toMatch(/pct set
|
|
109
|
+
expect(change.refuse[0]).toMatch(/pct set 900 --delete features/);
|
|
110
110
|
});
|
|
111
111
|
});
|
|
112
112
|
|
|
@@ -147,7 +147,7 @@ describe('create', () => {
|
|
|
147
147
|
ostemplate: 'local:vztmpl/t.tar.zst',
|
|
148
148
|
start: '1',
|
|
149
149
|
unprivileged: '1',
|
|
150
|
-
vmid: '
|
|
150
|
+
vmid: '900',
|
|
151
151
|
});
|
|
152
152
|
});
|
|
153
153
|
|
|
@@ -72,7 +72,7 @@ export const readLive = (where: LxcWhere) =>
|
|
|
72
72
|
'GET',
|
|
73
73
|
'cluster/resources?type=vm',
|
|
74
74
|
);
|
|
75
|
-
// ⚠️ `Number()`, NOT `===` ON THE RAW VALUE: a vmid that came back as `"
|
|
75
|
+
// ⚠️ `Number()`, NOT `===` ON THE RAW VALUE: a vmid that came back as `"900"` would otherwise
|
|
76
76
|
// match nothing, and "listed nowhere" is the one answer that turns into a create.
|
|
77
77
|
const found = (rows ?? []).find((row) => Number(row.vmid) === where.vmid);
|
|
78
78
|
if (found !== undefined && found.node === where.node && found.type === 'lxc') {
|
package/src/proxmox/lxc-props.ts
CHANGED
|
@@ -58,7 +58,7 @@ export interface LxcProps extends WithTarget {
|
|
|
58
58
|
/** MiB. */
|
|
59
59
|
swap?: number;
|
|
60
60
|
/**
|
|
61
|
-
* `local-zfs:subvol-
|
|
61
|
+
* `local-zfs:subvol-900-disk-0,size=40G` as read, or `local-zfs:40` (40 GiB, new volume) to
|
|
62
62
|
* create. See lxc-volume.ts for what may change: options in place, size up only.
|
|
63
63
|
*/
|
|
64
64
|
rootfs?: string;
|
|
@@ -79,7 +79,7 @@ export interface LxcProps extends WithTarget {
|
|
|
79
79
|
console?: PveFlag;
|
|
80
80
|
tty?: number;
|
|
81
81
|
cmode?: string;
|
|
82
|
-
/** `tank:subvol-
|
|
82
|
+
/** `tank:subvol-900-disk-1,mp=/data,backup=1,size=100G`, or `tank:100,mp=/data` to create. */
|
|
83
83
|
[mount: `mp${number}`]: string | undefined;
|
|
84
84
|
/** `name=eth0,bridge=vmbr0,ip=dhcp`. ⚠️ An undeclared `hwaddr` keeps the live MAC. */
|
|
85
85
|
[nic: `net${number}`]: string | undefined;
|
|
@@ -40,10 +40,10 @@ describe('absent means the cluster does not list the vmid', () => {
|
|
|
40
40
|
|
|
41
41
|
test('a 500 for a guest the cluster lists on this node fails with PVE’s error', async () => {
|
|
42
42
|
fake = fakePve();
|
|
43
|
-
seed(fake, NODE,
|
|
43
|
+
seed(fake, NODE, 900, LIVE);
|
|
44
44
|
const message = 'got timeout\n';
|
|
45
|
-
fake.configErrors.set(`${NODE}/
|
|
46
|
-
const run = await lxcEngine(fake).plan(declared(
|
|
45
|
+
fake.configErrors.set(`${NODE}/900`, { body: { data: null, message }, status: 500 });
|
|
46
|
+
const run = await lxcEngine(fake).plan(declared(900));
|
|
47
47
|
expect(run.failure).toContain('got timeout');
|
|
48
48
|
expect(run.actions).toEqual({});
|
|
49
49
|
expect(writesOf(fake)).toEqual([]);
|
|
@@ -59,7 +59,7 @@ describe('a cold adoption that differs from the live guest fails the plan', () =
|
|
|
59
59
|
['an unprivileged flip', { unprivileged: 0 as const }, 'unprivileged', ['live 1']],
|
|
60
60
|
[
|
|
61
61
|
'a smaller mount point',
|
|
62
|
-
{ mp0: 'tank:subvol-
|
|
62
|
+
{ mp0: 'tank:subvol-900-disk-0,mp=/data,size=100G' },
|
|
63
63
|
'mp0',
|
|
64
64
|
['100G'],
|
|
65
65
|
],
|