@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.CephFs` — a CephFS on the cluster's own Ceph. The filesystem a `cephfs` storage mounts.
|
|
3
|
+
*
|
|
4
|
+
* ⛔ A REPLACE OF THIS RESOURCE DESTROYS A LIVE FILESYSTEM, AND THERE IS NO PUT TO SAVE YOU FROM
|
|
5
|
+
* IT. MEASURED from the published schema on n2, 2026-09-13: `/nodes/{node}/ceph/fs/{name}` has
|
|
6
|
+
* exactly two methods, POST and DELETE. A CephFS cannot be edited in place, so `updateForm` is
|
|
7
|
+
* left undefined and anything the factory can see as changed becomes a REPLACE — and Alchemy's
|
|
8
|
+
* replace is create-then-delete, where the delete is `destroyfs`: the entry leaves the MDS map,
|
|
9
|
+
* every mount on every node breaks, and whether the bytes survive rests on `remove-pools` alone.
|
|
10
|
+
* ★ WHICH IS THE WHOLE REASON `matches` COMPARES NOTHING. Read its ⛔ before adding a field to
|
|
11
|
+
* it. On every other resource in this package a forever-diff is an annoying plan line; on this
|
|
12
|
+
* one it is a live filesystem destroyed on every deploy, forever, because a background daemon
|
|
13
|
+
* moved a number nobody declared.
|
|
14
|
+
*
|
|
15
|
+
* ⛔ THERE IS NO SINGLE-OBJECT READ, WHICH INVERTS `path` AND `collection`. MEASURED on n2:
|
|
16
|
+
* pvesh get /nodes/n2/ceph/fs/cephfs-tb4
|
|
17
|
+
* -> No 'get' handler defined for '/nodes/n2/ceph/fs/cephfs-tb4'
|
|
18
|
+
* `GET /nodes/{node}/ceph/fs` is a directory index and is the only read there is, so `path()`
|
|
19
|
+
* below returns the INDEX and `attributes` picks this filesystem out of the array client-side —
|
|
20
|
+
* the shape acl.ts uses for the one flat `GET /access/acl`. The POST goes to the `{name}` path,
|
|
21
|
+
* so `collection()` is the LONGER of the two strings, the mirror of metric-server.ts.
|
|
22
|
+
* ⚠️ AND THAT IS WHY `delete` AND `reconcile` ARE HAND-WRITTEN BELOW rather than taken from
|
|
23
|
+
* `pveHandlers`. `pveOperations.destroy` deletes `spec.path`, which here is the index:
|
|
24
|
+
* `DELETE /nodes/{node}/ceph/fs` is not a route, so a factory delete would fail every destroy
|
|
25
|
+
* while the filesystem stayed exactly where it was. `reconcile` is hand-written for a second,
|
|
26
|
+
* independent reason — the forked worker — set out in ceph-fs-wire.ts. acl.ts is the
|
|
27
|
+
* precedent for a family that genuinely does not fit; this is the second.
|
|
28
|
+
*
|
|
29
|
+
* ⛔ CREATING A CephFS CREATES TWO CEPH POOLS, AND A `Proxmox.CephPool` MUST NOT ALSO DECLARE THEM.
|
|
30
|
+
* `createfs` builds `<name>_data` and `<name>_metadata` itself and refuses outright if either
|
|
31
|
+
* exists ("ceph pools '…_data' and/or '…_metadata' already exist"). MEASURED on TB4: the live
|
|
32
|
+
* `cephfs-tb4` owns `cephfs-tb4_data` (id 6) and `cephfs-tb4_metadata` (id 7), both of which
|
|
33
|
+
* `GET /nodes/n2/ceph/pool` lists like any other pool — and `/nodes/{node}/ceph/pool/{name}` DOES
|
|
34
|
+
* have GET, PUT and DELETE, so a pool resource would be a clean `pveHandlers` fit and would
|
|
35
|
+
* adopt them without noticing. Declaring both is double management of one object: whichever
|
|
36
|
+
* reconciles first wins, and the loser either fails its create or PUTs `size`/`pg_num` onto pools
|
|
37
|
+
* this resource believes it owns. Declare the filesystem here and leave its two pools alone.
|
|
38
|
+
*
|
|
39
|
+
* ⚠️ `add-storage` IS A SECOND SIDE EFFECT AND IT LANDS IN storage.cfg. Set, `createfs` also calls
|
|
40
|
+
* `PVE::API2::Storage::Config->create` with type `cephfs`, content `backup,iso,vztmpl` and
|
|
41
|
+
* `fs-name <name>`. MEASURED: /etc/pve/storage.cfg on n2 holds exactly that — `cephfs:
|
|
42
|
+
* cephfs-tb4` / `content backup,vztmpl,iso` / `fs-name cephfs-tb4`. So a `Proxmox.Storage`
|
|
43
|
+
* declaring `cephfs-tb4` would be managing an entry this resource created. Declare ONE of them:
|
|
44
|
+
* either `add-storage` here and no storage resource, or `add-storage` off and a
|
|
45
|
+
* `Proxmox.Storage` that reads this resource's `name` attribute — which also gets the ordering
|
|
46
|
+
* right for free, because Alchemy orders by data flow.
|
|
47
|
+
*
|
|
48
|
+
* ⚠️ `node` IS A ROUTE, NOT IDENTITY, AND MUST NEVER BE DIFFED. A CephFS is cluster-wide; the node
|
|
49
|
+
* in the path only says which node PVE proxies the call to. MEASURED: `GET /nodes/n2/ceph/fs`,
|
|
50
|
+
* `…/n3/…` and `…/n4/…` returned the byte-identical array. So changing `node` plans `noop`, and
|
|
51
|
+
* two resources naming the same `name` on different nodes are the SAME filesystem — the vmid
|
|
52
|
+
* hazard from lxc.ts, with a filesystem on the end of it instead of a container.
|
|
53
|
+
*
|
|
54
|
+
* ⚠️ PRIVILEGES: the read lane needs `Sys.Audit` OR `Datastore.Audit` on `/`, which an auditor
|
|
55
|
+
* role already holds — unlike storage.ts, nothing needs widening before the first plan. The
|
|
56
|
+
* write side is in ceph-fs-wire.ts beside the calls, and in docs/privileges.md with the rest.
|
|
57
|
+
*/
|
|
58
|
+
import { Resource } from 'alchemy';
|
|
59
|
+
import { isResolved } from 'alchemy/Diff';
|
|
60
|
+
import * as Provider from 'alchemy/Provider';
|
|
61
|
+
import * as Effect from 'effect/Effect';
|
|
62
|
+
import {
|
|
63
|
+
createForm,
|
|
64
|
+
createFs,
|
|
65
|
+
destroyFs,
|
|
66
|
+
notCreated,
|
|
67
|
+
notDestroyed,
|
|
68
|
+
objectPath,
|
|
69
|
+
readRow,
|
|
70
|
+
} from './ceph-fs-wire.ts';
|
|
71
|
+
import { type PveRequirements, type WithTarget, pveOperations } from './resource.ts';
|
|
72
|
+
|
|
73
|
+
export interface CephFsProps extends WithTarget {
|
|
74
|
+
/** ⚠️ Which node answers the call, NOT which node holds the filesystem. See the header. */
|
|
75
|
+
node: string;
|
|
76
|
+
/**
|
|
77
|
+
* The filesystem name, and the cluster-wide primary key. PVE's pattern is `^[^:/\s]+$`.
|
|
78
|
+
* ⛔ CHANGING IT IS A REPLACE, WHICH DESTROYS THE OLD FILESYSTEM. `diff` says so explicitly
|
|
79
|
+
* below rather than letting the factory report a create-shaped `update` and orphan the old one.
|
|
80
|
+
*/
|
|
81
|
+
name: string;
|
|
82
|
+
/**
|
|
83
|
+
* Placement groups for the backing data pool; the metadata pool gets a quarter of it, or 8 when
|
|
84
|
+
* this is under 32. PVE's default is 128.
|
|
85
|
+
*
|
|
86
|
+
* ⛔ CREATE-TIME ONLY, UNREADABLE, AND OWNED BY THE AUTOSCALER FROM THE SECOND IT LANDS. It is
|
|
87
|
+
* never compared — see the ⛔ on `matches` — and this is not caution, it is measured. TB4's
|
|
88
|
+
* `cephfs-tb4` was created with the default 128 (the arithmetic proves it: `cephfs-tb4_metadata`
|
|
89
|
+
* sits at 32, which is 128/4), and `cephfs-tb4_data` reads pg_num 32 TODAY. The autoscaler moved
|
|
90
|
+
* it by a factor of four, and it is not finished: `pg_autoscale_mode` is `on` for every pool on
|
|
91
|
+
* this cluster and `cephfs-tb4_metadata` already reports `pg_num_final: 16` against its live 32.
|
|
92
|
+
*/
|
|
93
|
+
pg_num?: number;
|
|
94
|
+
/**
|
|
95
|
+
* Also write a `cephfs` storage entry for this filesystem. ⚠️ Create-time only and never read
|
|
96
|
+
* back — it is an ACTION, not a field. See the header for what it does to `Proxmox.Storage`.
|
|
97
|
+
*/
|
|
98
|
+
'add-storage'?: boolean;
|
|
99
|
+
/** ⛔ DELETE-TIME ONLY, AND IT ERASES THE DATA. Read the ⛔ on `destroyFs` before setting it. */
|
|
100
|
+
'remove-pools'?: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Delete the pveceph-managed storage entries too. ⚠️ PVE refuses unless they are already
|
|
103
|
+
* disabled ("storage '…' is not disabled, make sure to disable and unmount the storage first"),
|
|
104
|
+
* so this fails the destroy rather than unmounting anything out from under a running guest.
|
|
105
|
+
*/
|
|
106
|
+
'remove-storages'?: boolean;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* ⚠️ EVERY FIELD HERE IS REPORTED AND NONE IS COMPARED. They exist so a plan, and the state entry
|
|
111
|
+
* behind it, can say what the declaration actually points at — which pools hold the bytes.
|
|
112
|
+
*/
|
|
113
|
+
export interface CephFsAttributes {
|
|
114
|
+
name: string;
|
|
115
|
+
/** The first data pool. PVE keeps it for compatibility; `data_pools` is the real answer. */
|
|
116
|
+
data_pool: string;
|
|
117
|
+
/**
|
|
118
|
+
* ⚠️ SORTED, AND IT IS A SET RATHER THAN A LIST. A CephFS can hold several data pools — added
|
|
119
|
+
* with `ceph fs add_data_pool`, which this resource has no way to express — and PVE reports
|
|
120
|
+
* them in the MDS map's own order. Sorting is what keeps this attribute stable across reads.
|
|
121
|
+
*/
|
|
122
|
+
data_pools: string;
|
|
123
|
+
metadata_pool: string;
|
|
124
|
+
/**
|
|
125
|
+
* ⚠️ AN INTEGER THAT NAMES SOMETHING WRITTEN AS A NAME, AND THEREFORE A CLASSIC FOREVER-DIFF IF
|
|
126
|
+
* IT EVER REACHED `matches`. PVE answers `metadata_pool_id: 7` while every write in this family
|
|
127
|
+
* spells a pool by name. It is here to be read, never to be compared. `-1` means absent: the
|
|
128
|
+
* field is optional in the schema and older PVE omits it.
|
|
129
|
+
*/
|
|
130
|
+
metadata_pool_id: number;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface ProxmoxCephFs extends Resource<
|
|
134
|
+
'Proxmox.CephFs',
|
|
135
|
+
CephFsProps,
|
|
136
|
+
CephFsAttributes,
|
|
137
|
+
never,
|
|
138
|
+
PveRequirements
|
|
139
|
+
> {}
|
|
140
|
+
|
|
141
|
+
/** ★ `retain` by default — a filesystem holding data cannot be rebuilt. See the ★ in resource.ts. */
|
|
142
|
+
export const ProxmoxCephFs = Resource<ProxmoxCephFs>('Proxmox.CephFs', {
|
|
143
|
+
defaultRemovalPolicy: 'retain',
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const ops = pveOperations<CephFsProps, CephFsAttributes>({
|
|
147
|
+
attributes: readRow,
|
|
148
|
+
/** ⚠️ The `{name}` path — longer than `path()`, deliberately. See the second ⛔ in the header. */
|
|
149
|
+
collection: objectPath,
|
|
150
|
+
createForm,
|
|
151
|
+
/**
|
|
152
|
+
* ⛔ NOTHING IS COMPARED, AND THAT IS THE MOST DELIBERATE LINE IN THIS FILE. A CephFS has no
|
|
153
|
+
* readable, writable field: the index returns only the name and the pools behind it, and every
|
|
154
|
+
* prop this resource accepts is create-time or delete-time. So "present" IS "settled", and the
|
|
155
|
+
* only honest comparison is none at all.
|
|
156
|
+
* ⚠️ THE TEMPTING ADDITION IS `pg_num`, AND IT WOULD BE THE WORST BUG THIS PACKAGE HAS SHIPPED.
|
|
157
|
+
* It is not in the index at all, so it can only be read from the pool endpoint — where the
|
|
158
|
+
* autoscaler owns it. MEASURED on TB4 today: created at 128, live at 32, with the metadata
|
|
159
|
+
* pool already scheduled down to 16. Compared, that is an eternal mismatch; and because
|
|
160
|
+
* `updateForm` is undefined, the action it produces is REPLACE. The plan would destroy and
|
|
161
|
+
* rebuild a live filesystem on every deploy, triggered by a daemon on its own schedule.
|
|
162
|
+
* ⚠️ `data_pools` AND `metadata_pool_id` ARE OUT FOR THE ORDINARY REASONS: a set PVE returns
|
|
163
|
+
* in its own order, and an integer id for something written as a name. `name` is out because
|
|
164
|
+
* it is the FILTER that produced these attributes, so comparing it is true by construction —
|
|
165
|
+
* an identity change is handled in `diff`, not here. `node` is out because it is a route.
|
|
166
|
+
*/
|
|
167
|
+
matches: () => true,
|
|
168
|
+
/** ⚠️ THE INDEX, NOT THE OBJECT. There is no GET on `{name}`; see the second ⛔ in the header. */
|
|
169
|
+
path: (props) => `nodes/${props.node}/ceph/fs`,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
export const ProxmoxCephFsProvider = () =>
|
|
173
|
+
Provider.effect(
|
|
174
|
+
ProxmoxCephFs,
|
|
175
|
+
Effect.succeed(
|
|
176
|
+
ProxmoxCephFs.Provider.of({
|
|
177
|
+
/**
|
|
178
|
+
* ⛔ EMPTY, LIKE EVERY OTHER RESOURCE HERE. The index answers with every filesystem on the
|
|
179
|
+
* cluster, `cephfs-tb4` included — the one holding this estate's ISOs and templates.
|
|
180
|
+
* Adopting it would put Alchemy one `destroy` away from it. Adoption stays explicit.
|
|
181
|
+
*/
|
|
182
|
+
list: () => Effect.succeed([]),
|
|
183
|
+
read: Effect.fn(function* ({ olds }) {
|
|
184
|
+
return yield* ops.read(olds);
|
|
185
|
+
}),
|
|
186
|
+
/**
|
|
187
|
+
* ⛔ A CHANGED `name` IS A REPLACE, AND THE FACTORY CANNOT SAY SO ALONE. Left to delegate,
|
|
188
|
+
* a rename reads the NEW name, finds nothing, and returns the drift-shaped `update` that
|
|
189
|
+
* `pveOperations.diff` uses for "Alchemy has state, the cluster does not" — so reconcile
|
|
190
|
+
* would build the new filesystem and LEAVE THE OLD ONE, with its pools, its storage entry
|
|
191
|
+
* and its bytes, owned by nobody and invisible in every later plan.
|
|
192
|
+
* ⛔ SAYING `replace` IS HONEST, AND IT IS ALSO WHAT MAKES A TYPO EXPENSIVE. Alchemy
|
|
193
|
+
* creates first and deletes second, so the old filesystem is destroyed AFTER the new
|
|
194
|
+
* one exists — with `remove-pools` unset the bytes are stranded rather than erased, and
|
|
195
|
+
* that default is the only thing between a mis-typed rename and an unrecoverable one.
|
|
196
|
+
* Do not "simplify" this back to the factory: the alternative is not safer, it is
|
|
197
|
+
* silent. ⚠️ `node` is NOT part of identity and must never join this comparison.
|
|
198
|
+
*/
|
|
199
|
+
diff: Effect.fn(function* ({ news, output }) {
|
|
200
|
+
if (output !== undefined && isResolved(news) && news.name !== output.name) {
|
|
201
|
+
return { action: 'replace' } as const;
|
|
202
|
+
}
|
|
203
|
+
return yield* ops.diff(news, output);
|
|
204
|
+
}),
|
|
205
|
+
/**
|
|
206
|
+
* ⛔ NOT `ops.reconcile`, BECAUSE ITS READ-BACK RACES A FORKED WORKER. The POST returns a
|
|
207
|
+
* UPID and nothing else; `createFs` waits the task out and surfaces its real error. The
|
|
208
|
+
* read-back the factory does is kept here rather than dropped — waiting proves the task
|
|
209
|
+
* ENDED, reading proves the filesystem EXISTS, and PVE has shipped tasks that end OK
|
|
210
|
+
* having done nothing.
|
|
211
|
+
* ⚠️ AN EXISTING FILESYSTEM IS RETURNED UNTOUCHED. There is no update call to make, so the
|
|
212
|
+
* only write this handler can perform is a create; `diff` has already decided that a
|
|
213
|
+
* real change is a replace, and Alchemy calls delete for the old generation itself.
|
|
214
|
+
*/
|
|
215
|
+
reconcile: Effect.fn(function* ({ news }) {
|
|
216
|
+
const live = yield* ops.read(news);
|
|
217
|
+
if (live !== undefined) return live;
|
|
218
|
+
yield* createFs(news);
|
|
219
|
+
const after = yield* ops.read(news);
|
|
220
|
+
if (after === undefined) return yield* Effect.die(notCreated(news));
|
|
221
|
+
return after;
|
|
222
|
+
}),
|
|
223
|
+
/**
|
|
224
|
+
* ⛔ NOT `ops.destroy`, AND NOT AN OVERSIGHT. It would DELETE `spec.path`, which on this
|
|
225
|
+
* family is the index — a route PVE does not implement — so every destroy would fail
|
|
226
|
+
* while the filesystem stayed exactly where it was. The read-back afterwards is what
|
|
227
|
+
* turns "the task said OK" into "it is actually gone": `destroyfs` refuses while a
|
|
228
|
+
* non-disabled `cephfs` storage still references the filesystem, and that refusal must
|
|
229
|
+
* fail the destroy rather than letting Alchemy drop the state entry for a live object.
|
|
230
|
+
*/
|
|
231
|
+
delete: Effect.fn(function* ({ olds }) {
|
|
232
|
+
/**
|
|
233
|
+
* ⚠️ A FILESYSTEM SOMEBODY ALREADY REMOVED BY HAND IS NOT AN ERROR. `destroyfs` dies
|
|
234
|
+
* synchronously with "no such cephfs", which would fail every destroy of a stack whose
|
|
235
|
+
* filesystem was cleaned up outside Alchemy and leave the state entry unremovable
|
|
236
|
+
* without editing the store. ⚠️ THE COST IS STATED PLAINLY: `ops.read` folds a FAILED
|
|
237
|
+
* read into "absent" (the storage.ts trap), so a read this credential cannot perform
|
|
238
|
+
* turns this into a destroy that reports success having done nothing. That is not a
|
|
239
|
+
* new hazard — a broken read defeats the read-back below in exactly the same way — but
|
|
240
|
+
* it is the reason a 403 on this family must be fixed rather than lived with.
|
|
241
|
+
*/
|
|
242
|
+
if ((yield* ops.read(olds)) === undefined) return;
|
|
243
|
+
yield* destroyFs(olds);
|
|
244
|
+
const after = yield* ops.read(olds);
|
|
245
|
+
if (after !== undefined) return yield* Effect.die(notDestroyed(olds));
|
|
246
|
+
}),
|
|
247
|
+
}),
|
|
248
|
+
),
|
|
249
|
+
);
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CRUSH tree, walked down to one OSD leaf.
|
|
3
|
+
*
|
|
4
|
+
* ★ SPLIT OUT OF ceph-osd.ts TO KEEP BOTH FILES UNDER THE 250-LINE CAP, and the seam is the same
|
|
5
|
+
* one metric-server-form.ts cuts: this file answers "what does the cluster say about osd.N",
|
|
6
|
+
* ceph-osd.ts answers "what is an OSD and what may a plan do about it". Nothing here calls the
|
|
7
|
+
* API and nothing here decides a diff. `CephOsdAttributes` lives on this side of the seam
|
|
8
|
+
* because it is a description of what the TREE says — keeping each field's
|
|
9
|
+
* reported-never-compared reasoning against the line that builds it — and ceph-osd.ts re-exports
|
|
10
|
+
* it so a consumer still imports one name from one place. The `import type` back for
|
|
11
|
+
* `CephOsdProps` is a cycle on paper only: it is erased before anything runs.
|
|
12
|
+
*
|
|
13
|
+
* ⛔ `GET /nodes/{node}/ceph/osd` ANSWERS A TREE, NOT A LIST, AND IT IS THE WHOLE CLUSTER'S TREE.
|
|
14
|
+
* MEASURED 2026-09-13: asked of n2 it answers `{"flags":…,"root":{"children":[…]}}` whose
|
|
15
|
+
* leaves include osd.0 and osd.5 on n4; asked of n3 it answers the identical six leaves. The
|
|
16
|
+
* `{node}` in the path chooses WHO ANSWERS, not what is listed. A provider that read it as "the
|
|
17
|
+
* OSDs on this node" would find osd.0 through n2 and then report it as living there.
|
|
18
|
+
*
|
|
19
|
+
* ⛔ AND THE SINGLE-OBJECT PATH CANNOT BE USED INSTEAD — THIS WALK EXISTS BECAUSE IT LIES.
|
|
20
|
+
* MEASURED: `GET /nodes/n2/ceph/osd/99`, an id with no OSD behind it, answers HTTP 200 with
|
|
21
|
+
* `[{"name":"metadata"},{"name":"lv-info"}]` — byte for byte what `…/osd/2` answers for a real
|
|
22
|
+
* one. It is a directory index (its schema says `permissions: {"user":"all"}`), not a read of
|
|
23
|
+
* the OSD. Point `PveSpec.path` at it and every declared OSD is "present": `diff` reports
|
|
24
|
+
* `noop` over a dead replica forever, and `reconcile`'s read-back guard — the one thing that
|
|
25
|
+
* stops this package recording objects that do not exist — passes on an OSD that was never
|
|
26
|
+
* built. The only endpoint that can tell present from absent is the tree.
|
|
27
|
+
*
|
|
28
|
+
* ★ MEASURED ON TB4 ON 2026-09-13, `GET /nodes/n2/ceph/osd`. Six leaves, all `up` and `in`, Ceph
|
|
29
|
+
* 20.2.2 tentacle:
|
|
30
|
+
*
|
|
31
|
+
* id name host device_class crush_weight reweight pgs status
|
|
32
|
+
* 0 osd.0 n4 ssd 1.86299133300781 1 99 up
|
|
33
|
+
* 5 osd.5 n4 ssd 1.86299133300781 1 94 up
|
|
34
|
+
* 1 osd.1 n3 ssd 1.86299133300781 1 96 up
|
|
35
|
+
* 4 osd.4 n3 ssd 1.81939697265625 1 97 up
|
|
36
|
+
* 2 osd.2 n2 ssd 1.81939697265625 1 88 up
|
|
37
|
+
* 3 osd.3 n2 ssd 1.81939697265625 1 105 up
|
|
38
|
+
*
|
|
39
|
+
* `{ node: 'n2', osdid: 2, host: 'n2', device_class: 'ssd' }` plans `noop` against that row.
|
|
40
|
+
* That is the acceptance test this file was written to pass.
|
|
41
|
+
*
|
|
42
|
+
* ⚠️ EVERY OTHER COLUMN IS REPORTED AND NEVER COMPARED, FOR FOUR DIFFERENT REASONS. `pgs` is the
|
|
43
|
+
* autoscaler's placement rather than anybody's declaration — MEASURED, it spans 88 to 105 across
|
|
44
|
+
* six identical disks, and Ceph moves PGs whenever it rebalances. `crush_weight` is a FLOAT
|
|
45
|
+
* derived from device size (MEASURED: 1.86299133300781), so comparing it is float equality
|
|
46
|
+
* against a number Ceph recomputes on `crush reweight`. `reweight` is what
|
|
47
|
+
* `reweight-by-utilization` writes, unasked. `status` and `in` are daemon state: an OSD that
|
|
48
|
+
* flaps down while a node reboots would otherwise put work in the plan that no write this
|
|
49
|
+
* provider owns could ever clear. Each of those is the forever-update this package has already
|
|
50
|
+
* been bitten by, and each is out of `settled` below.
|
|
51
|
+
*
|
|
52
|
+
* ⚠️ THE CLUSTER'S OSD FLAGS ARE NOT AN ATTRIBUTE EITHER. The read carries
|
|
53
|
+
* `"flags":"sortbitwise,recovery_deletes,purged_snapdirs,pglog_hardlimit"` NEXT TO the root
|
|
54
|
+
* rather than inside any leaf: they describe the OSDMap, not this OSD. Recording them per-OSD
|
|
55
|
+
* would rewrite all six resources' state the moment somebody set `noout` for ten minutes of
|
|
56
|
+
* maintenance — the churn-that-reads-like-drift that keeps `digest` out of storage.ts.
|
|
57
|
+
*
|
|
58
|
+
*/
|
|
59
|
+
import * as Effect from 'effect/Effect';
|
|
60
|
+
import type { CephOsdProps } from './ceph-osd.ts';
|
|
61
|
+
import { pve } from './client.ts';
|
|
62
|
+
import { bool, int, num, text } from './values.ts';
|
|
63
|
+
|
|
64
|
+
/** A CRUSH node. Buckets carry `children`; an OSD leaf carries `type: 'osd'`. */
|
|
65
|
+
type CrushNode = Record<string, unknown>;
|
|
66
|
+
|
|
67
|
+
const branches = (node: CrushNode): CrushNode[] =>
|
|
68
|
+
(Array.isArray(node['children']) ? node['children'] : []).filter(
|
|
69
|
+
(child): child is CrushNode => typeof child === 'object' && child !== null,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Every OSD leaf under the tree root, at any depth.
|
|
74
|
+
*
|
|
75
|
+
* ⚠️ RECURSIVE BECAUSE THE TREE IS NOT THREE LEVELS DEEP BY LAW. TB4 measures as
|
|
76
|
+
* root(`default`) -> host(n2|n3|n4) -> osd, but CRUSH admits datacenter, rack and chassis
|
|
77
|
+
* buckets between them, and an OSD created outside a host bucket hangs off the root. Reaching
|
|
78
|
+
* in as `root.children[].children[]` would read this cluster correctly and answer "absent" —
|
|
79
|
+
* i.e. "the replica is gone" — on the first cluster that has a rack in it.
|
|
80
|
+
*
|
|
81
|
+
* ⚠️ THE OUTERMOST OBJECT IS NOT ITSELF A BUCKET. MEASURED: `root` is `{"children":[…],"leaf":0}`
|
|
82
|
+
* with no `type` and no `name`; the bucket called `default` is its first child. So the walk
|
|
83
|
+
* starts at `tree.root` and selects on `type === 'osd'` rather than assuming a shape.
|
|
84
|
+
*/
|
|
85
|
+
export const osdLeaves = (tree: CrushNode): CrushNode[] => {
|
|
86
|
+
const root = tree['root'];
|
|
87
|
+
const start: CrushNode = typeof root === 'object' && root !== null ? (root as CrushNode) : {};
|
|
88
|
+
const walk = (node: CrushNode): CrushNode[] => [
|
|
89
|
+
...(node['type'] === 'osd' ? [node] : []),
|
|
90
|
+
...branches(node).flatMap(walk),
|
|
91
|
+
];
|
|
92
|
+
return walk(start);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The leaf for one OSD id, or undefined when nothing in the tree carries it.
|
|
97
|
+
*
|
|
98
|
+
* ⛔ `id` COMES BACK AS A STRING HERE AND AS AN INTEGER EVERYWHERE ELSE, which is the "an id
|
|
99
|
+
* returned one way and written another" forever-diff in its most expensive form. MEASURED: the
|
|
100
|
+
* leaf for osd.2 carries `"id":"2"` (and the host buckets carry `"id":"-7"`), while
|
|
101
|
+
* `GET …/osd/2/metadata` carries `"id":2` and the `osdid` parameter on POST and DELETE is typed
|
|
102
|
+
* integer. `'2' === 2` is false, so a provider comparing the raw value finds NOTHING, calls
|
|
103
|
+
* every declared OSD absent — and, with a create path wired up, offers to build six OSDs over
|
|
104
|
+
* six live disks. `int` from values.ts is the narrowing, and `Number.NaN` is the fallback
|
|
105
|
+
* precisely because it equals nothing, id 0 included.
|
|
106
|
+
*
|
|
107
|
+
* ⚠️ FIRST MATCH WINS. OSD ids are unique cluster-wide, and PVE does not put the per-device-class
|
|
108
|
+
* shadow buckets (`default~ssd`) in this tree — MEASURED: the whole tree is one `default` root,
|
|
109
|
+
* three hosts, six leaves. If a future PVE did include them, taking the first match keeps the
|
|
110
|
+
* answer stable instead of reporting drift depending on which copy was walked into first.
|
|
111
|
+
*/
|
|
112
|
+
export const findOsd = (tree: CrushNode, osdid: number) =>
|
|
113
|
+
osdLeaves(tree).find((leaf) => int(leaf['id'], Number.NaN) === osdid);
|
|
114
|
+
|
|
115
|
+
export interface CephOsdAttributes {
|
|
116
|
+
/** Normalised to an integer here even though the tree spells it `"2"`. */
|
|
117
|
+
osdid: number;
|
|
118
|
+
/** `osd.2` — how Ceph, the UI and every log line name it. */
|
|
119
|
+
name: string;
|
|
120
|
+
/** COMPARED, but only when `props.host` is declared. */
|
|
121
|
+
host: string;
|
|
122
|
+
/** COMPARED, but only when `props.device_class` is declared. */
|
|
123
|
+
device_class: string;
|
|
124
|
+
/**
|
|
125
|
+
* ⚠️ REPORTED, NEVER COMPARED — `bluestore` on every live OSD and filestore has been gone for
|
|
126
|
+
* years, so declaring it would add a field that cannot disagree. It is recorded because a
|
|
127
|
+
* future objectstore change is exactly the kind of thing state should have caught in writing.
|
|
128
|
+
*/
|
|
129
|
+
osdtype: string;
|
|
130
|
+
/** ⚠️ REPORTED, NEVER COMPARED. Daemon state — see the second ⚠️ in the header. */
|
|
131
|
+
status: string;
|
|
132
|
+
/** ⚠️ REPORTED, NEVER COMPARED. Ceph itself sets this to 0 after `mon_osd_down_out_interval`. */
|
|
133
|
+
in: boolean;
|
|
134
|
+
/** ⚠️ REPORTED, NEVER COMPARED. A float Ceph recomputes; comparing it is a permanent diff. */
|
|
135
|
+
crush_weight: number;
|
|
136
|
+
/** ⚠️ REPORTED, NEVER COMPARED. `reweight-by-utilization` owns this number, not a declaration. */
|
|
137
|
+
reweight: number;
|
|
138
|
+
/** ⚠️ REPORTED, NEVER COMPARED. The autoscaler moves PGs between OSDs whenever it likes. */
|
|
139
|
+
pgs: number;
|
|
140
|
+
/** Device capacity in bytes. Stable per disk, and the honest answer to "how big is this OSD". */
|
|
141
|
+
total_space: number;
|
|
142
|
+
/** `20.2.2`. ⚠️ Reported, never compared — an upgrade is not this resource's drift. */
|
|
143
|
+
ceph_version: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* One leaf, as the attributes a plan and the state store get to see.
|
|
148
|
+
*
|
|
149
|
+
* ⚠️ THE TELEMETRY IS DELIBERATELY DROPPED, NOT FORGOTTEN. The leaf also carries `bytes_used`,
|
|
150
|
+
* `percent_used`, `apply_latency_ms` and `commit_latency_ms`. MEASURED, AND THE MEASUREMENT SAYS
|
|
151
|
+
* THE OPPOSITE OF WHAT YOU WOULD EXPECT: across two reads minutes apart, all four were identical
|
|
152
|
+
* on every OSD, and both latencies read 0 — TB4 was idle, so nothing was caught in the act. That
|
|
153
|
+
* these move is therefore REASONED, from what they count, not observed. Alchemy persists
|
|
154
|
+
* attributes; on a cluster doing work, keeping them would rewrite all six resources' state on
|
|
155
|
+
* every deploy and record a number that is stale by the time anybody reads it. What is kept is
|
|
156
|
+
* what a human asking "what is this replica, and is it healthy" needs; it is not a metrics feed.
|
|
157
|
+
*
|
|
158
|
+
* ⚠️ `osdid` AND `name` ARE THE NORMALISED ID AND ITS SPELLING, taken from the leaf rather than
|
|
159
|
+
* echoed from props, so that state records what the cluster said. They agree by construction
|
|
160
|
+
* here — the leaf was found BY id — but echoing props into attributes is how a resource ends up
|
|
161
|
+
* recording a declaration as though it were an observation.
|
|
162
|
+
*/
|
|
163
|
+
export const osdAttributes = (leaf: CrushNode, props: CephOsdProps): CephOsdAttributes => ({
|
|
164
|
+
ceph_version: text(leaf['ceph_version_short']),
|
|
165
|
+
crush_weight: num(leaf['crush_weight'], 0),
|
|
166
|
+
device_class: text(leaf['device_class']),
|
|
167
|
+
host: text(leaf['host']),
|
|
168
|
+
in: bool(leaf['in']),
|
|
169
|
+
name: text(leaf['name'], `osd.${String(props.osdid)}`),
|
|
170
|
+
osdid: int(leaf['id'], props.osdid),
|
|
171
|
+
osdtype: text(leaf['osdtype']),
|
|
172
|
+
pgs: num(leaf['pgs'], 0),
|
|
173
|
+
reweight: num(leaf['reweight'], 0),
|
|
174
|
+
status: text(leaf['status']),
|
|
175
|
+
total_space: num(leaf['total_space'], 0),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The live OSD as attributes, or undefined.
|
|
180
|
+
*
|
|
181
|
+
* ★ IT LIVES HERE RATHER THAN IN ceph-osd.ts BECAUSE THE READ SIDE IS THIS FILE'S WHOLE JOB —
|
|
182
|
+
* `findOsd` and `osdAttributes` are the two halves it composes, and keeping the caller beside
|
|
183
|
+
* them is what let ceph-osd.ts come back under the 250-line cap when create and delete were
|
|
184
|
+
* added. ⚠️ A 404 or a 403 both arrive here as `undefined`: see the ⛔ on `readRole` in
|
|
185
|
+
* resource.ts for why that distinction is invisible and what it costs.
|
|
186
|
+
*/
|
|
187
|
+
export const readOsd = (props: CephOsdProps) =>
|
|
188
|
+
pve<Record<string, unknown>>(props.target, 'read', 'GET', `nodes/${props.node}/ceph/osd`).pipe(
|
|
189
|
+
Effect.map((t) => {
|
|
190
|
+
const leaf = t === undefined ? undefined : findOsd(t, props.osdid);
|
|
191
|
+
return leaf === undefined ? undefined : osdAttributes(leaf, props);
|
|
192
|
+
}),
|
|
193
|
+
);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two operations that change a disk: building an OSD and removing one.
|
|
3
|
+
*
|
|
4
|
+
* ★ SPLIT OUT OF ceph-osd.ts FOR THE 250-LINE CAP, and the seam is the honest one — this file is
|
|
5
|
+
* everything that WRITES, and ceph-osd.ts is the declaration and the assertion. A reader asking
|
|
6
|
+
* "can this destroy my data" has one file to read.
|
|
7
|
+
*
|
|
8
|
+
* ⛔ BOTH OF THESE ARE IRREVERSIBLE. `create` hands a block device to `ceph-volume lvm create`,
|
|
9
|
+
* which zaps it; `delete` takes a replica out of a live pool and Ceph immediately begins
|
|
10
|
+
* rewriting the missing copies across the remaining nodes. `Proxmox.CephOsd` therefore declares
|
|
11
|
+
* `defaultRemovalPolicy: 'retain'`, so removing a declaration does NOT reach `destroyOsd` — a
|
|
12
|
+
* caller opts in with `.pipe(RemovalPolicy.destroy())`.
|
|
13
|
+
*/
|
|
14
|
+
import * as Effect from 'effect/Effect';
|
|
15
|
+
import { readOsd } from './ceph-osd-tree.ts';
|
|
16
|
+
import type { CephOsdProps } from './ceph-osd.ts';
|
|
17
|
+
import { pve } from './client.ts';
|
|
18
|
+
|
|
19
|
+
const describe = (props: CephOsdProps) => `osd.${String(props.osdid)} on ${props.node}`;
|
|
20
|
+
|
|
21
|
+
export const createOsd = Effect.fn(function* (news: CephOsdProps) {
|
|
22
|
+
if (news.dev === undefined) {
|
|
23
|
+
return yield* Effect.die(
|
|
24
|
+
new Error(
|
|
25
|
+
`${describe(news)}: no such OSD in the CRUSH tree, and no \`dev\` was declared. ` +
|
|
26
|
+
'Without a device this resource is an ASSERTION over an OSD that must already ' +
|
|
27
|
+
'exist. Declare `dev` to let it create one -- which WIPES that device -- or fix ' +
|
|
28
|
+
'the declaration; if the disk was there yesterday, a replica is missing and Ceph ' +
|
|
29
|
+
'is degraded.',
|
|
30
|
+
),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
yield* pve(news.target, 'provision', 'POST', `nodes/${news.node}/ceph/osd`, {
|
|
34
|
+
dev: news.dev,
|
|
35
|
+
...(news.device_class === undefined ? {} : { 'crush-device-class': news.device_class }),
|
|
36
|
+
});
|
|
37
|
+
const created = yield* readOsd(news);
|
|
38
|
+
if (created === undefined) {
|
|
39
|
+
return yield* Effect.die(
|
|
40
|
+
new Error(
|
|
41
|
+
`${describe(news)}: the create returned no error but osd.${String(news.osdid)} is ` +
|
|
42
|
+
'still absent. Ceph allocates OSD ids and cannot be told which to use, so it most ' +
|
|
43
|
+
'likely assigned a different number -- check `ceph osd tree`, and declare the id it ' +
|
|
44
|
+
'actually gave the device rather than deploying again.',
|
|
45
|
+
),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
return created;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* ⛔ REMOVING THE DECLARATION REMOVES THE ASSERTION, NOT THE REPLICA, AND THAT ASYMMETRY IS THE
|
|
53
|
+
* POINT. `DELETE /nodes/{node}/ceph/osd/{osdid}` takes a replica out of a live pool — with
|
|
54
|
+
* `nobackfill`/`norecover` unset, Ceph immediately starts rewriting the missing copies across
|
|
55
|
+
* the two remaining nodes — and with `cleanup=1` it zaps the LVs as well. A stack file is
|
|
56
|
+
* edited by people in a hurry; a disk is not a thing a deleted line gets to destroy. Take an
|
|
57
|
+
* OSD out by hand, with the cluster healthy and `ceph -s` in front of you: `ceph osd out N`,
|
|
58
|
+
* wait for the backfill to finish, then `pveceph osd destroy N`.
|
|
59
|
+
*
|
|
60
|
+
* ★ SO IT IS IMPLEMENTED, AND THE RESOURCE DEFAULTS TO `retain` INSTEAD. Removing a declaration
|
|
61
|
+
* drops the state row and leaves the replica alone; a caller who really means it opts in with
|
|
62
|
+
* `.pipe(RemovalPolicy.destroy())`. That is the same shape Terraform gives `prevent_destroy`,
|
|
63
|
+
* and it beats a `delete` that silently does nothing — which is a lie to anyone reading the
|
|
64
|
+
* plan.
|
|
65
|
+
*
|
|
66
|
+
* ⚠️ `cleanup` DEFAULTS TO FALSE, so the logical volumes survive and the disk can be re-added
|
|
67
|
+
* without a rebuild. Ceph still starts backfilling the missing copies the moment the OSD goes.
|
|
68
|
+
*/
|
|
69
|
+
export const destroyOsd = (olds: CephOsdProps) =>
|
|
70
|
+
pve(
|
|
71
|
+
olds.target,
|
|
72
|
+
'provision',
|
|
73
|
+
'DELETE',
|
|
74
|
+
`nodes/${olds.node}/ceph/osd/${String(olds.osdid)}`,
|
|
75
|
+
olds.cleanup === true ? { cleanup: '1' } : {},
|
|
76
|
+
);
|