@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,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A PBS datastore on the wire: the transport that reaches PBS, the form it wants, and the coercion
|
|
3
|
+
* back to something comparable.
|
|
4
|
+
*
|
|
5
|
+
* ★ SPLIT OUT OF pbs-datastore.ts FOR THE 250-LINE CAP, the seam notification-target-form.ts uses:
|
|
6
|
+
* this file is everything that touches PBS's encoding, and the resource file is the declaration
|
|
7
|
+
* and the five handlers. The `import type` back is erased, so it is a cycle on paper only.
|
|
8
|
+
*
|
|
9
|
+
* ⛔ THIS FILE IS OVER THE 250-LINE CAP AND THE EXTRACTION IS NAMED RATHER THAN FUDGED. `PbsTarget`,
|
|
10
|
+
* `PbsRole`, `PbsError`, `pbsAuthorization`, `PveForm`, `encode`, `pbs` and `settle` — about 150
|
|
11
|
+
* lines — are a PBS CLIENT, not a datastore form, and belong in `pbs-client.ts`. Lifting them
|
|
12
|
+
* leaves this file near 200 and costs nothing but a move. It was not done here only because the
|
|
13
|
+
* family was commissioned as a two-file split; ceph-pool is the in-package precedent for three
|
|
14
|
+
* (ceph-pool.ts + ceph-pool-form.ts + ceph-pool-settle.ts, 532 lines for one family). Everything
|
|
15
|
+
* in that list is also what Pbs.SyncJob, Pbs.PruneJob and Pbs.Remote will each need unchanged,
|
|
16
|
+
* so the second PBS family forces the move anyway. ⛔ DO NOT MEET THE CAP BY CUTTING COMMENTS.
|
|
17
|
+
*/
|
|
18
|
+
import * as Effect from 'effect/Effect';
|
|
19
|
+
import { type PveForm, pve } from './client.ts';
|
|
20
|
+
import type { PbsTarget as ApiPbsTarget } from './credentials.ts';
|
|
21
|
+
import type { PbsDatastoreProps } from './pbs-datastore.ts';
|
|
22
|
+
import { bool, flag, int, propertyString, text } from './values.ts';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* ★ PBS REUSES THE PVE CLIENT RATHER THAN OWNING ONE. An earlier draft of this file carried its
|
|
26
|
+
* own `PbsTarget`, `PbsError`, `pbsAuthorization`, form encoder and `pve()` — a near-copy of
|
|
27
|
+
* client.ts differing in one header. They are gone: `PveTarget` now carries a `scheme`, and
|
|
28
|
+
* `authorization()` spells the header `PBSAPIToken=<id>:<secret>` when it is `'pbs'`. The
|
|
29
|
+
* insight that survived is the FORM TYPE — PBS decodes a multi-valued field from repeated keys
|
|
30
|
+
* where PVE wants a comma string — and that now lives on `PveForm` in client.ts, so both
|
|
31
|
+
* products share one encoder and neither can reach for the other's convention by accident.
|
|
32
|
+
*
|
|
33
|
+
* 🔴 THIS ALIAS SAID `= PveTarget` AND IT WAS WRONG — Pbs.Datastore REQUIRED A PVE TARGET. When
|
|
34
|
+
* `credentials.ts` grew a real `PbsTarget` with `scheme: 'pbs'`, this line was left pointing at
|
|
35
|
+
* the PVE type, which pins `scheme: 'pve'`. So the one family that most needs the discriminant
|
|
36
|
+
* was the one family that refused the correct value: declaring a datastore against the PBS host
|
|
37
|
+
* failed to compile with `Type '"pbs"' is not assignable to type 'Input<"pve">'`, and passing a
|
|
38
|
+
* PVE target compiled fine and would have 401'd every call — which `read` folds into "absent",
|
|
39
|
+
* so the plan would say CREATE for a datastore holding the estate's backups.
|
|
40
|
+
* ⛔ IT SURVIVED BECAUSE NOTHING EVER DECLARED ONE. The provider was registered in
|
|
41
|
+
* alchemy.run.ts from the day it was written and `pveHandlers`' `list` answers empty, so
|
|
42
|
+
* registration exercises no call site at all. Three sibling families import `PbsTarget` from
|
|
43
|
+
* credentials.ts directly and were always correct; only the one with its own alias drifted.
|
|
44
|
+
* ★ THE LESSON, WHICH IS NOT ABOUT THIS TYPE: a provider nobody has declared an instance of is
|
|
45
|
+
* not tested by anything, including tsc.
|
|
46
|
+
*/
|
|
47
|
+
export type PbsTarget = ApiPbsTarget;
|
|
48
|
+
|
|
49
|
+
export interface PbsDatastoreAttributes {
|
|
50
|
+
name: string;
|
|
51
|
+
/**
|
|
52
|
+
* ⚠️ READ FROM PBS, NEVER ECHOED FROM PROPS. Echoing the declared path would make a divergent or
|
|
53
|
+
* unreadable datastore look like agreement, and this is the field whose divergence means the
|
|
54
|
+
* backups are going somewhere nobody declared. `''` when PBS did not say.
|
|
55
|
+
*/
|
|
56
|
+
path: string;
|
|
57
|
+
comment: string;
|
|
58
|
+
'gc-schedule': string;
|
|
59
|
+
'prune-schedule': string;
|
|
60
|
+
'keep-last': number;
|
|
61
|
+
'keep-hourly': number;
|
|
62
|
+
'keep-daily': number;
|
|
63
|
+
'keep-weekly': number;
|
|
64
|
+
'keep-monthly': number;
|
|
65
|
+
'keep-yearly': number;
|
|
66
|
+
/** Normalised property strings — sorted and canonicalised, so key order is never a diff. */
|
|
67
|
+
notify: string;
|
|
68
|
+
'notify-user': string;
|
|
69
|
+
'verify-new': boolean;
|
|
70
|
+
tuning: string;
|
|
71
|
+
'maintenance-mode': string;
|
|
72
|
+
/** ⚠️ Create-only. Canonicalised property string; `''` on a plain local datastore. */
|
|
73
|
+
backend: string;
|
|
74
|
+
'counter-reset-schedule': string;
|
|
75
|
+
'notification-thresholds': string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* ⚠️ `digest` IS DELIBERATELY NOT AN ATTRIBUTE, for storage.ts's reason exactly: PBS returns one on
|
|
80
|
+
* every read and it covers `datastore.cfg` as a FILE, not this section of it, so keeping it would
|
|
81
|
+
* rewrite this resource's state whenever an unrelated datastore was edited — churn that reads
|
|
82
|
+
* like drift. It is not sent on writes either, so this family never joins PBS's optimistic-locking
|
|
83
|
+
* dance and last write wins, as everywhere else in this package.
|
|
84
|
+
*/
|
|
85
|
+
export const attributes = (
|
|
86
|
+
live: Record<string, unknown>,
|
|
87
|
+
props: PbsDatastoreProps,
|
|
88
|
+
): PbsDatastoreAttributes => ({
|
|
89
|
+
backend: propertyString(live['backend']),
|
|
90
|
+
comment: text(live['comment']),
|
|
91
|
+
'counter-reset-schedule': text(live['counter-reset-schedule']).trim(),
|
|
92
|
+
'gc-schedule': text(live['gc-schedule']).trim(),
|
|
93
|
+
'keep-daily': int(live['keep-daily'], 0),
|
|
94
|
+
'keep-hourly': int(live['keep-hourly'], 0),
|
|
95
|
+
'keep-last': int(live['keep-last'], 0),
|
|
96
|
+
'keep-monthly': int(live['keep-monthly'], 0),
|
|
97
|
+
'keep-weekly': int(live['keep-weekly'], 0),
|
|
98
|
+
'keep-yearly': int(live['keep-yearly'], 0),
|
|
99
|
+
'maintenance-mode': propertyString(live['maintenance-mode'], 'type'),
|
|
100
|
+
name: props.name,
|
|
101
|
+
'notification-thresholds': propertyString(live['notification-thresholds']),
|
|
102
|
+
notify: propertyString(live['notify']),
|
|
103
|
+
'notify-user': text(live['notify-user']),
|
|
104
|
+
path: text(live['path']),
|
|
105
|
+
'prune-schedule': text(live['prune-schedule']).trim(),
|
|
106
|
+
tuning: propertyString(live['tuning']),
|
|
107
|
+
'verify-new': bool(live['verify-new']),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const same = <T>(declared: T | undefined, live: T) => declared === undefined || declared === live;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* ⛔ DECLARING WHAT IS LIVE MUST PLAN noop, AND EVERY OMISSION IS DELIBERATE. Out: `name` (the
|
|
114
|
+
* address the read was made at — true by construction, never a diff); `path` (create-only, so
|
|
115
|
+
* comparing it could only plan an update no PUT can apply — pbs-datastore.ts guards it by dying
|
|
116
|
+
* instead, see the ⛔ on the prop); `digest` (not an attribute, above); and every field the
|
|
117
|
+
* declaration does not mention, because undeclared is UNMANAGED here.
|
|
118
|
+
* ⚠️ A SCHEDULE THAT NEVER SETTLES MEANS PBS NORMALISED IT. Calendar events are compared as trimmed
|
|
119
|
+
* strings; if a version rewrites `sat 18:15` as `Sat 18:15` the plan asks for the same update
|
|
120
|
+
* forever. The fix is to declare the spelling PBS stores, never to stop comparing it.
|
|
121
|
+
*/
|
|
122
|
+
export const matches = (live: PbsDatastoreAttributes, props: PbsDatastoreProps) =>
|
|
123
|
+
same(props.comment, live.comment) &&
|
|
124
|
+
same(props['gc-schedule']?.trim(), live['gc-schedule']) &&
|
|
125
|
+
same(props['prune-schedule']?.trim(), live['prune-schedule']) &&
|
|
126
|
+
same(props['keep-last'], live['keep-last']) &&
|
|
127
|
+
same(props['keep-hourly'], live['keep-hourly']) &&
|
|
128
|
+
same(props['keep-daily'], live['keep-daily']) &&
|
|
129
|
+
same(props['keep-weekly'], live['keep-weekly']) &&
|
|
130
|
+
same(props['keep-monthly'], live['keep-monthly']) &&
|
|
131
|
+
same(props['keep-yearly'], live['keep-yearly']) &&
|
|
132
|
+
same(props['notify-user'], live['notify-user']) &&
|
|
133
|
+
same(props['counter-reset-schedule']?.trim(), live['counter-reset-schedule']) &&
|
|
134
|
+
(props['notification-thresholds'] === undefined ||
|
|
135
|
+
propertyString(props['notification-thresholds']) === live['notification-thresholds']) &&
|
|
136
|
+
same(props['verify-new'], live['verify-new']) &&
|
|
137
|
+
(props.notify === undefined || propertyString(props.notify) === live.notify) &&
|
|
138
|
+
(props.tuning === undefined || propertyString(props.tuning) === live.tuning) &&
|
|
139
|
+
(props['maintenance-mode'] === undefined ||
|
|
140
|
+
propertyString(props['maintenance-mode'], 'type') === live['maintenance-mode']);
|
|
141
|
+
|
|
142
|
+
const set = (key: string, value: string | undefined): Record<string, string> =>
|
|
143
|
+
value === undefined ? {} : { [key]: value };
|
|
144
|
+
|
|
145
|
+
/** ⚠️ PBS's minimum for every keep-* is 1, so a declared `0` is refused rather than meaning "off". */
|
|
146
|
+
const count = (value: number | undefined) => (value === undefined ? undefined : String(value));
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The mutable half, shared by create and update.
|
|
150
|
+
*
|
|
151
|
+
* ⚠️ AN UNDECLARED FIELD IS NOT SENT — storage.ts's rule. PBS leaves a parameter it was not given
|
|
152
|
+
* alone, so this form is safe to re-apply to a datastore that already matches, which is what the
|
|
153
|
+
* adoption path does.
|
|
154
|
+
* ⛔ `name` AND `path` ARE ABSENT ON PURPOSE. Both are create-only: `name` is the address forever
|
|
155
|
+
* after, and PBS's update endpoint does not accept `path` at all, so including it here would turn
|
|
156
|
+
* every single update into a 400.
|
|
157
|
+
*/
|
|
158
|
+
const mutable = (props: PbsDatastoreProps): Record<string, string> => ({
|
|
159
|
+
...set('comment', props.comment),
|
|
160
|
+
...set('gc-schedule', props['gc-schedule']?.trim()),
|
|
161
|
+
...set('keep-daily', count(props['keep-daily'])),
|
|
162
|
+
...set('keep-hourly', count(props['keep-hourly'])),
|
|
163
|
+
...set('keep-last', count(props['keep-last'])),
|
|
164
|
+
...set('keep-monthly', count(props['keep-monthly'])),
|
|
165
|
+
...set('keep-weekly', count(props['keep-weekly'])),
|
|
166
|
+
...set('keep-yearly', count(props['keep-yearly'])),
|
|
167
|
+
...set('counter-reset-schedule', props['counter-reset-schedule']),
|
|
168
|
+
...set('maintenance-mode', props['maintenance-mode']),
|
|
169
|
+
...set('notification-thresholds', props['notification-thresholds']),
|
|
170
|
+
...set('notify', props.notify),
|
|
171
|
+
...set('notify-user', props['notify-user']),
|
|
172
|
+
...set('prune-schedule', props['prune-schedule']?.trim()),
|
|
173
|
+
...set('tuning', props.tuning),
|
|
174
|
+
...set('verify-new', flag(props['verify-new'])),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* ⚠️ `path` IS SENT ONCE, HERE, AND NEVER AGAIN — the fact the whole `path` guard hangs off.
|
|
179
|
+
* ⚠️ `reuse-datastore` IS NOT SENT, AND THE REFUSAL IT CAUSES IS A FEATURE: without it PBS refuses
|
|
180
|
+
* a create against a directory that already holds a chunk store, which is the difference between
|
|
181
|
+
* building a new store and silently adopting somebody else's backups. Adopt out of band.
|
|
182
|
+
* ⚠️ NEITHER IS `backing-device`. A removable datastore's UUID is a property of the hardware in the
|
|
183
|
+
* slot, not of the declaration; one that could re-point it is one that can send backups to the
|
|
184
|
+
* wrong drive.
|
|
185
|
+
*/
|
|
186
|
+
export const createForm = (props: PbsDatastoreProps): PveForm => ({
|
|
187
|
+
...mutable(props),
|
|
188
|
+
name: props.name,
|
|
189
|
+
path: props.path,
|
|
190
|
+
// ⛔ CREATE ONLY, AND ABSENT FROM `mutable` ON PURPOSE. PBS has no `--backend` on update and no
|
|
191
|
+
// `backend` in update's `--delete` enum (measured on 4.2), so sending it on a PUT is a
|
|
192
|
+
// parameter error at best. Divergence is caught by guardBackend, which DIES — the same shape
|
|
193
|
+
// as `path`, for a bigger consequence.
|
|
194
|
+
...(props.backend === undefined ? {} : { backend: props.backend }),
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* ⛔ THERE IS NO `delete` LIST HERE, AND THAT IS A CHOICE ABOUT BACKUPS RATHER THAN AN OMISSION.
|
|
199
|
+
* sdn-vnet.ts derives one from absent props, which is right for an alias and wrong for a
|
|
200
|
+
* retention policy: a declaration that simply fails to mention `keep-daily` would strip it, and a
|
|
201
|
+
* prune job whose keeps have all been stripped is one PBS version away from a prune job that
|
|
202
|
+
* keeps nothing. So undeclared is UNMANAGED, as in storage.ts and backup-job.ts.
|
|
203
|
+
* ⚠️ THE PRICE IS THAT DROPPING A LINE DOES NOT CLEAR A FIELD. Clear one out of band:
|
|
204
|
+
* `proxmox-backup-manager datastore update <name> --delete keep-daily`. It does not cost a
|
|
205
|
+
* forever-diff, because an undeclared field is not compared either.
|
|
206
|
+
* ⛔ WHEN SOMEBODY ADDS ONE, IT TAKES TWO STEPS AND SKIPPING EITHER IS A SILENT BUG. First build
|
|
207
|
+
* it with `values.withClears`, which drops a cleared key from the body — CLEAR WINS — because
|
|
208
|
+
* PBS applies its deletes BEFORE its sets, so a key both set and deleted keeps its value: the
|
|
209
|
+
* declaration would say "unset this", the PUT would succeed, and the next plan would ask for
|
|
210
|
+
* the very same update forever. (PVE instead DIES on that pair; the two behaviours want the
|
|
211
|
+
* same guard.) Second, split the comma-joined value `withClears` produces into a real array —
|
|
212
|
+
* PBS's `delete` is `type: array` and wants REPEATED KEYS, and a comma string arrives as one
|
|
213
|
+
* element that fails the enum check. `PveForm` and `encode` already carry arrays for exactly
|
|
214
|
+
* this. ⚠️ REASONED FROM THE PUBLISHED SCHEMA, NOT PROVEN ON A WIRE.
|
|
215
|
+
*/
|
|
216
|
+
export const updateForm = (props: PbsDatastoreProps): PveForm => mutable(props);
|
|
217
|
+
|
|
218
|
+
export const object = (props: PbsDatastoreProps) => `config/datastore/${props.name}`;
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The live datastore, or undefined.
|
|
222
|
+
*
|
|
223
|
+
* ⛔ `name` IS THE PRESENCE TEST, NOT THE STATUS CODE — PBS does not answer 404 for a section that
|
|
224
|
+
* is missing from `datastore.cfg`, it answers a generic error, so there is no code to key off.
|
|
225
|
+
* ⛔ A MISSING READ PRIVILEGE THEREFORE LOOKS EXACTLY LIKE ABSENCE, because the fold below cannot
|
|
226
|
+
* tell them apart. The signature is a plan that says CREATE followed by PBS answering
|
|
227
|
+
* "datastore '<name>' already exists." — that pair means Datastore.Audit, not a missing
|
|
228
|
+
* datastore. It is loud and non-destructive, which is the only reason the fold is tolerable here;
|
|
229
|
+
* see the ⛔ on `readRole` in resource.ts for the PVE families where it is not.
|
|
230
|
+
* ⛔ ON THE DELETE PATH THE SAME FOLD IS A LIE, AND THIS IS THE ONE HAZARD IN THIS FAMILY I COULD
|
|
231
|
+
* NOT CLOSE. `delete` settles by polling this read until it answers `undefined` — and a `read`
|
|
232
|
+
* role that lacks `Datastore.Audit` answers `undefined` from the very first poll, so EVERY delete
|
|
233
|
+
* would report success while the datastore stood. Worse, the two lanes are different leases: the
|
|
234
|
+
* DELETE goes out under `provision` and may genuinely fail, and this read cannot see that it did.
|
|
235
|
+
* ★ THE SHAPE OF THE FIX IS ALREADY IN THIS PACKAGE: `confirmAbsent` in ceph-pool-settle.ts asks
|
|
236
|
+
* a SECOND, DIFFERENT question — does the collection LIST this object? — and lets a failure
|
|
237
|
+
* PROPAGATE rather than folding it, so "I could not ask" fails the deploy instead of passing
|
|
238
|
+
* it. Add the same over `GET /config/datastore` before anyone relies on a reported deletion.
|
|
239
|
+
* ⚠️ UNTIL THEN THE PRECONDITION IS EXPLICIT: the `read` role MUST hold `Datastore.Audit` on the
|
|
240
|
+
* datastore, or this provider cannot tell a deletion from a refusal.
|
|
241
|
+
*/
|
|
242
|
+
export const readOne = (props: PbsDatastoreProps) =>
|
|
243
|
+
pve<Record<string, unknown>>(props.target, 'read', 'GET', object(props)).pipe(
|
|
244
|
+
Effect.map((live) =>
|
|
245
|
+
live === undefined || typeof live['name'] !== 'string' ? undefined : attributes(live, props),
|
|
246
|
+
),
|
|
247
|
+
Effect.orElseSucceed(() => undefined),
|
|
248
|
+
);
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two things a datastore write needs that are not form-building: refusing a moved `path`, and
|
|
3
|
+
* waiting for PBS to finish.
|
|
4
|
+
*
|
|
5
|
+
* ★ SPLIT OUT OF pbs-datastore-form.ts FOR THE 250-LINE CAP, and the seam is real. That file turns
|
|
6
|
+
* a declaration into a form; this one is the SAFETY GUARD and the POLLING. A reader asking "can
|
|
7
|
+
* this destroy my backups" has one short file to read rather than a long one to search.
|
|
8
|
+
*/
|
|
9
|
+
import * as Effect from 'effect/Effect';
|
|
10
|
+
import { type PbsDatastoreAttributes, object } from './pbs-datastore-form.ts';
|
|
11
|
+
import type { PbsDatastoreProps } from './pbs-datastore.ts';
|
|
12
|
+
import { propertyString } from './values.ts';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* ⛔ A TRAILING SLASH MUST NOT DEADLOCK EVERY PLAN, WHICH IS WHY THIS IS NOT `===`. `guardPath`
|
|
16
|
+
* DIES, so a false positive is not a spurious update — it is an `alchemy plan` that cannot run at
|
|
17
|
+
* all until somebody edits the declaration, on a family whose whole point is that the operator
|
|
18
|
+
* can see the state of their backups. `/mnt/backups/` and `/mnt/backups` are one directory, and
|
|
19
|
+
* whether PBS canonicalises the path it was handed is a property of the version you are talking
|
|
20
|
+
* to. Both sides are trimmed here so that guess cannot cost an outage of the plan itself.
|
|
21
|
+
* ⚠️ THAT IS THE ONLY NORMALISATION APPLIED. A symlink, a bind mount or `/mnt/./backups` still
|
|
22
|
+
* reads as divergent, and deliberately so: this guard's job is to refuse to GUESS that two
|
|
23
|
+
* spellings are the same directory, and only the trailing slash is certain.
|
|
24
|
+
*/
|
|
25
|
+
const samePath = (left: string, right: string) => {
|
|
26
|
+
const trim = (value: string) => (value.length > 1 ? value.replace(/\/+$/, '') : value);
|
|
27
|
+
return trim(left) === trim(right);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* ⛔ THE `path` GUARD, YIELDED FROM BOTH `diff` AND `reconcile`. Both need it: `diff` catches a
|
|
32
|
+
* changed declaration, and `reconcile` catches an ADOPTION, which Alchemy routes straight past
|
|
33
|
+
* `diff` (Apply.ts:980, :1045). `''` is skipped — an unreadable path is not a divergent one.
|
|
34
|
+
*/
|
|
35
|
+
export const guardPath = (live: PbsDatastoreAttributes, props: PbsDatastoreProps) =>
|
|
36
|
+
live.path === '' || samePath(live.path, props.path)
|
|
37
|
+
? Effect.void
|
|
38
|
+
: Effect.die(
|
|
39
|
+
new Error(
|
|
40
|
+
`${object(props)}: declared path ${props.path} but PBS holds ${live.path}. \`path\` ` +
|
|
41
|
+
'is create-only, so this cannot be reconciled and will NOT be planned as a silent ' +
|
|
42
|
+
'noop: backups would keep landing in the old directory behind a green plan. Correct ' +
|
|
43
|
+
'the declaration, or remove the datastore and declare a new one -- the contents live, ' +
|
|
44
|
+
'because `destroy-data` is never sent.',
|
|
45
|
+
),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* ⛔ THE `backend` GUARD, AND IT MATTERS MORE THAN `path`. A wrong `path` sends backups to the
|
|
50
|
+
* wrong directory on the same machine; a wrong `backend` is the difference between a datastore
|
|
51
|
+
* on local disk and one in a Cloudflare R2 bucket. MEASURED on 4.2: `datastore create` takes
|
|
52
|
+
* `--backend`, `datastore update` does not, and `backend` is not in update's `--delete` enum —
|
|
53
|
+
* so divergence CANNOT be reconciled, and the only honest options are to refuse or to lie.
|
|
54
|
+
*
|
|
55
|
+
* ⚠️ AN UNDECLARED `backend` IS NOT A DIVERGENCE. Undeclared is unmanaged everywhere in this
|
|
56
|
+
* family, and this guard keeps that rule: it fires only when the declaration says something and
|
|
57
|
+
* PBS says something else. Adopting `r2-offsite` without mentioning `backend` still plans noop.
|
|
58
|
+
*
|
|
59
|
+
* ⚠️ COMPARED AS A CANONICAL PROPERTY STRING, so `bucket=x,type=s3` and `type=s3,bucket=x` are the
|
|
60
|
+
* same backend and key order cannot deadlock a plan — the reason `samePath` trims a slash.
|
|
61
|
+
*/
|
|
62
|
+
export const guardBackend = (live: PbsDatastoreAttributes, props: PbsDatastoreProps) =>
|
|
63
|
+
props.backend === undefined || propertyString(props.backend) === live.backend
|
|
64
|
+
? Effect.void
|
|
65
|
+
: Effect.die(
|
|
66
|
+
new Error(
|
|
67
|
+
`${object(props)}: declared backend ${props.backend} but PBS holds ` +
|
|
68
|
+
`${live.backend === '' ? '(none — a local datastore)' : live.backend}. \`backend\` ` +
|
|
69
|
+
'is create-only: PBS has no update for it, so this cannot be reconciled and will NOT ' +
|
|
70
|
+
'be planned as a silent noop. Correct the declaration to match what is really there, ' +
|
|
71
|
+
'or remove the datastore and declare a new one -- the contents live, because ' +
|
|
72
|
+
'`destroy-data` is never sent.',
|
|
73
|
+
),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Wait for PBS to finish, then answer the last thing it said.
|
|
78
|
+
*
|
|
79
|
+
* ⛔ IT EXISTS BECAUSE A PBS CREATE IS A FORKED WORKER AND THE CONFIG SECTION IS WRITTEN LAST.
|
|
80
|
+
* `POST /config/datastore` answers with a UPID while a `create-datastore` task builds the chunk
|
|
81
|
+
* store's 65536 directories; only when that finishes does the section reach `datastore.cfg`. An
|
|
82
|
+
* immediate read-back therefore sees a datastore that is not there yet, and the factory's "the
|
|
83
|
+
* write returned no error but the object is still absent" would fire on a create going perfectly.
|
|
84
|
+
* The DELETE forks a worker too. ⚠️ DOCUMENTED FROM THE PUBLISHED SCHEMA, NOT MEASURED — I hold
|
|
85
|
+
* no PBS token. If a create turns out to be synchronous, the first poll simply succeeds.
|
|
86
|
+
* ★ IT TAKES THE READ AS AN EFFECT RATHER THAN A FUNCTION — the one simplification over
|
|
87
|
+
* ceph-pool-settle.ts: an Effect is a description, so re-running it re-reads and no dependency
|
|
88
|
+
* has to be threaded back in to avoid a cycle.
|
|
89
|
+
* ⚠️ 2s APART, 60 TRIES, ABOUT TWO MINUTES — longer than ceph-pool's minute, and REASONED rather
|
|
90
|
+
* than measured: 65536 `mkdir`s is seconds on NVMe and can be a minute on a slow or USB disk.
|
|
91
|
+
* Anything still unsettled after two minutes is a fault to surface, not a wait to lengthen, and
|
|
92
|
+
* the caller's message points at the task log, which is where the real answer is. `Effect.sleep`
|
|
93
|
+
* yields between polls; nothing here parks a call on one long wait.
|
|
94
|
+
* ⚠️ POLLING THE TASK STATUS INSTEAD WOULD BE WORSE, for ceph-pool-settle.ts's reason: every call
|
|
95
|
+
* mints a FRESH token, so the token asking about the task is not the one that started it.
|
|
96
|
+
*/
|
|
97
|
+
export const settle = <A, E, R>(read: Effect.Effect<A, E, R>, done: (live: A) => boolean) =>
|
|
98
|
+
Effect.gen(function* () {
|
|
99
|
+
for (let attempt = 0; attempt < 60; attempt += 1) {
|
|
100
|
+
const live = yield* read;
|
|
101
|
+
if (done(live)) return live;
|
|
102
|
+
yield* Effect.sleep('2 seconds');
|
|
103
|
+
}
|
|
104
|
+
return yield* read;
|
|
105
|
+
});
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Pbs.Datastore` — one section of Proxmox Backup Server's `datastore.cfg`, declared.
|
|
3
|
+
*
|
|
4
|
+
* ★ PBS IS A SIBLING OF PVE, NOT A PART OF IT. The estate runs both — PVE carries a storage of
|
|
5
|
+
* `type: pbs` — so declaring the datastore turns `Proxmox.Storage`'s `locator.datastore` from a
|
|
6
|
+
* string nothing checks into a reference Alchemy can order, as `Proxmox.SdnVnet` did for a bridge.
|
|
7
|
+
*
|
|
8
|
+
* ⛔ DIFFERENT HOST, DIFFERENT REALM, DIFFERENT AUTH HEADER. MEASURED 2026-09-13, strict TLS, no
|
|
9
|
+
* `-k` anywhere: `GET https://pbs.example.com:8007/api2/json/ping` answers 200
|
|
10
|
+
* `{"data":{"pong":true}}` against a Let's Encrypt certificate for CN=pbs.example.com;
|
|
11
|
+
* `/version` and `/config/datastore` answer 401 "authentication failed";
|
|
12
|
+
* `pbs.mgmt.example.com:8007` answers 401 and its certificate verifies too; and
|
|
13
|
+
* `/access/domains` lists the realms `pbs`, `pam` and an openid realm `Schenanigans`. So the
|
|
14
|
+
* prefix, the `{"data": …}` envelope and the strict-TLS story are PVE's exactly — and only the
|
|
15
|
+
* credential differs. There is no `pve` realm here; a PVE token cannot authenticate at all.
|
|
16
|
+
* The header differs too, which is why `pve()` exists rather than `pve()` being reused — see the
|
|
17
|
+
* ⛔ on `pbsAuthorization` in pbs-datastore-form.ts.
|
|
18
|
+
*
|
|
19
|
+
* ⛔ THERE IS NO OPENBAO MOUNT FOR PBS TODAY, SO NOTHING HERE CAN DEPLOY YET. `PbsTarget.mount` is
|
|
20
|
+
* a parameter for the reason `PveTarget.mount` is — the provider must be able to leave this
|
|
21
|
+
* estate — but the estate has nothing to put in it. MEASURED: under the `claude-code` approle,
|
|
22
|
+
* `bao token capabilities` answers `deny` for `proxmox-tb4/creds/{read,provision}`,
|
|
23
|
+
* `proxmox-ops/creds/read` and every `…pbs…` path tried, and `sys/mounts` is 403 — so I could
|
|
24
|
+
* not enumerate the mounts to PROVE a PBS one is absent. That it is absent is the brief's
|
|
25
|
+
* statement, not my measurement.
|
|
26
|
+
* ★ WHAT THE MOUNT MUST VEND, so `mint()` is reused unchanged: `bao read -format=json
|
|
27
|
+
* <mount>/creds/<role>` answering `{"data":{"token_id":…,"secret":…}}`, where `token_id` is a
|
|
28
|
+
* PBS token id `user@realm!tokenname` — `hf-read@pbs!…` and `hf-provision@pbs!…`. Same short,
|
|
29
|
+
* non-renewable lease contract as `proxmox-tb4`.
|
|
30
|
+
* ⚠️ PBS HAS NO DYNAMIC-SECRETS PLUGIN OF ITS OWN — OpenBao's Proxmox support is a PVE thing — so
|
|
31
|
+
* the mount is a small custom vendor against `POST /access/users/{id}/token/{name}`. Say which
|
|
32
|
+
* mount and role you needed rather than reaching for the read-only `monitoring@pbs` token on
|
|
33
|
+
* the kv shelf: widening that would delete the outer lock for every reader of the shelf.
|
|
34
|
+
*
|
|
35
|
+
* ⛔ A DATASTORE HOLDS THE BACKUPS, SO `retain` IS THE DEFAULT AND `delete` IS FULLY IMPLEMENTED —
|
|
36
|
+
* the ★ on removal policy in resource.ts has the whole reasoning. A stub returning `Effect.void`
|
|
37
|
+
* would report a deletion that never happened, and this is the family where that lie costs most.
|
|
38
|
+
*
|
|
39
|
+
* ⚠️ PRIVILEGES, READ OFF THE PUBLISHED SCHEMA RATHER THAN OFF THIS HOST:
|
|
40
|
+
* read GET config/datastore/{name} Datastore.Audit on /datastore/{name}
|
|
41
|
+
* create POST config/datastore Datastore.Allocate on /datastore
|
|
42
|
+
* update PUT config/datastore/{name} Datastore.Modify on /datastore/{name}
|
|
43
|
+
* delete DELETE config/datastore/{name} Datastore.Allocate on /datastore/{name}
|
|
44
|
+
* Built-in `DatastoreAdmin` at `/datastore` covers all four; `DatastoreAudit` covers the read
|
|
45
|
+
* lane alone, which is what the `read` role should hold.
|
|
46
|
+
*
|
|
47
|
+
* ⛔ NO SECRET IS A PROP OR AN ATTRIBUTE, AND THIS FAMILY IS CLEAN BY CONSTRUCTION: a datastore
|
|
48
|
+
* section holds no password, token or key — encryption keys belong to the backup CLIENT. Every
|
|
49
|
+
* field below is a name, a path, a schedule, a count or a policy string, all of which are safe in
|
|
50
|
+
* a state store Alchemy writes UNENCRYPTED and this estate dumps to CT100 nightly.
|
|
51
|
+
*/
|
|
52
|
+
import { Resource } from 'alchemy';
|
|
53
|
+
import { isResolved } from 'alchemy/Diff';
|
|
54
|
+
import * as Provider from 'alchemy/Provider';
|
|
55
|
+
import * as Effect from 'effect/Effect';
|
|
56
|
+
import { pve } from './client.ts';
|
|
57
|
+
import {
|
|
58
|
+
type PbsDatastoreAttributes,
|
|
59
|
+
type PbsTarget,
|
|
60
|
+
createForm,
|
|
61
|
+
matches,
|
|
62
|
+
object,
|
|
63
|
+
readOne,
|
|
64
|
+
updateForm,
|
|
65
|
+
} from './pbs-datastore-form.ts';
|
|
66
|
+
import { guardBackend, guardPath, settle } from './pbs-datastore-guard.ts';
|
|
67
|
+
import type { PveRequirements } from './resource.ts';
|
|
68
|
+
|
|
69
|
+
export type { PbsDatastoreAttributes, PbsTarget };
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* ⚠️ PBS'S OWN KEY NAMES, HYPHENS INCLUDED — backup-job.ts's rule: the form is then a copy rather
|
|
73
|
+
* than a translation table, and a translation table is one more place for a key to be renamed and
|
|
74
|
+
* silently never sent.
|
|
75
|
+
* ⚠️ AN UNDECLARED FIELD IS NEITHER SENT NOR COMPARED — storage.ts's rule, not sdn-vnet.ts's, and
|
|
76
|
+
* for a retention policy the choice is not close: see the ⛔ on `updateForm`.
|
|
77
|
+
*/
|
|
78
|
+
export interface PbsDatastoreProps {
|
|
79
|
+
target: PbsTarget;
|
|
80
|
+
/** PBS's primary key. Create-only: there is no rename, and a new name is a new datastore. */
|
|
81
|
+
name: string;
|
|
82
|
+
/**
|
|
83
|
+
* Absolute path to the datastore directory.
|
|
84
|
+
*
|
|
85
|
+
* ⛔ CREATE-ONLY, AND THE ONE FIELD THIS FILE REFUSES TO LET DRIFT QUIETLY. PBS's update endpoint
|
|
86
|
+
* does not accept `path`, so comparing it in `matches` could only plan an update no PUT can
|
|
87
|
+
* apply — the forever-diff storage.ts avoids by letting its create-only `type` plan as NOOP.
|
|
88
|
+
* Here a silent noop is the wrong trade: the operator would read a green plan while PBS kept
|
|
89
|
+
* writing backups to the old directory. So `diff` and `reconcile` compare it themselves and DIE
|
|
90
|
+
* with both values named. A path change is remove-and-redeclare, deliberately.
|
|
91
|
+
*/
|
|
92
|
+
path: string;
|
|
93
|
+
comment?: string;
|
|
94
|
+
/** Calendar event for garbage collection, e.g. `daily` or `sat 18:15`. */
|
|
95
|
+
'gc-schedule'?: string;
|
|
96
|
+
/** Calendar event for the built-in prune job. Retention itself is the `keep-*` fields below. */
|
|
97
|
+
'prune-schedule'?: string;
|
|
98
|
+
/** ⚠️ PBS's minimum is 1 for every keep-*. A declared `0` is refused, and does not mean "off". */
|
|
99
|
+
'keep-last'?: number;
|
|
100
|
+
'keep-hourly'?: number;
|
|
101
|
+
'keep-daily'?: number;
|
|
102
|
+
'keep-weekly'?: number;
|
|
103
|
+
'keep-monthly'?: number;
|
|
104
|
+
'keep-yearly'?: number;
|
|
105
|
+
/**
|
|
106
|
+
* Legacy notification routing: `gc=<never|always|error>`, and the same for `verify` and `sync`.
|
|
107
|
+
* ⚠️ PBS 3.2+ picks between this and the notification system with `notification-mode`, which is
|
|
108
|
+
* NOT declarable here: flipping a live datastore between the two silently changes who hears
|
|
109
|
+
* about a failed verify, and silence is the failure mode that costs most (notification-target.ts).
|
|
110
|
+
*/
|
|
111
|
+
notify?: string;
|
|
112
|
+
/** PBS user id legacy notifications go to. PBS's schema default is `root@pam`. */
|
|
113
|
+
'notify-user'?: string;
|
|
114
|
+
/** Verify every new backup the moment it completes. PBS's default is false. */
|
|
115
|
+
'verify-new'?: boolean;
|
|
116
|
+
/** Property string: `chunk-order=<none|inode>,sync-level=<none|file|filesystem>`, and more. */
|
|
117
|
+
tuning?: string;
|
|
118
|
+
/**
|
|
119
|
+
* Takes the datastore out of service: `type=<offline|read-only|unmount>`, optional
|
|
120
|
+
* `message=<text>`. PBS's default key is `type`, so the bare `offline` normalises to the same
|
|
121
|
+
* string here and either spelling is safe.
|
|
122
|
+
* ⛔ NEVER DECLARE `type=delete`. PBS sets that itself while destroying a datastore's contents.
|
|
123
|
+
* ⚠️ A `message` CONTAINING A COMMA BREAKS THE COMPARISON, not the write: `propertyString` splits
|
|
124
|
+
* on commas and does not honour PBS's quoting, so the two sides never compare equal and the
|
|
125
|
+
* plan asks for the same update forever. Keep the message comma-free.
|
|
126
|
+
*/
|
|
127
|
+
'maintenance-mode'?: string;
|
|
128
|
+
/**
|
|
129
|
+
* ⛔ CREATE-ONLY, LIKE `path`, AND FOR A SHARPER REASON. MEASURED on the live 4.2 host:
|
|
130
|
+
* `datastore create --help` offers `--backend`, `datastore update --help` does NOT, and
|
|
131
|
+
* `backend` is absent from update's `--delete` enum. PBS will not move a datastore between
|
|
132
|
+
* local disk and object storage, ever.
|
|
133
|
+
* ★ IT IS A PROP AT ALL BECAUSE THE ESTATE HAS ONE. `r2-offsite` holds
|
|
134
|
+
* `type=s3,client=cloudflare-r2,bucket=homeflare-pbs`. Leaving it undeclared would still plan
|
|
135
|
+
* noop — undeclared is unmanaged here — but the declaration would then describe a LOCAL
|
|
136
|
+
* datastore, and anyone recreating from it would get exactly that: an empty directory on the
|
|
137
|
+
* mini's disk where the offsite copy used to be, behind a green plan the whole way.
|
|
138
|
+
* ⚠️ A property string. Compared canonically, so key order is never a diff.
|
|
139
|
+
*/
|
|
140
|
+
backend?: string;
|
|
141
|
+
/** ⚠️ S3 request counters reset on this calendar event. Mutable; in update's `--delete` enum. */
|
|
142
|
+
'counter-reset-schedule'?: string;
|
|
143
|
+
/**
|
|
144
|
+
* ⚠️ S3 request-count thresholds that raise a notification, e.g. `s3-put=700000,s3-get=200000`.
|
|
145
|
+
* A property string, compared canonically. Mutable.
|
|
146
|
+
*/
|
|
147
|
+
'notification-thresholds'?: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface PbsDatastore extends Resource<
|
|
151
|
+
'Pbs.Datastore',
|
|
152
|
+
PbsDatastoreProps,
|
|
153
|
+
PbsDatastoreAttributes,
|
|
154
|
+
never,
|
|
155
|
+
PveRequirements
|
|
156
|
+
> {}
|
|
157
|
+
|
|
158
|
+
/** ★ `retain` by default — a datastore holds the backups. See the ★ in resource.ts. */
|
|
159
|
+
export const PbsDatastore = Resource<PbsDatastore>('Pbs.Datastore', {
|
|
160
|
+
defaultRemovalPolicy: 'retain',
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
export const PbsDatastoreProvider = () =>
|
|
164
|
+
Provider.effect(
|
|
165
|
+
PbsDatastore,
|
|
166
|
+
Effect.succeed(
|
|
167
|
+
PbsDatastore.Provider.of({
|
|
168
|
+
/**
|
|
169
|
+
* ⛔ EMPTY, AND HERE IT GUARDS THE ESTATE'S BACKUPS. `GET /config/datastore` returns every
|
|
170
|
+
* datastore on the host, the one PVE's `type: pbs` storage writes into included. See
|
|
171
|
+
* `pveHandlers` in resource.ts for why adoption stays an explicit act.
|
|
172
|
+
*/
|
|
173
|
+
list: () => Effect.succeed([]),
|
|
174
|
+
read: ({ olds }) => readOne(olds),
|
|
175
|
+
/**
|
|
176
|
+
* ⚠️ `isResolved` IS THE NARROWING, resource.ts's reason: at plan time a prop can still be an
|
|
177
|
+
* unresolved Output, and comparing a placeholder to a live value reports a phantom update.
|
|
178
|
+
* ⛔ `guardPath` IS A DELIBERATE DEPARTURE FROM storage.ts, which lets its create-only field
|
|
179
|
+
* plan as noop. Dying fails `alchemy plan` with both paths named — see the ⛔ on the prop.
|
|
180
|
+
*/
|
|
181
|
+
diff: ({ news, output }) =>
|
|
182
|
+
Effect.gen(function* () {
|
|
183
|
+
if (output === undefined || !isResolved(news)) return undefined;
|
|
184
|
+
const live = yield* readOne(news);
|
|
185
|
+
// ⚠️ `update`, not `create` — Alchemy's Diff admits only noop/update/replace, and an
|
|
186
|
+
// object Alchemy has state for but PBS does not is drift for reconcile to repair.
|
|
187
|
+
if (live === undefined) return { action: 'update' } as const;
|
|
188
|
+
yield* guardPath(live, news);
|
|
189
|
+
yield* guardBackend(live, news);
|
|
190
|
+
return matches(live, news)
|
|
191
|
+
? ({ action: 'noop' } as const)
|
|
192
|
+
: ({ action: 'update' } as const);
|
|
193
|
+
}),
|
|
194
|
+
/**
|
|
195
|
+
* ⚠️ NOT THE FACTORY'S reconcile: it reads back ONCE, immediately — see the ⛔ on `settle`.
|
|
196
|
+
* ⛔ `guardPath` RUNS HERE TOO, AND THAT IS NOT BELT-AND-BRACES — see the ⛔ on it.
|
|
197
|
+
* ⛔ THE `matches` GUARD BEFORE THE PUT IS resource.ts's, AND IT IS WHY ADOPTION IS FREE.
|
|
198
|
+
*/
|
|
199
|
+
reconcile: Effect.fn(function* ({ news }) {
|
|
200
|
+
const live = yield* readOne(news);
|
|
201
|
+
let upid: string | undefined;
|
|
202
|
+
if (live === undefined) {
|
|
203
|
+
upid = yield* pve<string>(
|
|
204
|
+
news.target,
|
|
205
|
+
'provision',
|
|
206
|
+
'POST',
|
|
207
|
+
'config/datastore',
|
|
208
|
+
createForm(news),
|
|
209
|
+
);
|
|
210
|
+
} else {
|
|
211
|
+
yield* guardPath(live, news);
|
|
212
|
+
yield* guardBackend(live, news);
|
|
213
|
+
// ⚠️ An empty form is not a write — resource.ts skips one for the same reason.
|
|
214
|
+
const form = updateForm(news);
|
|
215
|
+
if (!matches(live, news) && Object.keys(form).length > 0) {
|
|
216
|
+
yield* pve(news.target, 'provision', 'PUT', object(news), form);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const after = yield* settle(
|
|
220
|
+
readOne(news),
|
|
221
|
+
(row) => row !== undefined && matches(row, news),
|
|
222
|
+
);
|
|
223
|
+
if (after === undefined || !matches(after, news)) {
|
|
224
|
+
/**
|
|
225
|
+
* ⛔ REFUSE RATHER THAN RETURN THE PROPS AS THOUGH THEY LANDED — resource.ts's rule.
|
|
226
|
+
* A create answers with a task id, so "no error" is not evidence of a datastore.
|
|
227
|
+
*/
|
|
228
|
+
return yield* Effect.die(
|
|
229
|
+
new Error(
|
|
230
|
+
`${object(news)}: the write returned no error but the datastore still does not ` +
|
|
231
|
+
`match the declaration after 120s. PBS answered with task ${upid ?? '(none)'} ` +
|
|
232
|
+
'-- read it with `proxmox-backup-manager task log <upid>`. A slow disk is a ' +
|
|
233
|
+
'plausible cause (see `settle`); a create over an existing chunk store is not, ' +
|
|
234
|
+
'because PBS refuses that outright rather than timing out.',
|
|
235
|
+
),
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
return after;
|
|
239
|
+
}),
|
|
240
|
+
/**
|
|
241
|
+
* ⛔ FULLY IMPLEMENTED, NEVER A STUB, and it only ever runs on an explicit
|
|
242
|
+
* `.pipe(RemovalPolicy.destroy())` because this family defaults to `retain`.
|
|
243
|
+
* ⛔ NO `destroy-data`. PBS's DELETE drops the section from `datastore.cfg` and leaves every
|
|
244
|
+
* chunk and snapshot on disk; `destroy-data=1` erases the contents. It is not a prop and
|
|
245
|
+
* not a flag here, so this provider can un-declare a datastore and can never erase one.
|
|
246
|
+
* ⚠️ IT IS STILL A ONE-WAY DOOR FOR A DECLARATION: re-adding a datastore over an existing
|
|
247
|
+
* chunk store needs `reuse-datastore`, which is deliberately not declarable — see the
|
|
248
|
+
* ⚠️ on `createForm`. Re-adopt with `proxmox-backup-manager datastore create …`.
|
|
249
|
+
* ⚠️ NO `keep-job-configs` EITHER, SO THE JOBS GO TOO: PBS defaults it false and removes the
|
|
250
|
+
* verify, sync and prune jobs referencing this datastore — objects nothing here declares.
|
|
251
|
+
* PBS's default stands, so the blast radius is what a human clicking Remove would get.
|
|
252
|
+
* ⚠️ AND THE REFUSAL CAN ARRIVE AFTER THE RESPONSE: the delete forks a worker too, so a store
|
|
253
|
+
* held open by a running backup can fail once the call has already returned 200.
|
|
254
|
+
*/
|
|
255
|
+
delete: Effect.fn(function* ({ olds }) {
|
|
256
|
+
yield* pve<string>(olds.target, 'provision', 'DELETE', object(olds));
|
|
257
|
+
const left = yield* settle(readOne(olds), (row) => row === undefined);
|
|
258
|
+
if (left !== undefined) {
|
|
259
|
+
return yield* Effect.die(
|
|
260
|
+
new Error(
|
|
261
|
+
`${object(olds)}: the DELETE returned no error but the datastore is still in ` +
|
|
262
|
+
'datastore.cfg after 120s. Either a backup, verify or GC task still holds it, ' +
|
|
263
|
+
'or the provision role lacks Datastore.Allocate on /datastore/' +
|
|
264
|
+
`${olds.name}. Read the task log with \`proxmox-backup-manager task log <upid>\`.`,
|
|
265
|
+
),
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
}),
|
|
269
|
+
}),
|
|
270
|
+
),
|
|
271
|
+
);
|