@homeflare/alchemy 0.1.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/LICENSE +21 -0
- package/README.md +62 -0
- package/dist/cloudflare/client.d.ts +45 -0
- package/dist/cloudflare/client.d.ts.map +1 -0
- package/dist/cloudflare/index.d.ts +11 -0
- package/dist/cloudflare/index.d.ts.map +1 -0
- package/dist/cloudflare/index.js +129 -0
- package/dist/cloudflare/index.js.map +13 -0
- package/dist/cloudflare/lock-rules.d.ts +50 -0
- package/dist/cloudflare/lock-rules.d.ts.map +1 -0
- package/dist/cloudflare/providers.d.ts +19 -0
- package/dist/cloudflare/providers.d.ts.map +1 -0
- package/dist/cloudflare/r2-bucket-lock.d.ts +100 -0
- package/dist/cloudflare/r2-bucket-lock.d.ts.map +1 -0
- package/dist/forgejo/branch-protection-form.d.ts +19 -0
- package/dist/forgejo/branch-protection-form.d.ts.map +1 -0
- package/dist/forgejo/branch-protection.d.ts +47 -0
- package/dist/forgejo/branch-protection.d.ts.map +1 -0
- package/dist/forgejo/client.d.ts +37 -0
- package/dist/forgejo/client.d.ts.map +1 -0
- package/dist/forgejo/index.d.ts +18 -0
- package/dist/forgejo/index.d.ts.map +1 -0
- package/dist/forgejo/index.js +479 -0
- package/dist/forgejo/index.js.map +21 -0
- package/dist/forgejo/org-actions-secrets.d.ts +45 -0
- package/dist/forgejo/org-actions-secrets.d.ts.map +1 -0
- package/dist/forgejo/org-label.d.ts +46 -0
- package/dist/forgejo/org-label.d.ts.map +1 -0
- package/dist/forgejo/org-team.d.ts +42 -0
- package/dist/forgejo/org-team.d.ts.map +1 -0
- package/dist/forgejo/repo-webhook-form.d.ts +27 -0
- package/dist/forgejo/repo-webhook-form.d.ts.map +1 -0
- package/dist/forgejo/repo-webhook.d.ts +46 -0
- package/dist/forgejo/repo-webhook.d.ts.map +1 -0
- package/dist/forgejo/repository.d.ts +45 -0
- package/dist/forgejo/repository.d.ts.map +1 -0
- package/dist/forgejo/resource.d.ts +83 -0
- package/dist/forgejo/resource.d.ts.map +1 -0
- package/dist/forgejo/team-member.d.ts +39 -0
- package/dist/forgejo/team-member.d.ts.map +1 -0
- package/dist/forgejo/values.d.ts +24 -0
- package/dist/forgejo/values.d.ts.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +9 -0
- package/dist/openbao/auth-role-form.d.ts +38 -0
- package/dist/openbao/auth-role-form.d.ts.map +1 -0
- package/dist/openbao/auth-role.d.ts +20 -0
- package/dist/openbao/auth-role.d.ts.map +1 -0
- package/dist/openbao/bao-address.d.ts +72 -0
- package/dist/openbao/bao-address.d.ts.map +1 -0
- package/dist/openbao/bao-http.d.ts +71 -0
- package/dist/openbao/bao-http.d.ts.map +1 -0
- package/dist/openbao/bao-status.d.ts +74 -0
- package/dist/openbao/bao-status.d.ts.map +1 -0
- package/dist/openbao/cloudflare-group-scope.d.ts +73 -0
- package/dist/openbao/cloudflare-group-scope.d.ts.map +1 -0
- package/dist/openbao/cloudflare-parity-catalog.d.ts +28 -0
- package/dist/openbao/cloudflare-parity-catalog.d.ts.map +1 -0
- package/dist/openbao/cloudflare-parity-snapshot.d.ts +31 -0
- package/dist/openbao/cloudflare-parity-snapshot.d.ts.map +1 -0
- package/dist/openbao/cloudflare-parity.d.ts +14 -0
- package/dist/openbao/cloudflare-parity.d.ts.map +1 -0
- package/dist/openbao/cloudflare-permission-groups-solve.d.ts +85 -0
- package/dist/openbao/cloudflare-permission-groups-solve.d.ts.map +1 -0
- package/dist/openbao/cloudflare-permission-groups.d.ts +87 -0
- package/dist/openbao/cloudflare-permission-groups.d.ts.map +1 -0
- package/dist/openbao/cloudflare-policy.d.ts +65 -0
- package/dist/openbao/cloudflare-policy.d.ts.map +1 -0
- package/dist/openbao/cloudflare-role-form.d.ts +110 -0
- package/dist/openbao/cloudflare-role-form.d.ts.map +1 -0
- package/dist/openbao/cloudflare-role-wire.d.ts +27 -0
- package/dist/openbao/cloudflare-role-wire.d.ts.map +1 -0
- package/dist/openbao/cloudflare-role.d.ts +35 -0
- package/dist/openbao/cloudflare-role.d.ts.map +1 -0
- package/dist/openbao/cloudflare-roles-config.d.ts +67 -0
- package/dist/openbao/cloudflare-roles-config.d.ts.map +1 -0
- package/dist/openbao/cloudflare-roles-expand.d.ts +76 -0
- package/dist/openbao/cloudflare-roles-expand.d.ts.map +1 -0
- package/dist/openbao/digest.d.ts +24 -0
- package/dist/openbao/digest.d.ts.map +1 -0
- package/dist/openbao/fake-bao.d.ts +40 -0
- package/dist/openbao/fake-bao.d.ts.map +1 -0
- package/dist/openbao/forgejo-bootstrap-parse.d.ts +46 -0
- package/dist/openbao/forgejo-bootstrap-parse.d.ts.map +1 -0
- package/dist/openbao/forgejo-bootstrap.d.ts +29 -0
- package/dist/openbao/forgejo-bootstrap.d.ts.map +1 -0
- package/dist/openbao/index.d.ts +18 -0
- package/dist/openbao/index.d.ts.map +1 -0
- package/dist/openbao/index.js +1158 -0
- package/dist/openbao/index.js.map +33 -0
- package/dist/openbao/mount-form.d.ts +60 -0
- package/dist/openbao/mount-form.d.ts.map +1 -0
- package/dist/openbao/mount-wire.d.ts +24 -0
- package/dist/openbao/mount-wire.d.ts.map +1 -0
- package/dist/openbao/mount.d.ts +20 -0
- package/dist/openbao/mount.d.ts.map +1 -0
- package/dist/openbao/pki-role-form.d.ts +118 -0
- package/dist/openbao/pki-role-form.d.ts.map +1 -0
- package/dist/openbao/pki-role.d.ts +50 -0
- package/dist/openbao/pki-role.d.ts.map +1 -0
- package/dist/openbao/policy-assembly.d.ts +13 -0
- package/dist/openbao/policy-assembly.d.ts.map +1 -0
- package/dist/openbao/policy-wire.d.ts +41 -0
- package/dist/openbao/policy-wire.d.ts.map +1 -0
- package/dist/openbao/policy.d.ts +83 -0
- package/dist/openbao/policy.d.ts.map +1 -0
- package/dist/openbao/proxmox-role-form.d.ts +92 -0
- package/dist/openbao/proxmox-role-form.d.ts.map +1 -0
- package/dist/openbao/proxmox-role.d.ts +47 -0
- package/dist/openbao/proxmox-role.d.ts.map +1 -0
- package/dist/openbao/ssh-role-form.d.ts +99 -0
- package/dist/openbao/ssh-role-form.d.ts.map +1 -0
- package/dist/openbao/ssh-role.d.ts +43 -0
- package/dist/openbao/ssh-role.d.ts.map +1 -0
- package/dist/proxmox/acl.d.ts +72 -0
- package/dist/proxmox/acl.d.ts.map +1 -0
- package/dist/proxmox/api-token-form.d.ts +59 -0
- package/dist/proxmox/api-token-form.d.ts.map +1 -0
- package/dist/proxmox/api-token.d.ts +159 -0
- package/dist/proxmox/api-token.d.ts.map +1 -0
- package/dist/proxmox/backup-job.d.ts +112 -0
- package/dist/proxmox/backup-job.d.ts.map +1 -0
- package/dist/proxmox/ceph-daemon-form.d.ts +70 -0
- package/dist/proxmox/ceph-daemon-form.d.ts.map +1 -0
- package/dist/proxmox/ceph-daemon.d.ts +162 -0
- package/dist/proxmox/ceph-daemon.d.ts.map +1 -0
- package/dist/proxmox/ceph-flag.d.ts +131 -0
- package/dist/proxmox/ceph-flag.d.ts.map +1 -0
- package/dist/proxmox/ceph-fs-wire.d.ts +106 -0
- package/dist/proxmox/ceph-fs-wire.d.ts.map +1 -0
- package/dist/proxmox/ceph-fs.d.ts +124 -0
- package/dist/proxmox/ceph-fs.d.ts.map +1 -0
- package/dist/proxmox/ceph-osd-tree.d.ts +154 -0
- package/dist/proxmox/ceph-osd-tree.d.ts.map +1 -0
- package/dist/proxmox/ceph-osd-write.d.ts +36 -0
- package/dist/proxmox/ceph-osd-write.d.ts.map +1 -0
- package/dist/proxmox/ceph-osd.d.ts +102 -0
- package/dist/proxmox/ceph-osd.d.ts.map +1 -0
- package/dist/proxmox/ceph-pool-form.d.ts +217 -0
- package/dist/proxmox/ceph-pool-form.d.ts.map +1 -0
- package/dist/proxmox/ceph-pool-settle.d.ts +39 -0
- package/dist/proxmox/ceph-pool-settle.d.ts.map +1 -0
- package/dist/proxmox/ceph-pool.d.ts +48 -0
- package/dist/proxmox/ceph-pool.d.ts.map +1 -0
- package/dist/proxmox/client.d.ts +64 -0
- package/dist/proxmox/client.d.ts.map +1 -0
- package/dist/proxmox/credentials.d.ts +153 -0
- package/dist/proxmox/credentials.d.ts.map +1 -0
- package/dist/proxmox/firewall-alias-form.d.ts +98 -0
- package/dist/proxmox/firewall-alias-form.d.ts.map +1 -0
- package/dist/proxmox/firewall-alias.d.ts +141 -0
- package/dist/proxmox/firewall-alias.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs.d.ts +388 -0
- package/dist/proxmox/generated/pbs.d.ts.map +1 -0
- package/dist/proxmox/generated/pve.d.ts +4713 -0
- package/dist/proxmox/generated/pve.d.ts.map +1 -0
- package/dist/proxmox/group.d.ts +104 -0
- package/dist/proxmox/group.d.ts.map +1 -0
- package/dist/proxmox/ha-resource.d.ts +104 -0
- package/dist/proxmox/ha-resource.d.ts.map +1 -0
- package/dist/proxmox/ha-rule-form.d.ts +103 -0
- package/dist/proxmox/ha-rule-form.d.ts.map +1 -0
- package/dist/proxmox/ha-rule.d.ts +156 -0
- package/dist/proxmox/ha-rule.d.ts.map +1 -0
- package/dist/proxmox/index.d.ts +43 -0
- package/dist/proxmox/index.d.ts.map +1 -0
- package/dist/proxmox/index.js +2322 -0
- package/dist/proxmox/index.js.map +70 -0
- package/dist/proxmox/lease-cache.d.ts +98 -0
- package/dist/proxmox/lease-cache.d.ts.map +1 -0
- package/dist/proxmox/lxc.d.ts +59 -0
- package/dist/proxmox/lxc.d.ts.map +1 -0
- package/dist/proxmox/members.d.ts +56 -0
- package/dist/proxmox/members.d.ts.map +1 -0
- package/dist/proxmox/metric-server-form.d.ts +53 -0
- package/dist/proxmox/metric-server-form.d.ts.map +1 -0
- package/dist/proxmox/metric-server-otel.d.ts +49 -0
- package/dist/proxmox/metric-server-otel.d.ts.map +1 -0
- package/dist/proxmox/metric-server.d.ts +123 -0
- package/dist/proxmox/metric-server.d.ts.map +1 -0
- package/dist/proxmox/network-apply-read.d.ts +93 -0
- package/dist/proxmox/network-apply-read.d.ts.map +1 -0
- package/dist/proxmox/network-apply.d.ts +102 -0
- package/dist/proxmox/network-apply.d.ts.map +1 -0
- package/dist/proxmox/node-network-form.d.ts +128 -0
- package/dist/proxmox/node-network-form.d.ts.map +1 -0
- package/dist/proxmox/node-network.d.ts +173 -0
- package/dist/proxmox/node-network.d.ts.map +1 -0
- package/dist/proxmox/notification-target-form.d.ts +44 -0
- package/dist/proxmox/notification-target-form.d.ts.map +1 -0
- package/dist/proxmox/notification-target.d.ts +146 -0
- package/dist/proxmox/notification-target.d.ts.map +1 -0
- package/dist/proxmox/pbs-datastore-form.d.ts +147 -0
- package/dist/proxmox/pbs-datastore-form.d.ts.map +1 -0
- package/dist/proxmox/pbs-datastore-guard.d.ts +55 -0
- package/dist/proxmox/pbs-datastore-guard.d.ts.map +1 -0
- package/dist/proxmox/pbs-datastore.d.ts +140 -0
- package/dist/proxmox/pbs-datastore.d.ts.map +1 -0
- package/dist/proxmox/pbs-prune-job-form.d.ts +97 -0
- package/dist/proxmox/pbs-prune-job-form.d.ts.map +1 -0
- package/dist/proxmox/pbs-prune-job.d.ts +168 -0
- package/dist/proxmox/pbs-prune-job.d.ts.map +1 -0
- package/dist/proxmox/pbs-sync-job-form.d.ts +83 -0
- package/dist/proxmox/pbs-sync-job-form.d.ts.map +1 -0
- package/dist/proxmox/pbs-sync-job.d.ts +246 -0
- package/dist/proxmox/pbs-sync-job.d.ts.map +1 -0
- package/dist/proxmox/pbs-verify-job-form.d.ts +114 -0
- package/dist/proxmox/pbs-verify-job-form.d.ts.map +1 -0
- package/dist/proxmox/pbs-verify-job.d.ts +145 -0
- package/dist/proxmox/pbs-verify-job.d.ts.map +1 -0
- package/dist/proxmox/pool.d.ts +42 -0
- package/dist/proxmox/pool.d.ts.map +1 -0
- package/dist/proxmox/qemu.d.ts +46 -0
- package/dist/proxmox/qemu.d.ts.map +1 -0
- package/dist/proxmox/replication-job-form.d.ts +71 -0
- package/dist/proxmox/replication-job-form.d.ts.map +1 -0
- package/dist/proxmox/replication-job.d.ts +162 -0
- package/dist/proxmox/replication-job.d.ts.map +1 -0
- package/dist/proxmox/resource.d.ts +157 -0
- package/dist/proxmox/resource.d.ts.map +1 -0
- package/dist/proxmox/role.d.ts +97 -0
- package/dist/proxmox/role.d.ts.map +1 -0
- package/dist/proxmox/sdn-apply-read.d.ts +80 -0
- package/dist/proxmox/sdn-apply-read.d.ts.map +1 -0
- package/dist/proxmox/sdn-apply.d.ts +63 -0
- package/dist/proxmox/sdn-apply.d.ts.map +1 -0
- package/dist/proxmox/sdn-subnet-form.d.ts +111 -0
- package/dist/proxmox/sdn-subnet-form.d.ts.map +1 -0
- package/dist/proxmox/sdn-subnet.d.ts +161 -0
- package/dist/proxmox/sdn-subnet.d.ts.map +1 -0
- package/dist/proxmox/sdn-vnet.d.ts +113 -0
- package/dist/proxmox/sdn-vnet.d.ts.map +1 -0
- package/dist/proxmox/sdn-zone.d.ts +126 -0
- package/dist/proxmox/sdn-zone.d.ts.map +1 -0
- package/dist/proxmox/storage.d.ts +120 -0
- package/dist/proxmox/storage.d.ts.map +1 -0
- package/dist/proxmox/user.d.ts +107 -0
- package/dist/proxmox/user.d.ts.map +1 -0
- package/dist/proxmox/values.d.ts +134 -0
- package/dist/proxmox/values.d.ts.map +1 -0
- package/dist/proxmox/zfs-pool-write.d.ts +76 -0
- package/dist/proxmox/zfs-pool-write.d.ts.map +1 -0
- package/dist/proxmox/zfs-pool.d.ts +146 -0
- package/dist/proxmox/zfs-pool.d.ts.map +1 -0
- package/dist/talos/credentials.d.ts +30 -0
- package/dist/talos/credentials.d.ts.map +1 -0
- package/dist/talos/index.d.ts +15 -0
- package/dist/talos/index.d.ts.map +1 -0
- package/dist/talos/index.js +392 -0
- package/dist/talos/index.js.map +16 -0
- package/dist/talos/kubeconfig.d.ts +53 -0
- package/dist/talos/kubeconfig.d.ts.map +1 -0
- package/dist/talos/resource.d.ts +32 -0
- package/dist/talos/resource.d.ts.map +1 -0
- package/dist/talos/talos-bootstrap.d.ts +27 -0
- package/dist/talos/talos-bootstrap.d.ts.map +1 -0
- package/dist/talos/talos-cluster-health.d.ts +36 -0
- package/dist/talos/talos-cluster-health.d.ts.map +1 -0
- package/dist/talos/talos-machine-config.d.ts +41 -0
- package/dist/talos/talos-machine-config.d.ts.map +1 -0
- package/dist/talos/talosctl.d.ts +29 -0
- package/dist/talos/talosctl.d.ts.map +1 -0
- package/dist/talos/values.d.ts +20 -0
- package/dist/talos/values.d.ts.map +1 -0
- package/package.json +57 -0
- package/src/cloudflare/client.ts +76 -0
- package/src/cloudflare/index.ts +10 -0
- package/src/cloudflare/lock-rules.ts +60 -0
- package/src/cloudflare/providers.ts +23 -0
- package/src/cloudflare/r2-bucket-lock.ts +220 -0
- package/src/forgejo/branch-protection-form.ts +29 -0
- package/src/forgejo/branch-protection.ts +101 -0
- package/src/forgejo/client.ts +136 -0
- package/src/forgejo/index.ts +17 -0
- package/src/forgejo/org-actions-secrets.ts +99 -0
- package/src/forgejo/org-label.ts +100 -0
- package/src/forgejo/org-team.ts +105 -0
- package/src/forgejo/repo-webhook-form.ts +47 -0
- package/src/forgejo/repo-webhook.ts +118 -0
- package/src/forgejo/repository.ts +126 -0
- package/src/forgejo/resource.ts +171 -0
- package/src/forgejo/team-member.ts +97 -0
- package/src/forgejo/values.test.ts +48 -0
- package/src/forgejo/values.ts +61 -0
- package/src/index.ts +18 -0
- package/src/openbao/auth-role-form.ts +112 -0
- package/src/openbao/auth-role.ts +101 -0
- package/src/openbao/bao-address.test.ts +89 -0
- package/src/openbao/bao-address.ts +97 -0
- package/src/openbao/bao-http-gate.test.ts +88 -0
- package/src/openbao/bao-http-unix.test.ts +70 -0
- package/src/openbao/bao-http.test.ts +161 -0
- package/src/openbao/bao-http.ts +185 -0
- package/src/openbao/bao-status.test.ts +114 -0
- package/src/openbao/bao-status.ts +137 -0
- package/src/openbao/cloudflare-group-scope.test.ts +103 -0
- package/src/openbao/cloudflare-group-scope.ts +83 -0
- package/src/openbao/cloudflare-parity-catalog.ts +104 -0
- package/src/openbao/cloudflare-parity-snapshot.ts +84 -0
- package/src/openbao/cloudflare-parity.ts +152 -0
- package/src/openbao/cloudflare-permission-groups-solve.test.ts +171 -0
- package/src/openbao/cloudflare-permission-groups-solve.ts +197 -0
- package/src/openbao/cloudflare-permission-groups.test.ts +369 -0
- package/src/openbao/cloudflare-permission-groups.ts +299 -0
- package/src/openbao/cloudflare-policy.ts +180 -0
- package/src/openbao/cloudflare-role-form.test.ts +177 -0
- package/src/openbao/cloudflare-role-form.ts +183 -0
- package/src/openbao/cloudflare-role-wire.test.ts +132 -0
- package/src/openbao/cloudflare-role-wire.ts +41 -0
- package/src/openbao/cloudflare-role.ts +189 -0
- package/src/openbao/cloudflare-roles-config.ts +230 -0
- package/src/openbao/cloudflare-roles-expand-bucket.test.ts +134 -0
- package/src/openbao/cloudflare-roles-expand.ts +231 -0
- package/src/openbao/digest.ts +26 -0
- package/src/openbao/fake-bao.ts +84 -0
- package/src/openbao/forgejo-bootstrap-parse.test.ts +76 -0
- package/src/openbao/forgejo-bootstrap-parse.ts +90 -0
- package/src/openbao/forgejo-bootstrap.ts +181 -0
- package/src/openbao/index.ts +17 -0
- package/src/openbao/mount-form.ts +147 -0
- package/src/openbao/mount-wire.test.ts +108 -0
- package/src/openbao/mount-wire.ts +79 -0
- package/src/openbao/mount.ts +132 -0
- package/src/openbao/pki-role-form.ts +248 -0
- package/src/openbao/pki-role.ts +202 -0
- package/src/openbao/policy-assembly.test.ts +28 -0
- package/src/openbao/policy-assembly.ts +13 -0
- package/src/openbao/policy-wire.test.ts +90 -0
- package/src/openbao/policy-wire.ts +53 -0
- package/src/openbao/policy.ts +212 -0
- package/src/openbao/proxmox-role-form.ts +174 -0
- package/src/openbao/proxmox-role.ts +214 -0
- package/src/openbao/ssh-role-form.ts +248 -0
- package/src/openbao/ssh-role.ts +240 -0
- package/src/proxmox/acl.ts +267 -0
- package/src/proxmox/api-token-form.ts +122 -0
- package/src/proxmox/api-token.ts +222 -0
- package/src/proxmox/backup-job.ts +213 -0
- package/src/proxmox/ceph-daemon-form.ts +84 -0
- package/src/proxmox/ceph-daemon.ts +243 -0
- package/src/proxmox/ceph-flag.ts +249 -0
- package/src/proxmox/ceph-fs-wire.ts +246 -0
- package/src/proxmox/ceph-fs.ts +249 -0
- package/src/proxmox/ceph-osd-tree.ts +193 -0
- package/src/proxmox/ceph-osd-write.ts +76 -0
- package/src/proxmox/ceph-osd.ts +244 -0
- package/src/proxmox/ceph-pool-form.ts +249 -0
- package/src/proxmox/ceph-pool-settle.ts +70 -0
- package/src/proxmox/ceph-pool.ts +212 -0
- package/src/proxmox/client.test.ts +185 -0
- package/src/proxmox/client.ts +240 -0
- package/src/proxmox/credentials.test.ts +144 -0
- package/src/proxmox/credentials.ts +254 -0
- package/src/proxmox/firewall-alias-form.ts +112 -0
- package/src/proxmox/firewall-alias.ts +208 -0
- package/src/proxmox/generated/pbs.ts +461 -0
- package/src/proxmox/generated/pve.ts +7344 -0
- package/src/proxmox/group.ts +199 -0
- package/src/proxmox/ha-resource.ts +169 -0
- package/src/proxmox/ha-rule-form.ts +185 -0
- package/src/proxmox/ha-rule.ts +240 -0
- package/src/proxmox/index.ts +42 -0
- package/src/proxmox/lease-cache.test.ts +163 -0
- package/src/proxmox/lease-cache.ts +131 -0
- package/src/proxmox/lxc.ts +117 -0
- package/src/proxmox/members.test.ts +54 -0
- package/src/proxmox/members.ts +174 -0
- package/src/proxmox/metric-server-form.ts +128 -0
- package/src/proxmox/metric-server-otel.ts +99 -0
- package/src/proxmox/metric-server.ts +214 -0
- package/src/proxmox/network-apply-read.ts +228 -0
- package/src/proxmox/network-apply.ts +244 -0
- package/src/proxmox/node-network-form.ts +213 -0
- package/src/proxmox/node-network.ts +243 -0
- package/src/proxmox/notification-target-form.ts +104 -0
- package/src/proxmox/notification-target.ts +222 -0
- package/src/proxmox/pbs-datastore-form.ts +248 -0
- package/src/proxmox/pbs-datastore-guard.ts +105 -0
- package/src/proxmox/pbs-datastore.ts +271 -0
- package/src/proxmox/pbs-prune-job-form.ts +208 -0
- package/src/proxmox/pbs-prune-job.ts +227 -0
- package/src/proxmox/pbs-sync-job-form.ts +234 -0
- package/src/proxmox/pbs-sync-job.ts +258 -0
- package/src/proxmox/pbs-verify-job-form.ts +131 -0
- package/src/proxmox/pbs-verify-job.ts +227 -0
- package/src/proxmox/pool.test.ts +42 -0
- package/src/proxmox/pool.ts +80 -0
- package/src/proxmox/qemu.ts +89 -0
- package/src/proxmox/replication-job-form.ts +89 -0
- package/src/proxmox/replication-job.ts +248 -0
- package/src/proxmox/resource.ts +257 -0
- package/src/proxmox/role.ts +161 -0
- package/src/proxmox/sdn-apply-read.test.ts +152 -0
- package/src/proxmox/sdn-apply-read.ts +189 -0
- package/src/proxmox/sdn-apply.ts +154 -0
- package/src/proxmox/sdn-subnet-form.ts +156 -0
- package/src/proxmox/sdn-subnet.ts +238 -0
- package/src/proxmox/sdn-vnet.ts +228 -0
- package/src/proxmox/sdn-zone.ts +246 -0
- package/src/proxmox/storage.ts +245 -0
- package/src/proxmox/user.ts +214 -0
- package/src/proxmox/values.test.ts +193 -0
- package/src/proxmox/values.ts +203 -0
- package/src/proxmox/zfs-pool-write.ts +130 -0
- package/src/proxmox/zfs-pool.ts +253 -0
- package/src/talos/credentials.ts +136 -0
- package/src/talos/index.ts +14 -0
- package/src/talos/kubeconfig.ts +170 -0
- package/src/talos/resource.ts +31 -0
- package/src/talos/talos-bootstrap.ts +109 -0
- package/src/talos/talos-cluster-health.ts +119 -0
- package/src/talos/talos-machine-config.ts +146 -0
- package/src/talos/talosctl.ts +94 -0
- package/src/talos/values.test.ts +80 -0
- package/src/talos/values.ts +70 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Proxmox.Group` — a PVE group: a name, a comment, and the set of users PVE hangs off it.
|
|
3
|
+
*
|
|
4
|
+
* ★ DECLARING `hf-mint` IS THE WHOLE POINT OF THIS FAMILY. `vaultmint@pve` holds the `HFMint` role
|
|
5
|
+
* ON THE PATH `/access/groups/hf-mint` — measured from `GET /access/acl`, 2026-09-13 — which is
|
|
6
|
+
* what confines the OpenBao proxmox engine to minting inside that one group. The group is the
|
|
7
|
+
* fence around the credential every other resource in this package runs on, and until now it was
|
|
8
|
+
* something a human typed on a node once. `user.ts` calls that account the branch it sits on;
|
|
9
|
+
* this is the branch's other end.
|
|
10
|
+
*
|
|
11
|
+
* ⛔ MEMBERSHIP IS NOT SETTABLE HERE, SO IT IS REPORTED AND NEVER DIFFED. Measured from the
|
|
12
|
+
* cluster's own schema: `POST /access/groups` and `PUT /access/groups/{groupid}` each accept
|
|
13
|
+
* EXACTLY `groupid` and `comment`, both with `additionalProperties: 0`. There is no members
|
|
14
|
+
* parameter to send. Membership is written from the other side — the `groups` field on each
|
|
15
|
+
* USER, which `user.ts` already owns — so `members` below is an attribute, out of `matches`,
|
|
16
|
+
* exactly like `pool.members` and `user.tokens`. Declared here it would be a diff that no write
|
|
17
|
+
* in this file could ever settle.
|
|
18
|
+
*
|
|
19
|
+
* ⛔ THE INDEX AND THE ITEM DISAGREE ABOUT BOTH THE NAME AND THE TYPE, and this file reads only the
|
|
20
|
+
* item. MEASURED against n2, 2026-09-13:
|
|
21
|
+
* GET /access/groups -> [{"groupid":"hf-mint","comment":"…",
|
|
22
|
+
* "users":"hf-provision@pve,hf-read@pve"}, …]
|
|
23
|
+
* GET /access/groups/hf-mint -> {"comment":"…","members":["hf-provision@pve","hf-read@pve"]}
|
|
24
|
+
* The index says `users` and hands back a COMMA STRING; the item says `members` and hands back an
|
|
25
|
+
* ARRAY. A reader that looked for `users` on the item path would find nothing, report an empty
|
|
26
|
+
* group, and quietly show a delete as harmless.
|
|
27
|
+
*
|
|
28
|
+
* ⛔ AND THE ITEM'S ORDER IS NOT STABLE BETWEEN TWO CONSECUTIVE CALLS. MEASURED, seconds apart, on
|
|
29
|
+
* the same endpoint:
|
|
30
|
+
* ["root@pam","tim@pve","tim@pam","tim@<account>.com@Schenanigans"]
|
|
31
|
+
* ["tim@pam","tim@pve","tim@<account>.com@Schenanigans","root@pam"]
|
|
32
|
+
* `read_group` builds it as `[keys %{ $data->{users} }]` — a bare Perl hash key list, whose order
|
|
33
|
+
* is randomised per process — while the INDEX sorts (`join(',', sort keys …)`). So the array is
|
|
34
|
+
* sorted on the way into `attributes` as well as being kept out of `matches`: unsorted, Alchemy's
|
|
35
|
+
* stored state would churn on every read even with nothing to diff.
|
|
36
|
+
*
|
|
37
|
+
* ⚠️ `groupid` IS NOT ECHOED BY THE ITEM READ. Its return schema is `additionalProperties: 0` over
|
|
38
|
+
* exactly `comment` and `members`, so the id comes from props — the same arrangement, and the
|
|
39
|
+
* same reason, as `user.ts`.
|
|
40
|
+
*
|
|
41
|
+
* ★ PRIVILEGES: NOTHING HAD TO BE WIDENED, WHICH MAKES THIS THE FIRST FAMILY HERE THAT COST
|
|
42
|
+
* NOTHING. Measured with `pveum user permissions`, 2026-09-13:
|
|
43
|
+
* · item GET checks `['perm','/access/groups',['Sys.Audit','Group.Allocate'], any => 1]`, and
|
|
44
|
+
* `hf-read@pve` already holds `Sys.Audit` there (propagated from `/` by `PVEAuditor`).
|
|
45
|
+
* So `readRole` is left at the default `read` — unlike storage/sdn-zone/sdn-vnet, whose ITEM
|
|
46
|
+
* reads PVE gates on an allocate privilege. See the ⛔ on `readRole` in `resource.ts`.
|
|
47
|
+
* · POST, PUT and DELETE each check `Group.Allocate` on `/access/groups`, and
|
|
48
|
+
* `hf-provision@pve` holds it via `LXCProvisioner`.
|
|
49
|
+
*
|
|
50
|
+
* ⛔ `retain` BY DEFAULT, BECAUSE DELETING A GROUP DESTROYS TWO THINGS THIS FILE CANNOT PUT BACK.
|
|
51
|
+
* Measured in `PVE::AccessControl` on the node:
|
|
52
|
+
* · The member list IS the group. `user.cfg` stores it on the GROUP line (`group:<id>:<users>:
|
|
53
|
+
* <comment>:`) and DERIVES each user's `groups` map from it at parse time. Deleting the group
|
|
54
|
+
* deletes the list, and this resource has no `members` prop to restore it from.
|
|
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 `Schenanigans -> Administrator on /` and
|
|
57
|
+
* `HomeAssistant -> PVEAuditor on /`: whole populations of access, gone in one call, with no
|
|
58
|
+
* confirmation and nothing in the plan to suggest it.
|
|
59
|
+
* `delete` is FULLY IMPLEMENTED — `DELETE /access/groups/{groupid}` exists, unlike the ACL
|
|
60
|
+
* family's — so `.pipe(RemovalPolicy.destroy())` really removes the group. See the ★ in
|
|
61
|
+
* `resource.ts` for why the policy is a guard rather than a stubbed-out operation.
|
|
62
|
+
*
|
|
63
|
+
* ⛔ NO SECRET REACHES STATE. A group holds a comment and a list of userids; there is no password,
|
|
64
|
+
* no token and no key anywhere on these endpoints. `user.ts`'s ⛔ about unencrypted state applies
|
|
65
|
+
* to this package, and this family simply has nothing to trip it.
|
|
66
|
+
*/
|
|
67
|
+
import { Resource } from 'alchemy';
|
|
68
|
+
import * as Provider from 'alchemy/Provider';
|
|
69
|
+
import * as Effect from 'effect/Effect';
|
|
70
|
+
import { type PveRequirements, type WithTarget, pveHandlers } from './resource.ts';
|
|
71
|
+
import { csv, text } from './values.ts';
|
|
72
|
+
|
|
73
|
+
export interface GroupProps extends WithTarget {
|
|
74
|
+
/**
|
|
75
|
+
* PVE's primary key for a group.
|
|
76
|
+
*
|
|
77
|
+
* ⚠️ THERE IS NO RENAME, AND THE FAILURE IS WORSE HERE THAN FOR A ROLE. Editing this makes `path`
|
|
78
|
+
* point at a different group, which reads as absent and is created empty — while the old group
|
|
79
|
+
* keeps its members and its ACL grants with nobody managing it. Two groups then both look
|
|
80
|
+
* right in the UI and only one of them grants anything.
|
|
81
|
+
*/
|
|
82
|
+
groupid: string;
|
|
83
|
+
/** Free text shown in the UI — the only mutable field a group has. See `storedComment` below. */
|
|
84
|
+
comment?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface GroupAttributes {
|
|
88
|
+
groupid: string;
|
|
89
|
+
comment: string;
|
|
90
|
+
/**
|
|
91
|
+
* Who is in the group, sorted — reported so a plan can say what a delete would strip, never
|
|
92
|
+
* compared, because this endpoint cannot set it. See the two ⛔s in the header.
|
|
93
|
+
*/
|
|
94
|
+
members: string[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface ProxmoxGroup extends Resource<
|
|
98
|
+
'Proxmox.Group',
|
|
99
|
+
GroupProps,
|
|
100
|
+
GroupAttributes,
|
|
101
|
+
never,
|
|
102
|
+
PveRequirements
|
|
103
|
+
> {}
|
|
104
|
+
|
|
105
|
+
/** ★ `retain` by default — a delete takes the membership and the group's grants. See the header. */
|
|
106
|
+
export const ProxmoxGroup = Resource<ProxmoxGroup>('Proxmox.Group', {
|
|
107
|
+
defaultRemovalPolicy: 'retain',
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The comment PVE will actually END UP HOLDING for a given declaration.
|
|
112
|
+
*
|
|
113
|
+
* ⛔ PVE CANNOT STORE THE COMMENT `'0'`, AND A PROVIDER THAT DOES NOT MODEL THAT DIFFS FOREVER.
|
|
114
|
+
* Three places in the shipped Perl test the comment for TRUTH rather than for definedness, and
|
|
115
|
+
* in Perl the one-character string `'0'` is false:
|
|
116
|
+
* `create_group`: `$group->{comment} = $param->{comment} if $param->{comment};`
|
|
117
|
+
* `write_user_config`: `my $comment = $d->{comment} ? encode_text($d->{comment}) : '';`
|
|
118
|
+
* the config parser: `$cfg->{groups}->{$g}->{comment} = decode_text($comment) if $comment;`
|
|
119
|
+
* So a declared `comment: '0'` is accepted by the API, dropped on the way to `user.cfg`, and read
|
|
120
|
+
* back as absent. Left alone, `matches` would be false on every plan and every deploy would
|
|
121
|
+
* rewrite the same value into the same hole. Normalising BOTH the comparison and the form is how
|
|
122
|
+
* `values.ts` already treats `''` as the absence of a boolean rather than as `false`.
|
|
123
|
+
* ⚠️ MEASURED IN THE SOURCE ON n2, NOT ON THE WIRE — writing `'0'` to the cluster to watch it
|
|
124
|
+
* vanish would have been a write, and this agent had read access only.
|
|
125
|
+
* ⚠️ `pool.ts` HAS THE SAME LATENT HOLE — `pool:$pool:$comment:…` is written by that same
|
|
126
|
+
* truthiness test and is not guarded. Not fixed from here; flagged so it is fixed on purpose.
|
|
127
|
+
*
|
|
128
|
+
* ⚠️ EVERY OTHER COMMENT ROUND-TRIPS EXACTLY, colons and newlines included: `write_user_config`
|
|
129
|
+
* runs it through `encode_text` and the parser through `decode_text`, so the field separator in
|
|
130
|
+
* `user.cfg` cannot be smuggled in.
|
|
131
|
+
*/
|
|
132
|
+
const storedComment = (comment: string | undefined) => {
|
|
133
|
+
const value = comment ?? '';
|
|
134
|
+
return value === '0' ? '' : value;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The item read's `members` array, sorted into something two plans can agree on.
|
|
139
|
+
*
|
|
140
|
+
* ⚠️ `csv` IS REUSED RATHER THAN RE-IMPLEMENTED, AND ITS OWN ⚠️ IS EXACTLY THIS CASE: a list PVE
|
|
141
|
+
* does not promise to give back in the order it was handed. Going out through the joined form
|
|
142
|
+
* and back is the price of that reuse, and cheaper than a fourth hand-rolled sorter.
|
|
143
|
+
* ⚠️ THE ARRAY ONLY — THE COMMA STRING IS NOT ACCEPTED HERE, ON PURPOSE. That spelling belongs to
|
|
144
|
+
* the INDEX (`users`), which this provider never reads, so a branch for it would be a guess at a
|
|
145
|
+
* shape that cannot arrive. `user.ts` refuses the same guess for `tokens` and for the same
|
|
146
|
+
* reason: a wrong guess on a reported-only field surfaces in a plan as a phantom membership.
|
|
147
|
+
* ⚠️ NO DEDUPLICATION, UNLIKE `user.ts`'s `groupSet`. These names are Perl HASH KEYS —
|
|
148
|
+
* `[keys %{ $data->{users} }]` — so a duplicate is not representable. `user.groups` needs the
|
|
149
|
+
* dedupe because its value also arrives from a hand-written prop; this one never does.
|
|
150
|
+
*/
|
|
151
|
+
const memberList = (live: unknown): string[] => {
|
|
152
|
+
const joined = csv((Array.isArray(live) ? live : []).map((member: unknown) => text(member)));
|
|
153
|
+
return joined === '' ? [] : joined.split(',');
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const handlers = pveHandlers<GroupProps, GroupAttributes>({
|
|
157
|
+
attributes: (live, props) => ({
|
|
158
|
+
/**
|
|
159
|
+
* ⚠️ ABSENT IS EMPTY, NOT MISSING. `read_group` sets `comment` only `if defined(…)`, so a group
|
|
160
|
+
* without one answers `{"members":[…]}` and nothing else. THAT ONE IS READ OFF THE HANDLER'S
|
|
161
|
+
* SOURCE, not off the wire — every group on this cluster happens to carry a comment, so the
|
|
162
|
+
* commentless shape was not there to measure. The empty-MEMBERS shape WAS measured —
|
|
163
|
+
* `HomeAssistant` answers `{"comment":"…","members":[]}`. `''` is what an undeclared
|
|
164
|
+
* `comment` prop normalises to, so the two sides meet either way.
|
|
165
|
+
*/
|
|
166
|
+
comment: text(live['comment']),
|
|
167
|
+
/** ⚠️ FROM PROPS: the item read's schema is `additionalProperties: 0` over comment/members. */
|
|
168
|
+
groupid: props.groupid,
|
|
169
|
+
members: memberList(live['members']),
|
|
170
|
+
}),
|
|
171
|
+
collection: () => 'access/groups',
|
|
172
|
+
createForm: (props) => ({ comment: storedComment(props.comment), groupid: props.groupid }),
|
|
173
|
+
/**
|
|
174
|
+
* ⚠️ `members` IS NOT COMPARED, AND NOTHING ELSE IS LEFT TO COMPARE. `comment` is the only field
|
|
175
|
+
* a PUT here accepts, so it is the only field whose drift this provider can repair — the rule
|
|
176
|
+
* `resource.ts` states once in the ⛔ on adoption: a field deliberately out of `matches` is a
|
|
177
|
+
* field this resource does not manage.
|
|
178
|
+
*/
|
|
179
|
+
matches: (attributes, props) => attributes.comment === storedComment(props.comment),
|
|
180
|
+
path: (props) => `access/groups/${props.groupid}`,
|
|
181
|
+
/**
|
|
182
|
+
* ⚠️ ALWAYS SENT, EVEN EMPTY — that is how a comment is CLEARED. `update_group` assigns
|
|
183
|
+
* `if defined($param->{comment})`, so an omitted field leaves the old text in place and a
|
|
184
|
+
* declaration that dropped its comment would report an update the update could not settle. The
|
|
185
|
+
* empty string then falls out of `user.cfg` on the truthiness test above and reads back as
|
|
186
|
+
* `''`, which is what the props side normalises to. The loop closes.
|
|
187
|
+
*/
|
|
188
|
+
updateForm: (props) => ({ comment: storedComment(props.comment) }),
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
/**
|
|
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 `Schenanigans`, which grants
|
|
194
|
+
* `Administrator` on `/` to every human who logs in. Handing it to Alchemy would invite adoption,
|
|
195
|
+
* and therefore one day a delete that takes the whole admin group's access with it. Adoption
|
|
196
|
+
* stays an explicit act, here as everywhere else in this package.
|
|
197
|
+
*/
|
|
198
|
+
export const ProxmoxGroupProvider = () =>
|
|
199
|
+
Provider.effect(ProxmoxGroup, Effect.succeed(ProxmoxGroup.Provider.of(handlers)));
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Proxmox.HaResource` — a guest's HA membership, declared.
|
|
3
|
+
*
|
|
4
|
+
* ★ WITHOUT ONE OF THESE, EVERY GUEST THIS PACKAGE DECLARES IS PINNED TO THE NODE THE LINE NAMED
|
|
5
|
+
* AND DIES WITH IT. The target is a three-node quorate cluster holding zero HA resources, so
|
|
6
|
+
* `node` in `LxcProps` is the whole of a container's availability story: lose that node and the
|
|
7
|
+
* container is simply gone until a person notices. Declaring a guest without declaring its HA
|
|
8
|
+
* membership is half a declaration, which is why this lives beside `lxc.ts` rather than in a
|
|
9
|
+
* runbook somebody reads after an outage.
|
|
10
|
+
*
|
|
11
|
+
* ⚠️ `state` READS LIKE STATUS AND IS CONFIGURATION. `started | stopped | disabled | ignored` is
|
|
12
|
+
* the state the CRM is ASKED for; a line in the HA config file, not a reading off the cluster,
|
|
13
|
+
* so it is a prop and it is compared. `LxcAttributes.status` is the opposite case — reported,
|
|
14
|
+
* never declared — and confusing the two inverts the meaning: `state: 'stopped'` here says "HA
|
|
15
|
+
* should hold this guest stopped and still relocate it on node failure", not "it is stopped".
|
|
16
|
+
*
|
|
17
|
+
* ⛔ AND YET IT MOVES POWER, WHICH IS THE ONE PLACE THIS PACKAGE'S "PLANS DO NOT START THINGS" RULE
|
|
18
|
+
* BENDS. `lxc.ts` and `qemu.ts` refuse to own power state precisely so a deploy is not a
|
|
19
|
+
* maintenance window — but the CRM acts on whatever this file says. The default `started` will
|
|
20
|
+
* BOOT a stopped guest within seconds of the write, and `stopped` will SHUT DOWN a running one.
|
|
21
|
+
* There is no way to declare HA membership without declaring intent, so the intent is at least
|
|
22
|
+
* explicit and diffable here; read a plan that touches `state` as a plan that touches uptime.
|
|
23
|
+
*
|
|
24
|
+
* ⛔ `sid` CARRIES ITS TYPE PREFIX — `ct:101`, `vm:100` — AND READ ANSWERS IN THE PREFIXED FORM.
|
|
25
|
+
* POST also takes the bare `101` shortcut; taking it would leave a resource keyed on a spelling
|
|
26
|
+
* PVE never returns, and every plan comparing `101` against `ct:101`. So the prefixed form is
|
|
27
|
+
* required here and it is what goes into the path.
|
|
28
|
+
* ⚠️ EDITING `sid` IN PLACE ORPHANS THE OLD MEMBERSHIP. `diff` reads the NEW path, finds nothing
|
|
29
|
+
* and calls that drift, so reconcile creates the new entry and the old one stays under HA with
|
|
30
|
+
* nothing declaring it. Retire the declaration and add a second one instead of renaming.
|
|
31
|
+
*
|
|
32
|
+
* ⛔ MEASURED ON PVE 9.2: `/cluster/ha/groups` ANSWERS HTTP 500 — groups were removed in PVE 9,
|
|
33
|
+
* even though the published API schema still documents the endpoint AND a `group` parameter on
|
|
34
|
+
* this very resource. Nothing here sends `group`. The successor is `/cluster/ha/rules`
|
|
35
|
+
* (node-affinity and resource-affinity) and that is the next family to add; do not port the old
|
|
36
|
+
* groups endpoint back just because the docs still describe it.
|
|
37
|
+
*
|
|
38
|
+
* ⚠️ RECONCILE NEEDS `Sys.Console` ON `/`, WHICH `LXCProvisioner` DOES NOT HOLD. Read and diff are
|
|
39
|
+
* fine — `GET /cluster/ha/resources/{sid}` checks `Sys.Audit`, which the role already has — while
|
|
40
|
+
* POST, PUT and DELETE all check `Sys.Console` on `/` and will answer "Permission check failed
|
|
41
|
+
* (/, Sys.Console)" until the role is widened. Widen it knowingly: `Sys.Console` is also what
|
|
42
|
+
* opens a root shell on every node (`/nodes/{node}/vncshell`, `termproxy`), so granting it to the
|
|
43
|
+
* provision credential buys HA membership at the price of node console access. A separate role
|
|
44
|
+
* for HA writes is the narrower answer if that trade is not wanted.
|
|
45
|
+
*/
|
|
46
|
+
import { Resource } from 'alchemy';
|
|
47
|
+
import * as Provider from 'alchemy/Provider';
|
|
48
|
+
import * as Effect from 'effect/Effect';
|
|
49
|
+
import { type PveRequirements, type WithTarget, pveHandlers } from './resource.ts';
|
|
50
|
+
import { num } from './values.ts';
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The requested states this provider admits.
|
|
54
|
+
*
|
|
55
|
+
* ⚠️ `enabled` IS DELIBERATELY ABSENT. PVE accepts it and documents it as an alias for `started`,
|
|
56
|
+
* which means a resource declared as `enabled` can be read back as `started` — and `matches`
|
|
57
|
+
* would then report an update on every plan, forever, over a difference in spelling. One spelling
|
|
58
|
+
* per state is the only version of this that settles.
|
|
59
|
+
*/
|
|
60
|
+
export type HaState = 'started' | 'stopped' | 'disabled' | 'ignored';
|
|
61
|
+
|
|
62
|
+
export interface HaResourceProps extends WithTarget {
|
|
63
|
+
/** ⛔ PREFIXED AND CLUSTER-WIDE: `ct:101`, `vm:100`. Changing it is a different object. */
|
|
64
|
+
sid: string;
|
|
65
|
+
/** What the CRM should aim for. Defaults to `started`, PVE's own default. */
|
|
66
|
+
state?: HaState;
|
|
67
|
+
/** Free text shown in the HA panel. Max 4096 characters. */
|
|
68
|
+
comment?: string;
|
|
69
|
+
/** Restart tries on the SAME node before the manager gives up and relocates. Default 1. */
|
|
70
|
+
max_restart?: number;
|
|
71
|
+
/** Relocation tries before the resource is left in `error`. Default 1. */
|
|
72
|
+
max_relocate?: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface HaResourceAttributes {
|
|
76
|
+
/** As PVE returns it, prefix included. */
|
|
77
|
+
sid: string;
|
|
78
|
+
/** `ct` | `vm`. Implied by the prefix, reported so a plan can say what is being protected. */
|
|
79
|
+
type: string;
|
|
80
|
+
/**
|
|
81
|
+
* ⚠️ TYPED `string`, NOT `HaState`, because this is whatever the config file holds — including an
|
|
82
|
+
* `enabled` a person wrote in the UI. Narrowing it here would be a claim about someone else's
|
|
83
|
+
* edit rather than a report of it.
|
|
84
|
+
*/
|
|
85
|
+
state: string;
|
|
86
|
+
comment: string;
|
|
87
|
+
max_restart: number;
|
|
88
|
+
max_relocate: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface ProxmoxHaResource extends Resource<
|
|
92
|
+
'Proxmox.HaResource',
|
|
93
|
+
HaResourceProps,
|
|
94
|
+
HaResourceAttributes,
|
|
95
|
+
never,
|
|
96
|
+
PveRequirements
|
|
97
|
+
> {}
|
|
98
|
+
|
|
99
|
+
export const ProxmoxHaResource = Resource<ProxmoxHaResource>('Proxmox.HaResource');
|
|
100
|
+
|
|
101
|
+
/** `ct:101` -> `ct`. ⚠️ `?? ''` because noUncheckedIndexedAccess types `split()[0]` as optional. */
|
|
102
|
+
const kind = (sid: string) => sid.split(':')[0] ?? '';
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* ⚠️ NO `type` IN EITHER FORM, ON PURPOSE. The prefix in `sid` already names it on create, and PUT
|
|
106
|
+
* does not accept `type` at all — sending it on create only would make the two shapes diverge for
|
|
107
|
+
* no gain, and sending it on update is a parameter-verification failure.
|
|
108
|
+
*/
|
|
109
|
+
const shape = (props: HaResourceProps) => ({
|
|
110
|
+
comment: props.comment ?? '',
|
|
111
|
+
max_relocate: String(props.max_relocate ?? 1),
|
|
112
|
+
max_restart: String(props.max_restart ?? 1),
|
|
113
|
+
state: props.state ?? 'started',
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const handlers = pveHandlers<HaResourceProps, HaResourceAttributes>({
|
|
117
|
+
attributes: (live, props) => ({
|
|
118
|
+
comment: typeof live['comment'] === 'string' ? live['comment'] : '',
|
|
119
|
+
max_relocate: num(live['max_relocate'], 1),
|
|
120
|
+
max_restart: num(live['max_restart'], 1),
|
|
121
|
+
sid: typeof live['sid'] === 'string' ? live['sid'] : props.sid,
|
|
122
|
+
state: typeof live['state'] === 'string' ? live['state'] : 'started',
|
|
123
|
+
type: typeof live['type'] === 'string' ? live['type'] : kind(props.sid),
|
|
124
|
+
}),
|
|
125
|
+
/**
|
|
126
|
+
* ⚠️ CLUSTER-SCOPED, SO THERE IS NO NODE IN THE PATH — and that is the point of the object. The
|
|
127
|
+
* guest's node is where it happens to run now; this resource is the statement that it may run
|
|
128
|
+
* somewhere else tomorrow.
|
|
129
|
+
*/
|
|
130
|
+
collection: () => 'cluster/ha/resources',
|
|
131
|
+
createForm: (props) => ({ ...shape(props), sid: props.sid }),
|
|
132
|
+
/**
|
|
133
|
+
* ⚠️ `digest` IS NOT COMPARED AND IS NOT AN ATTRIBUTE. It changes whenever ANY line of the HA
|
|
134
|
+
* config changes, including one written for a different guest, so diffing it would report an
|
|
135
|
+
* update on this resource because somebody else edited theirs.
|
|
136
|
+
* ⚠️ `sid` AND `type` ARE NOT COMPARED EITHER: sid is the path, so a change there is a different
|
|
137
|
+
* object, and type is create-only — it is read back from the prefix, never edited.
|
|
138
|
+
*/
|
|
139
|
+
matches: (attributes, props) =>
|
|
140
|
+
attributes.state === (props.state ?? 'started') &&
|
|
141
|
+
attributes.comment === (props.comment ?? '') &&
|
|
142
|
+
attributes.max_restart === (props.max_restart ?? 1) &&
|
|
143
|
+
attributes.max_relocate === (props.max_relocate ?? 1),
|
|
144
|
+
/**
|
|
145
|
+
* ⚠️ THE COLON GOES INTO THE URL RAW. RFC 3986 allows `:` inside a path segment and PVE matches
|
|
146
|
+
* the sid as one segment, so no `encodeURIComponent` here; percent-encoding it would make the
|
|
147
|
+
* request depend on a proxy normalising `%3A` back before the dispatcher sees it.
|
|
148
|
+
*/
|
|
149
|
+
path: (props) => `cluster/ha/resources/${props.sid}`,
|
|
150
|
+
updateForm: shape,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* ⛔ Empty for the same reason as every other resource here: adoption must be explicit.
|
|
155
|
+
* `GET /cluster/ha/resources` would hand back every guest an operator has ever put under
|
|
156
|
+
* HA, and adopting those means one day deleting them.
|
|
157
|
+
|
|
158
|
+
*
|
|
159
|
+
* ⛔ DELETE DEFAULTS TO `purge=1` AND THE FACTORY SENDS NO FORM, SO THAT DEFAULT APPLIES:
|
|
160
|
+
* PVE strips the sid out of every HA rule referencing it and DELETES a rule that had no
|
|
161
|
+
* other member. Destroying an HA membership can therefore take an affinity rule with it,
|
|
162
|
+
* which matters once the rules family lands.
|
|
163
|
+
* ⚠️ It does NOT stop or remove the guest. The container keeps running exactly where it is,
|
|
164
|
+
* only unmanaged — so removing this resource is how a guest is handed back to its node,
|
|
165
|
+
* not how it is decommissioned.
|
|
166
|
+
|
|
167
|
+
*/
|
|
168
|
+
export const ProxmoxHaResourceProvider = () =>
|
|
169
|
+
Provider.effect(ProxmoxHaResource, Effect.succeed(ProxmoxHaResource.Provider.of(handlers)));
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An HA rule's props as the form PVE wants, and the two lists PVE re-spells on the way back.
|
|
3
|
+
*
|
|
4
|
+
* ★ SPLIT OUT OF ha-rule.ts TO KEEP BOTH FILES UNDER THE 250-LINE CAP, and the seam is the same
|
|
5
|
+
* one metric-server-form.ts and node-network-form.ts cut: this file answers "how does a
|
|
6
|
+
* declaration become a PVE form, and what shape does PVE hand back", ha-rule.ts answers "what is
|
|
7
|
+
* an HA rule and when has it changed". Nothing here reads the cluster or decides a diff.
|
|
8
|
+
*
|
|
9
|
+
* ⚠️ THE `import type` BACK TO ha-rule.ts IS A CYCLE ON PAPER ONLY. It is type-only, so it is
|
|
10
|
+
* erased before anything runs, and `HaRuleProps` stays public in the file declaring the resource.
|
|
11
|
+
*/
|
|
12
|
+
import type { HaRuleProps } from './ha-rule.ts';
|
|
13
|
+
import { csv } from './values.ts';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A `<node>[:<pri>]` list, spelled the way PVE gives it back.
|
|
17
|
+
*
|
|
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 n2 (2026-09-13): `decode_plugin_value`
|
|
20
|
+
* turns the list into a HASH keyed by node name, and `encode_plugin_value` rebuilds it with
|
|
21
|
+
* `for my $node (sort keys %$value)` — sorted by NODE NAME, never by the token — emitting the
|
|
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 `n3:1,n2:0` is handed back as `n2,n3:1`: re-ordered AND
|
|
24
|
+
* re-spelled. Compare the raw strings and every plan reports an update, forever.
|
|
25
|
+
*
|
|
26
|
+
* ⚠️ SORTED BY NAME, NOT BY TOKEN, AND THE TWO GENUINELY DIVERGE. `csv` sorts whole tokens. Node
|
|
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 `n2-a`
|
|
29
|
+
* and `n2:5` come out in one order by token and the other by name. PVE sorts by the name, so
|
|
30
|
+
* this does too.
|
|
31
|
+
*
|
|
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 `n2:1,n2:5` is stored as `n2:5`; matching that
|
|
34
|
+
* is what a Map gives for free.
|
|
35
|
+
*/
|
|
36
|
+
export const nodeList = (value: readonly string[] | string | undefined) => {
|
|
37
|
+
const ranked = new Map<string, number>();
|
|
38
|
+
for (const entry of typeof value === 'string' ? value.split(',') : (value ?? [])) {
|
|
39
|
+
const [node, priority] = entry.trim().split(':');
|
|
40
|
+
if (node === undefined || node === '') continue;
|
|
41
|
+
const rank = Number.parseInt(priority ?? '0', 10);
|
|
42
|
+
ranked.set(node, Number.isNaN(rank) ? 0 : rank);
|
|
43
|
+
}
|
|
44
|
+
return [...ranked.keys()]
|
|
45
|
+
.sort()
|
|
46
|
+
.map((node) => {
|
|
47
|
+
const rank = ranked.get(node) ?? 0;
|
|
48
|
+
return rank === 0 ? node : `${node}:${String(rank)}`;
|
|
49
|
+
})
|
|
50
|
+
.join(',');
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* PVE's own verdict on whether the rule can be satisfied, flattened to one reportable string.
|
|
55
|
+
*
|
|
56
|
+
* ⚠️ COMPUTED, NEVER DECLARED, AND NEVER COMPARED. `PVE::API2::HA::Rules` runs `check_feasibility`
|
|
57
|
+
* over the WHOLE rule set on every read and attaches whatever it finds to each rule as
|
|
58
|
+
* `errors => { <option> => "<message>, <message>" }`. It therefore changes when somebody else's
|
|
59
|
+
* rule changes, and none of it is a parameter any write accepts. It belongs in attributes so a
|
|
60
|
+
* plan can show that PVE considers this rule unsatisfiable, and nowhere near `matches`.
|
|
61
|
+
*/
|
|
62
|
+
export const problems = (value: unknown) =>
|
|
63
|
+
typeof value === 'object' && value !== null
|
|
64
|
+
? Object.entries(value)
|
|
65
|
+
.map(([option, message]) => `${option}: ${String(message)}`)
|
|
66
|
+
.sort()
|
|
67
|
+
.join('; ')
|
|
68
|
+
: '';
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A comment as PVE will actually STORE it, which is not always the string it was handed.
|
|
72
|
+
*
|
|
73
|
+
* ⛔ PVE DECODES `%XX` OUT OF EVERY COMMENT ON THE WAY IN AND NEVER PUTS IT BACK. MEASURED:
|
|
74
|
+
* `PVE::SectionConfig::check_config` runs `decode_value` over each submitted field (line 1625 on
|
|
75
|
+
* this node), the base rule plugin's `decode_value` sends `comment` through
|
|
76
|
+
* `PVE::ParseUtils::decode_text` — `uri_unescape` then a utf8 decode — and `$get_api_ha_rule`
|
|
77
|
+
* hands the comment straight back WITHOUT re-encoding it (only `resources` and `nodes` are
|
|
78
|
+
* re-encoded there). So a declared `deploy%20window` is stored and returned as `deploy window`,
|
|
79
|
+
* and a `matches` comparing the declared string would report an update on every plan forever
|
|
80
|
+
* while every PUT wrote exactly what was already there.
|
|
81
|
+
*
|
|
82
|
+
* ★ SO THE COMPARISON MIRRORS THE DECODE RATHER THAN FIGHTING IT. Both sides then say what the
|
|
83
|
+
* cluster will really hold, which is the same move `nodeList` above makes for a re-sorted list.
|
|
84
|
+
* ⚠️ GROUPS OF `%XX` ARE DECODED TOGETHER so a multi-byte utf8 sequence survives; a lone `%` or a
|
|
85
|
+
* malformed sequence is left alone, as `uri_unescape` leaves it. The one place this differs from
|
|
86
|
+
* Perl is a sequence that is valid percent-encoding but invalid utf8: Perl substitutes U+FFFD,
|
|
87
|
+
* this leaves the escape. Nothing in a comment should be reaching for that.
|
|
88
|
+
*/
|
|
89
|
+
export const commentText = (raw: string) =>
|
|
90
|
+
raw.replace(/(?:%[0-9a-fA-F]{2})+/g, (escaped) => {
|
|
91
|
+
try {
|
|
92
|
+
return decodeURIComponent(escaped);
|
|
93
|
+
} catch {
|
|
94
|
+
return escaped;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Every managed optional, per type: the value to send, or undefined for "clear it".
|
|
100
|
+
*
|
|
101
|
+
* ⛔ `disable=0` DOES NOT CLEAR A DISABLED RULE, AND THAT IS THE WORST TRAP IN THIS FAMILY.
|
|
102
|
+
* MEASURED in `/usr/share/perl5/PVE/API2/HA/Rules.pm`: both `create_rule` and `update_rule` run
|
|
103
|
+
* `delete $param->{disable} if !$param->{disable};` BEFORE the plugin sees the form. A PUT
|
|
104
|
+
* carrying `disable=0` is therefore a PUT carrying nothing — PVE answers 200, the rule stays
|
|
105
|
+
* disabled, `matches` reports an update on the next plan, and the loop never settles. The only
|
|
106
|
+
* way back on is `delete=disable`, which is why `disable` is in this table rather than in
|
|
107
|
+
* `required` below.
|
|
108
|
+
*
|
|
109
|
+
* ⛔ AND THE BRANCHES CANNOT BE ONE SHARED MAP. `PVE::SectionConfig::delete_from_config` dies
|
|
110
|
+
* `no such option '<k>'` for an option the plugin does not declare and `unable to delete
|
|
111
|
+
* required option '<k>'` for one it requires — so `delete=strict` against a resource-affinity
|
|
112
|
+
* rule is a hard failure, and `delete=nodes` or `delete=resources` would be one against either.
|
|
113
|
+
* Only `comment`, `disable` and (node-affinity only) `strict` are clearable at all.
|
|
114
|
+
*
|
|
115
|
+
* ⚠️ AN EMPTY `comment` IS A CLEARED ONE. PVE stores whatever string it is given, so writing `''`
|
|
116
|
+
* would leave the option present and empty rather than absent; `matches` reads both as `''`, but
|
|
117
|
+
* only the delete actually removes the line.
|
|
118
|
+
*/
|
|
119
|
+
export const optional = (props: HaRuleProps): Record<string, string | undefined> => ({
|
|
120
|
+
comment: props.comment === undefined || props.comment === '' ? undefined : props.comment,
|
|
121
|
+
disable: props.disable === true ? '1' : undefined,
|
|
122
|
+
...(props.type === 'node-affinity' ? { strict: props.strict === true ? '1' : undefined } : {}),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The fields sent on EVERY write, create and update alike.
|
|
127
|
+
*
|
|
128
|
+
* ⛔ `type` GOES OUT ON THE UPDATE TOO, AND LEAVING IT OFF BREAKS EVERY PUT. It reads like a
|
|
129
|
+
* create-only discriminant — the handler certainly treats it as one, taking the plugin from the
|
|
130
|
+
* STORED rule — but the PUT is still validated against a `oneOf` keyed on it. MEASURED in
|
|
131
|
+
* `PVE::JSONSchema::check_one_of` on this node: when the type property is absent the validator
|
|
132
|
+
* marks EVERY other key unknown and, unless the whole `oneOf` is itself optional, errors on the
|
|
133
|
+
* type property; its own comment says "Its type property is otherwise never optional", and
|
|
134
|
+
* `SectionConfig::updateSchema` builds that `oneOf` through `combine_schemas` without an
|
|
135
|
+
* `optional` flag. So an update that omitted it would fail parameter verification with a pile of
|
|
136
|
+
* "unexpected property" errors pointing at the fields it did send. It is safe to send because a
|
|
137
|
+
* rule of the other type is reported ABSENT by `attributes` and never reaches a PUT.
|
|
138
|
+
*
|
|
139
|
+
* ⚠️ `affinity` IS SENT UNCONDITIONALLY FOR BOTH TYPES. PVE requires it on a resource-affinity
|
|
140
|
+
* create and defaults it to `positive` on a node-affinity one; sending it either way means the
|
|
141
|
+
* read-back matches on the first plan after the create rather than relying on PVE materialising
|
|
142
|
+
* its own default into the file.
|
|
143
|
+
*
|
|
144
|
+
* ⚠️ THE LISTS GO OUT IN THEIR NORMALISED FORM, which is not required but is free: PVE re-sorts
|
|
145
|
+
* them anyway, so writing what it will hand back keeps the config file readable next to the
|
|
146
|
+
* declaration that produced it.
|
|
147
|
+
*
|
|
148
|
+
* ⚠️ NO `digest`. PVE accepts one on PUT as an optimistic lock, but the only digest this provider
|
|
149
|
+
* could send is the one from its own read moments earlier — and it is a digest of the WHOLE
|
|
150
|
+
* rules file, so any unrelated rule written in between would turn a correct update into a
|
|
151
|
+
* spurious failure. The factory's read-back guard is the check that stays.
|
|
152
|
+
*/
|
|
153
|
+
const required = (props: HaRuleProps): Record<string, string> => ({
|
|
154
|
+
affinity: props.affinity,
|
|
155
|
+
resources: csv(props.resources),
|
|
156
|
+
type: props.type,
|
|
157
|
+
...(props.type === 'node-affinity' ? { nodes: nodeList(props.nodes) } : {}),
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
export const body = (props: HaRuleProps) => {
|
|
161
|
+
const fields = required(props);
|
|
162
|
+
for (const [option, value] of Object.entries(optional(props))) {
|
|
163
|
+
if (value !== undefined) fields[option] = value;
|
|
164
|
+
}
|
|
165
|
+
return fields;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The options an update must explicitly remove.
|
|
170
|
+
*
|
|
171
|
+
* ⛔ A PUT THAT OMITS A FIELD DOES NOT CLEAR IT — `update_rule` merges the form into the stored
|
|
172
|
+
* rule (`$rule->{$_} = $opts->{$_} for keys $opts->%*`) — so a managed optional needs a matching
|
|
173
|
+
* `delete=` the moment its prop goes away. Both halves come off the one table above, which makes
|
|
174
|
+
* "managed but not clearable" impossible to write here.
|
|
175
|
+
* ⚠️ NEVER BOTH AT ONCE: `delete_from_config` dies `cannot set and delete property '<k>' at the
|
|
176
|
+
* same time!`. The table answers a value or `undefined`, never both, so the two lists are
|
|
177
|
+
* disjoint by construction.
|
|
178
|
+
* ⚠️ THE CREATE FORM CANNOT USE THIS. `POST /cluster/ha/rules` has no `delete` parameter at all
|
|
179
|
+
* (MEASURED from the cluster's own schema), and it needs none: an omitted optional on a brand
|
|
180
|
+
* new rule is simply absent.
|
|
181
|
+
*/
|
|
182
|
+
export const clearList = (props: HaRuleProps) =>
|
|
183
|
+
Object.entries(optional(props))
|
|
184
|
+
.filter(([, value]) => value === undefined)
|
|
185
|
+
.map(([option]) => option);
|