@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.
Files changed (516) hide show
  1. package/README.md +29 -0
  2. package/dist/linux/directory-lifecycle.d.ts +50 -0
  3. package/dist/linux/directory-lifecycle.d.ts.map +1 -0
  4. package/dist/linux/directory.d.ts +19 -0
  5. package/dist/linux/directory.d.ts.map +1 -0
  6. package/dist/linux/fake-linux-host.d.ts +35 -0
  7. package/dist/linux/fake-linux-host.d.ts.map +1 -0
  8. package/dist/linux/fake-systemd.d.ts +34 -0
  9. package/dist/linux/fake-systemd.d.ts.map +1 -0
  10. package/dist/linux/index.d.ts +27 -0
  11. package/dist/linux/index.d.ts.map +1 -0
  12. package/dist/linux/index.js +1575 -0
  13. package/dist/linux/index.js.map +38 -0
  14. package/dist/linux/providers.d.ts +17 -0
  15. package/dist/linux/providers.d.ts.map +1 -0
  16. package/dist/linux/region.d.ts +43 -0
  17. package/dist/linux/region.d.ts.map +1 -0
  18. package/dist/linux/remote-file-form.d.ts +47 -0
  19. package/dist/linux/remote-file-form.d.ts.map +1 -0
  20. package/dist/linux/remote-file-lifecycle.d.ts +36 -0
  21. package/dist/linux/remote-file-lifecycle.d.ts.map +1 -0
  22. package/dist/linux/remote-file-plan.d.ts +33 -0
  23. package/dist/linux/remote-file-plan.d.ts.map +1 -0
  24. package/dist/linux/remote-file.d.ts +28 -0
  25. package/dist/linux/remote-file.d.ts.map +1 -0
  26. package/dist/linux/ssh-command.d.ts +68 -0
  27. package/dist/linux/ssh-command.d.ts.map +1 -0
  28. package/dist/linux/ssh-runner.d.ts +22 -0
  29. package/dist/linux/ssh-runner.d.ts.map +1 -0
  30. package/dist/linux/ssh-scripts.d.ts +27 -0
  31. package/dist/linux/ssh-scripts.d.ts.map +1 -0
  32. package/dist/linux/systemctl.d.ts +67 -0
  33. package/dist/linux/systemctl.d.ts.map +1 -0
  34. package/dist/linux/unit-form.d.ts +78 -0
  35. package/dist/linux/unit-form.d.ts.map +1 -0
  36. package/dist/linux/unit-handlers.d.ts +15 -0
  37. package/dist/linux/unit-handlers.d.ts.map +1 -0
  38. package/dist/linux/unit-lifecycle.d.ts +40 -0
  39. package/dist/linux/unit-lifecycle.d.ts.map +1 -0
  40. package/dist/linux/unit-preflight.d.ts +41 -0
  41. package/dist/linux/unit-preflight.d.ts.map +1 -0
  42. package/dist/linux/unit-settle.d.ts +31 -0
  43. package/dist/linux/unit-settle.d.ts.map +1 -0
  44. package/dist/linux/unit.d.ts +35 -0
  45. package/dist/linux/unit.d.ts.map +1 -0
  46. package/dist/netbox/client.d.ts +52 -0
  47. package/dist/netbox/client.d.ts.map +1 -0
  48. package/dist/netbox/constraint-guard.d.ts +26 -0
  49. package/dist/netbox/constraint-guard.d.ts.map +1 -0
  50. package/dist/netbox/constraints.d.ts +76 -0
  51. package/dist/netbox/constraints.d.ts.map +1 -0
  52. package/dist/netbox/generated/constraints/index.d.ts +19 -0
  53. package/dist/netbox/generated/constraints/index.d.ts.map +1 -0
  54. package/dist/netbox/generated/constraints/netbox-ipam.d.ts +21 -0
  55. package/dist/netbox/generated/constraints/netbox-ipam.d.ts.map +1 -0
  56. package/dist/netbox/index.d.ts +17 -0
  57. package/dist/netbox/index.d.ts.map +1 -0
  58. package/dist/netbox/index.js +331 -0
  59. package/dist/netbox/index.js.map +18 -0
  60. package/dist/netbox/prefix-form.d.ts +54 -0
  61. package/dist/netbox/prefix-form.d.ts.map +1 -0
  62. package/dist/netbox/prefix.d.ts +80 -0
  63. package/dist/netbox/prefix.d.ts.map +1 -0
  64. package/dist/netbox/resource.d.ts +94 -0
  65. package/dist/netbox/resource.d.ts.map +1 -0
  66. package/dist/netbox/values.d.ts +24 -0
  67. package/dist/netbox/values.d.ts.map +1 -0
  68. package/dist/proxmox/api-token-form.d.ts.map +1 -1
  69. package/dist/proxmox/apply-endpoints.d.ts +30 -0
  70. package/dist/proxmox/apply-endpoints.d.ts.map +1 -0
  71. package/dist/proxmox/ceph-daemon-form.d.ts +29 -1
  72. package/dist/proxmox/ceph-daemon-form.d.ts.map +1 -1
  73. package/dist/proxmox/ceph-daemon.d.ts.map +1 -1
  74. package/dist/proxmox/ceph-flag.d.ts.map +1 -1
  75. package/dist/proxmox/ceph-fs.d.ts.map +1 -1
  76. package/dist/proxmox/ceph-osd-write.d.ts +19 -0
  77. package/dist/proxmox/ceph-osd-write.d.ts.map +1 -1
  78. package/dist/proxmox/ceph-pool.d.ts.map +1 -1
  79. package/dist/proxmox/constraints.d.ts +28 -5
  80. package/dist/proxmox/constraints.d.ts.map +1 -1
  81. package/dist/proxmox/generated/constraints/index.d.ts +1 -1
  82. package/dist/proxmox/generated/constraints/index.d.ts.map +1 -1
  83. package/dist/proxmox/generated/constraints/pbs-config.d.ts +6 -1
  84. package/dist/proxmox/generated/constraints/pbs-config.d.ts.map +1 -1
  85. package/dist/proxmox/generated/constraints/pve-access.d.ts +6 -1
  86. package/dist/proxmox/generated/constraints/pve-access.d.ts.map +1 -1
  87. package/dist/proxmox/generated/constraints/pve-cluster-backup.d.ts +23 -0
  88. package/dist/proxmox/generated/constraints/pve-cluster-backup.d.ts.map +1 -0
  89. package/dist/proxmox/generated/constraints/pve-cluster-ceph.d.ts +23 -0
  90. package/dist/proxmox/generated/constraints/pve-cluster-ceph.d.ts.map +1 -0
  91. package/dist/proxmox/generated/constraints/pve-cluster-firewall.d.ts +23 -0
  92. package/dist/proxmox/generated/constraints/pve-cluster-firewall.d.ts.map +1 -0
  93. package/dist/proxmox/generated/constraints/pve-cluster-ha.d.ts +23 -0
  94. package/dist/proxmox/generated/constraints/pve-cluster-ha.d.ts.map +1 -0
  95. package/dist/proxmox/generated/constraints/pve-cluster-metrics.d.ts +23 -0
  96. package/dist/proxmox/generated/constraints/pve-cluster-metrics.d.ts.map +1 -0
  97. package/dist/proxmox/generated/constraints/pve-cluster-notifications.d.ts +23 -0
  98. package/dist/proxmox/generated/constraints/pve-cluster-notifications.d.ts.map +1 -0
  99. package/dist/proxmox/generated/constraints/pve-cluster-replication.d.ts +23 -0
  100. package/dist/proxmox/generated/constraints/pve-cluster-replication.d.ts.map +1 -0
  101. package/dist/proxmox/generated/constraints/pve-cluster-sdn.d.ts +23 -0
  102. package/dist/proxmox/generated/constraints/pve-cluster-sdn.d.ts.map +1 -0
  103. package/dist/proxmox/generated/constraints/pve-nodes-ceph.d.ts +23 -0
  104. package/dist/proxmox/generated/constraints/pve-nodes-ceph.d.ts.map +1 -0
  105. package/dist/proxmox/generated/constraints/pve-nodes-disks.d.ts +23 -0
  106. package/dist/proxmox/generated/constraints/pve-nodes-disks.d.ts.map +1 -0
  107. package/dist/proxmox/generated/constraints/pve-nodes-lxc.d.ts +23 -0
  108. package/dist/proxmox/generated/constraints/pve-nodes-lxc.d.ts.map +1 -0
  109. package/dist/proxmox/generated/constraints/pve-nodes-network.d.ts +23 -0
  110. package/dist/proxmox/generated/constraints/pve-nodes-network.d.ts.map +1 -0
  111. package/dist/proxmox/generated/constraints/pve-nodes-qemu.d.ts +23 -0
  112. package/dist/proxmox/generated/constraints/pve-nodes-qemu.d.ts.map +1 -0
  113. package/dist/proxmox/generated/constraints/pve-pools.d.ts +6 -1
  114. package/dist/proxmox/generated/constraints/pve-pools.d.ts.map +1 -1
  115. package/dist/proxmox/generated/constraints/pve-storage.d.ts +6 -1
  116. package/dist/proxmox/generated/constraints/pve-storage.d.ts.map +1 -1
  117. package/dist/proxmox/generated/pbs/access-users.d.ts +137 -0
  118. package/dist/proxmox/generated/pbs/access-users.d.ts.map +1 -0
  119. package/dist/proxmox/generated/pbs/access.d.ts +171 -0
  120. package/dist/proxmox/generated/pbs/access.d.ts.map +1 -0
  121. package/dist/proxmox/generated/pbs/admin-datastore-store.d.ts +252 -0
  122. package/dist/proxmox/generated/pbs/admin-datastore-store.d.ts.map +1 -0
  123. package/dist/proxmox/generated/pbs/admin-metrics.d.ts +147 -0
  124. package/dist/proxmox/generated/pbs/admin-metrics.d.ts.map +1 -0
  125. package/dist/proxmox/generated/pbs/admin.d.ts +217 -0
  126. package/dist/proxmox/generated/pbs/admin.d.ts.map +1 -0
  127. package/dist/proxmox/generated/pbs/backup.d.ts +114 -0
  128. package/dist/proxmox/generated/pbs/backup.d.ts.map +1 -0
  129. package/dist/proxmox/generated/pbs/config-access-pam.d.ts +218 -0
  130. package/dist/proxmox/generated/pbs/config-access-pam.d.ts.map +1 -0
  131. package/dist/proxmox/generated/pbs/config-access.d.ts +203 -0
  132. package/dist/proxmox/generated/pbs/config-access.d.ts.map +1 -0
  133. package/dist/proxmox/generated/pbs/config-datastore.d.ts +195 -0
  134. package/dist/proxmox/generated/pbs/config-datastore.d.ts.map +1 -0
  135. package/dist/proxmox/generated/pbs/config-media-pool.d.ts +207 -0
  136. package/dist/proxmox/generated/pbs/config-media-pool.d.ts.map +1 -0
  137. package/dist/proxmox/generated/pbs/config-notifications-matchers.d.ts +159 -0
  138. package/dist/proxmox/generated/pbs/config-notifications-matchers.d.ts.map +1 -0
  139. package/dist/proxmox/generated/pbs/config-notifications.d.ts +199 -0
  140. package/dist/proxmox/generated/pbs/config-notifications.d.ts.map +1 -0
  141. package/dist/proxmox/generated/pbs/config-remote.d.ts +175 -0
  142. package/dist/proxmox/generated/pbs/config-remote.d.ts.map +1 -0
  143. package/dist/proxmox/generated/pbs/config-sync.d.ts +145 -0
  144. package/dist/proxmox/generated/pbs/config-sync.d.ts.map +1 -0
  145. package/dist/proxmox/generated/pbs/config-tape.d.ts +200 -0
  146. package/dist/proxmox/generated/pbs/config-tape.d.ts.map +1 -0
  147. package/dist/proxmox/generated/pbs/config-verify.d.ts +77 -0
  148. package/dist/proxmox/generated/pbs/config-verify.d.ts.map +1 -0
  149. package/dist/proxmox/generated/pbs/config.d.ts +119 -0
  150. package/dist/proxmox/generated/pbs/config.d.ts.map +1 -0
  151. package/dist/proxmox/generated/pbs/nodes-node-dns.d.ts +178 -0
  152. package/dist/proxmox/generated/pbs/nodes-node-dns.d.ts.map +1 -0
  153. package/dist/proxmox/generated/pbs/nodes-node-services.d.ts +205 -0
  154. package/dist/proxmox/generated/pbs/nodes-node-services.d.ts.map +1 -0
  155. package/dist/proxmox/generated/pbs/nodes-node.d.ts +166 -0
  156. package/dist/proxmox/generated/pbs/nodes-node.d.ts.map +1 -0
  157. package/dist/proxmox/generated/pbs/nodes.d.ts +171 -0
  158. package/dist/proxmox/generated/pbs/nodes.d.ts.map +1 -0
  159. package/dist/proxmox/generated/pbs/ping.d.ts +18 -0
  160. package/dist/proxmox/generated/pbs/ping.d.ts.map +1 -0
  161. package/dist/proxmox/generated/pbs/pull.d.ts +37 -0
  162. package/dist/proxmox/generated/pbs/pull.d.ts.map +1 -0
  163. package/dist/proxmox/generated/pbs/push.d.ts +36 -0
  164. package/dist/proxmox/generated/pbs/push.d.ts.map +1 -0
  165. package/dist/proxmox/generated/pbs/reader.d.ts +41 -0
  166. package/dist/proxmox/generated/pbs/reader.d.ts.map +1 -0
  167. package/dist/proxmox/generated/pbs/root.d.ts +16 -0
  168. package/dist/proxmox/generated/pbs/root.d.ts.map +1 -0
  169. package/dist/proxmox/generated/pbs/status.d.ts +54 -0
  170. package/dist/proxmox/generated/pbs/status.d.ts.map +1 -0
  171. package/dist/proxmox/generated/pbs/tape-drive.d.ts +181 -0
  172. package/dist/proxmox/generated/pbs/tape-drive.d.ts.map +1 -0
  173. package/dist/proxmox/generated/pbs/tape-media.d.ts +126 -0
  174. package/dist/proxmox/generated/pbs/tape-media.d.ts.map +1 -0
  175. package/dist/proxmox/generated/pbs/tape.d.ts +90 -0
  176. package/dist/proxmox/generated/pbs/tape.d.ts.map +1 -0
  177. package/dist/proxmox/generated/pbs/version.d.ts +20 -0
  178. package/dist/proxmox/generated/pbs/version.d.ts.map +1 -0
  179. package/dist/proxmox/generated/pbs.d.ts +44 -662
  180. package/dist/proxmox/generated/pbs.d.ts.map +1 -1
  181. package/dist/proxmox/generated/pve/access-roles.d.ts +164 -0
  182. package/dist/proxmox/generated/pve/access-roles.d.ts.map +1 -0
  183. package/dist/proxmox/generated/pve/access-users.d.ts +152 -0
  184. package/dist/proxmox/generated/pve/access-users.d.ts.map +1 -0
  185. package/dist/proxmox/generated/pve/access.d.ts +221 -0
  186. package/dist/proxmox/generated/pve/access.d.ts.map +1 -0
  187. package/dist/proxmox/generated/pve/cluster-acme-plugins-id.d.ts +45 -0
  188. package/dist/proxmox/generated/pve/cluster-acme-plugins-id.d.ts.map +1 -0
  189. package/dist/proxmox/generated/pve/cluster-acme-plugins.d.ts +47 -0
  190. package/dist/proxmox/generated/pve/cluster-acme-plugins.d.ts.map +1 -0
  191. package/dist/proxmox/generated/pve/cluster-acme-tos.d.ts +240 -0
  192. package/dist/proxmox/generated/pve/cluster-acme-tos.d.ts.map +1 -0
  193. package/dist/proxmox/generated/pve/cluster-bulk-action.d.ts +200 -0
  194. package/dist/proxmox/generated/pve/cluster-bulk-action.d.ts.map +1 -0
  195. package/dist/proxmox/generated/pve/cluster-firewall-refs.d.ts +109 -0
  196. package/dist/proxmox/generated/pve/cluster-firewall-refs.d.ts.map +1 -0
  197. package/dist/proxmox/generated/pve/cluster-firewall.d.ts +225 -0
  198. package/dist/proxmox/generated/pve/cluster-firewall.d.ts.map +1 -0
  199. package/dist/proxmox/generated/pve/cluster-ha.d.ts +199 -0
  200. package/dist/proxmox/generated/pve/cluster-ha.d.ts.map +1 -0
  201. package/dist/proxmox/generated/pve/cluster-jobs.d.ts +183 -0
  202. package/dist/proxmox/generated/pve/cluster-jobs.d.ts.map +1 -0
  203. package/dist/proxmox/generated/pve/cluster-metrics.d.ts +110 -0
  204. package/dist/proxmox/generated/pve/cluster-metrics.d.ts.map +1 -0
  205. package/dist/proxmox/generated/pve/cluster-notifications-endpoints.d.ts +224 -0
  206. package/dist/proxmox/generated/pve/cluster-notifications-endpoints.d.ts.map +1 -0
  207. package/dist/proxmox/generated/pve/cluster-notifications.d.ts +91 -0
  208. package/dist/proxmox/generated/pve/cluster-notifications.d.ts.map +1 -0
  209. package/dist/proxmox/generated/pve/cluster-options.d.ts +85 -0
  210. package/dist/proxmox/generated/pve/cluster-options.d.ts.map +1 -0
  211. package/dist/proxmox/generated/pve/cluster-qemu-custom-cpu-models-cputype.d.ts +48 -0
  212. package/dist/proxmox/generated/pve/cluster-qemu-custom-cpu-models-cputype.d.ts.map +1 -0
  213. package/dist/proxmox/generated/pve/cluster-qemu-custom-cpu-models.d.ts +45 -0
  214. package/dist/proxmox/generated/pve/cluster-qemu-custom-cpu-models.d.ts.map +1 -0
  215. package/dist/proxmox/generated/pve/cluster-replication.d.ts +130 -0
  216. package/dist/proxmox/generated/pve/cluster-replication.d.ts.map +1 -0
  217. package/dist/proxmox/generated/pve/cluster-sdn-fabrics.d.ts +220 -0
  218. package/dist/proxmox/generated/pve/cluster-sdn-fabrics.d.ts.map +1 -0
  219. package/dist/proxmox/generated/pve/cluster-sdn-ipams.d.ts +229 -0
  220. package/dist/proxmox/generated/pve/cluster-sdn-ipams.d.ts.map +1 -0
  221. package/dist/proxmox/generated/pve/cluster-sdn-vnets-vnet.d.ts +229 -0
  222. package/dist/proxmox/generated/pve/cluster-sdn-vnets-vnet.d.ts.map +1 -0
  223. package/dist/proxmox/generated/pve/cluster-sdn-vnets.d.ts +51 -0
  224. package/dist/proxmox/generated/pve/cluster-sdn-vnets.d.ts.map +1 -0
  225. package/dist/proxmox/generated/pve/cluster-sdn-zones.d.ts +225 -0
  226. package/dist/proxmox/generated/pve/cluster-sdn-zones.d.ts.map +1 -0
  227. package/dist/proxmox/generated/pve/cluster-sdn.d.ts +202 -0
  228. package/dist/proxmox/generated/pve/cluster-sdn.d.ts.map +1 -0
  229. package/dist/proxmox/generated/pve/cluster.d.ts +69 -0
  230. package/dist/proxmox/generated/pve/cluster.d.ts.map +1 -0
  231. package/dist/proxmox/generated/pve/nodes-node-ceph.d.ts +167 -0
  232. package/dist/proxmox/generated/pve/nodes-node-ceph.d.ts.map +1 -0
  233. package/dist/proxmox/generated/pve/nodes-node-config.d.ts +43 -0
  234. package/dist/proxmox/generated/pve/nodes-node-config.d.ts.map +1 -0
  235. package/dist/proxmox/generated/pve/nodes-node-disks.d.ts +225 -0
  236. package/dist/proxmox/generated/pve/nodes-node-disks.d.ts.map +1 -0
  237. package/dist/proxmox/generated/pve/nodes-node-firewall.d.ts +145 -0
  238. package/dist/proxmox/generated/pve/nodes-node-firewall.d.ts.map +1 -0
  239. package/dist/proxmox/generated/pve/nodes-node-hardware.d.ts +195 -0
  240. package/dist/proxmox/generated/pve/nodes-node-hardware.d.ts.map +1 -0
  241. package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-firewall.d.ts +191 -0
  242. package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-firewall.d.ts.map +1 -0
  243. package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-move-volume.d.ts +33 -0
  244. package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-move-volume.d.ts.map +1 -0
  245. package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-mtunnel.d.ts +56 -0
  246. package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-mtunnel.d.ts.map +1 -0
  247. package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-resize.d.ts +28 -0
  248. package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-resize.d.ts.map +1 -0
  249. package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-rrd.d.ts +186 -0
  250. package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-rrd.d.ts.map +1 -0
  251. package/dist/proxmox/generated/pve/nodes-node-lxc-vmid.d.ts +191 -0
  252. package/dist/proxmox/generated/pve/nodes-node-lxc-vmid.d.ts.map +1 -0
  253. package/dist/proxmox/generated/pve/nodes-node-network.d.ts +150 -0
  254. package/dist/proxmox/generated/pve/nodes-node-network.d.ts.map +1 -0
  255. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-config.d.ts +301 -0
  256. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-config.d.ts.map +1 -0
  257. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-dbus-vmstate.d.ts +170 -0
  258. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-dbus-vmstate.d.ts.map +1 -0
  259. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-firewall-rules.d.ts +144 -0
  260. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-firewall-rules.d.ts.map +1 -0
  261. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-move-disk.d.ts +34 -0
  262. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-move-disk.d.ts.map +1 -0
  263. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-mtunnel.d.ts +137 -0
  264. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-mtunnel.d.ts.map +1 -0
  265. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-status.d.ts +179 -0
  266. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-status.d.ts.map +1 -0
  267. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid.d.ts +143 -0
  268. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid.d.ts.map +1 -0
  269. package/dist/proxmox/generated/pve/nodes-node-qemu.d.ts +158 -0
  270. package/dist/proxmox/generated/pve/nodes-node-qemu.d.ts.map +1 -0
  271. package/dist/proxmox/generated/pve/nodes-node-replication.d.ts +187 -0
  272. package/dist/proxmox/generated/pve/nodes-node-replication.d.ts.map +1 -0
  273. package/dist/proxmox/generated/pve/nodes-node-services.d.ts +115 -0
  274. package/dist/proxmox/generated/pve/nodes-node-services.d.ts.map +1 -0
  275. package/dist/proxmox/generated/pve/nodes-node-storage-storage.d.ts +202 -0
  276. package/dist/proxmox/generated/pve/nodes-node-storage-storage.d.ts.map +1 -0
  277. package/dist/proxmox/generated/pve/nodes-node-storage.d.ts +40 -0
  278. package/dist/proxmox/generated/pve/nodes-node-storage.d.ts.map +1 -0
  279. package/dist/proxmox/generated/pve/nodes-node-subscription.d.ts +173 -0
  280. package/dist/proxmox/generated/pve/nodes-node-subscription.d.ts.map +1 -0
  281. package/dist/proxmox/generated/pve/nodes-node-vzdump.d.ts +97 -0
  282. package/dist/proxmox/generated/pve/nodes-node-vzdump.d.ts.map +1 -0
  283. package/dist/proxmox/generated/pve/nodes-node.d.ts +207 -0
  284. package/dist/proxmox/generated/pve/nodes-node.d.ts.map +1 -0
  285. package/dist/proxmox/generated/pve/nodes.d.ts +236 -0
  286. package/dist/proxmox/generated/pve/nodes.d.ts.map +1 -0
  287. package/dist/proxmox/generated/pve/pools.d.ts +82 -0
  288. package/dist/proxmox/generated/pve/pools.d.ts.map +1 -0
  289. package/dist/proxmox/generated/pve/storage.d.ts +159 -0
  290. package/dist/proxmox/generated/pve/storage.d.ts.map +1 -0
  291. package/dist/proxmox/generated/pve/version.d.ts +21 -0
  292. package/dist/proxmox/generated/pve/version.d.ts.map +1 -0
  293. package/dist/proxmox/generated/pve.d.ts +69 -4766
  294. package/dist/proxmox/generated/pve.d.ts.map +1 -1
  295. package/dist/proxmox/index.js +996 -372
  296. package/dist/proxmox/index.js.map +48 -32
  297. package/dist/proxmox/lxc-lifecycle.d.ts.map +1 -1
  298. package/dist/proxmox/metric-server-form.d.ts +11 -0
  299. package/dist/proxmox/metric-server-form.d.ts.map +1 -1
  300. package/dist/proxmox/metric-server.d.ts.map +1 -1
  301. package/dist/proxmox/network-apply.d.ts.map +1 -1
  302. package/dist/proxmox/node-network-form.d.ts +15 -0
  303. package/dist/proxmox/node-network-form.d.ts.map +1 -1
  304. package/dist/proxmox/node-network.d.ts.map +1 -1
  305. package/dist/proxmox/notification-target-endpoint.d.ts +26 -0
  306. package/dist/proxmox/notification-target-endpoint.d.ts.map +1 -0
  307. package/dist/proxmox/notification-target-form.d.ts +7 -0
  308. package/dist/proxmox/notification-target-form.d.ts.map +1 -1
  309. package/dist/proxmox/notification-target.d.ts.map +1 -1
  310. package/dist/proxmox/pbs-notification-target-endpoint.d.ts +29 -0
  311. package/dist/proxmox/pbs-notification-target-endpoint.d.ts.map +1 -0
  312. package/dist/proxmox/pbs-notification-target-lifecycle.d.ts.map +1 -1
  313. package/dist/proxmox/qemu.d.ts +10 -0
  314. package/dist/proxmox/qemu.d.ts.map +1 -1
  315. package/dist/proxmox/resource-guard.d.ts +15 -0
  316. package/dist/proxmox/resource-guard.d.ts.map +1 -0
  317. package/dist/proxmox/resource-spec.d.ts +15 -4
  318. package/dist/proxmox/resource-spec.d.ts.map +1 -1
  319. package/dist/proxmox/resource.d.ts +8 -0
  320. package/dist/proxmox/resource.d.ts.map +1 -1
  321. package/dist/proxmox/sdn-apply.d.ts.map +1 -1
  322. package/dist/proxmox/sdn-subnet.d.ts.map +1 -1
  323. package/dist/proxmox/zfs-pool-write.d.ts +11 -0
  324. package/dist/proxmox/zfs-pool-write.d.ts.map +1 -1
  325. package/dist/proxmox/zfs-pool.d.ts.map +1 -1
  326. package/docs/linux-host-measured.md +56 -0
  327. package/docs/linux-host.md +192 -0
  328. package/docs/netbox.md +103 -0
  329. package/package.json +9 -1
  330. package/src/linux/directory-lifecycle.ts +220 -0
  331. package/src/linux/directory.test.ts +132 -0
  332. package/src/linux/directory.ts +71 -0
  333. package/src/linux/fake-linux-host.ts +197 -0
  334. package/src/linux/fake-systemd.ts +103 -0
  335. package/src/linux/index.ts +32 -0
  336. package/src/linux/providers.ts +26 -0
  337. package/src/linux/region.test.ts +89 -0
  338. package/src/linux/region.ts +118 -0
  339. package/src/linux/remote-file-form.ts +96 -0
  340. package/src/linux/remote-file-lifecycle.test.ts +191 -0
  341. package/src/linux/remote-file-lifecycle.ts +161 -0
  342. package/src/linux/remote-file-plan.ts +154 -0
  343. package/src/linux/remote-file.ts +88 -0
  344. package/src/linux/ssh-command.test.ts +140 -0
  345. package/src/linux/ssh-command.ts +99 -0
  346. package/src/linux/ssh-runner.ts +188 -0
  347. package/src/linux/ssh-scripts.ts +105 -0
  348. package/src/linux/systemctl.ts +145 -0
  349. package/src/linux/unit-form.ts +183 -0
  350. package/src/linux/unit-handlers.ts +73 -0
  351. package/src/linux/unit-lifecycle.test.ts +220 -0
  352. package/src/linux/unit-lifecycle.ts +175 -0
  353. package/src/linux/unit-preflight.test.ts +128 -0
  354. package/src/linux/unit-preflight.ts +100 -0
  355. package/src/linux/unit-settle.ts +110 -0
  356. package/src/linux/unit.ts +81 -0
  357. package/src/netbox/client.test.ts +67 -0
  358. package/src/netbox/client.ts +146 -0
  359. package/src/netbox/constraint-guard.ts +57 -0
  360. package/src/netbox/constraints.test.ts +114 -0
  361. package/src/netbox/constraints.ts +152 -0
  362. package/src/netbox/generated/constraints/index.ts +23 -0
  363. package/src/netbox/generated/constraints/netbox-ipam.ts +32 -0
  364. package/src/netbox/index.ts +34 -0
  365. package/src/netbox/prefix-form.test.ts +127 -0
  366. package/src/netbox/prefix-form.ts +84 -0
  367. package/src/netbox/prefix.ts +136 -0
  368. package/src/netbox/resource.ts +208 -0
  369. package/src/netbox/values.test.ts +66 -0
  370. package/src/netbox/values.ts +40 -0
  371. package/src/proxmox/api-token-form.ts +5 -0
  372. package/src/proxmox/apply-endpoints.ts +34 -0
  373. package/src/proxmox/ceph-daemon-form.ts +56 -2
  374. package/src/proxmox/ceph-daemon.ts +10 -21
  375. package/src/proxmox/ceph-flag.ts +1 -0
  376. package/src/proxmox/ceph-fs.ts +1 -0
  377. package/src/proxmox/ceph-osd-write.ts +30 -4
  378. package/src/proxmox/ceph-pool.ts +10 -0
  379. package/src/proxmox/constraints-dialect.test.ts +165 -0
  380. package/src/proxmox/constraints-forms.test.ts +228 -0
  381. package/src/proxmox/constraints-host-forms.test.ts +132 -0
  382. package/src/proxmox/constraints-live.test.ts +155 -0
  383. package/src/proxmox/constraints-node-forms.test.ts +197 -0
  384. package/src/proxmox/constraints-required.test.ts +172 -0
  385. package/src/proxmox/constraints.test.ts +8 -63
  386. package/src/proxmox/constraints.ts +31 -6
  387. package/src/proxmox/generated/constraints/index.ts +27 -3
  388. package/src/proxmox/generated/constraints/pbs-config.ts +63 -1
  389. package/src/proxmox/generated/constraints/pve-access.ts +15 -3
  390. package/src/proxmox/generated/constraints/pve-cluster-backup.ts +74 -0
  391. package/src/proxmox/generated/constraints/pve-cluster-ceph.ts +27 -0
  392. package/src/proxmox/generated/constraints/pve-cluster-firewall.ts +35 -0
  393. package/src/proxmox/generated/constraints/pve-cluster-ha.ts +55 -0
  394. package/src/proxmox/generated/constraints/pve-cluster-metrics.ts +59 -0
  395. package/src/proxmox/generated/constraints/pve-cluster-notifications.ts +80 -0
  396. package/src/proxmox/generated/constraints/pve-cluster-replication.ts +43 -0
  397. package/src/proxmox/generated/constraints/pve-cluster-sdn.ts +90 -0
  398. package/src/proxmox/generated/constraints/pve-nodes-ceph.ts +57 -0
  399. package/src/proxmox/generated/constraints/pve-nodes-disks.ts +31 -0
  400. package/src/proxmox/generated/constraints/pve-nodes-lxc.ts +82 -0
  401. package/src/proxmox/generated/constraints/pve-nodes-network.ts +77 -0
  402. package/src/proxmox/generated/constraints/pve-nodes-qemu.ts +125 -0
  403. package/src/proxmox/generated/constraints/pve-pools.ts +6 -1
  404. package/src/proxmox/generated/constraints/pve-storage.ts +8 -3
  405. package/src/proxmox/generated/pbs/access-users.ts +137 -0
  406. package/src/proxmox/generated/pbs/access.ts +232 -0
  407. package/src/proxmox/generated/pbs/admin-datastore-store.ts +238 -0
  408. package/src/proxmox/generated/pbs/admin-metrics.ts +147 -0
  409. package/src/proxmox/generated/pbs/admin.ts +225 -0
  410. package/src/proxmox/generated/pbs/backup.ts +121 -0
  411. package/src/proxmox/generated/pbs/config-access-pam.ts +228 -0
  412. package/src/proxmox/generated/pbs/config-access.ts +228 -0
  413. package/src/proxmox/generated/pbs/config-datastore.ts +217 -0
  414. package/src/proxmox/generated/pbs/config-media-pool.ts +232 -0
  415. package/src/proxmox/generated/pbs/config-notifications-matchers.ts +187 -0
  416. package/src/proxmox/generated/pbs/config-notifications.ts +221 -0
  417. package/src/proxmox/generated/pbs/config-remote.ts +192 -0
  418. package/src/proxmox/generated/pbs/config-sync.ts +167 -0
  419. package/src/proxmox/generated/pbs/config-tape.ts +226 -0
  420. package/src/proxmox/generated/pbs/config-verify.ts +87 -0
  421. package/src/proxmox/generated/pbs/config.ts +135 -0
  422. package/src/proxmox/generated/pbs/nodes-node-dns.ts +229 -0
  423. package/src/proxmox/generated/pbs/nodes-node-services.ts +181 -0
  424. package/src/proxmox/generated/pbs/nodes-node.ts +244 -0
  425. package/src/proxmox/generated/pbs/nodes.ts +160 -0
  426. package/src/proxmox/generated/pbs/ping.ts +16 -0
  427. package/src/proxmox/generated/pbs/pull.ts +37 -0
  428. package/src/proxmox/generated/pbs/push.ts +36 -0
  429. package/src/proxmox/generated/pbs/reader.ts +41 -0
  430. package/src/proxmox/generated/pbs/root.ts +16 -0
  431. package/src/proxmox/generated/pbs/status.ts +50 -0
  432. package/src/proxmox/generated/pbs/tape-drive.ts +220 -0
  433. package/src/proxmox/generated/pbs/tape-media.ts +133 -0
  434. package/src/proxmox/generated/pbs/tape.ts +92 -0
  435. package/src/proxmox/generated/pbs/version.ts +16 -0
  436. package/src/proxmox/generated/pbs.ts +44 -787
  437. package/src/proxmox/generated/pve/access-roles.ts +168 -0
  438. package/src/proxmox/generated/pve/access-users.ts +155 -0
  439. package/src/proxmox/generated/pve/access.ts +217 -0
  440. package/src/proxmox/generated/pve/cluster-acme-plugins-id.ts +367 -0
  441. package/src/proxmox/generated/pve/cluster-acme-plugins.ts +366 -0
  442. package/src/proxmox/generated/pve/cluster-acme-tos.ts +232 -0
  443. package/src/proxmox/generated/pve/cluster-bulk-action.ts +227 -0
  444. package/src/proxmox/generated/pve/cluster-firewall-refs.ts +110 -0
  445. package/src/proxmox/generated/pve/cluster-firewall.ts +236 -0
  446. package/src/proxmox/generated/pve/cluster-ha.ts +203 -0
  447. package/src/proxmox/generated/pve/cluster-jobs.ts +190 -0
  448. package/src/proxmox/generated/pve/cluster-metrics.ts +116 -0
  449. package/src/proxmox/generated/pve/cluster-notifications-endpoints.ts +244 -0
  450. package/src/proxmox/generated/pve/cluster-notifications.ts +99 -0
  451. package/src/proxmox/generated/pve/cluster-options.ts +189 -0
  452. package/src/proxmox/generated/pve/cluster-qemu-custom-cpu-models-cputype.ts +272 -0
  453. package/src/proxmox/generated/pve/cluster-qemu-custom-cpu-models.ts +268 -0
  454. package/src/proxmox/generated/pve/cluster-replication.ts +127 -0
  455. package/src/proxmox/generated/pve/cluster-sdn-fabrics.ts +218 -0
  456. package/src/proxmox/generated/pve/cluster-sdn-ipams.ts +229 -0
  457. package/src/proxmox/generated/pve/cluster-sdn-vnets-vnet.ts +245 -0
  458. package/src/proxmox/generated/pve/cluster-sdn-vnets.ts +49 -0
  459. package/src/proxmox/generated/pve/cluster-sdn-zones.ts +224 -0
  460. package/src/proxmox/generated/pve/cluster-sdn.ts +201 -0
  461. package/src/proxmox/generated/pve/cluster.ts +71 -0
  462. package/src/proxmox/generated/pve/nodes-node-ceph.ts +167 -0
  463. package/src/proxmox/generated/pve/nodes-node-config.ts +56 -0
  464. package/src/proxmox/generated/pve/nodes-node-disks.ts +237 -0
  465. package/src/proxmox/generated/pve/nodes-node-firewall.ts +239 -0
  466. package/src/proxmox/generated/pve/nodes-node-hardware.ts +215 -0
  467. package/src/proxmox/generated/pve/nodes-node-lxc-vmid-firewall.ts +232 -0
  468. package/src/proxmox/generated/pve/nodes-node-lxc-vmid-move-volume.ts +1059 -0
  469. package/src/proxmox/generated/pve/nodes-node-lxc-vmid-mtunnel.ts +49 -0
  470. package/src/proxmox/generated/pve/nodes-node-lxc-vmid-resize.ts +285 -0
  471. package/src/proxmox/generated/pve/nodes-node-lxc-vmid-rrd.ts +184 -0
  472. package/src/proxmox/generated/pve/nodes-node-lxc-vmid.ts +240 -0
  473. package/src/proxmox/generated/pve/nodes-node-network.ts +234 -0
  474. package/src/proxmox/generated/pve/nodes-node-qemu-vmid-config.ts +441 -0
  475. package/src/proxmox/generated/pve/nodes-node-qemu-vmid-dbus-vmstate.ts +215 -0
  476. package/src/proxmox/generated/pve/nodes-node-qemu-vmid-firewall-rules.ts +145 -0
  477. package/src/proxmox/generated/pve/nodes-node-qemu-vmid-move-disk.ts +664 -0
  478. package/src/proxmox/generated/pve/nodes-node-qemu-vmid-mtunnel.ts +188 -0
  479. package/src/proxmox/generated/pve/nodes-node-qemu-vmid-status.ts +233 -0
  480. package/src/proxmox/generated/pve/nodes-node-qemu-vmid.ts +187 -0
  481. package/src/proxmox/generated/pve/nodes-node-qemu.ts +204 -0
  482. package/src/proxmox/generated/pve/nodes-node-replication.ts +187 -0
  483. package/src/proxmox/generated/pve/nodes-node-services.ts +221 -0
  484. package/src/proxmox/generated/pve/nodes-node-storage-storage.ts +226 -0
  485. package/src/proxmox/generated/pve/nodes-node-storage.ts +40 -0
  486. package/src/proxmox/generated/pve/nodes-node-subscription.ts +168 -0
  487. package/src/proxmox/generated/pve/nodes-node-vzdump.ts +96 -0
  488. package/src/proxmox/generated/pve/nodes-node.ts +211 -0
  489. package/src/proxmox/generated/pve/nodes.ts +235 -0
  490. package/src/proxmox/generated/pve/pools.ts +78 -0
  491. package/src/proxmox/generated/pve/storage.ts +213 -0
  492. package/src/proxmox/generated/pve/version.ts +21 -0
  493. package/src/proxmox/generated/pve.ts +69 -7403
  494. package/src/proxmox/lxc-interior.test.ts +17 -1
  495. package/src/proxmox/lxc-lifecycle.ts +48 -9
  496. package/src/proxmox/metric-server-form.ts +10 -0
  497. package/src/proxmox/metric-server.ts +11 -2
  498. package/src/proxmox/network-apply.ts +4 -0
  499. package/src/proxmox/node-network-form.ts +19 -0
  500. package/src/proxmox/node-network.ts +9 -3
  501. package/src/proxmox/notification-target-endpoint.ts +46 -0
  502. package/src/proxmox/notification-target-form.ts +11 -0
  503. package/src/proxmox/notification-target.ts +5 -2
  504. package/src/proxmox/pbs-notification-target-endpoint.ts +45 -0
  505. package/src/proxmox/pbs-notification-target-lifecycle.ts +16 -14
  506. package/src/proxmox/qemu.ts +9 -1
  507. package/src/proxmox/resource-guard.ts +61 -0
  508. package/src/proxmox/resource-spec.ts +16 -4
  509. package/src/proxmox/resource.ts +27 -27
  510. package/src/proxmox/sdn-apply.ts +4 -0
  511. package/src/proxmox/sdn-subnet.ts +5 -0
  512. package/src/proxmox/zfs-pool-write.ts +12 -0
  513. package/src/proxmox/zfs-pool.ts +2 -1
  514. package/dist/proxmox/generated/constraints/pve-cluster.d.ts +0 -18
  515. package/dist/proxmox/generated/constraints/pve-cluster.d.ts.map +0 -1
  516. 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