@homeflare/alchemy 0.15.0 → 0.19.1
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/README.md +29 -0
- package/dist/linux/directory-lifecycle.d.ts +50 -0
- package/dist/linux/directory-lifecycle.d.ts.map +1 -0
- package/dist/linux/directory.d.ts +19 -0
- package/dist/linux/directory.d.ts.map +1 -0
- package/dist/linux/fake-linux-host.d.ts +35 -0
- package/dist/linux/fake-linux-host.d.ts.map +1 -0
- package/dist/linux/fake-systemd.d.ts +34 -0
- package/dist/linux/fake-systemd.d.ts.map +1 -0
- package/dist/linux/index.d.ts +27 -0
- package/dist/linux/index.d.ts.map +1 -0
- package/dist/linux/index.js +1575 -0
- package/dist/linux/index.js.map +38 -0
- package/dist/linux/providers.d.ts +17 -0
- package/dist/linux/providers.d.ts.map +1 -0
- package/dist/linux/region.d.ts +43 -0
- package/dist/linux/region.d.ts.map +1 -0
- package/dist/linux/remote-file-form.d.ts +47 -0
- package/dist/linux/remote-file-form.d.ts.map +1 -0
- package/dist/linux/remote-file-lifecycle.d.ts +36 -0
- package/dist/linux/remote-file-lifecycle.d.ts.map +1 -0
- package/dist/linux/remote-file-plan.d.ts +33 -0
- package/dist/linux/remote-file-plan.d.ts.map +1 -0
- package/dist/linux/remote-file.d.ts +28 -0
- package/dist/linux/remote-file.d.ts.map +1 -0
- package/dist/linux/ssh-command.d.ts +68 -0
- package/dist/linux/ssh-command.d.ts.map +1 -0
- package/dist/linux/ssh-runner.d.ts +22 -0
- package/dist/linux/ssh-runner.d.ts.map +1 -0
- package/dist/linux/ssh-scripts.d.ts +27 -0
- package/dist/linux/ssh-scripts.d.ts.map +1 -0
- package/dist/linux/systemctl.d.ts +67 -0
- package/dist/linux/systemctl.d.ts.map +1 -0
- package/dist/linux/unit-form.d.ts +78 -0
- package/dist/linux/unit-form.d.ts.map +1 -0
- package/dist/linux/unit-handlers.d.ts +15 -0
- package/dist/linux/unit-handlers.d.ts.map +1 -0
- package/dist/linux/unit-lifecycle.d.ts +40 -0
- package/dist/linux/unit-lifecycle.d.ts.map +1 -0
- package/dist/linux/unit-preflight.d.ts +41 -0
- package/dist/linux/unit-preflight.d.ts.map +1 -0
- package/dist/linux/unit-settle.d.ts +31 -0
- package/dist/linux/unit-settle.d.ts.map +1 -0
- package/dist/linux/unit.d.ts +35 -0
- package/dist/linux/unit.d.ts.map +1 -0
- package/dist/netbox/client.d.ts +52 -0
- package/dist/netbox/client.d.ts.map +1 -0
- package/dist/netbox/constraint-guard.d.ts +26 -0
- package/dist/netbox/constraint-guard.d.ts.map +1 -0
- package/dist/netbox/constraints.d.ts +76 -0
- package/dist/netbox/constraints.d.ts.map +1 -0
- package/dist/netbox/generated/constraints/index.d.ts +19 -0
- package/dist/netbox/generated/constraints/index.d.ts.map +1 -0
- package/dist/netbox/generated/constraints/netbox-ipam.d.ts +21 -0
- package/dist/netbox/generated/constraints/netbox-ipam.d.ts.map +1 -0
- package/dist/netbox/index.d.ts +17 -0
- package/dist/netbox/index.d.ts.map +1 -0
- package/dist/netbox/index.js +331 -0
- package/dist/netbox/index.js.map +18 -0
- package/dist/netbox/prefix-form.d.ts +54 -0
- package/dist/netbox/prefix-form.d.ts.map +1 -0
- package/dist/netbox/prefix.d.ts +80 -0
- package/dist/netbox/prefix.d.ts.map +1 -0
- package/dist/netbox/resource.d.ts +94 -0
- package/dist/netbox/resource.d.ts.map +1 -0
- package/dist/netbox/values.d.ts +24 -0
- package/dist/netbox/values.d.ts.map +1 -0
- package/dist/proxmox/api-token-form.d.ts.map +1 -1
- package/dist/proxmox/apply-endpoints.d.ts +30 -0
- package/dist/proxmox/apply-endpoints.d.ts.map +1 -0
- package/dist/proxmox/ceph-daemon-form.d.ts +29 -1
- package/dist/proxmox/ceph-daemon-form.d.ts.map +1 -1
- package/dist/proxmox/ceph-daemon.d.ts.map +1 -1
- package/dist/proxmox/ceph-flag.d.ts.map +1 -1
- package/dist/proxmox/ceph-fs.d.ts.map +1 -1
- package/dist/proxmox/ceph-osd-write.d.ts +19 -0
- package/dist/proxmox/ceph-osd-write.d.ts.map +1 -1
- package/dist/proxmox/ceph-pool.d.ts.map +1 -1
- package/dist/proxmox/constraints.d.ts +28 -5
- package/dist/proxmox/constraints.d.ts.map +1 -1
- package/dist/proxmox/generated/constraints/index.d.ts +1 -1
- package/dist/proxmox/generated/constraints/index.d.ts.map +1 -1
- package/dist/proxmox/generated/constraints/pbs-config.d.ts +6 -1
- package/dist/proxmox/generated/constraints/pbs-config.d.ts.map +1 -1
- package/dist/proxmox/generated/constraints/pve-access.d.ts +6 -1
- package/dist/proxmox/generated/constraints/pve-access.d.ts.map +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-backup.d.ts +23 -0
- package/dist/proxmox/generated/constraints/pve-cluster-backup.d.ts.map +1 -0
- package/dist/proxmox/generated/constraints/pve-cluster-ceph.d.ts +23 -0
- package/dist/proxmox/generated/constraints/pve-cluster-ceph.d.ts.map +1 -0
- package/dist/proxmox/generated/constraints/pve-cluster-firewall.d.ts +23 -0
- package/dist/proxmox/generated/constraints/pve-cluster-firewall.d.ts.map +1 -0
- package/dist/proxmox/generated/constraints/pve-cluster-ha.d.ts +23 -0
- package/dist/proxmox/generated/constraints/pve-cluster-ha.d.ts.map +1 -0
- package/dist/proxmox/generated/constraints/pve-cluster-metrics.d.ts +23 -0
- package/dist/proxmox/generated/constraints/pve-cluster-metrics.d.ts.map +1 -0
- package/dist/proxmox/generated/constraints/pve-cluster-notifications.d.ts +23 -0
- package/dist/proxmox/generated/constraints/pve-cluster-notifications.d.ts.map +1 -0
- package/dist/proxmox/generated/constraints/pve-cluster-replication.d.ts +23 -0
- package/dist/proxmox/generated/constraints/pve-cluster-replication.d.ts.map +1 -0
- package/dist/proxmox/generated/constraints/pve-cluster-sdn.d.ts +23 -0
- package/dist/proxmox/generated/constraints/pve-cluster-sdn.d.ts.map +1 -0
- package/dist/proxmox/generated/constraints/pve-nodes-ceph.d.ts +23 -0
- package/dist/proxmox/generated/constraints/pve-nodes-ceph.d.ts.map +1 -0
- package/dist/proxmox/generated/constraints/pve-nodes-disks.d.ts +23 -0
- package/dist/proxmox/generated/constraints/pve-nodes-disks.d.ts.map +1 -0
- package/dist/proxmox/generated/constraints/pve-nodes-lxc.d.ts +23 -0
- package/dist/proxmox/generated/constraints/pve-nodes-lxc.d.ts.map +1 -0
- package/dist/proxmox/generated/constraints/pve-nodes-network.d.ts +23 -0
- package/dist/proxmox/generated/constraints/pve-nodes-network.d.ts.map +1 -0
- package/dist/proxmox/generated/constraints/pve-nodes-qemu.d.ts +23 -0
- package/dist/proxmox/generated/constraints/pve-nodes-qemu.d.ts.map +1 -0
- package/dist/proxmox/generated/constraints/pve-pools.d.ts +6 -1
- package/dist/proxmox/generated/constraints/pve-pools.d.ts.map +1 -1
- package/dist/proxmox/generated/constraints/pve-storage.d.ts +6 -1
- package/dist/proxmox/generated/constraints/pve-storage.d.ts.map +1 -1
- package/dist/proxmox/generated/pbs/access-users.d.ts +137 -0
- package/dist/proxmox/generated/pbs/access-users.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/access.d.ts +171 -0
- package/dist/proxmox/generated/pbs/access.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/admin-datastore-store.d.ts +252 -0
- package/dist/proxmox/generated/pbs/admin-datastore-store.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/admin-metrics.d.ts +147 -0
- package/dist/proxmox/generated/pbs/admin-metrics.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/admin.d.ts +217 -0
- package/dist/proxmox/generated/pbs/admin.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/backup.d.ts +114 -0
- package/dist/proxmox/generated/pbs/backup.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/config-access-pam.d.ts +218 -0
- package/dist/proxmox/generated/pbs/config-access-pam.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/config-access.d.ts +203 -0
- package/dist/proxmox/generated/pbs/config-access.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/config-datastore.d.ts +195 -0
- package/dist/proxmox/generated/pbs/config-datastore.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/config-media-pool.d.ts +207 -0
- package/dist/proxmox/generated/pbs/config-media-pool.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/config-notifications-matchers.d.ts +159 -0
- package/dist/proxmox/generated/pbs/config-notifications-matchers.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/config-notifications.d.ts +199 -0
- package/dist/proxmox/generated/pbs/config-notifications.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/config-remote.d.ts +175 -0
- package/dist/proxmox/generated/pbs/config-remote.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/config-sync.d.ts +145 -0
- package/dist/proxmox/generated/pbs/config-sync.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/config-tape.d.ts +200 -0
- package/dist/proxmox/generated/pbs/config-tape.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/config-verify.d.ts +77 -0
- package/dist/proxmox/generated/pbs/config-verify.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/config.d.ts +119 -0
- package/dist/proxmox/generated/pbs/config.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/nodes-node-dns.d.ts +178 -0
- package/dist/proxmox/generated/pbs/nodes-node-dns.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/nodes-node-services.d.ts +205 -0
- package/dist/proxmox/generated/pbs/nodes-node-services.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/nodes-node.d.ts +166 -0
- package/dist/proxmox/generated/pbs/nodes-node.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/nodes.d.ts +171 -0
- package/dist/proxmox/generated/pbs/nodes.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/ping.d.ts +18 -0
- package/dist/proxmox/generated/pbs/ping.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/pull.d.ts +37 -0
- package/dist/proxmox/generated/pbs/pull.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/push.d.ts +36 -0
- package/dist/proxmox/generated/pbs/push.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/reader.d.ts +41 -0
- package/dist/proxmox/generated/pbs/reader.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/root.d.ts +16 -0
- package/dist/proxmox/generated/pbs/root.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/status.d.ts +54 -0
- package/dist/proxmox/generated/pbs/status.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/tape-drive.d.ts +181 -0
- package/dist/proxmox/generated/pbs/tape-drive.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/tape-media.d.ts +126 -0
- package/dist/proxmox/generated/pbs/tape-media.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/tape.d.ts +90 -0
- package/dist/proxmox/generated/pbs/tape.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/version.d.ts +20 -0
- package/dist/proxmox/generated/pbs/version.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs.d.ts +44 -662
- package/dist/proxmox/generated/pbs.d.ts.map +1 -1
- package/dist/proxmox/generated/pve/access-roles.d.ts +164 -0
- package/dist/proxmox/generated/pve/access-roles.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/access-users.d.ts +152 -0
- package/dist/proxmox/generated/pve/access-users.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/access.d.ts +221 -0
- package/dist/proxmox/generated/pve/access.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-acme-plugins-id.d.ts +45 -0
- package/dist/proxmox/generated/pve/cluster-acme-plugins-id.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-acme-plugins.d.ts +47 -0
- package/dist/proxmox/generated/pve/cluster-acme-plugins.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-acme-tos.d.ts +240 -0
- package/dist/proxmox/generated/pve/cluster-acme-tos.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-bulk-action.d.ts +200 -0
- package/dist/proxmox/generated/pve/cluster-bulk-action.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-firewall-refs.d.ts +109 -0
- package/dist/proxmox/generated/pve/cluster-firewall-refs.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-firewall.d.ts +225 -0
- package/dist/proxmox/generated/pve/cluster-firewall.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-ha.d.ts +199 -0
- package/dist/proxmox/generated/pve/cluster-ha.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-jobs.d.ts +183 -0
- package/dist/proxmox/generated/pve/cluster-jobs.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-metrics.d.ts +110 -0
- package/dist/proxmox/generated/pve/cluster-metrics.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-notifications-endpoints.d.ts +224 -0
- package/dist/proxmox/generated/pve/cluster-notifications-endpoints.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-notifications.d.ts +91 -0
- package/dist/proxmox/generated/pve/cluster-notifications.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-options.d.ts +85 -0
- package/dist/proxmox/generated/pve/cluster-options.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-qemu-custom-cpu-models-cputype.d.ts +48 -0
- package/dist/proxmox/generated/pve/cluster-qemu-custom-cpu-models-cputype.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-qemu-custom-cpu-models.d.ts +45 -0
- package/dist/proxmox/generated/pve/cluster-qemu-custom-cpu-models.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-replication.d.ts +130 -0
- package/dist/proxmox/generated/pve/cluster-replication.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-sdn-fabrics.d.ts +220 -0
- package/dist/proxmox/generated/pve/cluster-sdn-fabrics.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-sdn-ipams.d.ts +229 -0
- package/dist/proxmox/generated/pve/cluster-sdn-ipams.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-sdn-vnets-vnet.d.ts +229 -0
- package/dist/proxmox/generated/pve/cluster-sdn-vnets-vnet.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-sdn-vnets.d.ts +51 -0
- package/dist/proxmox/generated/pve/cluster-sdn-vnets.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-sdn-zones.d.ts +225 -0
- package/dist/proxmox/generated/pve/cluster-sdn-zones.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-sdn.d.ts +202 -0
- package/dist/proxmox/generated/pve/cluster-sdn.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster.d.ts +69 -0
- package/dist/proxmox/generated/pve/cluster.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-ceph.d.ts +167 -0
- package/dist/proxmox/generated/pve/nodes-node-ceph.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-config.d.ts +43 -0
- package/dist/proxmox/generated/pve/nodes-node-config.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-disks.d.ts +225 -0
- package/dist/proxmox/generated/pve/nodes-node-disks.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-firewall.d.ts +145 -0
- package/dist/proxmox/generated/pve/nodes-node-firewall.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-hardware.d.ts +195 -0
- package/dist/proxmox/generated/pve/nodes-node-hardware.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-firewall.d.ts +191 -0
- package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-firewall.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-move-volume.d.ts +33 -0
- package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-move-volume.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-mtunnel.d.ts +56 -0
- package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-mtunnel.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-resize.d.ts +28 -0
- package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-resize.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-rrd.d.ts +186 -0
- package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-rrd.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-lxc-vmid.d.ts +191 -0
- package/dist/proxmox/generated/pve/nodes-node-lxc-vmid.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-network.d.ts +150 -0
- package/dist/proxmox/generated/pve/nodes-node-network.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-config.d.ts +301 -0
- package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-config.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-dbus-vmstate.d.ts +170 -0
- package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-dbus-vmstate.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-firewall-rules.d.ts +144 -0
- package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-firewall-rules.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-move-disk.d.ts +34 -0
- package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-move-disk.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-mtunnel.d.ts +137 -0
- package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-mtunnel.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-status.d.ts +179 -0
- package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-status.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-qemu-vmid.d.ts +143 -0
- package/dist/proxmox/generated/pve/nodes-node-qemu-vmid.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-qemu.d.ts +158 -0
- package/dist/proxmox/generated/pve/nodes-node-qemu.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-replication.d.ts +187 -0
- package/dist/proxmox/generated/pve/nodes-node-replication.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-services.d.ts +115 -0
- package/dist/proxmox/generated/pve/nodes-node-services.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-storage-storage.d.ts +202 -0
- package/dist/proxmox/generated/pve/nodes-node-storage-storage.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-storage.d.ts +40 -0
- package/dist/proxmox/generated/pve/nodes-node-storage.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-subscription.d.ts +173 -0
- package/dist/proxmox/generated/pve/nodes-node-subscription.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-vzdump.d.ts +97 -0
- package/dist/proxmox/generated/pve/nodes-node-vzdump.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node.d.ts +207 -0
- package/dist/proxmox/generated/pve/nodes-node.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes.d.ts +236 -0
- package/dist/proxmox/generated/pve/nodes.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/pools.d.ts +82 -0
- package/dist/proxmox/generated/pve/pools.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/storage.d.ts +159 -0
- package/dist/proxmox/generated/pve/storage.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/version.d.ts +21 -0
- package/dist/proxmox/generated/pve/version.d.ts.map +1 -0
- package/dist/proxmox/generated/pve.d.ts +69 -4766
- package/dist/proxmox/generated/pve.d.ts.map +1 -1
- package/dist/proxmox/index.js +996 -372
- package/dist/proxmox/index.js.map +48 -32
- package/dist/proxmox/lxc-lifecycle.d.ts.map +1 -1
- package/dist/proxmox/metric-server-form.d.ts +11 -0
- package/dist/proxmox/metric-server-form.d.ts.map +1 -1
- package/dist/proxmox/metric-server.d.ts.map +1 -1
- package/dist/proxmox/network-apply.d.ts.map +1 -1
- package/dist/proxmox/node-network-form.d.ts +15 -0
- package/dist/proxmox/node-network-form.d.ts.map +1 -1
- package/dist/proxmox/node-network.d.ts.map +1 -1
- package/dist/proxmox/notification-target-endpoint.d.ts +26 -0
- package/dist/proxmox/notification-target-endpoint.d.ts.map +1 -0
- package/dist/proxmox/notification-target-form.d.ts +7 -0
- package/dist/proxmox/notification-target-form.d.ts.map +1 -1
- package/dist/proxmox/notification-target.d.ts.map +1 -1
- package/dist/proxmox/pbs-notification-target-endpoint.d.ts +29 -0
- package/dist/proxmox/pbs-notification-target-endpoint.d.ts.map +1 -0
- package/dist/proxmox/pbs-notification-target-lifecycle.d.ts.map +1 -1
- package/dist/proxmox/qemu.d.ts +10 -0
- package/dist/proxmox/qemu.d.ts.map +1 -1
- package/dist/proxmox/resource-guard.d.ts +15 -0
- package/dist/proxmox/resource-guard.d.ts.map +1 -0
- package/dist/proxmox/resource-spec.d.ts +15 -4
- package/dist/proxmox/resource-spec.d.ts.map +1 -1
- package/dist/proxmox/resource.d.ts +8 -0
- package/dist/proxmox/resource.d.ts.map +1 -1
- package/dist/proxmox/sdn-apply.d.ts.map +1 -1
- package/dist/proxmox/sdn-subnet.d.ts.map +1 -1
- package/dist/proxmox/zfs-pool-write.d.ts +11 -0
- package/dist/proxmox/zfs-pool-write.d.ts.map +1 -1
- package/dist/proxmox/zfs-pool.d.ts.map +1 -1
- package/docs/linux-host-measured.md +56 -0
- package/docs/linux-host.md +192 -0
- package/docs/netbox.md +103 -0
- package/package.json +9 -1
- package/src/linux/directory-lifecycle.ts +220 -0
- package/src/linux/directory.test.ts +132 -0
- package/src/linux/directory.ts +71 -0
- package/src/linux/fake-linux-host.ts +197 -0
- package/src/linux/fake-systemd.ts +103 -0
- package/src/linux/index.ts +32 -0
- package/src/linux/providers.ts +26 -0
- package/src/linux/region.test.ts +89 -0
- package/src/linux/region.ts +118 -0
- package/src/linux/remote-file-form.ts +96 -0
- package/src/linux/remote-file-lifecycle.test.ts +191 -0
- package/src/linux/remote-file-lifecycle.ts +161 -0
- package/src/linux/remote-file-plan.ts +154 -0
- package/src/linux/remote-file.ts +88 -0
- package/src/linux/ssh-command.test.ts +140 -0
- package/src/linux/ssh-command.ts +99 -0
- package/src/linux/ssh-runner.ts +188 -0
- package/src/linux/ssh-scripts.ts +105 -0
- package/src/linux/systemctl.ts +145 -0
- package/src/linux/unit-form.ts +183 -0
- package/src/linux/unit-handlers.ts +73 -0
- package/src/linux/unit-lifecycle.test.ts +220 -0
- package/src/linux/unit-lifecycle.ts +175 -0
- package/src/linux/unit-preflight.test.ts +128 -0
- package/src/linux/unit-preflight.ts +100 -0
- package/src/linux/unit-settle.ts +110 -0
- package/src/linux/unit.ts +81 -0
- package/src/netbox/client.test.ts +67 -0
- package/src/netbox/client.ts +146 -0
- package/src/netbox/constraint-guard.ts +57 -0
- package/src/netbox/constraints.test.ts +114 -0
- package/src/netbox/constraints.ts +152 -0
- package/src/netbox/generated/constraints/index.ts +23 -0
- package/src/netbox/generated/constraints/netbox-ipam.ts +32 -0
- package/src/netbox/index.ts +34 -0
- package/src/netbox/prefix-form.test.ts +127 -0
- package/src/netbox/prefix-form.ts +84 -0
- package/src/netbox/prefix.ts +136 -0
- package/src/netbox/resource.ts +208 -0
- package/src/netbox/values.test.ts +66 -0
- package/src/netbox/values.ts +40 -0
- package/src/proxmox/api-token-form.ts +5 -0
- package/src/proxmox/apply-endpoints.ts +34 -0
- package/src/proxmox/ceph-daemon-form.ts +56 -2
- package/src/proxmox/ceph-daemon.ts +10 -21
- package/src/proxmox/ceph-flag.ts +1 -0
- package/src/proxmox/ceph-fs.ts +1 -0
- package/src/proxmox/ceph-osd-write.ts +30 -4
- package/src/proxmox/ceph-pool.ts +10 -0
- package/src/proxmox/constraints-dialect.test.ts +165 -0
- package/src/proxmox/constraints-forms.test.ts +228 -0
- package/src/proxmox/constraints-host-forms.test.ts +132 -0
- package/src/proxmox/constraints-live.test.ts +155 -0
- package/src/proxmox/constraints-node-forms.test.ts +197 -0
- package/src/proxmox/constraints-required.test.ts +172 -0
- package/src/proxmox/constraints.test.ts +8 -63
- package/src/proxmox/constraints.ts +31 -6
- package/src/proxmox/generated/constraints/index.ts +27 -3
- package/src/proxmox/generated/constraints/pbs-config.ts +63 -1
- package/src/proxmox/generated/constraints/pve-access.ts +15 -3
- package/src/proxmox/generated/constraints/pve-cluster-backup.ts +74 -0
- package/src/proxmox/generated/constraints/pve-cluster-ceph.ts +27 -0
- package/src/proxmox/generated/constraints/pve-cluster-firewall.ts +35 -0
- package/src/proxmox/generated/constraints/pve-cluster-ha.ts +55 -0
- package/src/proxmox/generated/constraints/pve-cluster-metrics.ts +59 -0
- package/src/proxmox/generated/constraints/pve-cluster-notifications.ts +80 -0
- package/src/proxmox/generated/constraints/pve-cluster-replication.ts +43 -0
- package/src/proxmox/generated/constraints/pve-cluster-sdn.ts +90 -0
- package/src/proxmox/generated/constraints/pve-nodes-ceph.ts +57 -0
- package/src/proxmox/generated/constraints/pve-nodes-disks.ts +31 -0
- package/src/proxmox/generated/constraints/pve-nodes-lxc.ts +82 -0
- package/src/proxmox/generated/constraints/pve-nodes-network.ts +77 -0
- package/src/proxmox/generated/constraints/pve-nodes-qemu.ts +125 -0
- package/src/proxmox/generated/constraints/pve-pools.ts +6 -1
- package/src/proxmox/generated/constraints/pve-storage.ts +8 -3
- package/src/proxmox/generated/pbs/access-users.ts +137 -0
- package/src/proxmox/generated/pbs/access.ts +232 -0
- package/src/proxmox/generated/pbs/admin-datastore-store.ts +238 -0
- package/src/proxmox/generated/pbs/admin-metrics.ts +147 -0
- package/src/proxmox/generated/pbs/admin.ts +225 -0
- package/src/proxmox/generated/pbs/backup.ts +121 -0
- package/src/proxmox/generated/pbs/config-access-pam.ts +228 -0
- package/src/proxmox/generated/pbs/config-access.ts +228 -0
- package/src/proxmox/generated/pbs/config-datastore.ts +217 -0
- package/src/proxmox/generated/pbs/config-media-pool.ts +232 -0
- package/src/proxmox/generated/pbs/config-notifications-matchers.ts +187 -0
- package/src/proxmox/generated/pbs/config-notifications.ts +221 -0
- package/src/proxmox/generated/pbs/config-remote.ts +192 -0
- package/src/proxmox/generated/pbs/config-sync.ts +167 -0
- package/src/proxmox/generated/pbs/config-tape.ts +226 -0
- package/src/proxmox/generated/pbs/config-verify.ts +87 -0
- package/src/proxmox/generated/pbs/config.ts +135 -0
- package/src/proxmox/generated/pbs/nodes-node-dns.ts +229 -0
- package/src/proxmox/generated/pbs/nodes-node-services.ts +181 -0
- package/src/proxmox/generated/pbs/nodes-node.ts +244 -0
- package/src/proxmox/generated/pbs/nodes.ts +160 -0
- package/src/proxmox/generated/pbs/ping.ts +16 -0
- package/src/proxmox/generated/pbs/pull.ts +37 -0
- package/src/proxmox/generated/pbs/push.ts +36 -0
- package/src/proxmox/generated/pbs/reader.ts +41 -0
- package/src/proxmox/generated/pbs/root.ts +16 -0
- package/src/proxmox/generated/pbs/status.ts +50 -0
- package/src/proxmox/generated/pbs/tape-drive.ts +220 -0
- package/src/proxmox/generated/pbs/tape-media.ts +133 -0
- package/src/proxmox/generated/pbs/tape.ts +92 -0
- package/src/proxmox/generated/pbs/version.ts +16 -0
- package/src/proxmox/generated/pbs.ts +44 -787
- package/src/proxmox/generated/pve/access-roles.ts +168 -0
- package/src/proxmox/generated/pve/access-users.ts +155 -0
- package/src/proxmox/generated/pve/access.ts +217 -0
- package/src/proxmox/generated/pve/cluster-acme-plugins-id.ts +367 -0
- package/src/proxmox/generated/pve/cluster-acme-plugins.ts +366 -0
- package/src/proxmox/generated/pve/cluster-acme-tos.ts +232 -0
- package/src/proxmox/generated/pve/cluster-bulk-action.ts +227 -0
- package/src/proxmox/generated/pve/cluster-firewall-refs.ts +110 -0
- package/src/proxmox/generated/pve/cluster-firewall.ts +236 -0
- package/src/proxmox/generated/pve/cluster-ha.ts +203 -0
- package/src/proxmox/generated/pve/cluster-jobs.ts +190 -0
- package/src/proxmox/generated/pve/cluster-metrics.ts +116 -0
- package/src/proxmox/generated/pve/cluster-notifications-endpoints.ts +244 -0
- package/src/proxmox/generated/pve/cluster-notifications.ts +99 -0
- package/src/proxmox/generated/pve/cluster-options.ts +189 -0
- package/src/proxmox/generated/pve/cluster-qemu-custom-cpu-models-cputype.ts +272 -0
- package/src/proxmox/generated/pve/cluster-qemu-custom-cpu-models.ts +268 -0
- package/src/proxmox/generated/pve/cluster-replication.ts +127 -0
- package/src/proxmox/generated/pve/cluster-sdn-fabrics.ts +218 -0
- package/src/proxmox/generated/pve/cluster-sdn-ipams.ts +229 -0
- package/src/proxmox/generated/pve/cluster-sdn-vnets-vnet.ts +245 -0
- package/src/proxmox/generated/pve/cluster-sdn-vnets.ts +49 -0
- package/src/proxmox/generated/pve/cluster-sdn-zones.ts +224 -0
- package/src/proxmox/generated/pve/cluster-sdn.ts +201 -0
- package/src/proxmox/generated/pve/cluster.ts +71 -0
- package/src/proxmox/generated/pve/nodes-node-ceph.ts +167 -0
- package/src/proxmox/generated/pve/nodes-node-config.ts +56 -0
- package/src/proxmox/generated/pve/nodes-node-disks.ts +237 -0
- package/src/proxmox/generated/pve/nodes-node-firewall.ts +239 -0
- package/src/proxmox/generated/pve/nodes-node-hardware.ts +215 -0
- package/src/proxmox/generated/pve/nodes-node-lxc-vmid-firewall.ts +232 -0
- package/src/proxmox/generated/pve/nodes-node-lxc-vmid-move-volume.ts +1059 -0
- package/src/proxmox/generated/pve/nodes-node-lxc-vmid-mtunnel.ts +49 -0
- package/src/proxmox/generated/pve/nodes-node-lxc-vmid-resize.ts +285 -0
- package/src/proxmox/generated/pve/nodes-node-lxc-vmid-rrd.ts +184 -0
- package/src/proxmox/generated/pve/nodes-node-lxc-vmid.ts +240 -0
- package/src/proxmox/generated/pve/nodes-node-network.ts +234 -0
- package/src/proxmox/generated/pve/nodes-node-qemu-vmid-config.ts +441 -0
- package/src/proxmox/generated/pve/nodes-node-qemu-vmid-dbus-vmstate.ts +215 -0
- package/src/proxmox/generated/pve/nodes-node-qemu-vmid-firewall-rules.ts +145 -0
- package/src/proxmox/generated/pve/nodes-node-qemu-vmid-move-disk.ts +664 -0
- package/src/proxmox/generated/pve/nodes-node-qemu-vmid-mtunnel.ts +188 -0
- package/src/proxmox/generated/pve/nodes-node-qemu-vmid-status.ts +233 -0
- package/src/proxmox/generated/pve/nodes-node-qemu-vmid.ts +187 -0
- package/src/proxmox/generated/pve/nodes-node-qemu.ts +204 -0
- package/src/proxmox/generated/pve/nodes-node-replication.ts +187 -0
- package/src/proxmox/generated/pve/nodes-node-services.ts +221 -0
- package/src/proxmox/generated/pve/nodes-node-storage-storage.ts +226 -0
- package/src/proxmox/generated/pve/nodes-node-storage.ts +40 -0
- package/src/proxmox/generated/pve/nodes-node-subscription.ts +168 -0
- package/src/proxmox/generated/pve/nodes-node-vzdump.ts +96 -0
- package/src/proxmox/generated/pve/nodes-node.ts +211 -0
- package/src/proxmox/generated/pve/nodes.ts +235 -0
- package/src/proxmox/generated/pve/pools.ts +78 -0
- package/src/proxmox/generated/pve/storage.ts +213 -0
- package/src/proxmox/generated/pve/version.ts +21 -0
- package/src/proxmox/generated/pve.ts +69 -7403
- package/src/proxmox/lxc-interior.test.ts +17 -1
- package/src/proxmox/lxc-lifecycle.ts +48 -9
- package/src/proxmox/metric-server-form.ts +10 -0
- package/src/proxmox/metric-server.ts +11 -2
- package/src/proxmox/network-apply.ts +4 -0
- package/src/proxmox/node-network-form.ts +19 -0
- package/src/proxmox/node-network.ts +9 -3
- package/src/proxmox/notification-target-endpoint.ts +46 -0
- package/src/proxmox/notification-target-form.ts +11 -0
- package/src/proxmox/notification-target.ts +5 -2
- package/src/proxmox/pbs-notification-target-endpoint.ts +45 -0
- package/src/proxmox/pbs-notification-target-lifecycle.ts +16 -14
- package/src/proxmox/qemu.ts +9 -1
- package/src/proxmox/resource-guard.ts +61 -0
- package/src/proxmox/resource-spec.ts +16 -4
- package/src/proxmox/resource.ts +27 -27
- package/src/proxmox/sdn-apply.ts +4 -0
- package/src/proxmox/sdn-subnet.ts +5 -0
- package/src/proxmox/zfs-pool-write.ts +12 -0
- package/src/proxmox/zfs-pool.ts +2 -1
- package/dist/proxmox/generated/constraints/pve-cluster.d.ts +0 -18
- package/dist/proxmox/generated/constraints/pve-cluster.d.ts.map +0 -1
- package/src/proxmox/generated/constraints/pve-cluster.ts +0 -174
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an undeclared property means — and the day it meant "delete that".
|
|
3
|
+
*
|
|
4
|
+
* 🔴 THE BUG THIS PINS, FOUND BY REVIEW RATHER THAN BY AN INCIDENT. `Netbox.Prefix` shipped
|
|
5
|
+
* sending `description: ''` whenever the prop was absent. On a create that is invisible. ⛔ On
|
|
6
|
+
* an ADOPT it is data loss: NetBox is the estate's record of DECISIONS, and a prefix's
|
|
7
|
+
* description is usually the only written trace of why that range exists. The first deploy that
|
|
8
|
+
* adopted one would have PATCHed it to empty, and the plan would have read `update` — a diff
|
|
9
|
+
* that looks like converging a declaration and is really deleting a sentence.
|
|
10
|
+
*
|
|
11
|
+
* ★ THE TELL WAS THE INCONSISTENCY, not a failure. Foreign keys were already omitted when
|
|
12
|
+
* undeclared, with a comment saying why; free text was not. Two fields, the same hazard, two
|
|
13
|
+
* answers — that gap is the defect.
|
|
14
|
+
*/
|
|
15
|
+
import { describe, expect, test } from 'bun:test';
|
|
16
|
+
import { prefixBody, prefixMatches } from './prefix-form.ts';
|
|
17
|
+
import type { PrefixAttributes } from './prefix.ts';
|
|
18
|
+
|
|
19
|
+
const live = (over: Partial<PrefixAttributes> = {}): PrefixAttributes => ({
|
|
20
|
+
comments: '',
|
|
21
|
+
description: '',
|
|
22
|
+
isPool: false,
|
|
23
|
+
markUtilized: false,
|
|
24
|
+
prefix: '10.0.0.0/24',
|
|
25
|
+
prefixId: 1,
|
|
26
|
+
status: 'active',
|
|
27
|
+
tenant: undefined,
|
|
28
|
+
vlan: undefined,
|
|
29
|
+
vrf: undefined,
|
|
30
|
+
...over,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('an undeclared field is not an instruction to clear it', () => {
|
|
34
|
+
test('the body omits description and comments entirely when they are absent', () => {
|
|
35
|
+
const body = prefixBody({ prefix: '10.0.0.0/24' });
|
|
36
|
+
expect(Object.hasOwn(body, 'description')).toBe(false);
|
|
37
|
+
expect(Object.hasOwn(body, 'comments')).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/** ⛔ The regression itself: a prefix carrying a human's note must read as converged. */
|
|
41
|
+
test('a live description nobody declared does NOT count as drift', () => {
|
|
42
|
+
expect(
|
|
43
|
+
prefixMatches(live({ description: 'retired 2026-09-14, do not reuse' }), {
|
|
44
|
+
prefix: '10.0.0.0/24',
|
|
45
|
+
}),
|
|
46
|
+
).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('the same is true of comments and of an optional foreign key', () => {
|
|
50
|
+
const attributes = live({ comments: 'see the retirement doc', tenant: 4, vlan: 9 });
|
|
51
|
+
expect(prefixMatches(attributes, { prefix: '10.0.0.0/24' })).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* ⚠️ THE COST, STATED: prose is cleared by declaring it empty, on purpose. That is the readable
|
|
56
|
+
* way to say a destructive thing, and it still reaches the wire.
|
|
57
|
+
*/
|
|
58
|
+
test('an explicitly empty description IS sent and IS compared', () => {
|
|
59
|
+
expect(prefixBody({ description: '', prefix: '10.0.0.0/24' })['description']).toBe('');
|
|
60
|
+
expect(
|
|
61
|
+
prefixMatches(live({ description: 'a note' }), { description: '', prefix: '10.0.0.0/24' }),
|
|
62
|
+
).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('a field NetBox itself defaults is still settled', () => {
|
|
67
|
+
/**
|
|
68
|
+
* ★ THE LINE BETWEEN THE TWO GROUPS. `status`, `is_pool` and `mark_utilized` have vendor
|
|
69
|
+
* defaults in the schema, so omitting one genuinely means "the default" and settling it says
|
|
70
|
+
* what NetBox would have done anyway. Free text has no such default.
|
|
71
|
+
*/
|
|
72
|
+
test('status, is_pool and mark_utilized are always on the wire', () => {
|
|
73
|
+
const body = prefixBody({ prefix: '10.0.0.0/24' });
|
|
74
|
+
expect(body['status']).toBe('active');
|
|
75
|
+
expect(body['is_pool']).toBe(false);
|
|
76
|
+
expect(body['mark_utilized']).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('a live status that differs from the settled default IS drift', () => {
|
|
80
|
+
expect(prefixMatches(live({ status: 'reserved' }), { prefix: '10.0.0.0/24' })).toBe(false);
|
|
81
|
+
expect(
|
|
82
|
+
prefixMatches(live({ status: 'reserved' }), {
|
|
83
|
+
prefix: '10.0.0.0/24',
|
|
84
|
+
status: 'reserved',
|
|
85
|
+
}),
|
|
86
|
+
).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
/** ★ The move this resource exists for: recording a retirement as a declaration. */
|
|
90
|
+
test('declaring deprecated is drift against an active prefix, and converges', () => {
|
|
91
|
+
const props = { prefix: '10.1.1.0/24', status: 'deprecated' } as const;
|
|
92
|
+
expect(prefixMatches(live({ prefix: '10.1.1.0/24' }), props)).toBe(false);
|
|
93
|
+
expect(prefixBody(props)['status']).toBe('deprecated');
|
|
94
|
+
expect(prefixMatches(live({ prefix: '10.1.1.0/24', status: 'deprecated' }), props)).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* ⛔ THE INVARIANT THAT KEEPS THE TWO IN STEP. A field compared but never sent produces a plan
|
|
100
|
+
* that says `update` forever: the PATCH omits it, so the next read is unchanged.
|
|
101
|
+
*/
|
|
102
|
+
describe('body and matches agree about which fields are ours', () => {
|
|
103
|
+
test('every field matches compares as optional is one the body sends when declared', () => {
|
|
104
|
+
const declared = prefixBody({
|
|
105
|
+
comments: 'c',
|
|
106
|
+
description: 'd',
|
|
107
|
+
prefix: '10.0.0.0/24',
|
|
108
|
+
tenant: 4,
|
|
109
|
+
vlan: 9,
|
|
110
|
+
});
|
|
111
|
+
for (const key of ['description', 'comments', 'tenant', 'vlan']) {
|
|
112
|
+
expect(Object.hasOwn(declared, key)).toBe(true);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('an undeclared optional is neither sent nor compared', () => {
|
|
117
|
+
const bare = prefixBody({ prefix: '10.0.0.0/24' });
|
|
118
|
+
for (const key of ['description', 'comments', 'tenant', 'vlan', 'vrf']) {
|
|
119
|
+
expect(Object.hasOwn(bare, key)).toBe(false);
|
|
120
|
+
}
|
|
121
|
+
expect(
|
|
122
|
+
prefixMatches(live({ comments: 'x', description: 'y', tenant: 1, vlan: 2 }), {
|
|
123
|
+
prefix: '10.0.0.0/24',
|
|
124
|
+
}),
|
|
125
|
+
).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire body for a `Netbox.Prefix`, and the comparison that decides whether it needs writing.
|
|
3
|
+
*
|
|
4
|
+
* ★ EXTRACTED SO IT CAN BE TESTED WITHOUT A SERVER. `body` and `matches` are where a NetBox
|
|
5
|
+
* resource either converges or quietly destroys something, and both are pure functions of the
|
|
6
|
+
* props — so they belong somewhere a test can call them with a literal, the way the Proxmox
|
|
7
|
+
* families keep their `*-form.ts` beside the resource.
|
|
8
|
+
*/
|
|
9
|
+
import type { PrefixAttributes, PrefixProps, PrefixStatus } from './prefix.ts';
|
|
10
|
+
|
|
11
|
+
/** ⚠️ NetBox's own default when the property is omitted, copied from the schema, not assumed. */
|
|
12
|
+
const DEFAULT_STATUS: PrefixStatus = 'active';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The fields an undeclared prop still settles, because NetBox itself would.
|
|
16
|
+
*
|
|
17
|
+
* ⛔ FREE TEXT IS NOT IN HERE, AND THE LINE IS NOT COSMETIC. `status`, `is_pool` and
|
|
18
|
+
* `mark_utilized` have vendor defaults in the schema — omitting one genuinely means "the
|
|
19
|
+
* default", so settling it says what NetBox would have done anyway. `description` and
|
|
20
|
+
* `comments` have no such default: the schema's `''` is the absence of a value, not a decision.
|
|
21
|
+
* Treating "I did not mention it" as "set it to empty" is how a declaration that says nothing
|
|
22
|
+
* about prose ERASES prose somebody wrote.
|
|
23
|
+
*/
|
|
24
|
+
export const settled = (props: PrefixProps) => ({
|
|
25
|
+
isPool: props.isPool ?? false,
|
|
26
|
+
markUtilized: props.markUtilized ?? false,
|
|
27
|
+
status: props.status ?? DEFAULT_STATUS,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* ⛔ THE BODY IS BUILT ONCE AND USED FOR BOTH CREATE AND UPDATE, so a field cannot be settable on
|
|
32
|
+
* one path and forgotten on the other — the failure mode that makes a resource converge on
|
|
33
|
+
* create and drift forever after.
|
|
34
|
+
*
|
|
35
|
+
* 🔴 AN UNDECLARED FIELD THAT NETBOX DOES NOT DEFAULT IS OMITTED, NEVER SENT EMPTY OR NULL. This
|
|
36
|
+
* resource shipped sending `description: ''` whenever the prop was absent. On a fresh create
|
|
37
|
+
* that is invisible — the field was empty anyway. ⛔ ON AN ADOPT IT IS DATA LOSS: NetBox is the
|
|
38
|
+
* estate's record of DECISIONS, so the description on an existing prefix is usually the only
|
|
39
|
+
* written trace of why that range exists, and the first deploy that adopted it would have
|
|
40
|
+
* PATCHed it to empty. `matches` would have reported drift, the plan would have said `update`,
|
|
41
|
+
* and the diff would have read as converging a declaration rather than deleting a sentence.
|
|
42
|
+
* ★ THE TELL WAS ALREADY IN THIS FILE: foreign keys were omitted for exactly this reason while
|
|
43
|
+
* free text was not. An inconsistency between two fields with the same hazard is the bug.
|
|
44
|
+
* ⚠️ THE COST IS THAT PROSE CANNOT BE CLEARED BY OMISSION — clearing it is `description: ''`,
|
|
45
|
+
* written on purpose, which is the readable way to say a destructive thing.
|
|
46
|
+
*/
|
|
47
|
+
export const prefixBody = (props: PrefixProps): Record<string, unknown> => {
|
|
48
|
+
const fixed = settled(props);
|
|
49
|
+
const out: Record<string, unknown> = {
|
|
50
|
+
is_pool: fixed.isPool,
|
|
51
|
+
mark_utilized: fixed.markUtilized,
|
|
52
|
+
prefix: props.prefix,
|
|
53
|
+
status: fixed.status,
|
|
54
|
+
};
|
|
55
|
+
if (props.description !== undefined) out['description'] = props.description;
|
|
56
|
+
if (props.comments !== undefined) out['comments'] = props.comments;
|
|
57
|
+
if (props.vrf !== undefined) out['vrf'] = props.vrf;
|
|
58
|
+
if (props.tenant !== undefined) out['tenant'] = props.tenant;
|
|
59
|
+
if (props.vlan !== undefined) out['vlan'] = props.vlan;
|
|
60
|
+
return out;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Whether the live object already says what the declaration says.
|
|
65
|
+
*
|
|
66
|
+
* ⛔ EVERY FIELD `prefixBody` OMITS IS ALSO UNCOMPARED HERE, AND THE TWO MUST MOVE TOGETHER. A
|
|
67
|
+
* comparison against a field the body does not send reports drift no write can fix: the plan
|
|
68
|
+
* says `update`, the PATCH omits the field, and the next plan says `update` again — forever.
|
|
69
|
+
* An undeclared prop means "not mine", in both directions.
|
|
70
|
+
* ⚠️ `vrf` IS ABSENT ON PURPOSE. It is identity, not settings: `identifies` has already proved
|
|
71
|
+
* this row is in the declared VRF, and a prefix's VRF cannot be edited into another one here.
|
|
72
|
+
*/
|
|
73
|
+
export const prefixMatches = (attributes: PrefixAttributes, props: PrefixProps): boolean => {
|
|
74
|
+
const fixed = settled(props);
|
|
75
|
+
return (
|
|
76
|
+
attributes.status === fixed.status &&
|
|
77
|
+
attributes.isPool === fixed.isPool &&
|
|
78
|
+
attributes.markUtilized === fixed.markUtilized &&
|
|
79
|
+
(props.description === undefined || attributes.description === props.description) &&
|
|
80
|
+
(props.comments === undefined || attributes.comments === props.comments) &&
|
|
81
|
+
(props.tenant === undefined || attributes.tenant === props.tenant) &&
|
|
82
|
+
(props.vlan === undefined || attributes.vlan === props.vlan)
|
|
83
|
+
);
|
|
84
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Netbox.Prefix` — one IP prefix and the decision recorded against it.
|
|
3
|
+
*
|
|
4
|
+
* ★ WHY THIS OBJECT CLASS IS THE FIRST ONE, AND NOT `Device`, `VLAN` OR `Site`. Three reasons,
|
|
5
|
+
* each measurable rather than aesthetic:
|
|
6
|
+
*
|
|
7
|
+
* 1. **It is the only interesting NetBox object with no foreign-key prerequisite.** MEASURED in
|
|
8
|
+
* the 4.7.0 schema: `WritablePrefixRequest.required` is exactly `["prefix"]`. A `VLAN` needs
|
|
9
|
+
* `vid` AND `name` and belongs to a group or site; a `Device` needs a role, a type, a site
|
|
10
|
+
* and usually a manufacturer — four more Resources before the first one can be declared.
|
|
11
|
+
* A scaffold that cannot declare anything until five families exist proves nothing.
|
|
12
|
+
* 2. **It is what the estate's NetBox is FOR.** The house's own IPAM agent describes its job as
|
|
13
|
+
* reconciling "what the network IS against what NetBox says it was DECIDED to be". A prefix
|
|
14
|
+
* is the atom of that decision. The estate has prefixes whose status is currently recorded
|
|
15
|
+
* only in an SSH config comment and a retirement doc — a management VLAN, a Thunderbolt
|
|
16
|
+
* point-to-point, and three ranges that are retired and still resolve in people's heads.
|
|
17
|
+
* 3. **`status: 'deprecated'` makes retirement declarable.** That is the move this unlocks:
|
|
18
|
+
* a retired range stops being folklore and becomes a line in a stack file with a reviewable
|
|
19
|
+
* diff. No other NetBox class turns an estate decision into one field this cleanly.
|
|
20
|
+
*
|
|
21
|
+
* ⚠️ ADOPT-FRIENDLY BY CONSTRUCTION. `locate` filters on the exact `prefix`; `identifies` then
|
|
22
|
+
* picks the declared VRF, because NetBox allows the same CIDR in two VRFs and in the global
|
|
23
|
+
* table. An instance that already holds `10.0.0.0/24` is BOUND, never duplicated — and ⛔ an
|
|
24
|
+
* adopt never clears prose it did not declare (prefix-form.ts).
|
|
25
|
+
*
|
|
26
|
+
* ⛔ THIS RESOURCE DOES NOT REMOVE PREFIXES IT DOES NOT DECLARE, and `defaultRemovalPolicy` is
|
|
27
|
+
* `retain` (resource.ts). Deleting a prefix in NetBox reparents its children and detaches its
|
|
28
|
+
* IP assignments; the record of why it existed survives only in the change log.
|
|
29
|
+
*
|
|
30
|
+
* ⛔ NO CREDENTIAL IS A PROP. `NETBOX_URL` and `NETBOX_TOKEN` are read from the environment at
|
|
31
|
+
* call time — the house convention, and the only shape that keeps a token out of Alchemy state.
|
|
32
|
+
*/
|
|
33
|
+
import { Resource } from 'alchemy';
|
|
34
|
+
import * as Provider from 'alchemy/Provider';
|
|
35
|
+
import * as Effect from 'effect/Effect';
|
|
36
|
+
import { prefixBody, prefixMatches } from './prefix-form.ts';
|
|
37
|
+
import { type NetboxRequirements, netboxHandlers } from './resource.ts';
|
|
38
|
+
import { choice, fk, text } from './values.ts';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* ⚠️ THE FOUR VALUES ARE THE VENDOR'S, NOT OURS — copied from the generated table, which is
|
|
42
|
+
* generated from NetBox's own enum. The table is what ENFORCES them; this type is what makes
|
|
43
|
+
* the mistake visible in an editor.
|
|
44
|
+
*/
|
|
45
|
+
export type PrefixStatus = 'container' | 'active' | 'reserved' | 'deprecated';
|
|
46
|
+
|
|
47
|
+
export interface PrefixProps {
|
|
48
|
+
/** CIDR, exactly as NetBox stores it — `10.0.0.0/24`. The stable key for locate. */
|
|
49
|
+
prefix: string;
|
|
50
|
+
/**
|
|
51
|
+
* VRF id, or omitted for the global table.
|
|
52
|
+
*
|
|
53
|
+
* ⛔ PART OF THE IDENTITY, NOT A SETTING. NetBox's uniqueness on a prefix is per-VRF, so a
|
|
54
|
+
* locate that ignored it could match two rows — which `soleMatch` refuses rather than
|
|
55
|
+
* guessing between.
|
|
56
|
+
*/
|
|
57
|
+
vrf?: number;
|
|
58
|
+
status?: PrefixStatus;
|
|
59
|
+
/** ⚠️ `maxLength: 200` in the schema; the generated table enforces it at plan time. */
|
|
60
|
+
description?: string;
|
|
61
|
+
/** Free text. ⚠️ NetBox states no length limit, so nothing here checks one. */
|
|
62
|
+
comments?: string;
|
|
63
|
+
tenant?: number;
|
|
64
|
+
vlan?: number;
|
|
65
|
+
/** All addresses inside are usable. */
|
|
66
|
+
isPool?: boolean;
|
|
67
|
+
markUtilized?: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface PrefixAttributes {
|
|
71
|
+
prefixId: number;
|
|
72
|
+
prefix: string;
|
|
73
|
+
status: string;
|
|
74
|
+
description: string;
|
|
75
|
+
comments: string;
|
|
76
|
+
vrf: number | undefined;
|
|
77
|
+
tenant: number | undefined;
|
|
78
|
+
vlan: number | undefined;
|
|
79
|
+
isPool: boolean;
|
|
80
|
+
markUtilized: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface NetboxPrefix extends Resource<
|
|
84
|
+
'Netbox.Prefix',
|
|
85
|
+
PrefixProps,
|
|
86
|
+
PrefixAttributes,
|
|
87
|
+
never,
|
|
88
|
+
NetboxRequirements
|
|
89
|
+
> {}
|
|
90
|
+
|
|
91
|
+
export const NetboxPrefix = Resource<NetboxPrefix>('Netbox.Prefix', {
|
|
92
|
+
defaultRemovalPolicy: 'retain',
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const handlers = netboxHandlers<PrefixProps, PrefixAttributes>({
|
|
96
|
+
attributes: (live, props) => {
|
|
97
|
+
const id = live['id'];
|
|
98
|
+
if (typeof id !== 'number') return undefined;
|
|
99
|
+
return {
|
|
100
|
+
comments: text(live['comments']),
|
|
101
|
+
description: text(live['description']),
|
|
102
|
+
isPool: live['is_pool'] === true,
|
|
103
|
+
markUtilized: live['mark_utilized'] === true,
|
|
104
|
+
prefix: text(live['prefix']) || props.prefix,
|
|
105
|
+
prefixId: id,
|
|
106
|
+
status: choice(live['status']),
|
|
107
|
+
tenant: fk(live['tenant']),
|
|
108
|
+
vlan: fk(live['vlan']),
|
|
109
|
+
vrf: fk(live['vrf']),
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
collection: 'ipam/prefixes',
|
|
113
|
+
createBody: prefixBody,
|
|
114
|
+
describe: (props) => `ipam/prefixes ${props.prefix}`,
|
|
115
|
+
endpoint: {
|
|
116
|
+
create: 'netbox:POST /api/ipam/prefixes/',
|
|
117
|
+
update: 'netbox:PATCH /api/ipam/prefixes/{id}/',
|
|
118
|
+
},
|
|
119
|
+
/**
|
|
120
|
+
* 🔴 THE VRF IS **NOT** IN THIS FILTER, AND THE REASON IS A GUESS THAT WAS CAUGHT. This read
|
|
121
|
+
* `vrf_id=null` for the global table — NetBox does define `FILTERS_NULL_CHOICE_VALUE = 'null'`
|
|
122
|
+
* and does honour it on filters that opt in. ⛔ MEASURED IN THE VENDOR'S OWN SOURCE at
|
|
123
|
+
* v4.7.0: `PrefixFilterSet.vrf_id` is a plain `django_filters.ModelMultipleChoiceFilter` with
|
|
124
|
+
* no `null_value`, so `'null'` is validated against the VRF queryset, fails, and NetBox
|
|
125
|
+
* answers 400 under strict filtering — on EVERY plan for EVERY global prefix.
|
|
126
|
+
* ★ So the CIDR narrows server-side with a filter the document plainly declares, and `identifies`
|
|
127
|
+
* picks the VRF in this process. One extra page of candidates, no vendor semantics guessed.
|
|
128
|
+
*/
|
|
129
|
+
identifies: (live, props) => fk(live['vrf']) === props.vrf,
|
|
130
|
+
locate: (props) => [['prefix', props.prefix]],
|
|
131
|
+
matches: prefixMatches,
|
|
132
|
+
updateBody: prefixBody,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
export const NetboxPrefixProvider = () =>
|
|
136
|
+
Provider.effect(NetboxPrefix, Effect.succeed(NetboxPrefix.Provider.of(handlers)));
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One shape for every NetBox object — the same four operations, different path and JSON body.
|
|
3
|
+
*
|
|
4
|
+
* ⛔ THE ALTERNATIVE IS COPYING THE SAME HANDLERS PER RESOURCE. Prefixes, VLANs, devices, tenants
|
|
5
|
+
* and interfaces are all create / read / update / delete over `/api` with JSON bodies and a
|
|
6
|
+
* numeric primary key. Hand-rolling the fourth copy is where someone drops the read-back or the
|
|
7
|
+
* constraint guard and the plan lies.
|
|
8
|
+
*
|
|
9
|
+
* ★ ADOPT IS THE DEFAULT POSTURE, NOT A FLAG. `reconcile` LOCATES before it writes: an object the
|
|
10
|
+
* estate already has is bound, not duplicated. NetBox is a record that predates this code by
|
|
11
|
+
* years; a provider that created a second `10.20.10.0/24` on first deploy would be worse than
|
|
12
|
+
* no provider.
|
|
13
|
+
*
|
|
14
|
+
* ★ `defaultRemovalPolicy: 'retain'` ON EVERY FAMILY. NetBox holds the estate's record of what
|
|
15
|
+
* the network was DECIDED to be, and deleting a row deletes history — child prefixes reparent,
|
|
16
|
+
* IP assignments detach, and the change log is the only trace left. `retain` means the engine
|
|
17
|
+
* skips `delete` when a resource is orphaned unless the caller opts into
|
|
18
|
+
* `.pipe(RemovalPolicy.destroy())`. The `delete` handler is FULLY IMPLEMENTED anyway: a stub
|
|
19
|
+
* that silently does nothing lies to whoever reads the plan.
|
|
20
|
+
*/
|
|
21
|
+
import { isResolved } from 'alchemy/Diff';
|
|
22
|
+
import type { Input } from 'alchemy/Input';
|
|
23
|
+
import * as Effect from 'effect/Effect';
|
|
24
|
+
import type * as HttpClient from 'effect/unstable/http/HttpClient';
|
|
25
|
+
import { NetboxError, type NetboxList, type NetboxRow, netbox, soleMatch } from './client.ts';
|
|
26
|
+
import { guardBody } from './constraint-guard.ts';
|
|
27
|
+
import type { EndpointKey, NetboxBody } from './constraints.ts';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* ★ WHAT EVERY OPERATION HERE NEEDS FROM THE RUNTIME, AND NOTHING MORE. The token comes from
|
|
31
|
+
* `NETBOX_TOKEN` in the environment, so there is no process to spawn and no `bao` to shell out
|
|
32
|
+
* to. Declaring a service you never use is not harmless — it makes a stack provide a layer for
|
|
33
|
+
* nothing, and it hides the one service you DO need.
|
|
34
|
+
*/
|
|
35
|
+
export type NetboxRequirements = HttpClient.HttpClient;
|
|
36
|
+
|
|
37
|
+
export interface NetboxSpec<Props extends object, Attributes> {
|
|
38
|
+
/** `ipam/prefixes` — where a new one is POSTed and where `locate` filters. */
|
|
39
|
+
readonly collection: string;
|
|
40
|
+
/**
|
|
41
|
+
* The query that narrows the candidates, as `URLSearchParams` pairs.
|
|
42
|
+
*
|
|
43
|
+
* ⛔ ONLY FILTERS THE VENDOR DOCUMENT DECLARES, AND ONLY IN THE FORM IT DECLARES THEM. A
|
|
44
|
+
* filter NetBox rejects answers 400, which `absentOn404` correctly does NOT fold to "absent" —
|
|
45
|
+
* so a guessed filter shape is a hard failure on every plan, not a soft one.
|
|
46
|
+
* ★ IT DOES NOT HAVE TO BE UNIQUE. `identifies` finishes the job in this process, where the
|
|
47
|
+
* rule is readable and testable offline.
|
|
48
|
+
*/
|
|
49
|
+
readonly locate: (props: Props) => readonly (readonly [string, string])[];
|
|
50
|
+
/**
|
|
51
|
+
* Which of the returned rows IS this object.
|
|
52
|
+
*
|
|
53
|
+
* 🔴 THIS EXISTS BECAUSE THE OBVIOUS ALTERNATIVE WAS A GUESS. `Netbox.Prefix` first narrowed
|
|
54
|
+
* server-side with `vrf_id=null`, the sentinel NetBox uses elsewhere for "no foreign key".
|
|
55
|
+
* ⛔ MEASURED IN THE VENDOR'S OWN SOURCE at v4.7.0: `PrefixFilterSet.vrf_id` is a plain
|
|
56
|
+
* `django_filters.ModelMultipleChoiceFilter` with NO `null_value`, so `'null'` is validated
|
|
57
|
+
* against the VRF queryset, fails, and NetBox answers 400 under strict filtering. The
|
|
58
|
+
* sentinel is real (`FILTERS_NULL_CHOICE_VALUE = 'null'`) but it is opt-in per filter, and
|
|
59
|
+
* this one did not opt in.
|
|
60
|
+
* ★ SO THE DISCRIMINATOR MOVED INTO THIS PROCESS. It costs one page of candidates and buys a
|
|
61
|
+
* rule that can be read, tested without a server, and cannot be wrong about a vendor's
|
|
62
|
+
* filter semantics. Omit it when `locate` really is unique on its own.
|
|
63
|
+
*/
|
|
64
|
+
readonly identifies?: (live: NetboxRow, props: Props) => boolean;
|
|
65
|
+
readonly attributes: (live: NetboxRow, props: Props) => Attributes | undefined;
|
|
66
|
+
readonly createBody: (props: Props) => NetboxBody;
|
|
67
|
+
/** ⚠️ PARTIAL BY DESIGN: NetBox updates are PATCH, so presence is not checked on this body. */
|
|
68
|
+
readonly updateBody?: (props: Props) => NetboxBody;
|
|
69
|
+
readonly matches: (attributes: Attributes, props: Props) => boolean;
|
|
70
|
+
/** The generated constraint table to check each body against, by endpoint key. */
|
|
71
|
+
readonly endpoint: { readonly create: EndpointKey; readonly update?: EndpointKey };
|
|
72
|
+
/** For the message `soleMatch` raises and for read-back failures. */
|
|
73
|
+
readonly describe: (props: Props) => string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** ⚠️ Only 404 folds to "absent". A 403 or a 502 is a real failure — folding it would plan a create over an existing object. */
|
|
77
|
+
const absentOn404 = <A>(io: Effect.Effect<A, NetboxError, NetboxRequirements>) =>
|
|
78
|
+
io.pipe(
|
|
79
|
+
Effect.catchIf(
|
|
80
|
+
(cause): cause is NetboxError => cause instanceof NetboxError && cause.status === 404,
|
|
81
|
+
() => Effect.succeed(undefined as A),
|
|
82
|
+
),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* ⚠️ NOT `limit=1`, AND NOT `limit=2` EITHER. One row is all a caller wants, but the page has to
|
|
87
|
+
* be wide enough for `identifies` to see every candidate `locate` matched — a prefix that
|
|
88
|
+
* exists in several VRFs comes back several times, and a page of two would hide the third.
|
|
89
|
+
* ★ TWENTY, AND THE NUMBER IS AN ASSERTION: a natural key matching more than twenty rows is not
|
|
90
|
+
* a natural key. `count` is checked against it, so the failure is "your filter is too broad"
|
|
91
|
+
* rather than a silent truncation.
|
|
92
|
+
*/
|
|
93
|
+
const LOCATE_PAGE = 20;
|
|
94
|
+
|
|
95
|
+
const query = (pairs: readonly (readonly [string, string])[]): string =>
|
|
96
|
+
new URLSearchParams([
|
|
97
|
+
...pairs.map(([k, v]) => [k, v] as [string, string]),
|
|
98
|
+
['limit', String(LOCATE_PAGE)],
|
|
99
|
+
]).toString();
|
|
100
|
+
|
|
101
|
+
export const netboxOperations = <Props extends object, Attributes>(
|
|
102
|
+
spec: NetboxSpec<Props, Attributes>,
|
|
103
|
+
) => {
|
|
104
|
+
const fetchLive = (props: Props) =>
|
|
105
|
+
absentOn404(
|
|
106
|
+
netbox<NetboxList<NetboxRow>>('GET', `${spec.collection}/?${query(spec.locate(props))}`),
|
|
107
|
+
).pipe(
|
|
108
|
+
Effect.map((list) => {
|
|
109
|
+
if (list !== undefined && list.count > LOCATE_PAGE) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`${spec.describe(props)}: the locate filter matched ${String(list.count)} rows, more ` +
|
|
112
|
+
`than the ${String(LOCATE_PAGE)}-row page this reads. That is not a natural key — ` +
|
|
113
|
+
'narrow the filter rather than paging through candidates.',
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
const rows = list?.results ?? [];
|
|
117
|
+
const narrow = spec.identifies;
|
|
118
|
+
return soleMatch(
|
|
119
|
+
narrow === undefined ? rows : rows.filter((row) => narrow(row, props)),
|
|
120
|
+
spec.describe(props),
|
|
121
|
+
);
|
|
122
|
+
}),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
/** ⛔ The numeric `id` is the wire path. NetBox has no update-by-natural-key. */
|
|
126
|
+
const objectPath = (live: NetboxRow): string => `${spec.collection}/${String(live['id'])}`;
|
|
127
|
+
|
|
128
|
+
const read = (props: Props) =>
|
|
129
|
+
fetchLive(props).pipe(
|
|
130
|
+
Effect.map((live) => (live === undefined ? undefined : spec.attributes(live, props))),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
read,
|
|
135
|
+
|
|
136
|
+
diff: (news: Input<Props>, output: Attributes | undefined) =>
|
|
137
|
+
Effect.gen(function* () {
|
|
138
|
+
if (output === undefined || !isResolved(news)) return undefined;
|
|
139
|
+
const live = yield* read(news);
|
|
140
|
+
if (live === undefined) return { action: 'update' } as const;
|
|
141
|
+
if (spec.matches(live, news)) return { action: 'noop' } as const;
|
|
142
|
+
return spec.updateBody === undefined
|
|
143
|
+
? ({ action: 'replace' } as const)
|
|
144
|
+
: ({ action: 'update' } as const);
|
|
145
|
+
}),
|
|
146
|
+
|
|
147
|
+
reconcile: (news: Props) =>
|
|
148
|
+
Effect.gen(function* () {
|
|
149
|
+
const before = yield* fetchLive(news);
|
|
150
|
+
if (before === undefined) {
|
|
151
|
+
const body = spec.createBody(news);
|
|
152
|
+
// ⛔ GUARD BEFORE THE REQUEST, WITH PRESENCE ON. This is the only place a missing
|
|
153
|
+
// required property can still be caught locally.
|
|
154
|
+
yield* guardBody(spec.endpoint.create, body, true);
|
|
155
|
+
yield* netbox('POST', `${spec.collection}/`, body);
|
|
156
|
+
} else if (spec.updateBody !== undefined) {
|
|
157
|
+
const mapped = spec.attributes(before, news);
|
|
158
|
+
// ⚠️ ADOPTION IS THE `mapped === undefined` CASE TOO: a row this spec cannot map is not
|
|
159
|
+
// a row to PATCH blindly.
|
|
160
|
+
if (mapped !== undefined && !spec.matches(mapped, news)) {
|
|
161
|
+
const body = spec.updateBody(news);
|
|
162
|
+
yield* guardBody(spec.endpoint.update, body, false);
|
|
163
|
+
if (Object.keys(body).length > 0) {
|
|
164
|
+
yield* netbox('PATCH', `${objectPath(before)}/`, body);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const after = yield* read(news);
|
|
169
|
+
if (after === undefined) {
|
|
170
|
+
return yield* Effect.die(
|
|
171
|
+
new Error(
|
|
172
|
+
`${spec.describe(news)}: the write returned no error but the object is still ` +
|
|
173
|
+
'absent. NetBox returns the object it wrote — read back rather than trusting the ' +
|
|
174
|
+
'status code.',
|
|
175
|
+
),
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
return after;
|
|
179
|
+
}),
|
|
180
|
+
|
|
181
|
+
destroy: (olds: Props) =>
|
|
182
|
+
Effect.gen(function* () {
|
|
183
|
+
const live = yield* fetchLive(olds);
|
|
184
|
+
if (live === undefined) return;
|
|
185
|
+
yield* netbox('DELETE', `${objectPath(live)}/`);
|
|
186
|
+
}),
|
|
187
|
+
};
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The five provider handlers for a spec'd NetBox object, wired once.
|
|
192
|
+
*
|
|
193
|
+
* ⚠️ `list` answers empty. NetBox index endpoints return the whole estate — tens of thousands of
|
|
194
|
+
* rows on a real instance — so adoption stays explicit rather than sweeping.
|
|
195
|
+
*/
|
|
196
|
+
export const netboxHandlers = <Props extends object, Attributes extends object>(
|
|
197
|
+
spec: NetboxSpec<Props, Attributes>,
|
|
198
|
+
) => {
|
|
199
|
+
const ops = netboxOperations(spec);
|
|
200
|
+
return {
|
|
201
|
+
list: () => Effect.succeed([]),
|
|
202
|
+
read: ({ olds }: { olds: Props }) => ops.read(olds),
|
|
203
|
+
diff: ({ news, output }: { news: Input<Props>; output: Attributes | undefined }) =>
|
|
204
|
+
ops.diff(news, output),
|
|
205
|
+
reconcile: ({ news }: { news: Props }) => ops.reconcile(news),
|
|
206
|
+
delete: ({ olds }: { olds: Props }) => ops.destroy(olds),
|
|
207
|
+
};
|
|
208
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-versus-write asymmetry — the bug that makes a resource never converge.
|
|
3
|
+
*
|
|
4
|
+
* 🔴 THE SHAPE IS DIFFERENT IN EACH DIRECTION AND NOTHING IN THE TYPES SAYS SO. Measured in the
|
|
5
|
+
* NetBox 4.7.0 document: a prefix is WRITTEN with `status: "active"` and READ BACK as
|
|
6
|
+
* `status: {value: "active", label: "Active"}`; a foreign key is written as `4` and read back
|
|
7
|
+
* as `{id: 4, url: …, display: …}`. A `matches` that compared those directly would report drift
|
|
8
|
+
* on every plan, for every object, forever — and each "update" would PATCH the same value back.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, expect, test } from 'bun:test';
|
|
11
|
+
import { bool, choice, fk, text } from './values.ts';
|
|
12
|
+
|
|
13
|
+
describe('a choice field reads back as an object', () => {
|
|
14
|
+
test('the object form reduces to the string a declaration writes', () => {
|
|
15
|
+
expect(choice({ label: 'Active', value: 'active' })).toBe('active');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
/** ⚠️ Both directions must work: a fixture or a brief serialisation gives the bare string. */
|
|
19
|
+
test('the bare string passes through unchanged', () => {
|
|
20
|
+
expect(choice('deprecated')).toBe('deprecated');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('anything else is empty rather than undefined, so a comparison stays total', () => {
|
|
24
|
+
expect(choice(undefined)).toBe('');
|
|
25
|
+
expect(choice(null)).toBe('');
|
|
26
|
+
expect(choice({ label: 'Active' })).toBe('');
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('a foreign key reads back as a nested object', () => {
|
|
31
|
+
test('the nested object reduces to its id', () => {
|
|
32
|
+
expect(
|
|
33
|
+
fk({ display: 'HomeFlare', id: 4, url: 'https://example.invalid/api/tenancy/tenants/4/' }),
|
|
34
|
+
).toBe(4);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('the bare integer passes through', () => {
|
|
38
|
+
expect(fk(7)).toBe(7);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* ⛔ `undefined`, NOT `0`. A cleared foreign key and "tenant 0" are different facts, and zero
|
|
43
|
+
* is falsy — a `matches` written against it would treat an unset tenant as a match for every
|
|
44
|
+
* declaration that omitted one.
|
|
45
|
+
*/
|
|
46
|
+
test('an absent or unrecognisable key is undefined', () => {
|
|
47
|
+
expect(fk(null)).toBeUndefined();
|
|
48
|
+
expect(fk(undefined)).toBeUndefined();
|
|
49
|
+
expect(fk({ display: 'no id here' })).toBeUndefined();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('text and booleans', () => {
|
|
54
|
+
/** ⚠️ NetBox stores an empty string for absent text, never null — so `''` is the settled value. */
|
|
55
|
+
test('absent text settles to the empty string', () => {
|
|
56
|
+
expect(text(undefined)).toBe('');
|
|
57
|
+
expect(text(null)).toBe('');
|
|
58
|
+
expect(text('a note')).toBe('a note');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('only a real true is true', () => {
|
|
62
|
+
expect(bool(true)).toBe(true);
|
|
63
|
+
expect(bool('true')).toBe(false);
|
|
64
|
+
expect(bool(1)).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a NetBox row into settled values.
|
|
3
|
+
*
|
|
4
|
+
* ⛔ A NETBOX READ AND A NETBOX WRITE ARE NOT THE SAME SHAPE, AND THIS IS WHERE THAT IS HANDLED.
|
|
5
|
+
* MEASURED in the 4.7.0 document: `WritablePrefixRequest.status` is a bare string enum
|
|
6
|
+
* (`"active"`), while `Prefix.status` on the way back is an OBJECT — `{value, label}`. A `diff`
|
|
7
|
+
* that compared the declared string against the live object would see drift on every plan, for
|
|
8
|
+
* every prefix, forever, and each "update" would PATCH the same value back.
|
|
9
|
+
* ⚠️ The same asymmetry holds for every choice field in NetBox (`dcim` device `status`,
|
|
10
|
+
* `ipam` IP `role`, …). Anything read through `choice` is safe against it.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** ⚠️ `''` for absent, not `undefined` — NetBox stores an empty string, never null, for text. */
|
|
14
|
+
export const text = (value: unknown): string => (typeof value === 'string' ? value : '');
|
|
15
|
+
|
|
16
|
+
export const bool = (value: unknown): boolean => value === true;
|
|
17
|
+
|
|
18
|
+
/** A `{value, label}` choice object, reduced to the string a declaration writes. */
|
|
19
|
+
export const choice = (value: unknown): string => {
|
|
20
|
+
if (typeof value === 'string') return value;
|
|
21
|
+
if (typeof value === 'object' && value !== null && 'value' in value) {
|
|
22
|
+
return text((value as { value: unknown }).value);
|
|
23
|
+
}
|
|
24
|
+
return '';
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A foreign key, as the integer a declaration writes.
|
|
29
|
+
*
|
|
30
|
+
* ⚠️ IT ARRIVES AS A NESTED OBJECT ON READ AND AN INTEGER ON WRITE — `{id, url, display, …}`
|
|
31
|
+
* versus `4`. Comparing the two directly is the same never-converges bug as `choice`.
|
|
32
|
+
*/
|
|
33
|
+
export const fk = (value: unknown): number | undefined => {
|
|
34
|
+
if (typeof value === 'number') return value;
|
|
35
|
+
if (typeof value === 'object' && value !== null && 'id' in value) {
|
|
36
|
+
const id = (value as { id: unknown }).id;
|
|
37
|
+
return typeof id === 'number' ? id : undefined;
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
};
|
|
@@ -103,6 +103,11 @@ export const apiTokenSpec: PveSpec<ApiTokenProps, ApiTokenAttributes> = {
|
|
|
103
103
|
*/
|
|
104
104
|
collection: (props) => `access/users/${props.userid}/token/${props.tokenid}`,
|
|
105
105
|
createForm: shape,
|
|
106
|
+
/** The vendor rules both forms are checked against at plan time — resource-spec.ts. */
|
|
107
|
+
endpoint: {
|
|
108
|
+
create: 'pve:POST /access/users/{userid}/token/{tokenid}',
|
|
109
|
+
update: 'pve:PUT /access/users/{userid}/token/{tokenid}',
|
|
110
|
+
},
|
|
106
111
|
/**
|
|
107
112
|
* ⚠️ EXACTLY THE THREE FIELDS A PUT CAN PUT BACK, WHICH IS ALSO EXACTLY WHAT THE GET REPORTS.
|
|
108
113
|
* MEASURED, by replaying these two functions over the live GET bodies of all four token
|