@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,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a subnet crosses the wire in both directions, and what PVE actually calls its path.
|
|
3
|
+
*
|
|
4
|
+
* ★ SPLIT OUT OF sdn-subnet.ts TO KEEP BOTH FILES UNDER THE 250-LINE CAP, on the seam
|
|
5
|
+
* node-network-form.ts cuts rather than at a convenient line number: this file owns the
|
|
6
|
+
* COERCIONS in BOTH directions plus the derived id, because a read that normalises differently
|
|
7
|
+
* from the write that produced it is exactly how a forever-diff is born. sdn-subnet.ts answers
|
|
8
|
+
* "what is a subnet, and when has it changed". Nothing here calls the cluster.
|
|
9
|
+
*
|
|
10
|
+
* ⚠️ THE `import type` BACK TO sdn-subnet.ts IS A CYCLE ON PAPER ONLY — type-only, so it is erased
|
|
11
|
+
* before anything runs and `SdnSubnetProps` stays in the file that declares the resource.
|
|
12
|
+
*/
|
|
13
|
+
import type { SdnSubnetProps } from './sdn-subnet.ts';
|
|
14
|
+
import { propertyString, withClears } from './values.ts';
|
|
15
|
+
|
|
16
|
+
/** One DHCP pool. PVE's property-string keys are `start-address`/`end-address`; see `rangeString`. */
|
|
17
|
+
export type DhcpRange = { start: string; end: string };
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The id PVE files a subnet under — and therefore the path segment GET, PUT and DELETE address it
|
|
21
|
+
* by, which is NOT the value POST is handed.
|
|
22
|
+
*
|
|
23
|
+
* ⛔ MEASURED from `/usr/share/perl5/PVE/API2/Network/SDN/Subnets.pm` on pve-manager 9.2.11, in
|
|
24
|
+
* the create handler: `my $id = $cidr =~ s/\//-/r; $id = "$zoneid-$id";`. So `subnet=10.0.0.0/24`
|
|
25
|
+
* goes out and `house-10.0.0.0-24` is what exists afterwards. A CIDR cannot be a path segment
|
|
26
|
+
* anyway — it carries a slash — so there is no spelling of this endpoint that takes the CIDR.
|
|
27
|
+
*
|
|
28
|
+
* ⚠️ `.replace`, NOT `.replaceAll`, AND THE PARITY IS DELIBERATE. Perl's `s/\//-/r` has no `/g`, so
|
|
29
|
+
* it rewrites the FIRST slash only. A CIDR has exactly one, so the two agree on every legal
|
|
30
|
+
* input; they would stop agreeing on an illegal one, and agreeing with PVE is the point.
|
|
31
|
+
*/
|
|
32
|
+
export const subnetId = (props: Pick<SdnSubnetProps, 'cidr' | 'zone'>) =>
|
|
33
|
+
`${props.zone}-${props.cidr.replace('/', '-')}`;
|
|
34
|
+
|
|
35
|
+
/** A range on its way OUT. PVE stores the string verbatim; nothing rewrites it. */
|
|
36
|
+
const rangeString = (range: DhcpRange) => `start-address=${range.start},end-address=${range.end}`;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* DHCP ranges, from either shape, as one comparable string.
|
|
40
|
+
*
|
|
41
|
+
* ⛔ THIS FAMILY'S WRITE SHAPE AND READ SHAPE ARE DIFFERENT, WHICH IS THE CLASSIC FOREVER-DIFF.
|
|
42
|
+
* MEASURED from the API module: `$scfg->{'dhcp-range'} = get_dhcp_ranges($scfg)` runs
|
|
43
|
+
* `parse_property_string` over every stored element, so a range WRITTEN as the string
|
|
44
|
+
* `start-address=10.0.0.10,end-address=10.0.0.20` is READ BACK as the object
|
|
45
|
+
* `{"start-address":"10.0.0.10","end-address":"10.0.0.20"}`. `propertyString` in values.ts
|
|
46
|
+
* flattens both shapes to one sorted form and exists for exactly this — see its own ⚠️.
|
|
47
|
+
*
|
|
48
|
+
* ⚠️ AND IT IS A LIST, SO ORDER MUST NOT BE MEANING. PVE validates only that ranges do not overlap
|
|
49
|
+
* and stores them in the order given; two declarations naming the same pools the other way round
|
|
50
|
+
* are the same subnet. Sorting is what stops that being an update, exactly as `csv` does for a
|
|
51
|
+
* zone's `nodes`.
|
|
52
|
+
*
|
|
53
|
+
* ⚠️ `;` IS THE JOINER BECAUSE `,` AND `=` ARE BOTH INSIDE THE VALUES. It never appears in an IPv4
|
|
54
|
+
* or IPv6 address or in a property-string key, so the joined form cannot be ambiguous.
|
|
55
|
+
*/
|
|
56
|
+
export const dhcpRanges = (value: unknown): string =>
|
|
57
|
+
(Array.isArray(value) ? value : [])
|
|
58
|
+
.map((entry: unknown) => propertyString(entry))
|
|
59
|
+
.filter((entry) => entry !== '')
|
|
60
|
+
.sort()
|
|
61
|
+
.join(';');
|
|
62
|
+
|
|
63
|
+
/** The declared side of that same comparison, through the same funnel so it cannot drift. */
|
|
64
|
+
export const declaredRanges = (range: DhcpRange | undefined) =>
|
|
65
|
+
range === undefined ? '' : dhcpRanges([rangeString(range)]);
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* ⚠️ AN EMPTY STRING IS HOW A CALLER SPELLS "UNSET", AND IT MUST NOT REACH PVE. `gateway` and
|
|
69
|
+
* `dhcp-dns-server` are `format => 'ip'` and `dnszoneprefix` is `format => 'dns-name'`, so `''`
|
|
70
|
+
* is a parameter-verification 400 rather than a clear. Clearing is a separate verb — `delete=`.
|
|
71
|
+
*/
|
|
72
|
+
const unset = (value: string | undefined) =>
|
|
73
|
+
value === undefined || value === '' ? undefined : value;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Every optional this resource MANAGES: the value to send, or `undefined` meaning "clear it".
|
|
77
|
+
*
|
|
78
|
+
* ⛔ ONE TABLE FEEDS BOTH HALVES OF AN UPDATE, the way metric-server-form.ts does it, and for the
|
|
79
|
+
* same measured reason: A PUT THAT OMITS A FIELD DOES NOT CLEAR IT. PVE merges the form into the
|
|
80
|
+
* existing section (`$data->{$_} = $opts->{$_} for keys $opts->%*`), so dropping `gateway` from a
|
|
81
|
+
* declaration leaves the old gateway in place and `matches` asks for the same update forever.
|
|
82
|
+
* Deriving the form and the `delete=` list from one map makes "managed but not clearable"
|
|
83
|
+
* impossible to write here.
|
|
84
|
+
*
|
|
85
|
+
* ⛔ AND THE TWO HALVES MUST NOT OVERLAP. MEASURED in `PVE::SectionConfig::delete_from_config`
|
|
86
|
+
* (SectionConfig.pm:1853): naming a key in BOTH the form and `delete=` dies with "cannot set and
|
|
87
|
+
* delete property '<k>' at the same time!". Because both halves are read off this one map, an
|
|
88
|
+
* entry is in exactly one of them by construction.
|
|
89
|
+
*
|
|
90
|
+
* ⛔ NOTHING SET OUT OF BAND MAY BE LISTED HERE. A field in this map is CLEARED the moment it is
|
|
91
|
+
* undeclared, so a live subnet whose DHCP pool an operator widened in the UI is narrowed back on
|
|
92
|
+
* the next deploy. That is full ownership, the same bargain sdn-vnet.ts strikes over `alias`, and
|
|
93
|
+
* it is a bargain rather than an accident only while this list stays deliberate.
|
|
94
|
+
*
|
|
95
|
+
* ⚠️ `snat` IS ABSENT ON PURPOSE AND LIVES IN `body` INSTEAD. It is a boolean, and `0` and "never
|
|
96
|
+
* declared" are the same subnet to PVE and to `matches`, so sending an explicit `0` converges
|
|
97
|
+
* without a `delete=` — the same choice sdn-vnet.ts makes for `vlanaware` and `isolate-ports`.
|
|
98
|
+
*/
|
|
99
|
+
export const optional = (props: SdnSubnetProps): Record<string, string | undefined> => ({
|
|
100
|
+
'dhcp-dns-server': unset(props.dhcpDnsServer),
|
|
101
|
+
'dhcp-range': props.dhcpRange === undefined ? undefined : rangeString(props.dhcpRange),
|
|
102
|
+
dnszoneprefix: unset(props.dnszoneprefix),
|
|
103
|
+
gateway: unset(props.gateway),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The fields sent on EVERY write, create and update alike.
|
|
108
|
+
*
|
|
109
|
+
* ⚠️ `vnet` IS SENT THOUGH THE URL ALREADY CARRIES IT. PVE's handler reads `$param->{vnet}` to
|
|
110
|
+
* resolve the zone for its permission check, and it is a declared parameter of both the create
|
|
111
|
+
* and the update schema, so sending the same value the path carries is accepted and removes this
|
|
112
|
+
* file's dependence on the router injecting a path placeholder into the parameter hash.
|
|
113
|
+
*/
|
|
114
|
+
export const body = (props: SdnSubnetProps): Record<string, string> => ({
|
|
115
|
+
snat: props.snat === true ? '1' : '0',
|
|
116
|
+
vnet: props.vnet,
|
|
117
|
+
...Object.fromEntries(
|
|
118
|
+
Object.entries(optional(props)).filter(
|
|
119
|
+
(entry): entry is [string, string] => entry[1] !== undefined,
|
|
120
|
+
),
|
|
121
|
+
),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* ⚠️ `type` IS HARDCODED, NOT A PROP. PVE's create schema declares it as an enum with exactly one
|
|
126
|
+
* member, `subnet`, so a prop for it could only ever hold one value — and the POST refuses to
|
|
127
|
+
* proceed without it, which is the parameter-verification error sdn-vnet.ts records.
|
|
128
|
+
*/
|
|
129
|
+
export const createForm = (props: SdnSubnetProps): Record<string, string> => ({
|
|
130
|
+
...body(props),
|
|
131
|
+
subnet: props.cidr,
|
|
132
|
+
type: 'subnet',
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* ★ CLEARING AN OPTION THAT WAS NEVER SET IS A SILENT NO-OP, AND THIS IS MEASURED RATHER THAN
|
|
137
|
+
* HOPED FOR. sdn-vnet.ts flags the same `delete=` habit as REASONED-not-measured and warns that a
|
|
138
|
+
* first update on a bare object would carry `delete=alias,tag`. Read off the code both endpoints
|
|
139
|
+
* share — `PVE::SectionConfig::delete_from_config`, SectionConfig.pm:1853-1866 — the loop dies
|
|
140
|
+
* only for an option that is unknown to the plugin, not optional, fixed, or also being set; for
|
|
141
|
+
* an option simply absent from the section it runs `delete $config->{$k}` on a missing key and
|
|
142
|
+
* moves on. All four keys above are `optional => 1` in `SubnetPlugin::options`, so a bare
|
|
143
|
+
* subnet's first update carries `delete=dhcp-dns-server,dhcp-range,dnszoneprefix,gateway` and
|
|
144
|
+
* PVE accepts it. `vnet` is `optional => 0` there and is never in this list.
|
|
145
|
+
*
|
|
146
|
+
* ⚠️ TWO OF THOSE FOUR NAMES ARE HYPHENATED, WHICH NO OTHER FAMILY HERE HAS PUT IN A `delete=`.
|
|
147
|
+
* The parameter's format is `pve-configid-list`, and a name that failed it would 400 the whole
|
|
148
|
+
* update rather than skip one field. MEASURED: `$CONFIGID_RE` is `qr/[a-z][a-z0-9_-]+/i`
|
|
149
|
+
* (JSONSchema.pm:30), so a hyphen after the first character is legal and all four pass.
|
|
150
|
+
*/
|
|
151
|
+
export const updateForm = (props: SdnSubnetProps): Record<string, string> => {
|
|
152
|
+
const clear = Object.entries(optional(props))
|
|
153
|
+
.filter(([, value]) => value === undefined)
|
|
154
|
+
.map(([key]) => key);
|
|
155
|
+
return withClears(body(props), clear);
|
|
156
|
+
};
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Proxmox.SdnSubnet` — the addresses on a declared bridge: the CIDR, its gateway, its DHCP pool.
|
|
3
|
+
*
|
|
4
|
+
* ★ THE THIRD LINK OF ONE CHAIN, AND THE ORDER IS DATA FLOW RATHER THAN CONVENTION. A subnet
|
|
5
|
+
* belongs to a vnet and a vnet belongs to a zone. MEASURED and recorded in sdn-vnet.ts: creating
|
|
6
|
+
* a vnet is permission-checked at `/sdn/zones/-`, so a missing zone surfaces as a PERMISSION
|
|
7
|
+
* error rather than as "no such zone". One level deeper the same shape holds — PVE's
|
|
8
|
+
* `check_vnet_access` looks the vnet's ZONE up and checks `/sdn/zones/<zone>/<vnet>` — so a
|
|
9
|
+
* subnet declared against a vnet that does not exist yet dies inside that lookup, with the vnet
|
|
10
|
+
* named and the zone not. Pass the zone and vnet resources' OWN attributes into `zone` and
|
|
11
|
+
* `vnet`; a literal buys no ordering, and here it also buys a wrong path.
|
|
12
|
+
*
|
|
13
|
+
* ⛔ THE ID PVE FILES A SUBNET UNDER IS NOT THE ONE YOU WRITE, AND IT IS THIS FAMILY'S SHARPEST
|
|
14
|
+
* EDGE. `POST` takes `subnet=10.0.0.0/24` while GET, PUT and DELETE address `house-10.0.0.0-24`.
|
|
15
|
+
* One PVE parameter name, `subnet`, carrying two different values — so they are named APART here,
|
|
16
|
+
* `cidr` for the declared one and `subnetId` for the derived one, for the reason values.ts gives
|
|
17
|
+
* about `flag`: a name that means two things is a bug waiting for whoever reads one meaning. The
|
|
18
|
+
* derivation, and its measurement, live in `subnetId` in sdn-subnet-form.ts.
|
|
19
|
+
*
|
|
20
|
+
* ⛔ WHICH IS WHY `zone` IS A PROP THOUGH PVE NEVER ACCEPTS IT ON A SUBNET WRITE. The path cannot
|
|
21
|
+
* be built without it and it is knowable only from the VNET. Declare the wrong zone and the id is
|
|
22
|
+
* wrong: the POST still succeeds, because PVE computes the real id itself from the vnet, and the
|
|
23
|
+
* read-back then looks for a subnet that is not there. The factory's guard in resource.ts DIES on
|
|
24
|
+
* that first deploy — "the write returned no error but the object is still absent" — which is
|
|
25
|
+
* loud and immediate, and much better than the alternative it replaces: a plan that says create,
|
|
26
|
+
* forever, against a subnet PVE insists is already defined.
|
|
27
|
+
*
|
|
28
|
+
* ⛔ SDN WRITES ARE STAGED, NOT APPLIED — the same trap as sdn-zone.ts and sdn-vnet.ts, and it
|
|
29
|
+
* walks straight through the factory's read-back, which proves the staging file and nothing more.
|
|
30
|
+
* Nothing here applies. `Proxmox.SdnApply` (sdn-apply.ts) publishes the whole staged config with
|
|
31
|
+
* `PUT /cluster/sdn`; declare one and pass this resource's `subnetId` in its `after`.
|
|
32
|
+
*
|
|
33
|
+
* ⛔ AND `Proxmox.SdnApply` CANNOT YET SEE A STAGED SUBNET, SO ITS PLAN WILL LIE ABOUT THIS FAMILY.
|
|
34
|
+
* MEASURED: its `COLLECTIONS` list is `cluster/sdn/zones` and `cluster/sdn/vnets`, while
|
|
35
|
+
* `PVE::Network::SDN::pending_config` keys the pending view on a SEPARATE `subnets` section of
|
|
36
|
+
* the running config. A run that stages only a subnet therefore leaves both counted collections
|
|
37
|
+
* at zero, the apply diffs as `noop`, and the subnet never reaches a node. It is NOT fixable by
|
|
38
|
+
* adding a string to that list: the subnet index is PER VNET
|
|
39
|
+
* (`cluster/sdn/vnets/{vnet}/subnets?pending=1`), so SdnApply needs the vnet names before it can
|
|
40
|
+
* count them. Until that lands, a subnet-only change needs `pvesh set /cluster/sdn` by hand.
|
|
41
|
+
*
|
|
42
|
+
* ★ THE READ LANE IS ENOUGH HERE, AND THAT IS A REAL DIFFERENCE FROM BOTH SIBLINGS. The zone and
|
|
43
|
+
* vnet ITEM reads are gated on SDN.Allocate, which is why each sets `readRole: 'provision'`. This
|
|
44
|
+
* one is not. MEASURED from the cluster's own schema on 2026-09-13:
|
|
45
|
+
*
|
|
46
|
+
* GET /cluster/sdn/vnets/{vnet}/subnets/{subnet}
|
|
47
|
+
* -> "Require 'SDN.Audit' or 'SDN.Allocate' permissions on '/sdn/zones/<zone>/<vnet>'"
|
|
48
|
+
*
|
|
49
|
+
* and `GET /access/acl` the same day shows `hf-read@pve` holding the built-in PVEAuditor — whose
|
|
50
|
+
* seven privileges include SDN.Audit — at `/` with propagate=1. So the default 3600s auditor
|
|
51
|
+
* lease reads this family, and a `readRole: 'provision'` here would be privilege nobody needed.
|
|
52
|
+
* Writes are a different answer: POST, PUT and DELETE all require SDN.Allocate on
|
|
53
|
+
* `/sdn/zones/<zone>/<vnet>`, which `LXCProvisioner` gained on 2026-09-13.
|
|
54
|
+
*
|
|
55
|
+
* ⚠️ NO LIVE SUBNET EXISTS ON TB4 TO ROUND-TRIP AGAINST — there are no SDN zones and no vnets, so
|
|
56
|
+
* there can be no subnets. Every claim above is read off PVE 9.2.11's own schema and Perl sources
|
|
57
|
+
* on n2, and the noop argument below rests on those rather than on a plan this file has watched.
|
|
58
|
+
*
|
|
59
|
+
* ⚠️ TWO REFUSALS TO EXPECT, both from `SubnetPlugin::on_update_hook`: PVE will not put a subnet on
|
|
60
|
+
* a vnet with `vlanaware` set ("you can't add a subnet on a vlanaware vnet"), and it rejects a
|
|
61
|
+
* gateway outside the CIDR unless the mask is /32. Both are create-time 400s, not drift.
|
|
62
|
+
*/
|
|
63
|
+
import { Resource } from 'alchemy';
|
|
64
|
+
import * as Provider from 'alchemy/Provider';
|
|
65
|
+
import * as Effect from 'effect/Effect';
|
|
66
|
+
import { type PveRequirements, type WithTarget, pveHandlers } from './resource.ts';
|
|
67
|
+
import {
|
|
68
|
+
type DhcpRange,
|
|
69
|
+
createForm,
|
|
70
|
+
declaredRanges,
|
|
71
|
+
dhcpRanges,
|
|
72
|
+
subnetId,
|
|
73
|
+
updateForm,
|
|
74
|
+
} from './sdn-subnet-form.ts';
|
|
75
|
+
import { bool, int, text } from './values.ts';
|
|
76
|
+
|
|
77
|
+
export interface SdnSubnetProps extends WithTarget {
|
|
78
|
+
/**
|
|
79
|
+
* The vnet whose bridge carries these addresses. Identity AND a written field.
|
|
80
|
+
*
|
|
81
|
+
* ⛔ MOVING A SUBNET BETWEEN VNETS IS NOT SOMETHING THIS RESOURCE CAN DO, and the symptom is
|
|
82
|
+
* worth knowing before you meet it. The id is built from the zone and the CIDR, so it does not
|
|
83
|
+
* change; but the read goes to `.../vnets/<declared>/subnets/<id>`, and PVE answers a subnet
|
|
84
|
+
* filed under another vnet with `raise_param_exc({ vnet => "wrong vnet" })`. `read` folds that
|
|
85
|
+
* into "absent", reconcile POSTs, and PVE refuses with "sdn subnet object ID ... already
|
|
86
|
+
* defined". Loud and accurate, but it does not converge: change the vnet by removing the
|
|
87
|
+
* declaration and writing a new one.
|
|
88
|
+
*/
|
|
89
|
+
vnet: string;
|
|
90
|
+
/**
|
|
91
|
+
* The zone that vnet lives in. ⚠️ PATH INPUT ONLY — PVE accepts no `zone` on a subnet write, and
|
|
92
|
+
* nothing here sends one. It is a prop because the path cannot be spelled without it.
|
|
93
|
+
*/
|
|
94
|
+
zone: string;
|
|
95
|
+
/** `10.0.0.0/24`. Identity: PVE builds the id from it, so a new CIDR is a new subnet. */
|
|
96
|
+
cidr: string;
|
|
97
|
+
/** Handed to guests as their default route, and registered in the zone's IPAM as `<vnet>-gw`. */
|
|
98
|
+
gateway?: string;
|
|
99
|
+
/**
|
|
100
|
+
* Masquerade this subnet. ⚠️ INERT UNTIL THE CLUSTER FIREWALL IS ON — PVE's own description is
|
|
101
|
+
* "enable masquerade for this subnet if pve-firewall", and TB4's cluster firewall was empty and
|
|
102
|
+
* disabled when this was written. It still round-trips, so declaring it does not diff.
|
|
103
|
+
*/
|
|
104
|
+
snat?: boolean;
|
|
105
|
+
/** `adm` -> `<hostname>.adm.example.internal` for records the zone's DNS plugin registers. */
|
|
106
|
+
dnszoneprefix?: string;
|
|
107
|
+
/** ⚠️ WIRE NAME `dhcp-dns-server`; see the ⚠️ on `isolatePorts` in sdn-vnet.ts about mappings. */
|
|
108
|
+
dhcpDnsServer?: string;
|
|
109
|
+
/**
|
|
110
|
+
* ⛔ ONE POOL, NOT A LIST, AND THE LIMIT IS THE CLIENT'S RATHER THAN PVE'S. `dhcp-range` is a PVE
|
|
111
|
+
* `type => 'array'` parameter, and an array reaches a form-encoded body only as a REPEATED KEY
|
|
112
|
+
* — which `client.ts` cannot express, because `pve()` takes `Record<string, string>` and one
|
|
113
|
+
* key holds one value. A single occurrence works today because `PVE::RESTHandler`'s
|
|
114
|
+
* `$normalize_legacy_param_formats` wraps a scalar into `[$value]` for an array parameter, so
|
|
115
|
+
* the type is declared as one range and a second one is unspellable rather than silently lost.
|
|
116
|
+
* ⛔ THAT COERCION IS MARKED FOR REMOVAL. Its own comment in RESTHandler.pm reads "mark the
|
|
117
|
+
* behaviour deprecated with 9.x, and remove it with 10.x", and this cluster runs 9.2.11 — so on
|
|
118
|
+
* PVE 10 every write carrying a range becomes a hard 400. The fix is not here: it is a form
|
|
119
|
+
* type in client.ts that admits repeated keys.
|
|
120
|
+
* ⚠️ A LIVE SUBNET CARRYING TWO POOLS IS THEREFORE NARROWED TO ONE by the first deploy that
|
|
121
|
+
* declares one. `matches` compares the WHOLE live list, so the narrowing is visible in the plan
|
|
122
|
+
* as an update rather than happening quietly, and it converges.
|
|
123
|
+
*/
|
|
124
|
+
dhcpRange?: DhcpRange;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* ⛔ NO `digest` HERE, for the reason sdn-zone.ts gives: PVE returns the digest of the WHOLE
|
|
129
|
+
* subnets.cfg, so declaring a SECOND subnet would rewrite this one's stored attributes and
|
|
130
|
+
* comparing it would report an update on a subnet nobody touched.
|
|
131
|
+
*/
|
|
132
|
+
export interface SdnSubnetAttributes {
|
|
133
|
+
/** `<zone>-<network>-<mask>` — the path segment, and the name to use in `pvesh`. */
|
|
134
|
+
subnetId: string;
|
|
135
|
+
vnet: string;
|
|
136
|
+
zone: string;
|
|
137
|
+
/** `10.0.0.0/24`, as PVE reassembles it from the id. */
|
|
138
|
+
cidr: string;
|
|
139
|
+
/** Prefix length, so a guest's `ip=10.0.0.5/24` can be built from a reference. */
|
|
140
|
+
mask: number;
|
|
141
|
+
gateway: string;
|
|
142
|
+
snat: boolean;
|
|
143
|
+
dnszoneprefix: string;
|
|
144
|
+
dhcpDnsServer: string;
|
|
145
|
+
/** Normalised and sorted — never compare a raw PVE value to it. See `dhcpRanges`. */
|
|
146
|
+
dhcpRange: string;
|
|
147
|
+
/** ⚠️ THE REMINDER IN THE STATE ITSELF: staged config. No value here says a node has the subnet. */
|
|
148
|
+
readonly staged: true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface ProxmoxSdnSubnet extends Resource<
|
|
152
|
+
'Proxmox.SdnSubnet',
|
|
153
|
+
SdnSubnetProps,
|
|
154
|
+
SdnSubnetAttributes,
|
|
155
|
+
never,
|
|
156
|
+
PveRequirements
|
|
157
|
+
> {}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* ⚠️ NO `defaultRemovalPolicy: 'retain'`, UNLIKE THE SEVEN FAMILIES THAT CARRY IT, AND THE ★ IN
|
|
161
|
+
* resource.ts IS THE TEST IT FAILS: a subnet's whole content is a CIDR, a gateway and a pool,
|
|
162
|
+
* which a line of TypeScript rebuilds exactly. The part that is NOT rebuildable is the guest
|
|
163
|
+
* addresses the zone's IPAM handed out, and PVE guards that itself — MEASURED in the built-in
|
|
164
|
+
* `pve` IPAM plugin, which answers a delete with "cannot delete subnet '<cidr>', not empty"
|
|
165
|
+
* whenever anything beyond the gateway still holds an address. That refusal is surfaced as-is.
|
|
166
|
+
*/
|
|
167
|
+
export const ProxmoxSdnSubnet = Resource<ProxmoxSdnSubnet>('Proxmox.SdnSubnet');
|
|
168
|
+
|
|
169
|
+
const handlers = pveHandlers<SdnSubnetProps, SdnSubnetAttributes>({
|
|
170
|
+
attributes: (live, props) => {
|
|
171
|
+
/**
|
|
172
|
+
* ⚠️ UNREACHABLE ON A HEALTHY CLUSTER, AND KEPT BECAUSE "UNREACHABLE" IS A FACT ABOUT TODAY'S
|
|
173
|
+
* PVE. The read handler already refuses a subnet filed under another vnet, so this row should
|
|
174
|
+
* never arrive — see the ⛔ on `vnet` above for what that refusal costs. If a future PVE
|
|
175
|
+
* answers instead of raising, reporting the foreign subnet as absent makes reconcile POST and
|
|
176
|
+
* PVE say "already defined": still loud, rather than a silent noop over somebody else's
|
|
177
|
+
* addresses.
|
|
178
|
+
*/
|
|
179
|
+
const liveVnet = text(live['vnet']);
|
|
180
|
+
if (liveVnet !== '' && liveVnet !== props.vnet) return undefined;
|
|
181
|
+
return {
|
|
182
|
+
cidr: text(live['cidr']),
|
|
183
|
+
dhcpDnsServer: text(live['dhcp-dns-server']),
|
|
184
|
+
dhcpRange: dhcpRanges(live['dhcp-range']),
|
|
185
|
+
dnszoneprefix: text(live['dnszoneprefix']),
|
|
186
|
+
gateway: text(live['gateway']),
|
|
187
|
+
mask: int(live['mask'], 0),
|
|
188
|
+
snat: bool(live['snat']),
|
|
189
|
+
staged: true,
|
|
190
|
+
subnetId: subnetId(props),
|
|
191
|
+
vnet: props.vnet,
|
|
192
|
+
zone: text(live['zone']),
|
|
193
|
+
};
|
|
194
|
+
},
|
|
195
|
+
collection: (props) => `cluster/sdn/vnets/${props.vnet}/subnets`,
|
|
196
|
+
createForm,
|
|
197
|
+
/**
|
|
198
|
+
* ⛔ FIVE REPORTED FIELDS ARE DELIBERATELY ABSENT FROM THIS COMPARISON, and each is a
|
|
199
|
+
* forever-diff if it goes back in.
|
|
200
|
+
* `subnetId`, `cidr`, `zone` and `mask` are all DERIVED BY PVE FROM THE ID — measured in
|
|
201
|
+
* `Subnets::sdn_subnets_config`, which splits the id and synthesises cidr/zone/network/mask on
|
|
202
|
+
* every read. None of the four is a POST or PUT parameter, so a difference could never be
|
|
203
|
+
* written; and since the id IS the path this read used, a difference cannot arise either — the
|
|
204
|
+
* GET would have failed and the factory would report "absent" instead.
|
|
205
|
+
* `vnet` is the FILTER that produced this row rather than a reading of it: PVE refuses to
|
|
206
|
+
* answer with a subnet on another vnet, so comparing it can only ever be true.
|
|
207
|
+
* `staged` is a fact about the cluster, not about the declaration: diffing it would report an
|
|
208
|
+
* update on every plan until somebody applied, then another the moment anyone staged anything.
|
|
209
|
+
* `digest` is not even reported — see the ⛔ above the attributes.
|
|
210
|
+
*/
|
|
211
|
+
matches: (attributes, props) =>
|
|
212
|
+
attributes.gateway === (props.gateway ?? '') &&
|
|
213
|
+
attributes.snat === (props.snat === true) &&
|
|
214
|
+
attributes.dnszoneprefix === (props.dnszoneprefix ?? '') &&
|
|
215
|
+
attributes.dhcpDnsServer === (props.dhcpDnsServer ?? '') &&
|
|
216
|
+
attributes.dhcpRange === declaredRanges(props.dhcpRange),
|
|
217
|
+
/**
|
|
218
|
+
* ⚠️ NO `?pending=1` AND NO `?running=1`, HOWEVER TEMPTING — the same refusal sdn-vnet.ts makes.
|
|
219
|
+
* The factory uses this one path for GET, PUT and DELETE alike, every one of those schemas
|
|
220
|
+
* declares `additionalProperties => 0`, and PVE answers an unexpected parameter on a write with
|
|
221
|
+
* "parameter verification failed". Staged-ness is documented at the top of this file instead.
|
|
222
|
+
*/
|
|
223
|
+
path: (props) => `cluster/sdn/vnets/${props.vnet}/subnets/${subnetId(props)}`,
|
|
224
|
+
updateForm,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* ⛔ `list` IS EMPTY LIKE EVERY OTHER RESOURCE HERE. `GET /cluster/sdn/vnets/{vnet}/subnets` answers
|
|
229
|
+
* with every subnet on that bridge, guest addresses and all; adopting one would invite Alchemy to
|
|
230
|
+
* narrow or delete a network nobody declared. Adoption stays an explicit act.
|
|
231
|
+
*
|
|
232
|
+
* ⛔ AND A DELETE IS STAGED TOO, WHICH IS THE HALF PEOPLE FORGET. The section leaves subnets.cfg and
|
|
233
|
+
* Alchemy drops the resource, but the gateway stays on the bridge and DHCP keeps answering until
|
|
234
|
+
* something applies `PUT /cluster/sdn`: a destroy nobody applies reads as complete in the plan and
|
|
235
|
+
* has not happened on the cluster.
|
|
236
|
+
*/
|
|
237
|
+
export const ProxmoxSdnSubnetProvider = () =>
|
|
238
|
+
Provider.effect(ProxmoxSdnSubnet, Effect.succeed(ProxmoxSdnSubnet.Provider.of(handlers)));
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Proxmox.SdnVnet` — the bridge a guest actually attaches to.
|
|
3
|
+
*
|
|
4
|
+
* ★ THIS IS THE OBJECT THAT CLOSES THE GAP BETWEEN A DECLARED CONTAINER AND A DECLARED NETWORK.
|
|
5
|
+
* `Proxmox.Lxc` takes `net0: name=eth0,bridge=vmbr0,ip=dhcp`, in which the bridge is a string
|
|
6
|
+
* nothing checks. A vnet's id IS the bridge PVE builds on every node of its zone, so declaring
|
|
7
|
+
* the vnet and passing its `bridge` attribute into `net0` turns that string into a reference
|
|
8
|
+
* Alchemy can order and a plan can show.
|
|
9
|
+
*
|
|
10
|
+
* ⚠️ MEASURED: CREATING A VNET CHECKS PERMISSION AT `/sdn/zones/-`, NOT AT THE VNET. The ACL path
|
|
11
|
+
* is the ZONE's, so a vnet whose zone does not exist yet fails as a PERMISSION error rather than
|
|
12
|
+
* as "no such zone" — and you go off widening a role that was never the problem. `zone` is
|
|
13
|
+
* therefore a hard dependency edge, not a convention: pass the zone resource's own attribute so
|
|
14
|
+
* Alchemy creates the zone first. A zone name typed in as a literal buys no ordering at all.
|
|
15
|
+
*
|
|
16
|
+
* ⛔ THE READ-BACK AFTER A WRITE PROVES THE STAGING FILE AND NOTHING MORE — the same trap as
|
|
17
|
+
* `sdn-zone`, and it walks straight through the factory's guard. PVE's SDN endpoints write
|
|
18
|
+
* `/etc/pve/sdn/vnets.cfg`; nothing reaches a node's `/etc/network/interfaces` until
|
|
19
|
+
* `PUT /cluster/sdn` applies the staged config. `pveOperations.reconcile` reads back and refuses
|
|
20
|
+
* when an object is absent, and a vnet that exists only on paper SATISFIES that read: reconcile
|
|
21
|
+
* reports success, the next plan says noop, and no bridge exists anywhere on the cluster.
|
|
22
|
+
* This resource does NOT issue the apply, deliberately. One vnet's reconcile cannot decide to
|
|
23
|
+
* commit every other pending SDN change in the cluster, including ones a human staged in the UI
|
|
24
|
+
* and has not finished. Apply stays an operator's act — `pvesh set /cluster/sdn`.
|
|
25
|
+
*
|
|
26
|
+
* ★ SUBNETS ARE A SIBLING RESOURCE, NOT A FIELD HERE. The gateway and the DHCP ranges guests take
|
|
27
|
+
* addresses from hang off `cluster/sdn/vnets/{vnet}/subnets`, which MEASURED refuses a POST that
|
|
28
|
+
* omits `type` or `subnet` ("parameter verification failed"). Same factory, one path deeper, and
|
|
29
|
+
* it depends on this resource the way this one depends on the zone.
|
|
30
|
+
*
|
|
31
|
+
* ⛔ read/diff NEED SDN.Allocate TOO, NOT SDN.Audit — corrected 2026-09-13 against the cluster's
|
|
32
|
+
* schema. `GET /cluster/sdn/vnets/{vnet}` is checked against SDN.Allocate exactly as the zone
|
|
33
|
+
* item endpoint is, while only the COLLECTION read accepts SDN.Audit. An auditor-shaped `read`
|
|
34
|
+
* lease is therefore refused, and `pveOperations.read` turns that refusal into "absent" rather
|
|
35
|
+
* than an error — see the ⛔ in sdn-zone.ts and docs/privileges.md.
|
|
36
|
+
*
|
|
37
|
+
* ⚠️ PRIVILEGES, AND THE ESTATE'S CREDENTIAL HELD NONE OF THEM UNTIL 2026-09-13. reconcile and
|
|
38
|
+
* delete mint `provision` and need SDN.Allocate on `/sdn/zones`, or on the one zone if scoped. `LXCProvisioner` grants
|
|
39
|
+
* SDN.Use, which is what a GUEST needs in order to attach to a vnet and NOT what creating one
|
|
40
|
+
* needs, so a container declared on this vnet works today while the vnet itself 403s. Widen
|
|
41
|
+
* deliberately, the way Pool.Allocate was:
|
|
42
|
+
* `pveum role modify LXCProvisioner --privs "<existing>,SDN.Allocate,SDN.Audit"` over SSH,
|
|
43
|
+
* preserving every existing privilege.
|
|
44
|
+
*/
|
|
45
|
+
import { Resource } from 'alchemy';
|
|
46
|
+
import * as Provider from 'alchemy/Provider';
|
|
47
|
+
import * as Effect from 'effect/Effect';
|
|
48
|
+
import { type PveRequirements, type WithTarget, pveHandlers } from './resource.ts';
|
|
49
|
+
import { bool, withClears } from './values.ts';
|
|
50
|
+
|
|
51
|
+
export interface SdnVnetProps extends WithTarget {
|
|
52
|
+
/**
|
|
53
|
+
* ⛔ THE PRIMARY KEY AND THE BRIDGE NAME AT ONCE. PVE caps it at eight alphanumeric characters
|
|
54
|
+
* because it becomes a real interface on every node in the zone. There is no rename: a new id
|
|
55
|
+
* is a new bridge, and every guest whose `net0` still names the old one stays on the old one.
|
|
56
|
+
*/
|
|
57
|
+
vnet: string;
|
|
58
|
+
/**
|
|
59
|
+
* The SDN zone that owns it. Mutable — a vnet can be moved between zones — but see the ⚠️ at the
|
|
60
|
+
* top of this file: the zone has to exist first, and passing a zone resource's attribute instead
|
|
61
|
+
* of a literal is the only thing that makes Alchemy order them.
|
|
62
|
+
*/
|
|
63
|
+
zone: string;
|
|
64
|
+
/** Free text shown in the UI. Mutable; clear it by dropping the prop, never by passing `''`. */
|
|
65
|
+
alias?: string;
|
|
66
|
+
/** VLAN id or VXLAN VNI, depending on the zone's type. Mutable. Absent means untagged. */
|
|
67
|
+
tag?: number;
|
|
68
|
+
/** Let guests on this bridge carry their own VLAN tags. Mutable. */
|
|
69
|
+
vlanaware?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Stop guests on this bridge from reaching each other directly. Mutable.
|
|
72
|
+
* ⚠️ The wire name is `isolate-ports`, with a hyphen — see the ⚠️ on `shape` for why that costs
|
|
73
|
+
* attention in two places rather than one.
|
|
74
|
+
*/
|
|
75
|
+
isolatePorts?: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface SdnVnetAttributes {
|
|
79
|
+
vnet: string;
|
|
80
|
+
/**
|
|
81
|
+
* The same string as `vnet`, duplicated on purpose: it is the name a guest's `net0` must carry.
|
|
82
|
+
* Referencing it from a `Proxmox.Lxc`'s props is what makes Alchemy build the network before the
|
|
83
|
+
* container. A hardcoded `vmbr1` in `net0` is a string nothing checks and nothing orders.
|
|
84
|
+
*/
|
|
85
|
+
bridge: string;
|
|
86
|
+
zone: string;
|
|
87
|
+
alias: string;
|
|
88
|
+
/**
|
|
89
|
+
* Absent when the vnet is untagged — see `tagOf` for why this is optional and not `0`.
|
|
90
|
+
* ⚠️ `| undefined` IS LOAD-BEARING under exactOptionalPropertyTypes: the attributes
|
|
91
|
+
* builder always sets this key, writing `undefined` for an untagged vnet, so the type
|
|
92
|
+
* must permit a present-but-undefined value as well as an absent one. Without it the
|
|
93
|
+
* read path does not typecheck, and 0 is a real VLAN id so it cannot be the sentinel.
|
|
94
|
+
*/
|
|
95
|
+
tag?: number | undefined;
|
|
96
|
+
vlanaware: boolean;
|
|
97
|
+
isolatePorts: boolean;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface ProxmoxSdnVnet extends Resource<
|
|
101
|
+
'Proxmox.SdnVnet',
|
|
102
|
+
SdnVnetProps,
|
|
103
|
+
SdnVnetAttributes,
|
|
104
|
+
never,
|
|
105
|
+
PveRequirements
|
|
106
|
+
> {}
|
|
107
|
+
|
|
108
|
+
export const ProxmoxSdnVnet = Resource<ProxmoxSdnVnet>('Proxmox.SdnVnet');
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* ⚠️ AN ABSENT TAG IS `undefined`, NOT 0, which is why `SdnVnetAttributes.tag` is optional rather
|
|
112
|
+
* than falling back the way `memory` does in lxc.ts. VLAN id 0 and VXLAN VNI 0 are real values,
|
|
113
|
+
* so a 0 fallback would report an untagged vnet as tagged 0 and then diff forever against a
|
|
114
|
+
* declaration that asks for no tag at all.
|
|
115
|
+
*/
|
|
116
|
+
const tagOf = (value: unknown) => (typeof value === 'number' ? value : undefined);
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The fields PVE accepts on create and on update alike.
|
|
120
|
+
*
|
|
121
|
+
* ⚠️ THE WIRE NAME IS `isolate-ports`, WITH A HYPHEN; the prop is `isolatePorts` so no call site
|
|
122
|
+
* has to quote it. Both halves of that mapping have to be right: the form key here AND the read
|
|
123
|
+
* in `attributes`. `live['isolatePorts']` would be `undefined` forever, which reads as "the
|
|
124
|
+
* cluster does not have it set" and makes every plan ask for the same update, for good.
|
|
125
|
+
*/
|
|
126
|
+
const shape = (props: SdnVnetProps): Record<string, string> => {
|
|
127
|
+
const alias = props.alias ?? '';
|
|
128
|
+
return {
|
|
129
|
+
// ⚠️ THE TWO BOOLEANS ARE ALWAYS SENT, `0` INCLUDED, AND THAT IS NOT AN OVERSIGHT. `0` and
|
|
130
|
+
// "never declared" mean the same thing to PVE and to `matches` below, so an explicit `0`
|
|
131
|
+
// makes the two converge — which is why neither ever appears in the `delete` list.
|
|
132
|
+
'isolate-ports': props.isolatePorts === true ? '1' : '0',
|
|
133
|
+
vlanaware: props.vlanaware === true ? '1' : '0',
|
|
134
|
+
zone: props.zone,
|
|
135
|
+
// ⚠️ AN EMPTY ALIAS IS OMITTED, NOT SENT AS `''`. `''` is how this file spells "unset", and
|
|
136
|
+
// unset goes through the `delete` parameter below; sending it as a value would ask PVE to
|
|
137
|
+
// store an empty description, which is a different request from having none.
|
|
138
|
+
...(alias === '' ? {} : { alias }),
|
|
139
|
+
...(props.tag === undefined ? {} : { tag: String(props.tag) }),
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* ⛔ OMITTING A FIELD DOES NOT CLEAR IT. PVE keeps whatever the config already holds, so a
|
|
145
|
+
* declaration that DROPS its alias would diff as `update` forever: `matches` compares `''`
|
|
146
|
+
* against the live text, the PUT says nothing about alias, and the next plan asks for the very
|
|
147
|
+
* same update. PVE's own answer is the `delete` parameter — a comma-separated list of properties
|
|
148
|
+
* to unset — which exists on the update endpoint only. That is why create does not come through
|
|
149
|
+
* here and calls `shape` directly.
|
|
150
|
+
*
|
|
151
|
+
* ⚠️ REASONED FROM PVE'S UPDATE SCHEMA, NOT MEASURED ON A CLUSTER — unlike the two MEASURED facts
|
|
152
|
+
* at the top of this file, and the difference is worth a reader's attention. `delete` is what
|
|
153
|
+
* `SectionConfig::updateSchema` adds to every PVE update endpoint, and unsetting a property that
|
|
154
|
+
* was never set is a no-op there. If THIS endpoint turns out to disagree, the blast radius is
|
|
155
|
+
* wider than the feature: a plain `{ vnet, zone }` vnet has neither alias nor tag, so its very
|
|
156
|
+
* first update carries `delete=alias,tag` and every update fails, not just an unset. The symptom
|
|
157
|
+
* is a 400 "parameter verification failed" naming `delete`; the fix is to drop this list and
|
|
158
|
+
* accept the forever-update on unset instead. Verify it once against a live vnet.
|
|
159
|
+
*/
|
|
160
|
+
const updateForm = (props: SdnVnetProps): Record<string, string> => {
|
|
161
|
+
const unset = [
|
|
162
|
+
...((props.alias ?? '') === '' ? ['alias'] : []),
|
|
163
|
+
...(props.tag === undefined ? ['tag'] : []),
|
|
164
|
+
];
|
|
165
|
+
return withClears(shape(props), unset);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const handlers = pveHandlers<SdnVnetProps, SdnVnetAttributes>({
|
|
169
|
+
// ⛔ PVE gates this family's ITEM read on the allocate privilege, not the audit one — see
|
|
170
|
+
// `readRole` in resource.ts. The auditor-shaped lease reads 403, which `read` turns into
|
|
171
|
+
// "absent", and the plan then says create for an object that is plainly there.
|
|
172
|
+
readRole: 'provision',
|
|
173
|
+
attributes: (live, props) => ({
|
|
174
|
+
alias: typeof live['alias'] === 'string' ? live['alias'] : '',
|
|
175
|
+
bridge: props.vnet,
|
|
176
|
+
isolatePorts: bool(live['isolate-ports']),
|
|
177
|
+
tag: tagOf(live['tag']),
|
|
178
|
+
vlanaware: bool(live['vlanaware']),
|
|
179
|
+
vnet: props.vnet,
|
|
180
|
+
/**
|
|
181
|
+
* ⚠️ `''` RATHER THAN `props.zone` WHEN PVE DOES NOT SAY. Echoing the declared zone back would
|
|
182
|
+
* make an unreadable zone look like agreement; `''` matches no declared zone, so the plan
|
|
183
|
+
* reports drift instead. A vnet in the wrong zone is attached to the wrong network, and this
|
|
184
|
+
* is the one field here where a spurious update costs less than a missed one.
|
|
185
|
+
*/
|
|
186
|
+
zone: typeof live['zone'] === 'string' ? live['zone'] : '',
|
|
187
|
+
}),
|
|
188
|
+
collection: () => 'cluster/sdn/vnets',
|
|
189
|
+
createForm: (props) => ({ ...shape(props), vnet: props.vnet }),
|
|
190
|
+
/**
|
|
191
|
+
* ⚠️ `vnet`, `bridge` AND WHETHER THE CONFIG IS APPLIED ARE ABSENT FROM THIS COMPARISON ON
|
|
192
|
+
* PURPOSE. The first two are the path — a changed id is a different object, not an edit — and
|
|
193
|
+
* staged-vs-running is a fact about the cluster rather than about the declaration. Diffing it
|
|
194
|
+
* would report an update on every plan until somebody ran the apply, and then report another
|
|
195
|
+
* one the moment anybody else staged anything at all.
|
|
196
|
+
*/
|
|
197
|
+
matches: (attributes, props) =>
|
|
198
|
+
attributes.zone === props.zone &&
|
|
199
|
+
attributes.alias === (props.alias ?? '') &&
|
|
200
|
+
attributes.tag === props.tag &&
|
|
201
|
+
attributes.vlanaware === (props.vlanaware === true) &&
|
|
202
|
+
attributes.isolatePorts === (props.isolatePorts === true),
|
|
203
|
+
/**
|
|
204
|
+
* ⚠️ NO QUERY STRING HERE, HOWEVER TEMPTING. `?pending=1` would surface whether the staged
|
|
205
|
+
* config has been applied, but the factory uses this one path for GET, PUT and DELETE alike,
|
|
206
|
+
* and PVE answers an unexpected parameter on a write with "parameter verification failed".
|
|
207
|
+
* That is why staged-ness is documented at the top of this file rather than made an attribute.
|
|
208
|
+
*/
|
|
209
|
+
path: (props) => `cluster/sdn/vnets/${props.vnet}`,
|
|
210
|
+
updateForm,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* ⛔ EMPTY, LIKE EVERY OTHER RESOURCE IN THIS PACKAGE. `GET /cluster/sdn/vnets` answers
|
|
215
|
+
* with every vnet on the cluster, and on an estate already running SDN those are
|
|
216
|
+
* load-bearing bridges with guests on them. Returning them would invite Alchemy to
|
|
217
|
+
* adopt — and therefore one day delete — a network nobody declared here. Adoption is an
|
|
218
|
+
* explicit act.
|
|
219
|
+
|
|
220
|
+
*
|
|
221
|
+
* ⛔ A DELETE IS STAGED LIKE EVERY OTHER SDN WRITE. The vnet leaves `vnets.cfg` at once and
|
|
222
|
+
* the bridge stays up on every node until `PUT /cluster/sdn` applies the removal, so a
|
|
223
|
+
* plan reporting "1 deleted" has taken nothing off the network yet. Nothing here applies
|
|
224
|
+
* on the operator's behalf: see the ⛔ at the top of this file.
|
|
225
|
+
|
|
226
|
+
*/
|
|
227
|
+
export const ProxmoxSdnVnetProvider = () =>
|
|
228
|
+
Provider.effect(ProxmoxSdnVnet, Effect.succeed(ProxmoxSdnVnet.Provider.of(handlers)));
|