@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,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Proxmox.BackupJob` — the vzdump schedule, declared. The one object whose drift nothing else on
|
|
3
|
+
* the cluster reveals.
|
|
4
|
+
*
|
|
5
|
+
* ★ WHY THIS ONE EARNS ITS PLACE. A hand-made vzdump job drifts silently: a schedule moved by an
|
|
6
|
+
* hour, a retention trimmed to `keep-last=1`, or `enabled 0` set during a maintenance window and
|
|
7
|
+
* never set back. All three look like a healthy cluster from every angle except a restore.
|
|
8
|
+
* Declared here, `diff` reads the LIVE job on every plan, so a hand edit surfaces as
|
|
9
|
+
* `1 to update` that day rather than on the day somebody needs last night's backup and finds a
|
|
10
|
+
* week-old one, or none.
|
|
11
|
+
*
|
|
12
|
+
* ⛔ PVE GENERATES `id` WHEN YOU OMIT IT, WHICH MAKES OMITTING IT THE WORST BUG THIS RESOURCE
|
|
13
|
+
* COULD CARRY. PVE's own API schema says so in one parenthesis — `id`: "Job ID (will be
|
|
14
|
+
* autogenerated)." — and a generated id looks like `backup-f56d8ab7-d524`. A resource that
|
|
15
|
+
* POSTed without an id would create a SECOND job beside the first on EVERY reconcile, and the
|
|
16
|
+
* read-back in `pveOperations` would still pass, because a job did appear: that check asks "is
|
|
17
|
+
* the object there", not "is it the one I meant". So `id` is a REQUIRED prop and is always sent
|
|
18
|
+
* on create. To adopt a job that already exists, declare its existing id; any other id is a new
|
|
19
|
+
* job beside it, not an adoption.
|
|
20
|
+
* ★ AN EXPLICIT id ALSO CLOSES A SECOND HOLE. `pveOperations.read` turns ANY failed GET into
|
|
21
|
+
* "absent" — an expired lease, a 403, a node that did not answer — and reconcile then POSTs.
|
|
22
|
+
* With an id, PVE refuses a duplicate section id and the deploy fails loudly; without one, that
|
|
23
|
+
* same swallowed error would have quietly doubled the cluster's backup jobs.
|
|
24
|
+
*
|
|
25
|
+
* ⚠️ `Sys.Modify` ON `/` IS THE GATE AND IT IS NOT A SMALL GRANT. PVE has no Backup.* privilege:
|
|
26
|
+
* create, update and delete of a vzdump job are all checked against `Sys.Modify` at `/` — the
|
|
27
|
+
* same privilege that covers node network configuration. A credential that can declare backup
|
|
28
|
+
* jobs can also rewrite the cluster's networking. That is PVE's granularity rather than a choice
|
|
29
|
+
* made here, and it is exactly why widening a role for this should be a decision rather than a
|
|
30
|
+
* reaction to a 403.
|
|
31
|
+
*/
|
|
32
|
+
import { Resource } from 'alchemy';
|
|
33
|
+
import * as Provider from 'alchemy/Provider';
|
|
34
|
+
import * as Effect from 'effect/Effect';
|
|
35
|
+
import { type PveRequirements, type WithTarget, pveHandlers } from './resource.ts';
|
|
36
|
+
import { bool, guestList, propertyString, text } from './values.ts';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* ⚠️ THE PROPS ARE PVE'S OWN KEY NAMES, DASHES INCLUDED. `notes-template` rather than
|
|
40
|
+
* `notesTemplate` is deliberate: `createForm` is then a copy rather than a translation table,
|
|
41
|
+
* and a translation table is one more place for a key to be renamed and silently never sent. A
|
|
42
|
+
* field that is never sent reads back as whatever it already was, which is indistinguishable
|
|
43
|
+
* from "PVE ignored me".
|
|
44
|
+
*/
|
|
45
|
+
export interface BackupJobProps extends WithTarget {
|
|
46
|
+
/** ⛔ The primary key, REQUIRED. Never let PVE generate it — see the ⛔ above. */
|
|
47
|
+
id: string;
|
|
48
|
+
/** A subset of systemd calendar events, e.g. `01:00` or `mon..fri 02:30`. */
|
|
49
|
+
schedule: string;
|
|
50
|
+
/** Target storage id. That storage's own retention applies unless `prune-backups` overrides it. */
|
|
51
|
+
storage?: string;
|
|
52
|
+
mode?: 'snapshot' | 'suspend' | 'stop';
|
|
53
|
+
/** PVE's default is enabled. A disabled job is a backup that silently is not happening. */
|
|
54
|
+
enabled?: boolean;
|
|
55
|
+
/** Back up every guest. ⚠️ Pick exactly one of `all`, `vmid` and `pool` — see `shape` below. */
|
|
56
|
+
all?: boolean;
|
|
57
|
+
vmid?: readonly number[];
|
|
58
|
+
pool?: string;
|
|
59
|
+
/** Guest ids to skip. PVE reads this as "all guests except these", so it belongs with `all`. */
|
|
60
|
+
exclude?: readonly number[];
|
|
61
|
+
/** Retention, as a property string: `keep-daily=7,keep-weekly=4`. */
|
|
62
|
+
'prune-backups'?: string;
|
|
63
|
+
/** Supports {{cluster}}, {{guestname}}, {{node}} and {{vmid}}. Single line only. */
|
|
64
|
+
'notes-template'?: string;
|
|
65
|
+
'notification-mode'?: 'auto' | 'legacy-sendmail' | 'notification-system';
|
|
66
|
+
/** Run a missed job as soon as the scheduler is back. */
|
|
67
|
+
'repeat-missed'?: boolean;
|
|
68
|
+
/** VM-only fleecing, as a property string: `enabled=1,storage=<id>`. See `propertyString`. */
|
|
69
|
+
fleecing?: string;
|
|
70
|
+
comment?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface BackupJobAttributes {
|
|
74
|
+
id: string;
|
|
75
|
+
schedule: string;
|
|
76
|
+
storage: string;
|
|
77
|
+
mode: string;
|
|
78
|
+
enabled: boolean;
|
|
79
|
+
all: boolean;
|
|
80
|
+
/** Sorted and comma-joined, so re-ordering a declaration is not a diff. See `guestList`. */
|
|
81
|
+
vmid: string;
|
|
82
|
+
pool: string;
|
|
83
|
+
exclude: string;
|
|
84
|
+
/** Flattened and sorted — never the raw value PVE returned. See `propertyString`. */
|
|
85
|
+
'prune-backups': string;
|
|
86
|
+
'notes-template': string;
|
|
87
|
+
'notification-mode': string;
|
|
88
|
+
'repeat-missed': boolean;
|
|
89
|
+
fleecing: string;
|
|
90
|
+
comment: string;
|
|
91
|
+
/**
|
|
92
|
+
* PVE's own estimate, epoch seconds. REPORTED, NEVER COMPARED: `next-run` in `matches` would
|
|
93
|
+
* report an update every time the clock passed a scheduled run. It is here so a plan can show
|
|
94
|
+
* that a job is still due — a `next-run` of 0 is a job the scheduler will not run.
|
|
95
|
+
*/
|
|
96
|
+
'next-run': number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface ProxmoxBackupJob extends Resource<
|
|
100
|
+
'Proxmox.BackupJob',
|
|
101
|
+
BackupJobProps,
|
|
102
|
+
BackupJobAttributes,
|
|
103
|
+
never,
|
|
104
|
+
PveRequirements
|
|
105
|
+
> {}
|
|
106
|
+
|
|
107
|
+
export const ProxmoxBackupJob = Resource<ProxmoxBackupJob>('Proxmox.BackupJob');
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The form for both create and update.
|
|
111
|
+
*
|
|
112
|
+
* ⛔ A FIELD THE DECLARATION LEAVES OUT IS NOT SENT, AND THAT IS LOAD BEARING. PVE's PUT can only
|
|
113
|
+
* SET a key; clearing one needs `delete=<key>`, which this resource deliberately never sends.
|
|
114
|
+
* `updateForm` is handed the props and nothing else, so a `delete` list could only be "every
|
|
115
|
+
* field you did not declare" — and on a job adopted out of the UI that would silently strip
|
|
116
|
+
* settings this resource does not even model, `compress`, `mailto` and `bwlimit` among them, on
|
|
117
|
+
* the first deploy. Leaving undeclared fields alone is the smaller lie, and `matches` tells the
|
|
118
|
+
* same lie consistently: it does not compare what it cannot change.
|
|
119
|
+
*
|
|
120
|
+
* ⚠️ THE FIVE FIELDS PVE DOCUMENTS A DEFAULT FOR ARE ALWAYS SENT AND ALWAYS COMPARED — `enabled`
|
|
121
|
+
* (1), `all` (0), `mode` (snapshot), `notification-mode` (auto), `repeat-missed` (0). That is
|
|
122
|
+
* what makes a hand-flipped `enabled 0` show up as drift rather than as silence, and it is also
|
|
123
|
+
* what stops those five looping: a field compared against a default must be sent as that
|
|
124
|
+
* default, or every plan reports an update the PUT never performs. The cost is worth reading
|
|
125
|
+
* before the first deploy against an adopted job — an omitted `all` is a declaration that this
|
|
126
|
+
* job backs up NOTHING, and PVE accepts a job that selects nothing without a word. Read that
|
|
127
|
+
* first plan; do not deploy it unseen.
|
|
128
|
+
*/
|
|
129
|
+
const shape = (props: BackupJobProps) => ({
|
|
130
|
+
all: props.all === true ? '1' : '0',
|
|
131
|
+
enabled: props.enabled === false ? '0' : '1',
|
|
132
|
+
mode: props.mode ?? 'snapshot',
|
|
133
|
+
'notification-mode': props['notification-mode'] ?? 'auto',
|
|
134
|
+
'repeat-missed': props['repeat-missed'] === true ? '1' : '0',
|
|
135
|
+
schedule: props.schedule,
|
|
136
|
+
...(props.comment === undefined ? {} : { comment: props.comment }),
|
|
137
|
+
...(props.exclude === undefined ? {} : { exclude: guestList(props.exclude) }),
|
|
138
|
+
...(props.fleecing === undefined ? {} : { fleecing: props.fleecing }),
|
|
139
|
+
...(props['notes-template'] === undefined ? {} : { 'notes-template': props['notes-template'] }),
|
|
140
|
+
...(props.pool === undefined ? {} : { pool: props.pool }),
|
|
141
|
+
...(props['prune-backups'] === undefined ? {} : { 'prune-backups': props['prune-backups'] }),
|
|
142
|
+
...(props.storage === undefined ? {} : { storage: props.storage }),
|
|
143
|
+
...(props.vmid === undefined ? {} : { vmid: guestList(props.vmid) }),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const handlers = pveHandlers<BackupJobProps, BackupJobAttributes>({
|
|
147
|
+
/**
|
|
148
|
+
* ⛔ NO "IS IT REALLY THERE" GUARD, ON PURPOSE. Returning undefined for a job whose JSON is
|
|
149
|
+
* missing some key would be a guess about which keys PVE echoes back, and a wrong guess here
|
|
150
|
+
* does not read as absent-and-harmless: `reconcile` would POST, and POST creates ANOTHER job.
|
|
151
|
+
* Absence is decided by the API declining to answer, which `pveOperations.read` already
|
|
152
|
+
* handles — not by a key being missing from an answer that did arrive.
|
|
153
|
+
*/
|
|
154
|
+
attributes: (live, props) => ({
|
|
155
|
+
all: bool(live['all'], false),
|
|
156
|
+
comment: text(live['comment'], ''),
|
|
157
|
+
enabled: bool(live['enabled'], true),
|
|
158
|
+
exclude: guestList(live['exclude']),
|
|
159
|
+
fleecing: propertyString(live['fleecing'], 'enabled'),
|
|
160
|
+
id: props.id,
|
|
161
|
+
mode: text(live['mode'], 'snapshot'),
|
|
162
|
+
'next-run': typeof live['next-run'] === 'number' ? live['next-run'] : 0,
|
|
163
|
+
'notes-template': text(live['notes-template'], ''),
|
|
164
|
+
'notification-mode': text(live['notification-mode'], 'auto'),
|
|
165
|
+
pool: text(live['pool'], ''),
|
|
166
|
+
'prune-backups': propertyString(live['prune-backups']),
|
|
167
|
+
'repeat-missed': bool(live['repeat-missed'], false),
|
|
168
|
+
schedule: text(live['schedule'], ''),
|
|
169
|
+
storage: text(live['storage'], ''),
|
|
170
|
+
vmid: guestList(live['vmid']),
|
|
171
|
+
}),
|
|
172
|
+
collection: () => 'cluster/backup',
|
|
173
|
+
/** ⛔ `id` IS SENT AND IS NOT OPTIONAL. Everything above depends on PVE not inventing one. */
|
|
174
|
+
createForm: (props) => ({ ...shape(props), id: props.id }),
|
|
175
|
+
/** Each line reads "not declared, or equal"; the five defaulted fields have no undeclared case. */
|
|
176
|
+
matches: (attributes, props) =>
|
|
177
|
+
attributes.schedule === props.schedule &&
|
|
178
|
+
attributes.enabled === (props.enabled !== false) &&
|
|
179
|
+
attributes.all === (props.all === true) &&
|
|
180
|
+
attributes.mode === (props.mode ?? 'snapshot') &&
|
|
181
|
+
attributes['notification-mode'] === (props['notification-mode'] ?? 'auto') &&
|
|
182
|
+
attributes['repeat-missed'] === (props['repeat-missed'] === true) &&
|
|
183
|
+
(props.storage === undefined || attributes.storage === props.storage) &&
|
|
184
|
+
(props.pool === undefined || attributes.pool === props.pool) &&
|
|
185
|
+
(props.comment === undefined || attributes.comment === props.comment) &&
|
|
186
|
+
(props.vmid === undefined || attributes.vmid === guestList(props.vmid)) &&
|
|
187
|
+
(props.exclude === undefined || attributes.exclude === guestList(props.exclude)) &&
|
|
188
|
+
(props['notes-template'] === undefined ||
|
|
189
|
+
attributes['notes-template'] === props['notes-template']) &&
|
|
190
|
+
(props['prune-backups'] === undefined ||
|
|
191
|
+
attributes['prune-backups'] === propertyString(props['prune-backups'])) &&
|
|
192
|
+
(props.fleecing === undefined ||
|
|
193
|
+
attributes.fleecing === propertyString(props.fleecing, 'enabled')),
|
|
194
|
+
path: (props) => `cluster/backup/${props.id}`,
|
|
195
|
+
updateForm: shape,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* ⛔ Empty for the same reason as every other resource here: adoption must be explicit.
|
|
200
|
+
* `GET /cluster/backup` would hand back the job somebody made by hand, and adopting it
|
|
201
|
+
* implicitly would give Alchemy the right to DELETE a cluster's only backup schedule the
|
|
202
|
+
* first time a line left a stack file.
|
|
203
|
+
|
|
204
|
+
*
|
|
205
|
+
* ⚠️ DELETING THE JOB DOES NOT DELETE THE BACKUPS, AND THAT CUTS BOTH WAYS. The archives
|
|
206
|
+
* stay on the storage, so this looks harmless today; it is destructive in six months,
|
|
207
|
+
* when the schedule that should have replaced them has not run since. PVE will not
|
|
208
|
+
* refuse this the way it refuses to delete a pool still holding guests — nothing depends
|
|
209
|
+
* on a backup job — so the plan line is the only warning anyone gets.
|
|
210
|
+
|
|
211
|
+
*/
|
|
212
|
+
export const ProxmoxBackupJobProvider = () =>
|
|
213
|
+
Provider.effect(ProxmoxBackupJob, Effect.succeed(ProxmoxBackupJob.Provider.of(handlers)));
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a Ceph daemon lives on the API, and what a declaration of one looks like as a PVE form.
|
|
3
|
+
*
|
|
4
|
+
* ★ SPLIT OUT OF ceph-daemon.ts TO KEEP BOTH FILES UNDER THE 250-LINE CAP, and the seam is a real
|
|
5
|
+
* one rather than a convenient line number — the same seam metric-server-form.ts cuts on.
|
|
6
|
+
* This file answers "how does a declaration address ONE daemon" — which URL, which body, and
|
|
7
|
+
* which row of the collection's answer is it. ceph-daemon.ts answers "what does the cluster
|
|
8
|
+
* say about that daemon, and when has it changed". Nothing here calls the API and nothing
|
|
9
|
+
* here decides a diff; `findRow` picks a row out of an answer its caller already fetched.
|
|
10
|
+
*
|
|
11
|
+
* ⛔ THE TWO PATHS ARE DIFFERENT AND THAT IS THE WHOLE REASON THIS FILE HAS TWO OF THEM. MEASURED
|
|
12
|
+
* on n2, 2026-09-13: there is no GET on the id path — `pvesh get /nodes/n2/ceph/mon/n2` answers
|
|
13
|
+
* "No 'get' handler defined" — while POST and DELETE are registered ONLY there. So the read uses
|
|
14
|
+
* `collectionPath` and the two writes use `daemonPath`, and swapping either one is a silent
|
|
15
|
+
* 501: a create that never lands, or a destroy that reports failure while the daemon runs on.
|
|
16
|
+
*
|
|
17
|
+
* ⚠️ THE `import type` BACK TO ceph-daemon.ts IS A CYCLE ON PAPER ONLY — type-only, so it is
|
|
18
|
+
* erased before anything runs. `CephDaemonProps` stays the resource's public shape, in the file
|
|
19
|
+
* that declares the resource.
|
|
20
|
+
*
|
|
21
|
+
* ⛔ POST AND DELETE RETURN A UPID, NOT AN OBJECT — both are `protected` task endpoints whose
|
|
22
|
+
* schema `returns` is a bare string. REASONED, NOT MEASURED: this cluster is read-only to me, so
|
|
23
|
+
* no create was ever run. The consequence to expect is that `reconcile` reads back immediately
|
|
24
|
+
* after the POST, and if the worker has not yet written ceph.conf the read-back still says
|
|
25
|
+
* absent and reconcile dies with "the write returned no error but the object is still absent".
|
|
26
|
+
* That refusal is honest — nothing is recorded for an object that is not there — and a second
|
|
27
|
+
* deploy converges once the task has finished, but it is a confusing FIRST-RUN failure. Watch
|
|
28
|
+
* the task in the PVE UI before concluding the create failed.
|
|
29
|
+
*
|
|
30
|
+
*/
|
|
31
|
+
import type { CephDaemonProps } from './ceph-daemon.ts';
|
|
32
|
+
import { flag } from './values.ts';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* PVE defaults the id to the nodename for all three kinds, and that default is resolved HERE
|
|
36
|
+
* rather than left to PVE, because the id is a PATH SEGMENT: create, delete and the row lookup in
|
|
37
|
+
* ceph-daemon.ts all need the same string, and a default resolved on the far side would leave
|
|
38
|
+
* three call sites guessing. Live on this cluster every daemon's name equals its node.
|
|
39
|
+
*/
|
|
40
|
+
export const daemonId = (props: CephDaemonProps) => props.name ?? props.node;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The read path. ⚠️ `{node}` HERE IS ONLY THE NODE BEING ASKED, not a filter. MEASURED:
|
|
44
|
+
* `GET /nodes/n2/ceph/mon` returns n2, n3 AND n4, and `GET /nodes/n3/ceph/mon` returns the same
|
|
45
|
+
* three IN A DIFFERENT ORDER — [n4,n2,n3] from n2 against [n3,n2,n4] from n3. A caller that took
|
|
46
|
+
* row zero, or assumed the list was this node's daemons, would be wrong on both counts.
|
|
47
|
+
*/
|
|
48
|
+
export const collectionPath = (props: CephDaemonProps) => `nodes/${props.node}/ceph/${props.kind}`;
|
|
49
|
+
|
|
50
|
+
/** The write path: POST creates here, DELETE destroys here. Never read from — see the ⛔ above. */
|
|
51
|
+
export const daemonPath = (props: CephDaemonProps) => `${collectionPath(props)}/${daemonId(props)}`;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The create body, which is EMPTY for a mgr and nearly empty for the other two.
|
|
55
|
+
*
|
|
56
|
+
* ⚠️ THE ID IS NOT SENT, because it is already the last segment of the path being POSTed to — the
|
|
57
|
+
* same reasoning as metric-server.ts, where a second copy of the key can only disagree with the
|
|
58
|
+
* first. PVE's schema does accept `monid`/`id`/`name` as body parameters; the path wins, so
|
|
59
|
+
* sending one would be a second source of truth for the object's identity and nothing else.
|
|
60
|
+
*
|
|
61
|
+
* ⚠️ AND THE BRANCHES CANNOT BE ONE SHARED BODY. `hotstandby` is an mds parameter and
|
|
62
|
+
* `mon-address` a mon one; each POST schema lists only its own, so the wrong field is a 400
|
|
63
|
+
* rather than an ignored hint. Both are create-only — the ⛔s on those props say why neither is
|
|
64
|
+
* ever compared afterwards.
|
|
65
|
+
*/
|
|
66
|
+
export const createForm = (props: CephDaemonProps): Record<string, string> => {
|
|
67
|
+
if (props.kind === 'mon' && props['mon-address'] !== undefined) {
|
|
68
|
+
return { 'mon-address': props['mon-address'] };
|
|
69
|
+
}
|
|
70
|
+
const hotstandby = props.kind === 'mds' ? flag(props.hotstandby) : undefined;
|
|
71
|
+
return hotstandby === undefined ? {} : { hotstandby };
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* ⚠️ THE COLLECTION ANSWERS AN ARRAY while the factory types `attributes`' first parameter as the
|
|
76
|
+
* `Record<string, unknown>` every other PVE read is shaped like — the same mismatch acl.ts
|
|
77
|
+
* carries, handled the same way: narrow the rows, trust none of them, and match on `name`.
|
|
78
|
+
* ⚠️ MATCHED ON `name` ALONE, NEVER ON POSITION — the list comes back in a different order
|
|
79
|
+
* depending on which node was asked. See the ⚠️ on `collectionPath` above.
|
|
80
|
+
*/
|
|
81
|
+
export const findRow = (live: unknown, props: CephDaemonProps) =>
|
|
82
|
+
(Array.isArray(live) ? live : [])
|
|
83
|
+
.filter((row): row is Record<string, unknown> => typeof row === 'object' && row !== null)
|
|
84
|
+
.find((row) => row['name'] === daemonId(props));
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Proxmox.CephDaemon` — a Ceph monitor, manager or metadata server on one node.
|
|
3
|
+
*
|
|
4
|
+
* ★ ONE RESOURCE FOR THREE KINDS, AND THE SCHEMAS WERE CHECKED BEFORE THAT WAS DECIDED. Parsed
|
|
5
|
+
* from `/usr/share/pve-docs/api-viewer/apidoc.js` on n2, 2026-09-13: mon, mgr and mds each
|
|
6
|
+
* expose GET on the collection and POST + DELETE on the id below it, each returns
|
|
7
|
+
* name/host/state/addr/ceph_version*, and the only divergence is the create parameter
|
|
8
|
+
* (`mon-address` / none / `hotstandby`) and the read-only extras (mon: quorum, rank; mds: rank,
|
|
9
|
+
* fs_name, standby_replay). Three near-identical files would have carried the traps below three
|
|
10
|
+
* times over and let them drift — the argument resource.ts already makes for the factory itself.
|
|
11
|
+
*
|
|
12
|
+
* ⛔ THE ONLY GET IS THE COLLECTION'S, AND IT ANSWERS FOR THE WHOLE CLUSTER. MEASURED:
|
|
13
|
+
* `GET /nodes/n2/ceph/mon` returns n2, n3 AND n4, and `GET /nodes/n3/ceph/mon` returns the same
|
|
14
|
+
* three IN A DIFFERENT ORDER — [n4,n2,n3] from n2, [n3,n2,n4] from n3 — so a row is found by
|
|
15
|
+
* `name` and NEVER by position, and the `{node}` in the path is only the node being ASKED.
|
|
16
|
+
* MEASURED too: there is no GET on the id path at all; `pvesh get /nodes/n2/ceph/mon/n2` answers
|
|
17
|
+
* "No 'get' handler defined for '/nodes/n2/ceph/mon/n2'", and mgr and mds answer the same.
|
|
18
|
+
* That is why `path()` below is the COLLECTION, and that in turn is why `delete` is the one
|
|
19
|
+
* handler this file writes by hand: `pveOperations.destroy` sends its DELETE to `spec.path`,
|
|
20
|
+
* which here would be the collection — a 501 reported as a failed destroy, with the daemon still
|
|
21
|
+
* running. Same shape and same rule as the override in acl.ts: do not "restore symmetry" by
|
|
22
|
+
* pointing it back at `ops.destroy`.
|
|
23
|
+
*
|
|
24
|
+
* ⛔ WHAT A DESTROY ACTUALLY DOES, BECAUSE THE PLAN LINE SAYS ONLY "delete". REASONED FROM PVE'S
|
|
25
|
+
* AND CEPH'S DOCUMENTED BEHAVIOUR, NOT MEASURED — nothing on this cluster was destroyed to find
|
|
26
|
+
* out, and nothing should be to check. `mon`: stops ceph-mon@id, removes it from the monmap and
|
|
27
|
+
* strips its section from ceph.conf. THREE MONS TOLERATE ONE LOSS; TWO TOLERATE NONE. Removing
|
|
28
|
+
* two in one deploy — or removing the mon on the node whose API is serving the call — loses
|
|
29
|
+
* quorum, and a cluster without mon quorum BLOCKS every RBD and CephFS I/O rather than erroring
|
|
30
|
+
* it, so on this cluster every guest on `cephtb4` hangs.
|
|
31
|
+
* `mds`: destroying a STANDBY is a non-event; destroying the ACTIVE one fails CephFS over,
|
|
32
|
+
* and with no standby left `cephfs-tb4` goes unavailable. `mgr`: no guest I/O depends on it, but
|
|
33
|
+
* the last one takes the PG autoscaler, the dashboard and PVE's own Ceph status with it.
|
|
34
|
+
* Live on 2026-09-13: mon, mgr AND mds on each of n2, n3, n4.
|
|
35
|
+
*
|
|
36
|
+
* ★ AND THE ONLY WAY TO REACH THAT DESTROY IS TO DELETE THE DECLARATION. `matches` is deliberately
|
|
37
|
+
* total (see its ⛔), so this resource CANNOT plan a `replace` — which is the one action that
|
|
38
|
+
* would tear down a live mon off the back of an edited field rather than a removed line.
|
|
39
|
+
*
|
|
40
|
+
* ⚠️ THE PRIVILEGES ARE ASYMMETRIC AND THE READ SIDE IS THE CHEAP ONE. The GET checks `any` of
|
|
41
|
+
* `Sys.Audit`/`Datastore.Audit` on `/`, and PVEAuditor holds both (MEASURED:
|
|
42
|
+
* `GET /access/roles/PVEAuditor`), so the mount's `read` role can see this family — unlike
|
|
43
|
+
* `Proxmox.Storage`, which 403s under that lease. POST and DELETE both check `Sys.Modify` on
|
|
44
|
+
* `/`, and MEASURED on this cluster `LXCProvisioner` already holds it, so the provision lane
|
|
45
|
+
* needs no widening for once. ⚠️ `Sys.Modify` on the ROOT path is broad — it also buys
|
|
46
|
+
* datacenter options and every other cluster-wide config write — so it is worth knowing that
|
|
47
|
+
* this credential already has it rather than discovering it the next time something is scoped.
|
|
48
|
+
*/
|
|
49
|
+
import { Resource } from 'alchemy';
|
|
50
|
+
import * as Provider from 'alchemy/Provider';
|
|
51
|
+
import * as Effect from 'effect/Effect';
|
|
52
|
+
import { collectionPath, createForm, daemonId, daemonPath, findRow } from './ceph-daemon-form.ts';
|
|
53
|
+
import { pve } from './client.ts';
|
|
54
|
+
import { type PveRequirements, type WithTarget, pveHandlers } from './resource.ts';
|
|
55
|
+
import { bool, num, text } from './values.ts';
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Which Ceph daemon this is. The discriminant picks the collection, the create parameters and
|
|
59
|
+
* which of the read-only extras below will even be populated.
|
|
60
|
+
*
|
|
61
|
+
* ⚠️ `osd` IS NOT HERE AND MUST NOT BE ADDED TO THIS UNION. `/nodes/{node}/ceph/osd` takes a disk
|
|
62
|
+
* device, wipes it, and its DELETE has a `cleanup` flag — a create that destroys data on the way
|
|
63
|
+
* in is a different resource with a different set of refusals, not a fourth case in this one.
|
|
64
|
+
*/
|
|
65
|
+
export type CephDaemonKind = 'mon' | 'mgr' | 'mds';
|
|
66
|
+
|
|
67
|
+
export interface CephDaemonProps extends WithTarget {
|
|
68
|
+
kind: CephDaemonKind;
|
|
69
|
+
/**
|
|
70
|
+
* The node the daemon RUNS on. Identity — a daemon cannot be moved, only destroyed and rebuilt.
|
|
71
|
+
* ⚠️ It is also the node whose API is asked, and the read is only as available as that node: a
|
|
72
|
+
* node that is down — or has no Ceph installed, or answers 403 — reads as nothing at all,
|
|
73
|
+
* because `pveOperations.read` folds EVERY failure into "absent" (a 404 is a legitimate answer
|
|
74
|
+
* there and it cannot tell the two apart). Its daemons then plan as `update`, and reconcile
|
|
75
|
+
* POSTs a create for a mon that already exists. PVE refuses that with "monitor already
|
|
76
|
+
* exists", and the read-back refuses again — so it fails loudly rather than damaging anything,
|
|
77
|
+
* but the message will point at the daemon rather than at the node that would not answer.
|
|
78
|
+
*/
|
|
79
|
+
node: string;
|
|
80
|
+
/**
|
|
81
|
+
* The daemon id. PVE defaults it to the nodename, and `daemonId` in ceph-daemon-form.ts resolves
|
|
82
|
+
* that default HERE rather than leaving it to PVE, because the id is a PATH SEGMENT: create,
|
|
83
|
+
* delete and the row lookup all need the same string, and a default resolved on the far side
|
|
84
|
+
* would leave three call sites guessing. Live on this cluster every daemon's name is its node.
|
|
85
|
+
*/
|
|
86
|
+
name?: string;
|
|
87
|
+
/**
|
|
88
|
+
* mon only. Overrides the autodetected monitor IP; must sit in Ceph's public network — on this
|
|
89
|
+
* cluster that is 10.20.11.0/24, carried by `vmbr1.11`.
|
|
90
|
+
* ⛔ CREATE-ONLY AND NEVER COMPARED. PVE takes a bare ip-list on write and hands back `addr` as
|
|
91
|
+
* `10.20.11.12:6789/0` — an address, a port and a nonce. The two are not the same string and
|
|
92
|
+
* never will be, so diffing them would report a mismatch on every plan; with no PUT on this
|
|
93
|
+
* family that mismatch becomes a REPLACE, and a replace of a mon is the quorum loss in the
|
|
94
|
+
* header. Left out of `matches` on purpose.
|
|
95
|
+
*/
|
|
96
|
+
'mon-address'?: string;
|
|
97
|
+
/**
|
|
98
|
+
* mds only. Makes this standby replay the active MDS's journal for a faster failover.
|
|
99
|
+
* ⛔ CREATE-ONLY AND NEVER COMPARED, AND THE FIELD THAT LOOKS LIKE ITS READBACK IS NOT ONE.
|
|
100
|
+
* `standby_replay` in the list is the daemon's CURRENT state, not its configuration: MEASURED
|
|
101
|
+
* 2026-09-13, all three mds report `standby_replay: false`, n3 included — and n3 is the ACTIVE
|
|
102
|
+
* mds, which is not a standby at all and so can never report true however it was created.
|
|
103
|
+
* Comparing `hotstandby` to it would plan a replace against whichever mds Ceph happens to have
|
|
104
|
+
* elected, i.e. against a value no declaration controls.
|
|
105
|
+
* ⚠️ SET ON A mon OR mgr IT IS DROPPED RATHER THAN SENT, and `mon-address` likewise on an mds:
|
|
106
|
+
* each POST schema lists only its own parameter, so the wrong one is a 400 rather than an
|
|
107
|
+
* ignored hint. A field silently dropped is the lesser of the two, but it IS dropped.
|
|
108
|
+
*/
|
|
109
|
+
hotstandby?: boolean;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Everything the cluster says about this daemon — reported, and (bar its identity) none of it
|
|
114
|
+
* compared. Read the ⛔ on `matches` for why that is the only safe split on this family.
|
|
115
|
+
*/
|
|
116
|
+
export interface CephDaemonAttributes {
|
|
117
|
+
kind: CephDaemonKind;
|
|
118
|
+
node: string;
|
|
119
|
+
name: string;
|
|
120
|
+
/**
|
|
121
|
+
* The host Ceph says it runs on.
|
|
122
|
+
* ⚠️ REPORTED, NOT COMPARED, AND THAT IS A DELIBERATE BLIND SPOT. A daemon found under this name
|
|
123
|
+
* on a DIFFERENT node than `node` declares plans as `noop`, which does hide a misdeclaration.
|
|
124
|
+
* The alternative is worse: `host` is `optional` in the schema, so an absent one would read as
|
|
125
|
+
* a mismatch, and with no PUT a mismatch is a replace — i.e. a missing field in one API answer
|
|
126
|
+
* would destroy a live mon. When in doubt, out of `matches`; fix a wrong node by hand.
|
|
127
|
+
*/
|
|
128
|
+
host: string;
|
|
129
|
+
/**
|
|
130
|
+
* mon `running`/`stopped`/`unknown`, mgr `active`/`standby`, mds `up:active`/`up:standby`/…
|
|
131
|
+
* ⛔ CEPH ELECTS THIS AND REWRITES IT WITHOUT ANYONE DECLARING ANYTHING. MEASURED: mgr n2 is
|
|
132
|
+
* `active` while n3 and n4 are `standby`; mds n3 is `up:active` while n2 and n4 are
|
|
133
|
+
* `up:standby`. Restart a daemon and the roles move. It is reported so a plan can show what is
|
|
134
|
+
* live and kept out of `matches` so that an election is never a diff.
|
|
135
|
+
* ⚠️ IT ALSO MEANS THIS RESOURCE DOES NOT MANAGE WHETHER THE DAEMON IS RUNNING. A configured but
|
|
136
|
+
* stopped daemon is still a row in the list, so it reads as present and plans `noop`.
|
|
137
|
+
*/
|
|
138
|
+
state: string;
|
|
139
|
+
/** Ceph-formatted, e.g. `10.20.11.12:6789/0`. Reported; see `mon-address` for why never compared. */
|
|
140
|
+
addr: string;
|
|
141
|
+
/** `ceph_version_short`, e.g. `20.2.2`. Reported: a straggler after an upgrade is worth seeing. */
|
|
142
|
+
version: string;
|
|
143
|
+
/**
|
|
144
|
+
* mon rank in the monmap, mds rank in the filesystem, `-1` when the daemon has neither.
|
|
145
|
+
* ⛔ CEPH ASSIGNS IT AND NOTHING DECLARES IT. MEASURED: the mons are ranked n4=0, n3=1, n2=2 —
|
|
146
|
+
* monmap order, not declaration order — and the mds ranks are 0 for the active n3 and -1 for
|
|
147
|
+
* the two standbys. Rank moves when a daemon is added, removed or restarted.
|
|
148
|
+
*/
|
|
149
|
+
rank: number;
|
|
150
|
+
/** mon only: in the current quorum. Reported. MEASURED: all three mons are in quorum. */
|
|
151
|
+
quorum: boolean;
|
|
152
|
+
/** mds only, and NOT the readback of `hotstandby` — see the ⛔ on that prop. */
|
|
153
|
+
standbyReplay: boolean;
|
|
154
|
+
/**
|
|
155
|
+
* mds only: the CephFS this daemon currently serves — `cephfs-tb4` here.
|
|
156
|
+
* ⛔ PRESENT ONLY ON THE ACTIVE MDS. MEASURED: n3 carries `fs_name`, n2 and n4 have no such key
|
|
157
|
+
* at all. So it is empty for two of three identical declarations, and comparing it would
|
|
158
|
+
* report work on exactly the daemons that are healthy standbys.
|
|
159
|
+
*/
|
|
160
|
+
fsName: string;
|
|
161
|
+
/** A `ceph-<kind>@<id>` systemd unit is enabled on the host. Reported; absent reads as false. */
|
|
162
|
+
service: boolean;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface ProxmoxCephDaemon extends Resource<
|
|
166
|
+
'Proxmox.CephDaemon',
|
|
167
|
+
CephDaemonProps,
|
|
168
|
+
CephDaemonAttributes,
|
|
169
|
+
never,
|
|
170
|
+
PveRequirements
|
|
171
|
+
> {}
|
|
172
|
+
|
|
173
|
+
/** ★ `retain` by default — a mon carrying quorum cannot be rebuilt. See the ★ in resource.ts. */
|
|
174
|
+
export const ProxmoxCephDaemon = Resource<ProxmoxCephDaemon>('Proxmox.CephDaemon', {
|
|
175
|
+
defaultRemovalPolicy: 'retain',
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const handlers = pveHandlers<CephDaemonProps, CephDaemonAttributes>({
|
|
179
|
+
/** ⚠️ `undefined` when no row carries this name: that is how the factory learns to create. */
|
|
180
|
+
attributes: (live, props) => {
|
|
181
|
+
const row = findRow(live, props);
|
|
182
|
+
if (row === undefined) return undefined;
|
|
183
|
+
return {
|
|
184
|
+
addr: text(row['addr']),
|
|
185
|
+
fsName: text(row['fs_name']),
|
|
186
|
+
host: text(row['host']),
|
|
187
|
+
kind: props.kind,
|
|
188
|
+
name: daemonId(props),
|
|
189
|
+
node: props.node,
|
|
190
|
+
quorum: bool(row['quorum']),
|
|
191
|
+
rank: num(row['rank'], -1),
|
|
192
|
+
service: bool(row['service']),
|
|
193
|
+
standbyReplay: bool(row['standby_replay']),
|
|
194
|
+
state: text(row['state']),
|
|
195
|
+
version: text(row['ceph_version_short']),
|
|
196
|
+
};
|
|
197
|
+
},
|
|
198
|
+
/** ⛔ The ID path, not the collection — POST is registered on `{id}`, exactly like a metric server. */
|
|
199
|
+
collection: daemonPath,
|
|
200
|
+
createForm,
|
|
201
|
+
/**
|
|
202
|
+
* ⛔ TOTAL, AND THIS IS THE REASON THE FILE EXISTS RATHER THAN A SHORTCUT PAST WRITING IT. Every
|
|
203
|
+
* field a declaration can carry is CREATE-ONLY and unreadable — `mon-address` comes back as a
|
|
204
|
+
* different kind of string, `hotstandby` comes back as a state Ceph elects — and every field
|
|
205
|
+
* the read returns is assigned by Ceph: state, rank, quorum, fs_name, standby_replay, addr,
|
|
206
|
+
* version. Nothing is left that is both declared and readable. So the honest comparison is
|
|
207
|
+
* "does a daemon of this kind and name exist", which `attributes` has already answered by
|
|
208
|
+
* returning a value at all, and anything further would be a diff no write could ever satisfy.
|
|
209
|
+
* ⛔ AND ON THIS FAMILY THAT DIFF IS NOT MERELY NOISY. There is no PUT, so resource.ts answers
|
|
210
|
+
* `replace` rather than `update` whenever `matches` is false — and replace on a mon is
|
|
211
|
+
* delete-then-create against a live quorum. A forever-diff here is a forever-OUTAGE-RISK.
|
|
212
|
+
* ⚠️ THE PRICE, STATED PLAINLY: editing `hotstandby` or `mon-address` on a declared daemon
|
|
213
|
+
* plans as `noop` and never applies. Same bargain storage.ts strikes for a changed `type`.
|
|
214
|
+
* Change one by removing the declaration and re-adding it, ONE DAEMON AT A TIME, reading the
|
|
215
|
+
* ⛔ on destroy in the header first.
|
|
216
|
+
*/
|
|
217
|
+
matches: () => true,
|
|
218
|
+
/** ⛔ The COLLECTION, because there is no GET on the id path. See the second ⛔ in the header. */
|
|
219
|
+
path: collectionPath,
|
|
220
|
+
/** ⚠️ No `updateForm`: none of the three has a PUT, so nothing about a daemon is editable. */
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
export const ProxmoxCephDaemonProvider = () =>
|
|
224
|
+
Provider.effect(
|
|
225
|
+
ProxmoxCephDaemon,
|
|
226
|
+
Effect.succeed(
|
|
227
|
+
ProxmoxCephDaemon.Provider.of({
|
|
228
|
+
...handlers,
|
|
229
|
+
/**
|
|
230
|
+
* ⛔ THE ONE HANDLER NOT TAKEN FROM THE FACTORY, AND THE ONLY REASON IS THE PATH SPLIT.
|
|
231
|
+
* `ops.destroy` DELETEs `spec.path`, which this resource must point at the collection so
|
|
232
|
+
* that `read` works at all; DELETE on the collection is not implemented and would answer
|
|
233
|
+
* 501 while the daemon kept running — a failed destroy that reads like a permissions
|
|
234
|
+
* problem. Everything else here is the factory's.
|
|
235
|
+
* ⚠️ AND IT IS THE DANGEROUS ONE. Re-read the destroy ⛔ in the header before letting a
|
|
236
|
+
* plan that removes a mon run: two gone at once is a cluster with no quorum and every
|
|
237
|
+
* guest on `cephtb4` blocked on I/O.
|
|
238
|
+
*/
|
|
239
|
+
delete: ({ olds }: { olds: CephDaemonProps }) =>
|
|
240
|
+
pve(olds.target, 'provision', 'DELETE', daemonPath(olds)),
|
|
241
|
+
}),
|
|
242
|
+
),
|
|
243
|
+
);
|