@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,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Proxmox.CephFlag` — one cluster-wide Ceph OSD flag, declared: `noout`, `pause`, `norebalance`.
|
|
3
|
+
*
|
|
4
|
+
* ★ ONE RESOURCE PER FLAG, NOT ONE CARRYING ALL ELEVEN, AND THE CLUSTER'S OWN API DECIDED IT.
|
|
5
|
+
* MEASURED on TB4 2026-09-13 (pve-manager 9.2.11, ceph tentacle 20.2.2) by reading
|
|
6
|
+
* `/usr/share/perl5/PVE/API2/Cluster/Ceph.pm` and the published schema in
|
|
7
|
+
* `/usr/share/pve-docs/api-viewer/apidoc.js`:
|
|
8
|
+
* · `PUT /cluster/ceph/flags` ends in `fork_worker('cephsetflags', ...)` and returns a STRING
|
|
9
|
+
* — a UPID. The bulk write is ASYNCHRONOUS, and this package has no task-polling machinery.
|
|
10
|
+
* · `PUT /cluster/ceph/flags/{flag}` calls `$rados->mon_command` inline and returns null. The
|
|
11
|
+
* per-flag write is SYNCHRONOUS.
|
|
12
|
+
* `pveOperations.reconcile` READS BACK after every write and refuses to record a value it did
|
|
13
|
+
* not see. Against the bulk endpoint that read-back races a worker which has not run yet, so it
|
|
14
|
+
* would record the OLD value as the new attributes — the one honesty mechanism in this package
|
|
15
|
+
* turned into a lie generator. Per-flag, the read-back means what it says.
|
|
16
|
+
* ★ THE SECOND REASON IS SMALLER AND STILL REAL. The bulk GET answers rows of
|
|
17
|
+
* `{name, description, value}`, and `description` is PVE's own English prose that no write
|
|
18
|
+
* accepts: compare it and you have a forever-update, report it and this provider is pretending
|
|
19
|
+
* to manage prose. `GET /cluster/ceph/flags/{flag}` answers a BARE BOOLEAN, so that trap
|
|
20
|
+
* cannot be written here at all. The brief's plural `Proxmox.CephFlags` became singular for
|
|
21
|
+
* those two reasons; the bulk endpoint is read by nothing in this package.
|
|
22
|
+
*
|
|
23
|
+
* ⛔ THESE ARE NOT CONFIGURATION. THEY ARE A HAND ON THE BRAKE, AND A DECLARATION CAN PULL IT OFF.
|
|
24
|
+
* `noout` is what a person sets before pulling a disk. A stack saying `value: false` reasserts
|
|
25
|
+
* that off on EVERY deploy — including a deploy somebody else runs, for an unrelated resource,
|
|
26
|
+
* twenty minutes into a maintenance window — and the plan line reads `Proxmox.CephFlag noout
|
|
27
|
+
* update`, which nobody reads as "ceph is about to start rebalancing 128 PGs across three nodes
|
|
28
|
+
* while a disk is out of the chassis".
|
|
29
|
+
* ★ SO `value` IS REQUIRED AND HAS NO DEFAULT, AND THAT IS THE MITIGATION. An optional `value`
|
|
30
|
+
* defaulting to false would make `CephFlag('noout', { flag: 'noout', target })` mean "clear
|
|
31
|
+
* it" — the dangerous direction, chosen by OMISSION. Here the dangerous sentence has to be
|
|
32
|
+
* typed out by a person. Do not add a default, and do not declare a flag merely to document
|
|
33
|
+
* that it is off: MEASURED 2026-09-13, all eleven flags on TB4 read 0, so eleven
|
|
34
|
+
* `value: false` lines would plan green forever and do nothing except take the brake off
|
|
35
|
+
* whenever somebody happens to apply them.
|
|
36
|
+
*
|
|
37
|
+
* ⛔ `delete` DOES NOTHING, ON PURPOSE, AND THE ASYMMETRY OF THE TWO FAILURES IS THE ARGUMENT.
|
|
38
|
+
* `flags/{flag}` has exactly two methods, GET and PUT, so the factory's `destroy` would answer
|
|
39
|
+
* "Method 'DELETE /cluster/ceph/flags/noout' not implemented" on every teardown — whatever this
|
|
40
|
+
* handler does is INVENTED. The obvious invention, "clear the flag", is refused: removing a line
|
|
41
|
+
* from a stack file, or Alchemy collecting an old generation after a replace, would silently
|
|
42
|
+
* take the brake off and nothing would warn. Leaving a flag set that nothing declares any more
|
|
43
|
+
* is the opposite kind of failure — ceph reports `HEALTH_WARN ... flag(s) set` for every one of
|
|
44
|
+
* these, on `ceph -s`, in the PVE UI and in the `pve_*` metrics this estate already scrapes.
|
|
45
|
+
* A silent destructive failure loses to a loud inert one.
|
|
46
|
+
* ⚠️ THE RESIDUAL RISK, NAMED RATHER THAN DENIED: deleting a `value: true` declaration LEAVES
|
|
47
|
+
* THE FLAG SET. Retire a maintenance window by flipping the line to `value: false` and
|
|
48
|
+
* deploying BEFORE the line is removed, or a `noout` outlives the window that needed it.
|
|
49
|
+
* ⚠️ REASONED, NOT MEASURED: that health warning was not observed, because observing it means
|
|
50
|
+
* setting a flag on a live cluster and this file was researched read-only. Undeclaring stops
|
|
51
|
+
* MANAGING a flag; to clear one, declare `value: false` and deploy, where a plan says so.
|
|
52
|
+
*
|
|
53
|
+
* ⛔ `pause` IS TWO CEPH FLAGS AND THIS RESOURCE READS ONLY ONE OF THEM. MEASURED in
|
|
54
|
+
* `PVE::Ceph::Tools::get_real_flag_name`, whose own comment reads "the 'pause' flag gets always
|
|
55
|
+
* set to both 'pauserd' and 'pausewr'": PVE writes both and then decides the flag is set by
|
|
56
|
+
* looking at `pauserd` alone. A cluster where somebody ran `ceph osd unset pauserd` by hand
|
|
57
|
+
* still has `pausewr` — writes blocked, guests hung — and this resource reports `value: false`
|
|
58
|
+
* and plans `noop` straight past it. It cannot express a half-pause, so never use it to prove
|
|
59
|
+
* one is gone; `ceph osd dump | head -1` can.
|
|
60
|
+
* ⛔ `pause` AND `noup` ARE ALSO THE TWO THAT TAKE THE CLUSTER DOWN FROM A TYPO. `pause: true`
|
|
61
|
+
* stops all reads and writes, so every guest on `cephtb4` — the cluster's only rbd pool,
|
|
62
|
+
* MEASURED 2026-09-13 — freezes on its root disk, and so does `cephfs-tb4`. `noup: true`
|
|
63
|
+
* stops a rebooted OSD from ever rejoining. Neither is undone by deleting the line.
|
|
64
|
+
*
|
|
65
|
+
* ⚠️ PRIVILEGES, AND THE WRITE ONE IS A BIG ASK. Both GETs check `Sys.Audit` on `/`; both PUTs
|
|
66
|
+
* check `Sys.Modify` on `/`, and all four carry `allowtoken => 1`, so an API token may make the
|
|
67
|
+
* call at all. There is no `/ceph` ACL object to scope to — the check names the ROOT path
|
|
68
|
+
* literally — so granting the provision role `Sys.Modify` here also buys datacenter options,
|
|
69
|
+
* metric servers, notification targets and every other cluster-wide write, exactly as
|
|
70
|
+
* metric-server.ts warns about its own family.
|
|
71
|
+
* ⚠️ REASONED, NOT MEASURED: nothing here was exercised under a minted lease, because a mint
|
|
72
|
+
* writes an API token into the cluster and the brief for this file was reads only. Expect
|
|
73
|
+
* "Permission check failed (/, Sys.Modify)" on the first deploy, and widen deliberately.
|
|
74
|
+
*/
|
|
75
|
+
import { Resource } from 'alchemy';
|
|
76
|
+
import * as Provider from 'alchemy/Provider';
|
|
77
|
+
import * as Effect from 'effect/Effect';
|
|
78
|
+
import { type PveRequirements, type WithTarget, pveHandlers } from './resource.ts';
|
|
79
|
+
import { bool, flag } from './values.ts';
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* PVE's eleven flags, spelled as `PVE::Ceph::Tools::get_possible_osd_flags` spells them.
|
|
83
|
+
*
|
|
84
|
+
* ⚠️ THE ENUM IS CLOSED AND A TYPO IS A 400, WHICH IS WHY THIS IS A UNION AND NOT `string`. Both
|
|
85
|
+
* the GET and the PUT declare `additionalProperties => 0` over exactly this list.
|
|
86
|
+
* ⚠️ CEPH HAS FLAGS PVE DOES NOT MODEL, and four of them are always on. MEASURED on TB4:
|
|
87
|
+
* `ceph osd dump` reports `flags sortbitwise,recovery_deletes,purged_snapdirs,pglog_hardlimit`.
|
|
88
|
+
* PVE reads that same string and answers only about its own eleven, so the others can neither
|
|
89
|
+
* leak in here nor be set from here — `noautoscale` and `nosnaptrim` included.
|
|
90
|
+
* ⛔ IF SOMEBODY LATER "IMPROVES" THE READ BY PARSING `osd dump` DIRECTLY, those four become
|
|
91
|
+
* permanently-set flags that nothing declares: a forever-diff on a brand new cluster.
|
|
92
|
+
*/
|
|
93
|
+
export type CephFlagName =
|
|
94
|
+
| 'nobackfill'
|
|
95
|
+
| 'nodeep-scrub'
|
|
96
|
+
| 'nodown'
|
|
97
|
+
| 'noin'
|
|
98
|
+
| 'noout'
|
|
99
|
+
| 'norebalance'
|
|
100
|
+
| 'norecover'
|
|
101
|
+
| 'noscrub'
|
|
102
|
+
| 'notieragent'
|
|
103
|
+
| 'noup'
|
|
104
|
+
| 'pause';
|
|
105
|
+
|
|
106
|
+
export interface CephFlagProps extends WithTarget {
|
|
107
|
+
/**
|
|
108
|
+
* Which flag. PVE's primary key here, and the last segment of the path.
|
|
109
|
+
*
|
|
110
|
+
* ⚠️ EDITING IT IN PLACE ORPHANS THE OLD FLAG RATHER THAN MOVING ANYTHING. `diff` reads the NEW
|
|
111
|
+
* path and reconcile writes it, while the old flag keeps whatever this stack last put there.
|
|
112
|
+
* There is deliberately no replace override for it — acl.ts needs one because its `delete`
|
|
113
|
+
* removes a real grant, and `delete` here is inert by design, so a replace would do exactly
|
|
114
|
+
* what an update already does. Declare a second resource and set the old one to `value: false`
|
|
115
|
+
* rather than renaming this one.
|
|
116
|
+
* ⛔ TWO RESOURCES DECLARING THE SAME FLAG ARE ONE CLUSTER OBJECT, and Alchemy sees two ids
|
|
117
|
+
* rather than a collision — the acl.ts hazard exactly. Disagreeing, they take turns winning
|
|
118
|
+
* and BOTH plan `update` for ever; agreeing, deleting either leaves the flag where the other
|
|
119
|
+
* put it. One declaration per flag per cluster.
|
|
120
|
+
*/
|
|
121
|
+
flag: CephFlagName;
|
|
122
|
+
/**
|
|
123
|
+
* Set the flag (`true`) or clear it (`false`).
|
|
124
|
+
*
|
|
125
|
+
* ⛔ REQUIRED, WITH NO DEFAULT — see the second ⛔ in the header, which is the whole safety
|
|
126
|
+
* argument for this family. It is also the only field PVE accepts on this endpoint, and it is
|
|
127
|
+
* not optional in the schema either: omitting it from the form is a 400, not an untouched
|
|
128
|
+
* flag. (The BULK endpoint is the one where omission means "leave it alone"; this is not it.)
|
|
129
|
+
*/
|
|
130
|
+
value: boolean;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface CephFlagAttributes {
|
|
134
|
+
/**
|
|
135
|
+
* ⚠️ REPORTED, NEVER COMPARED. It is the path key the read was made WITH, copied back out of
|
|
136
|
+
* props, so comparing it against props would be true by construction — the same reasoning
|
|
137
|
+
* acl.ts gives for the four identity fields it also declines to diff.
|
|
138
|
+
*/
|
|
139
|
+
flag: CephFlagName;
|
|
140
|
+
/** Whether ceph has the flag set right now. The only field `matches` looks at. */
|
|
141
|
+
value: boolean;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface ProxmoxCephFlag extends Resource<
|
|
145
|
+
'Proxmox.CephFlag',
|
|
146
|
+
CephFlagProps,
|
|
147
|
+
CephFlagAttributes,
|
|
148
|
+
never,
|
|
149
|
+
PveRequirements
|
|
150
|
+
> {}
|
|
151
|
+
|
|
152
|
+
export const ProxmoxCephFlag = Resource<ProxmoxCephFlag>('Proxmox.CephFlag');
|
|
153
|
+
|
|
154
|
+
const handlers = pveHandlers<CephFlagProps, CephFlagAttributes>({
|
|
155
|
+
/**
|
|
156
|
+
* ⚠️ `live` IS A BARE BOOLEAN HERE, NOT AN OBJECT, AND IT IS HANDED TO `bool` WHOLE ON PURPOSE.
|
|
157
|
+
* `get_flag` returns perl `1` or `0` under a `type => 'boolean'` return schema, so the wire is
|
|
158
|
+
* `{"data":0}` or `{"data":false}` depending on how the REST layer renders it. `bool` accepts
|
|
159
|
+
* every one of those spellings, which is exactly why it is a shared coercion. The factory
|
|
160
|
+
* types this parameter `Record<string, unknown>` because every OTHER PVE read is an object;
|
|
161
|
+
* reaching for `live['value']` here would answer undefined on every plan and diff `false`
|
|
162
|
+
* forever against a set flag.
|
|
163
|
+
* ⚠️ THE SCALAR IS A MEASURED PROPERTY OF THIS PVE, NOT A PROMISE. A later version wrapping
|
|
164
|
+
* the answer in an object would make `bool` read a SET flag as false, and `value: false`
|
|
165
|
+
* would then plan `noop` over a live flag — silently. Re-measure
|
|
166
|
+
* `GET /cluster/ceph/flags/noout` after a major upgrade; cheaper than a second parse path.
|
|
167
|
+
*
|
|
168
|
+
* ⛔ IT NEVER ANSWERS undefined, WHICH MAKES THE CREATE BRANCH UNREACHABLE, AND ITS ERROR WILL
|
|
169
|
+
* MISLEAD YOU. All eleven flags always exist; there is nothing for "absent" to mean. So a POST
|
|
170
|
+
* to `collection` means the READ failed — an expired 300s lease, n2 down, or ceph simply not
|
|
171
|
+
* configured on the cluster (every one of these four handlers opens with
|
|
172
|
+
* `check_ceph_configured()`, which dies). Read the resulting "Method 'POST /cluster/ceph/flags'
|
|
173
|
+
* not implemented" as "the read failed" and go and look at the credential, not at ceph. Same
|
|
174
|
+
* shape as the unreachable POST in acl.ts, same advice.
|
|
175
|
+
*/
|
|
176
|
+
attributes: (live, props) => ({ flag: props.flag, value: bool(live) }),
|
|
177
|
+
collection: () => 'cluster/ceph/flags',
|
|
178
|
+
/**
|
|
179
|
+
* ⛔ UNREACHABLE — see the second ⛔ in `attributes`. It is written in the BULK endpoint's shape
|
|
180
|
+
* (the flag name is the parameter, not `value`) rather than left empty, because that is the
|
|
181
|
+
* only body `cluster/ceph/flags` has ever documented. Nothing sends it today.
|
|
182
|
+
*/
|
|
183
|
+
createForm: (props) => ({ [props.flag]: flag(props.value) ?? '0' }),
|
|
184
|
+
/**
|
|
185
|
+
* ⚠️ `value` IS THE ONLY THING COMPARED, AND THE LIST OF THINGS DELIBERATELY NOT COMPARED IS THE
|
|
186
|
+
* point of this resource. `flag` is props-derived (see `CephFlagAttributes`). `description`
|
|
187
|
+
* and `name` exist only on the bulk GET, which this file does not read, so neither can be
|
|
188
|
+
* compared by accident. Nothing PVE returns here is rewritten, re-ordered or re-typed by the
|
|
189
|
+
* cluster, because all PVE returns here is one boolean.
|
|
190
|
+
* ★ MEASURED: on TB4, `GET /cluster/ceph/flags` answers value 0 for all eleven flags, so a
|
|
191
|
+
* declaration of `value: false` reads back false and plans `noop`.
|
|
192
|
+
*/
|
|
193
|
+
matches: (attributes, props) => attributes.value === props.value,
|
|
194
|
+
path: (props) => `cluster/ceph/flags/${props.flag}`,
|
|
195
|
+
/**
|
|
196
|
+
* ⚠️ ONLY `value` GOES IN THE BODY. `flag` is already bound by the path and the PUT declares
|
|
197
|
+
* `additionalProperties => 0`, so a second copy in the form could only disagree with the URL.
|
|
198
|
+
* ⚠️ THE `?? '0'` IS UNREACHABLE, AND IS WRITTEN RATHER THAN CAST AWAY. `flag()` types its
|
|
199
|
+
* answer `string | undefined` because it serves OPTIONAL props, and `value` here is required,
|
|
200
|
+
* so the branch cannot be taken; a cast would claim a proof the type system has not made.
|
|
201
|
+
*/
|
|
202
|
+
updateForm: (props) => ({ value: flag(props.value) ?? '0' }),
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
export const ProxmoxCephFlagProvider = () =>
|
|
206
|
+
Provider.effect(
|
|
207
|
+
ProxmoxCephFlag,
|
|
208
|
+
Effect.succeed(
|
|
209
|
+
ProxmoxCephFlag.Provider.of({
|
|
210
|
+
/**
|
|
211
|
+
* ⚠️ NOT THE ONE-LINE `Provider.of(handlers)` EVERY OTHER FILE HERE USES, AND THE TWO
|
|
212
|
+
* REASONS ARE BOTH acl.ts's. PVE implements no DELETE on this path, and the factory's
|
|
213
|
+
* read-back guard is dead whenever `attributes` cannot answer undefined. Unlike acl.ts
|
|
214
|
+
* this SPREADS the factory's handlers instead of retyping them, so `list`, `read` and
|
|
215
|
+
* `diff` cannot drift from the shared ones — only the two that genuinely differ are
|
|
216
|
+
* written out below.
|
|
217
|
+
*/
|
|
218
|
+
...handlers,
|
|
219
|
+
/** ⛔ Inert, deliberately. The third ⛔ in the header is the whole argument; read it. */
|
|
220
|
+
delete: () => Effect.void,
|
|
221
|
+
/**
|
|
222
|
+
* ⚠️ THE READ-BACK GUARD, RESTORED FOR A FAMILY WHOSE "ABSENT" DOES NOT EXIST.
|
|
223
|
+
* `ops.reconcile` refuses when the object is still missing after a write, and a flag is
|
|
224
|
+
* never missing, so that check can never fire here — exactly the hole acl.ts patches
|
|
225
|
+
* with `bound`. PVE answers 200 with `{"data":null}` on calls that did nothing, and this
|
|
226
|
+
* PUT's documented return IS null, so the status code carries no evidence at all.
|
|
227
|
+
* ⚠️ THE PER-FLAG PUT IS SYNCHRONOUS (`$rados->mon_command` inline, MEASURED in Ceph.pm),
|
|
228
|
+
* which is what makes this check fair rather than a race against a worker task. If it
|
|
229
|
+
* ever fires spuriously the honest fix is to look at the mon, not to delete the guard.
|
|
230
|
+
*/
|
|
231
|
+
reconcile: Effect.fn(function* ({ news }) {
|
|
232
|
+
const after = yield* handlers.reconcile({ news });
|
|
233
|
+
if (after.value !== news.value) {
|
|
234
|
+
return yield* Effect.die(
|
|
235
|
+
new Error(
|
|
236
|
+
`cluster/ceph/flags/${news.flag}: the PUT returned no error but ceph still ` +
|
|
237
|
+
`reports the flag ${after.value ? 'set' : 'clear'} rather than ` +
|
|
238
|
+
`${news.value ? 'set' : 'clear'}. This endpoint's success is documented as a ` +
|
|
239
|
+
'null body, so the status code proves nothing -- check `ceph osd dump | ' +
|
|
240
|
+
'head -1` and the cluster log. `pause` in particular is two ceph flags read ' +
|
|
241
|
+
'as one; see the header.',
|
|
242
|
+
),
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
return after;
|
|
246
|
+
}),
|
|
247
|
+
}),
|
|
248
|
+
),
|
|
249
|
+
);
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire lane of `Proxmox.CephFs`: how a PVE answer becomes attributes, how a declaration becomes
|
|
3
|
+
* a PVE call, and how that call becomes a fact.
|
|
4
|
+
*
|
|
5
|
+
* ★ SPLIT OUT OF ceph-fs.ts TO KEEP BOTH FILES UNDER THE 250-LINE CAP, and the seam is a real one
|
|
6
|
+
* rather than a convenient line number. ceph-fs.ts answers "what is a CephFS, and when has it
|
|
7
|
+
* changed"; this file answers "what does PVE say, what does it want, and has it finished". On
|
|
8
|
+
* this family that is where all the strangeness lives — the read is an index and not an object,
|
|
9
|
+
* the paths are inverted, the delete takes its arguments in the query string, and both writes
|
|
10
|
+
* fork a worker — so keeping it in one place is what stops the next reader assuming the ordinary
|
|
11
|
+
* shape from the other nineteen resources. ⚠️ NOTHING HERE DECIDES A DIFF.
|
|
12
|
+
*
|
|
13
|
+
* ⚠️ THE `import type` BACK TO ceph-fs.ts IS A CYCLE ON PAPER ONLY. It is type-only and erased
|
|
14
|
+
* before anything runs, exactly as metric-server-form.ts imports `MetricServerProps`. The props
|
|
15
|
+
* stay the resource's public shape, declared in the file that declares the resource.
|
|
16
|
+
*
|
|
17
|
+
* ⛔ POST AND DELETE ANSWER WITH A UPID, NOT A RESULT — THE WRITE IS ASYNCHRONOUS AND A FAILED ONE
|
|
18
|
+
* IS STILL HTTP 200. Read from the cluster's own source, /usr/share/perl5/PVE/API2/Ceph/FS.pm on
|
|
19
|
+
* n2: `createfs` and `destroyfs` both end `return $rpcenv->fork_worker(...)`. Two different lies
|
|
20
|
+
* come out of trusting that status code. A read-back straight after the POST reports "still
|
|
21
|
+
* absent" about a filesystem that is being built as it says so — which is precisely the message
|
|
22
|
+
* `pveOperations.reconcile` would print, pointing at the wrong cause. And a worker that dies
|
|
23
|
+
* INSIDE — the pool create, or the add-storage step, both of which are wrapped in `eval` — is
|
|
24
|
+
* never reported to the caller at all. `settle` below is the answer to both.
|
|
25
|
+
* ⚠️ REASONED FROM THAT SOURCE, NOT MEASURED. Nothing was written to this cluster, so the race
|
|
26
|
+
* has not been observed; only the `fork_worker` that guarantees it is there.
|
|
27
|
+
*
|
|
28
|
+
* ⛔ A DELETE BODY IS SILENTLY DISCARDED BY PVE, SO `remove-pools` GOES IN THE QUERY STRING.
|
|
29
|
+
* MEASURED in /usr/share/perl5/PVE/APIServer/AnyEvent.pm on n2: line 928 reads the request
|
|
30
|
+
* content into params only `if ($method eq 'PUT' || $method eq 'POST')`, and line 1653 sends
|
|
31
|
+
* every other method down a branch that parses `$request->url->query()` and nothing else. So
|
|
32
|
+
* `pve(target, role, 'DELETE', path, form)` would send a body PVE never looks at: the flags would
|
|
33
|
+
* read as unset, the pools would quietly survive a delete that asked for them, and no error would
|
|
34
|
+
* be raised anywhere. sdn-apply.ts already puts `?pending=1` in the path for the same reason.
|
|
35
|
+
*
|
|
36
|
+
* ⚠️ THE TASK POLL RUNS ON THE `read` ROLE THOUGH IT SITS INSIDE reconcile, FOR TWO REASONS. It is
|
|
37
|
+
* a read, and resource.ts already mints `read` for its own read-back inside reconcile. And the
|
|
38
|
+
* leases differ where it matters: `provision` is 300s and explicitly non-renewable, so a create
|
|
39
|
+
* that waits out a slow worker on that lease can expire mid-poll, while `read` is 3600s.
|
|
40
|
+
*
|
|
41
|
+
* ⛔ THE POLLER IS NEVER THE TASK'S OWNER, WHICH COSTS A PRIVILEGE, AND THIS ONE IS MEASURED
|
|
42
|
+
* RATHER THAN FEARED. client.ts mints a fresh credential per call and the mount vends a NEW
|
|
43
|
+
* token id each time, so the token reading the status is never the token recorded in the UPID —
|
|
44
|
+
* and PVE compares them exactly. /usr/share/perl5/PVE/API2/Tasks.pm on n2, `$check_task_user`:
|
|
45
|
+
* `return $user eq $fulltoken || $user eq $task->{user};`, above it the comment "token only sees
|
|
46
|
+
* token tasks, user sees user + token tasks". Token B of the same user matches neither branch.
|
|
47
|
+
* So the fallback is the schema's other clause for `GET /nodes/{node}/tasks/{upid}/status`: "The
|
|
48
|
+
* user needs 'Sys.Audit' permissions on '/nodes/<node>' if they are not the owner of the task."
|
|
49
|
+
* Grant it on the `read` role, or every create burns the full cap below and reports the wrong
|
|
50
|
+
* cause.
|
|
51
|
+
*/
|
|
52
|
+
import * as Effect from 'effect/Effect';
|
|
53
|
+
import type { CephFsAttributes, CephFsProps } from './ceph-fs.ts';
|
|
54
|
+
import { pve } from './client.ts';
|
|
55
|
+
import type { PveTarget } from './credentials.ts';
|
|
56
|
+
import { csv, flag, int, text } from './values.ts';
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* This filesystem's row of the index, as attributes.
|
|
60
|
+
*
|
|
61
|
+
* ⚠️ `GET /nodes/{node}/ceph/fs` ANSWERS AN ARRAY, while the factory hands `attributes` the
|
|
62
|
+
* `Record<string, unknown>` every other PVE read is shaped like. Rows are narrowed rather than
|
|
63
|
+
* trusted, the same way acl.ts narrows its one flat grant list.
|
|
64
|
+
*
|
|
65
|
+
* ⛔ RETURNING undefined IS WHAT MAKES "ABSENT" MEAN ABSENT HERE, unlike acl.ts where the object is
|
|
66
|
+
* always present-but-unbound. A filesystem missing from the index has not been created, and
|
|
67
|
+
* `reconcile` takes the create branch on exactly that signal.
|
|
68
|
+
*/
|
|
69
|
+
export const readRow = (
|
|
70
|
+
live: Record<string, unknown>,
|
|
71
|
+
props: CephFsProps,
|
|
72
|
+
): CephFsAttributes | undefined => {
|
|
73
|
+
const row = (Array.isArray(live) ? live : [])
|
|
74
|
+
.filter(
|
|
75
|
+
(entry): entry is Record<string, unknown> => typeof entry === 'object' && entry !== null,
|
|
76
|
+
)
|
|
77
|
+
.find((entry) => entry['name'] === props.name);
|
|
78
|
+
if (row === undefined) return undefined;
|
|
79
|
+
return {
|
|
80
|
+
data_pool: text(row['data_pool']),
|
|
81
|
+
data_pools: csv(
|
|
82
|
+
Array.isArray(row['data_pools'])
|
|
83
|
+
? row['data_pools'].map((entry: unknown) => String(entry))
|
|
84
|
+
: [],
|
|
85
|
+
),
|
|
86
|
+
metadata_pool: text(row['metadata_pool']),
|
|
87
|
+
metadata_pool_id: int(row['metadata_pool_id'], -1),
|
|
88
|
+
name: props.name,
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/** ⚠️ `exitstatus` is ABSENT while a task runs, and is the exact string `OK` only on success. */
|
|
93
|
+
type TaskStatus = { readonly status?: string; readonly exitstatus?: string };
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* ⚠️ SHORT POLLS, LOW CAP. Ninety seconds covers two pool creates plus the ten seconds `createfs`
|
|
97
|
+
* itself spends waiting for an MDS to go active, and is short enough that a stuck task fails the
|
|
98
|
+
* deploy rather than parking it for the afternoon.
|
|
99
|
+
*/
|
|
100
|
+
const POLL_SECONDS = 2;
|
|
101
|
+
const POLL_ATTEMPTS = 45;
|
|
102
|
+
|
|
103
|
+
/** ⚠️ `nodes/{n}/ceph/fs/{name}` — the POST and DELETE target, and NOT where a read goes. */
|
|
104
|
+
export const objectPath = (props: CephFsProps) => `nodes/${props.node}/ceph/fs/${props.name}`;
|
|
105
|
+
|
|
106
|
+
/** Only the declared flags. ⚠️ An omitted one is PVE's own default, not a value to send. */
|
|
107
|
+
const field = (name: string, value: string | undefined): Record<string, string> =>
|
|
108
|
+
value === undefined ? {} : { [name]: value };
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* ⚠️ `name` IS DELIBERATELY ABSENT FROM THE FORM. It is already the last segment of the path being
|
|
112
|
+
* POSTed to — the metric-server.ts case again — and a second copy can only ever disagree with it.
|
|
113
|
+
*/
|
|
114
|
+
export const createForm = (props: CephFsProps): Record<string, string> => ({
|
|
115
|
+
...field('add-storage', flag(props['add-storage'])),
|
|
116
|
+
...field('pg_num', props.pg_num === undefined ? undefined : String(props.pg_num)),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
/** The delete target with its flags in the query string — see the ⛔ in the header. */
|
|
120
|
+
const destroyPath = (props: CephFsProps) => {
|
|
121
|
+
const query = [
|
|
122
|
+
props['remove-pools'] === true ? 'remove-pools=1' : undefined,
|
|
123
|
+
props['remove-storages'] === true ? 'remove-storages=1' : undefined,
|
|
124
|
+
].filter((part) => part !== undefined);
|
|
125
|
+
return query.length === 0 ? objectPath(props) : `${objectPath(props)}?${query.join('&')}`;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Wait for a forked PVE task, and fail loudly rather than quietly.
|
|
130
|
+
*
|
|
131
|
+
* ⚠️ THE UPID IS PERCENT-ENCODED BECAUSE IT IS FULL OF COLONS — `UPID:n2:00396D3A:…:root@pam:`
|
|
132
|
+
* (MEASURED shape, from `GET /nodes/n2/tasks`) is ONE path segment, not seven. Encoding is what
|
|
133
|
+
* guarantees it arrives as one; PVE's router decodes each segment before matching. REASONED from
|
|
134
|
+
* the URI handling rather than measured — no task of this provider's has been polled yet.
|
|
135
|
+
*/
|
|
136
|
+
const settle = (target: PveTarget, node: string, upid: string, what: string) =>
|
|
137
|
+
Effect.gen(function* () {
|
|
138
|
+
for (let attempt = 0; attempt < POLL_ATTEMPTS; attempt += 1) {
|
|
139
|
+
const status = yield* pve<TaskStatus>(
|
|
140
|
+
target,
|
|
141
|
+
'read',
|
|
142
|
+
'GET',
|
|
143
|
+
`nodes/${node}/tasks/${encodeURIComponent(upid)}/status`,
|
|
144
|
+
).pipe(
|
|
145
|
+
// ⚠️ AN UNREADABLE STATUS IS "NOT SETTLED YET", NOT A FAILURE. It is a missing Sys.Audit
|
|
146
|
+
// (see the header) or a node mid-restart; either way the task is still the cluster's
|
|
147
|
+
// business, and the cap is what ends the wait rather than one unlucky GET.
|
|
148
|
+
Effect.orElseSucceed(() => undefined),
|
|
149
|
+
);
|
|
150
|
+
if (status?.status === 'stopped') {
|
|
151
|
+
const exit = text(status.exitstatus);
|
|
152
|
+
if (exit === 'OK') return;
|
|
153
|
+
return yield* Effect.die(
|
|
154
|
+
new Error(
|
|
155
|
+
`${what}: PVE task ${upid} finished with "${exit === '' ? 'no exit status' : exit}". ` +
|
|
156
|
+
'The write returned HTTP 200 because it only forked the worker -- the real error is ' +
|
|
157
|
+
`in \`pvesh get /nodes/${node}/tasks/${upid}/log\`.`,
|
|
158
|
+
),
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
// ⚠️ A BARE NUMBER IS MILLISECONDS to Effect's Duration, not seconds. The cap in the
|
|
162
|
+
// message below is derived from the same constant so the two cannot drift apart.
|
|
163
|
+
yield* Effect.sleep(POLL_SECONDS * 1000);
|
|
164
|
+
}
|
|
165
|
+
return yield* Effect.die(
|
|
166
|
+
new Error(
|
|
167
|
+
`${what}: PVE task ${upid} was still running after ` +
|
|
168
|
+
`${String(POLL_ATTEMPTS * POLL_SECONDS)}s, or its ` +
|
|
169
|
+
'status could not be read. Check the task log, and check that the `read` role holds ' +
|
|
170
|
+
`Sys.Audit on /nodes/${node} -- each call mints a new token, so the poller is never the ` +
|
|
171
|
+
'task owner and is answered 403 rather than told to wait.',
|
|
172
|
+
),
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* POST the create, then wait for the worker. ⛔ The caller still reads back; see ceph-fs.ts.
|
|
178
|
+
*
|
|
179
|
+
* ⚠️ A CREATE NEEDS A RUNNING *AND* A STANDBY MDS, and refuses BEFORE it forks -- "no running
|
|
180
|
+
* Metadata Server (MDS) found!" / "no standby Metadata Server (MDS) found!" are synchronous, so
|
|
181
|
+
* they surface as a failed POST rather than as a silent worker. TB4 has three (MEASURED: n3
|
|
182
|
+
* `up:active` for cephfs-tb4, n2 and n4 `up:standby`), so this is a note for a smaller cluster.
|
|
183
|
+
* ⚠️ BOTH WRITES CHECK `Sys.Modify` ON `/` -- the root, with the breadth metric-server.ts warns
|
|
184
|
+
* about: granting it buys datacenter options and every other cluster-wide config write too.
|
|
185
|
+
*/
|
|
186
|
+
export const createFs = (props: CephFsProps) =>
|
|
187
|
+
Effect.gen(function* () {
|
|
188
|
+
const upid = text(
|
|
189
|
+
yield* pve<string>(props.target, 'provision', 'POST', objectPath(props), createForm(props)),
|
|
190
|
+
);
|
|
191
|
+
// ⛔ NO UPID MEANS NO WORKER. PVE answers 200 with `{"data":null}` on calls that did nothing,
|
|
192
|
+
// and here that is indistinguishable from success by status code alone. Refusing now is the
|
|
193
|
+
// difference between a clear failure and a state entry for a filesystem nobody built.
|
|
194
|
+
if (upid === '') {
|
|
195
|
+
return yield* Effect.die(
|
|
196
|
+
new Error(
|
|
197
|
+
`${objectPath(props)}: POST returned no task id. PVE wraps every answer in ` +
|
|
198
|
+
'{"data":...} and can report success on a call that did nothing.',
|
|
199
|
+
),
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
yield* settle(props.target, props.node, upid, objectPath(props));
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* DELETE the filesystem, then wait for the worker.
|
|
207
|
+
*
|
|
208
|
+
* ⛔ WHAT THIS TAKES AWAY DEPENDS ENTIRELY ON `remove-pools`, AND THE DEFAULT IS THE SAFE ONE. With
|
|
209
|
+
* it unset PVE removes the filesystem from the MDS map and LEAVES the two pools on disk: every
|
|
210
|
+
* mount breaks, but the bytes are still there for an operator to recover. With it set the pools
|
|
211
|
+
* go too and nothing is recoverable. Leaving it unset also means a later create of the SAME name
|
|
212
|
+
* fails with "ceph pools '<name>_data' and/or '<name>_metadata' already exist" -- that refusal is
|
|
213
|
+
* a BRAKE, not a bug, and it is the last thing standing between a mis-typed rename and the data.
|
|
214
|
+
*/
|
|
215
|
+
export const destroyFs = (props: CephFsProps) =>
|
|
216
|
+
Effect.gen(function* () {
|
|
217
|
+
const upid = text(yield* pve<string>(props.target, 'provision', 'DELETE', destroyPath(props)));
|
|
218
|
+
// ⚠️ A DESTROY THAT ANSWERS WITHOUT A UPID IS NOT REFUSED HERE. `destroyfs` dies synchronously
|
|
219
|
+
// with "no such cephfs" when the filesystem is already gone, which surfaces as a failed
|
|
220
|
+
// DELETE; an empty answer that is not an error is best treated as already-absent, and the
|
|
221
|
+
// caller's read-back is what proves it either way.
|
|
222
|
+
if (upid !== '') yield* settle(props.target, props.node, upid, destroyPath(props));
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* The two read-back failures, as messages rather than as guesses.
|
|
227
|
+
*
|
|
228
|
+
* ⛔ WAITING PROVES THE TASK ENDED; ONLY A READ PROVES THE FILESYSTEM DID OR DID NOT. `createfs`
|
|
229
|
+
* wraps its pool creates in an `eval` and CLEANS THEM UP on error before dying, so a worker can
|
|
230
|
+
* plausibly end having built and then removed everything it made. `destroyfs` refuses outright
|
|
231
|
+
* while a non-disabled `cephfs` storage still references the filesystem. Neither outcome is
|
|
232
|
+
* visible in a status code, which is why ceph-fs.ts reads back after both and dies with these.
|
|
233
|
+
*/
|
|
234
|
+
export const notCreated = (props: CephFsProps) =>
|
|
235
|
+
new Error(
|
|
236
|
+
`${objectPath(props)}: the create task finished but the filesystem is not in ` +
|
|
237
|
+
`GET nodes/${props.node}/ceph/fs. Check the task log -- a PVE worker can exit having ` +
|
|
238
|
+
'rolled back the pools it had just made.',
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
export const notDestroyed = (props: CephFsProps) =>
|
|
242
|
+
new Error(
|
|
243
|
+
`${objectPath(props)}: the destroy task finished but the filesystem is still in ` +
|
|
244
|
+
`GET nodes/${props.node}/ceph/fs. PVE refuses to destroy a CephFS while a PVE storage entry ` +
|
|
245
|
+
'of type cephfs still references it and is not disabled.',
|
|
246
|
+
);
|