@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,175 @@
1
+ /**
2
+ * Systemd.Unit's read / diff / reconcile / delete as plain async functions over a HostRunner, so
3
+ * the whole lifecycle runs against a fake host with no real systemd.
4
+ *
5
+ * ⛔ WHAT A DEPLOY DOES NOT DO — the rule this family was built around. It NEVER mass-restarts.
6
+ * A unit is restarted only when THIS resource's unit file changed, or when a digest the
7
+ * declaration itself listed in `restartOn` changed. A unit that is already running the content
8
+ * we declare is left strictly alone: no restart, no reload, not even a `start`. An estate rebuild
9
+ * must be able to reach a host that holds a vault, a UPS handler or a clock without bouncing
10
+ * them, and "the deploy ran" must never be a reason anything went down.
11
+ * ★ THE ORDER IS WRITE, RELOAD, ENABLE, THEN START OR RESTART. systemd reads the unit file at
12
+ * `daemon-reload`, so writing it while the old unit runs changes nothing yet, and a write that
13
+ * fails leaves the old unit running rather than leaving nothing running.
14
+ * ⚠️ A FAILED START LEAVES THE UNIT DOWN. On an update the new unit file stays and state keeps the
15
+ * previous digest, so the next deploy retries. On a CREATE the file this deploy wrote is removed
16
+ * and systemd reloaded again: left behind, the next plan's recovery read finds a unit with no
17
+ * state, reports it `Unowned`, and every later deploy demands `--adopt` for our own file.
18
+ * ⛔ A MASKED UNIT IS SOMEONE'S DECISION, NOT DRIFT. `systemctl mask` survives reboots and makes
19
+ * start fail; unmasking here would silently overrule whoever masked it.
20
+ */
21
+ import type { Diff } from 'alchemy/Diff';
22
+ import type { HostRunner } from '../launchd/runner.ts';
23
+ import { daemonReload, disableUnit, showUnit, stopUnit } from './systemctl.ts';
24
+ import {
25
+ type SystemdUnitAttributes,
26
+ type SystemdUnitProps,
27
+ UNIT_WRITE,
28
+ configDigest,
29
+ digestOf,
30
+ unitPathFor,
31
+ unitText,
32
+ } from './unit-form.ts';
33
+ import { assertMayWrite, assertReplaceable, assertUsable, assertValid } from './unit-preflight.ts';
34
+ import { attributesOf, refuse, settle } from './unit-settle.ts';
35
+
36
+ const encoder = new TextEncoder();
37
+ const decoder = new TextDecoder();
38
+
39
+ /** What is on the host now: the unit file's digest and systemd's view. `undefined` when neither. */
40
+ export const readUnit = async (
41
+ runner: HostRunner,
42
+ props: SystemdUnitProps,
43
+ ): Promise<SystemdUnitAttributes | undefined> => {
44
+ const bytes = await runner.readFile(unitPathFor(props));
45
+ const status = await showUnit(runner, props.name);
46
+ if (bytes === undefined && !status.known) return undefined;
47
+ return attributesOf(props, bytes === undefined ? '' : digestOf(decoder.decode(bytes)), status);
48
+ };
49
+
50
+ export const diffUnit = async (
51
+ runner: HostRunner,
52
+ news: SystemdUnitProps,
53
+ output: SystemdUnitAttributes,
54
+ expect?: string,
55
+ ): Promise<Diff> => {
56
+ assertValid(news, expect);
57
+ // ★ The unit's name or its directory is its identity on the host: a change is a replace, and
58
+ // delete-first, because two unit files for one name cannot both be the one systemd reads.
59
+ // ⛔ Which is why everything the new unit's reconcile would refuse is refused HERE, at plan
60
+ // time (unit-preflight.ts): after the delete it would be too late.
61
+ if (news.name !== output.name || unitPathFor(news) !== output.unitPath) {
62
+ await assertReplaceable(runner, news, output, expect);
63
+ return { action: 'replace', deleteFirst: true };
64
+ }
65
+ const desired = digestOf(unitText(news));
66
+ const update = async (): Promise<Diff> => {
67
+ assertMayWrite(runner, news.name);
68
+ await runner.checkWrite?.(unitPathFor(news), UNIT_WRITE);
69
+ return { action: 'update' };
70
+ };
71
+ // ⛔ BOTH digests: the stored one catches a start that failed after the write landed, the live
72
+ // one catches a hand edit. Either alone reports `noop` over a unit that is not what we say.
73
+ if (desired !== output.unitSha256) return update();
74
+ if (configDigest(news.restartOn) !== output.configSha256) return update();
75
+ const live = await readUnit(runner, news);
76
+ if (live === undefined || live.unitSha256 !== desired) return update();
77
+ if (live.enabled !== (news.enabled !== false)) return update();
78
+ if (live.active !== (news.started !== false)) return update();
79
+ // ⚠️ systemd's own word for "the file on disk is newer than what I loaded" — a write that landed
80
+ // without its reload. It is drift that neither digest can see.
81
+ const status = await showUnit(runner, news.name);
82
+ return status.needDaemonReload ? update() : { action: 'noop' };
83
+ };
84
+
85
+ /**
86
+ * `adopt` is whether adoption is on AND this apply is a create or an unfinished generation of our
87
+ * own (ownership/adopt.ts): it lets such a generation take over a unit already on the host.
88
+ * ★ AN ADOPTED UNIT THAT ALREADY MATCHES IS NOT RESTARTED. That is the whole point of adopting one:
89
+ * the estate's live services come under declaration without a single bounce.
90
+ */
91
+ export const reconcileUnit = async (
92
+ runner: HostRunner,
93
+ props: SystemdUnitProps,
94
+ output: SystemdUnitAttributes | undefined,
95
+ adopt = false,
96
+ expect?: string,
97
+ ): Promise<SystemdUnitAttributes> => {
98
+ assertValid(props, expect);
99
+ assertMayWrite(runner, props.name);
100
+ const path = unitPathFor(props);
101
+ const status = await showUnit(runner, props.name);
102
+ assertUsable(props, status);
103
+ const text = unitText(props);
104
+ const desired = digestOf(text);
105
+ const before = await runner.readFile(path);
106
+ const liveSha = before === undefined ? undefined : digestOf(decoder.decode(before));
107
+ const moved = output !== undefined && (output.name !== props.name || output.unitPath !== path);
108
+ /**
109
+ * ★ A rename the diff could not see — a name that was still an unresolved Output at plan time
110
+ * (unit-handlers.ts diffHandler) — arrives here as an update. Run the same read-only checks a
111
+ * plan-time replace runs, BEFORE deleteUnit, or the old unit is already gone when we refuse.
112
+ */
113
+ if (moved && output !== undefined) await assertReplaceable(runner, props, output, expect);
114
+ const prior = moved ? undefined : output;
115
+ /**
116
+ * ⛔ A UNIT THIS RESOURCE DOES NOT OWN IS NEVER OVERWRITTEN. Where the engine's probe never
117
+ * looked — a replace's new name, a create whose props were still an Output at plan time — a
118
+ * unit file already there with other content belongs to someone else, and writing ours over it
119
+ * would take their service. ★ A byte-identical file is this declaration's own leftover.
120
+ */
121
+ if (
122
+ prior === undefined &&
123
+ !(adopt && output === undefined) &&
124
+ liveSha !== undefined &&
125
+ liveSha !== desired
126
+ ) {
127
+ throw refuse(
128
+ props.name,
129
+ `${path} is already on this host and is not this resource. Remove it, or declare it as a ` +
130
+ 'new resource and deploy with --adopt.',
131
+ );
132
+ }
133
+ if (moved && output !== undefined) await deleteUnit(runner, output);
134
+ const wrote = liveSha !== desired;
135
+ /**
136
+ * ⛔ THE THREE THINGS THAT EARN A RESTART, and nothing else:
137
+ * - the unit file's bytes changed (`wrote`);
138
+ * - state holds an older digest than the file does — a previous deploy's write landed and its
139
+ * reload or restart never ran, so systemd is still running the old content;
140
+ * - systemd itself says its loaded unit is older than the file (`NeedDaemonReload`, measured);
141
+ * - a digest the declaration listed in `restartOn` changed.
142
+ * ★ An adopted, converged unit hits none of them, which is why adoption never bounces a service.
143
+ */
144
+ const stale = prior !== undefined && prior.unitSha256 !== desired;
145
+ const configChanged = prior !== undefined && prior.configSha256 !== configDigest(props.restartOn);
146
+ if (wrote) await runner.writeFileAtomic(path, encoder.encode(text), UNIT_WRITE);
147
+ if (wrote || status.needDaemonReload) await daemonReload(runner);
148
+ const changed = wrote || stale || status.needDaemonReload;
149
+ return settle(runner, props, { before, changed, configChanged, desired, path, status });
150
+ };
151
+
152
+ /**
153
+ * Stop, disable, remove the unit file, reload. Idempotent.
154
+ * ⛔ DELETING THE DECLARATION STOPS THE SERVICE. That is what removing it from the stack means, and
155
+ * it is why a unit that must outlive its declaration is adopted rather than deleted.
156
+ */
157
+ export const deleteUnit = async (
158
+ runner: HostRunner,
159
+ output: SystemdUnitAttributes,
160
+ ): Promise<void> => {
161
+ assertMayWrite(runner, output.name);
162
+ const status = await showUnit(runner, output.name);
163
+ if (status.activeState === 'active' || status.activeState === 'activating') {
164
+ await stopUnit(runner, output.name);
165
+ }
166
+ if (status.unitFileState === 'enabled' || status.unitFileState === 'enabled-runtime') {
167
+ await disableUnit(runner, output.name);
168
+ }
169
+ const stat = await runner.stat(output.unitPath);
170
+ if (stat !== undefined) {
171
+ if (stat.kind !== 'file') throw refuse(output.name, `${output.unitPath} is a ${stat.kind}`);
172
+ await runner.removeFile(output.unitPath);
173
+ }
174
+ await daemonReload(runner);
175
+ };
@@ -0,0 +1,128 @@
1
+ /**
2
+ * A systemd rename is a delete-first replace, so every refusal has to fire at PLAN time. Caught
3
+ * only in reconcile, Alchemy has already stopped and removed the old unit (Apply.ts,
4
+ * deleteOldGenerations under `node.deleteFirst`).
5
+ *
6
+ * ★ The last case is the one the diff cannot see: a name that was still an Output at plan time
7
+ * arrives at reconcile as an update (unit-handlers.ts). The same refusals have to fire there,
8
+ * before deleteUnit.
9
+ */
10
+ import { describe, expect, test } from 'bun:test';
11
+ import { fakeLinuxHost } from './fake-linux-host.ts';
12
+ import { renderUnit } from './unit-form.ts';
13
+ import { deleteUnit, diffUnit, reconcileUnit } from './unit-lifecycle.ts';
14
+
15
+ const NAME = 'hf-example.service';
16
+ const PATH = `/etc/systemd/system/${NAME}`;
17
+ const RENAMED = 'hf-renamed.service';
18
+ const RENAMED_PATH = `/etc/systemd/system/${RENAMED}`;
19
+
20
+ const body = (exec: string) =>
21
+ renderUnit([
22
+ { lines: [['Description', 'an example']], name: 'Unit' },
23
+ {
24
+ lines: [
25
+ ['ExecStart', exec],
26
+ ['Restart', 'on-failure'],
27
+ ],
28
+ name: 'Service',
29
+ },
30
+ { lines: [['WantedBy', 'multi-user.target']], name: 'Install' },
31
+ ]);
32
+
33
+ const props = { content: body('/usr/bin/example'), name: NAME };
34
+ const renamed = { ...props, name: RENAMED };
35
+ const host = () => fakeLinuxHost({ dirs: { '/etc/systemd/system': 0 } });
36
+
37
+ const running = async () => {
38
+ const fake = host();
39
+ const output = await reconcileUnit(fake.runner, props, undefined);
40
+ return { fake, output };
41
+ };
42
+
43
+ describe('a rename is refused at plan time, and the old unit is still running', () => {
44
+ test('a masked new name', async () => {
45
+ const { fake, output } = await running();
46
+ fake.placeUnit(RENAMED_PATH, body('/usr/bin/other'), { masked: true });
47
+ await expect(diffUnit(fake.runner, renamed, output)).rejects.toThrow(/is masked/);
48
+ expect(fake.units.get(NAME)?.active).toBe(true);
49
+ expect(fake.files.has(PATH)).toBe(true);
50
+ });
51
+
52
+ test('a new name whose file belongs to someone else', async () => {
53
+ const { fake, output } = await running();
54
+ fake.placeUnit(RENAMED_PATH, body('/usr/bin/other'), { active: true, enabled: true });
55
+ await expect(diffUnit(fake.runner, renamed, output)).rejects.toThrow(/not this resource/);
56
+ expect(fake.units.get(NAME)?.active).toBe(true);
57
+ expect(fake.units.get(RENAMED)?.active).toBe(true);
58
+ });
59
+
60
+ test('a file byte-identical to our render is our own leftover, not a claim', async () => {
61
+ const { fake, output } = await running();
62
+ fake.placeUnit(RENAMED_PATH, props.content);
63
+ expect(await diffUnit(fake.runner, renamed, output)).toEqual({
64
+ action: 'replace',
65
+ deleteFirst: true,
66
+ });
67
+ });
68
+
69
+ test('a runner that will not write the new path', async () => {
70
+ const { fake, output } = await running();
71
+ const refusing = {
72
+ ...fake.runner,
73
+ checkWrite: async () => {
74
+ throw new Error('HostRunner.checkWrite refused');
75
+ },
76
+ };
77
+ await expect(diffUnit(refusing, renamed, output)).rejects.toThrow(/checkWrite/);
78
+ expect(fake.units.get(NAME)?.active).toBe(true);
79
+ });
80
+
81
+ test('the old unit must be deletable before a delete-first replace is promised', async () => {
82
+ const { fake, output } = await running();
83
+ const unprivileged = { ...fake.runner, effectiveUid: () => 1001, privileged: false };
84
+ await expect(diffUnit(unprivileged, renamed, output)).rejects.toThrow(/never calls sudo/);
85
+ expect(fake.units.get(NAME)?.active).toBe(true);
86
+ expect(fake.files.has(PATH)).toBe(true);
87
+ });
88
+ });
89
+
90
+ describe('a free name', () => {
91
+ test('plans a delete-first replace, and delete-then-reconcile leaves only the new unit', async () => {
92
+ const { fake, output } = await running();
93
+ expect(await diffUnit(fake.runner, renamed, output)).toEqual({
94
+ action: 'replace',
95
+ deleteFirst: true,
96
+ });
97
+ await deleteUnit(fake.runner, output);
98
+ const next = await reconcileUnit(fake.runner, renamed, output);
99
+ expect(next).toMatchObject({
100
+ active: true,
101
+ enabled: true,
102
+ name: RENAMED,
103
+ unitPath: RENAMED_PATH,
104
+ });
105
+ expect(fake.files.has(PATH)).toBe(false);
106
+ expect(fake.files.has(RENAMED_PATH)).toBe(true);
107
+ expect(fake.units.get(NAME)?.active ?? false).toBe(false);
108
+ });
109
+ });
110
+
111
+ describe('a rename the diff could not see is refused at apply, before the old unit stops', () => {
112
+ test('a masked new name', async () => {
113
+ const { fake, output } = await running();
114
+ fake.placeUnit(RENAMED_PATH, body('/usr/bin/other'), { masked: true });
115
+ await expect(reconcileUnit(fake.runner, renamed, output)).rejects.toThrow(/is masked/);
116
+ expect(fake.units.get(NAME)?.active).toBe(true);
117
+ expect(fake.files.has(PATH)).toBe(true);
118
+ });
119
+
120
+ test('a new name whose file belongs to someone else', async () => {
121
+ const { fake, output } = await running();
122
+ fake.placeUnit(RENAMED_PATH, body('/usr/bin/other'), { active: true, enabled: true });
123
+ await expect(reconcileUnit(fake.runner, renamed, output)).rejects.toThrow(/not this resource/);
124
+ expect(fake.units.get(NAME)?.active).toBe(true);
125
+ expect(fake.files.has(PATH)).toBe(true);
126
+ expect(fake.units.get(RENAMED)?.active).toBe(true);
127
+ });
128
+ });
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Everything Systemd.Unit checks before it touches the host — split from unit-lifecycle.ts so that
3
+ * a reconcile and a plan-time REPLACE run the very same checks.
4
+ *
5
+ * ★ WHY A REPLACE IS CHECKED AT PLAN TIME. A name or directory change is a delete-first replace,
6
+ * and Alchemy runs `delete` on the old unit BEFORE `reconcile` on the new one (Apply.ts,
7
+ * deleteOldGenerations under `node.deleteFirst`). A refusal that fired only in reconcile would
8
+ * therefore arrive after the old unit was already stopped and its file removed: nothing running,
9
+ * and a plan that had promised a clean swap.
10
+ * ★ EVERY CHECK HERE IS READ-ONLY. systemd has no `print-disabled` equivalent in this family —
11
+ * masking is `systemctl show`'s LoadState / UnitFileState, which `showUnit` already returns.
12
+ * Validation, that show, a file read and `HostRunner.checkWrite` write nothing. `systemctl
13
+ * enable` itself stays an apply step; the ways it is known to fail (no `[Install]`, a directory
14
+ * systemd does not search) are `unitProblems`, checked here before the plan may promise a swap.
15
+ */
16
+ import { type HostRunner, canActAsRoot } from '../launchd/runner.ts';
17
+ import { type UnitStatus, showUnit } from './systemctl.ts';
18
+ import {
19
+ type SystemdUnitAttributes,
20
+ type SystemdUnitProps,
21
+ UNIT_WRITE,
22
+ digestOf,
23
+ unitPathFor,
24
+ unitProblems,
25
+ unitText,
26
+ } from './unit-form.ts';
27
+ import { refuse } from './unit-settle.ts';
28
+
29
+ const decoder = new TextDecoder();
30
+
31
+ /** Throw every refusal at once, so one plan shows the whole list. */
32
+ export const assertValid = (props: SystemdUnitProps, expect?: string): void => {
33
+ const found = unitProblems(props, expect);
34
+ if (found.length > 0) throw refuse(props.name, found.join('; '));
35
+ };
36
+
37
+ /** ⛔ Writing a unit file and driving systemd are root's, so refuse up front rather than mid-apply. */
38
+ export const assertMayWrite = (runner: HostRunner, name: string): void => {
39
+ if (canActAsRoot(runner)) return;
40
+ throw refuse(
41
+ name,
42
+ 'a systemd unit is root-owned and systemctl needs root. Point the runner at a destination ' +
43
+ 'whose ssh user is root, or provide a privileged HostRunner. This provider never calls sudo.',
44
+ );
45
+ };
46
+
47
+ /**
48
+ * ⛔ A MASKED UNIT IS SOMEONE'S DECISION, NOT DRIFT. `systemctl mask` survives reboots and makes
49
+ * start fail; unmasking here would silently overrule whoever masked it.
50
+ */
51
+ export const assertUsable = (props: SystemdUnitProps, status: UnitStatus): void => {
52
+ if (status.loadState === 'masked' || status.unitFileState === 'masked') {
53
+ throw refuse(
54
+ props.name,
55
+ 'is masked. If that is stale, `systemctl unmask` it deliberately, then redeploy. Nothing ' +
56
+ 'was written.',
57
+ );
58
+ }
59
+ };
60
+
61
+ /**
62
+ * ⛔ A UNIT THIS RESOURCE DOES NOT OWN IS NEVER OVERWRITTEN. Alchemy's adoption probe guards a
63
+ * create, but NOT the new name of a replace — the engine reads nothing there — so a rename onto
64
+ * a unit file someone else already has would stop the old unit and write ours over theirs.
65
+ * ★ A file byte-identical to our render is exempt: it is this declaration's own leftover (a deploy
66
+ * that died between write and reload), and rewriting it changes nothing.
67
+ */
68
+ export const assertUnclaimed = async (
69
+ runner: HostRunner,
70
+ props: SystemdUnitProps,
71
+ ): Promise<void> => {
72
+ const path = unitPathFor(props);
73
+ const bytes = await runner.readFile(path);
74
+ const live = bytes === undefined ? undefined : digestOf(decoder.decode(bytes));
75
+ if (live !== undefined && live !== digestOf(unitText(props))) {
76
+ throw refuse(
77
+ props.name,
78
+ `${path} is already on this host and is not this resource. Remove it, or declare it as a ` +
79
+ 'new resource and deploy with --adopt.',
80
+ );
81
+ }
82
+ };
83
+
84
+ /**
85
+ * A rename, checked at plan time: the new identity must be valid, writable, not masked and free,
86
+ * and the old one deletable — before the plan may promise a delete-first replace.
87
+ */
88
+ export const assertReplaceable = async (
89
+ runner: HostRunner,
90
+ next: SystemdUnitProps,
91
+ old: SystemdUnitAttributes,
92
+ expect?: string,
93
+ ): Promise<void> => {
94
+ assertValid(next, expect);
95
+ assertMayWrite(runner, old.name);
96
+ assertMayWrite(runner, next.name);
97
+ assertUsable(next, await showUnit(runner, next.name));
98
+ await assertUnclaimed(runner, next);
99
+ await runner.checkWrite?.(unitPathFor(next), UNIT_WRITE);
100
+ };
@@ -0,0 +1,110 @@
1
+ /**
2
+ * The step that decides whether anything on the host actually moves: enablement, running state and
3
+ * — only for a real change — a restart. Split from unit-lifecycle.ts because it is the rule the
4
+ * whole family exists to keep, and it should be readable on its own.
5
+ *
6
+ * ⛔ THE RESTART CONDITION IS THE WHOLE CONTRACT. `changed` is set by the caller from the four
7
+ * things that earn one (unit-lifecycle.ts reconcileUnit); `configChanged` is a digest the
8
+ * DECLARATION itself listed. Nothing else restarts a unit — not a re-run, not an unrelated
9
+ * resource in the same deploy, not `--adopt`.
10
+ */
11
+ import type { HostRunner } from '../launchd/runner.ts';
12
+ import {
13
+ type SystemdUnitAttributes,
14
+ type SystemdUnitProps,
15
+ configDigest,
16
+ unitPathFor,
17
+ } from './unit-form.ts';
18
+ import {
19
+ type UnitStatus,
20
+ daemonReload,
21
+ disableUnit,
22
+ enableUnit,
23
+ restartUnit,
24
+ showUnit,
25
+ startUnit,
26
+ stopUnit,
27
+ } from './systemctl.ts';
28
+
29
+ export const refuse = (name: string, message: string): Error =>
30
+ new Error(`Systemd.Unit ${name}: ${message}`);
31
+
32
+ export const attributesOf = (
33
+ props: SystemdUnitProps,
34
+ unitSha256: string,
35
+ status: UnitStatus,
36
+ ): SystemdUnitAttributes => ({
37
+ active: status.activeState === 'active' || status.activeState === 'activating',
38
+ activeState: status.activeState,
39
+ configSha256: configDigest(props.restartOn),
40
+ enabled: status.unitFileState === 'enabled' || status.unitFileState === 'enabled-runtime',
41
+ loadState: status.loadState,
42
+ name: props.name,
43
+ unitPath: unitPathFor(props),
44
+ unitSha256,
45
+ ...(status.unitFileState === undefined ? {} : { unitFileState: status.unitFileState }),
46
+ });
47
+
48
+ export type Settle = {
49
+ readonly before: Uint8Array | undefined;
50
+ /** The unit file or systemd's view of it changed in a way that earns a restart. */
51
+ readonly changed: boolean;
52
+ readonly configChanged: boolean;
53
+ readonly desired: string;
54
+ readonly path: string;
55
+ readonly status: UnitStatus;
56
+ };
57
+
58
+ /**
59
+ * Bring enablement and running state to the declaration, restarting ONLY for a real change.
60
+ * ⛔ THE RESTART CONDITION IS THE WHOLE CONTRACT: the unit file changed, or a digest the caller
61
+ * listed changed. Nothing else — not a re-run, not an unrelated resource, not `--adopt`.
62
+ */
63
+ export const settle = async (
64
+ runner: HostRunner,
65
+ props: SystemdUnitProps,
66
+ step: Settle,
67
+ ): Promise<SystemdUnitAttributes> => {
68
+ const wantEnabled = props.enabled !== false;
69
+ const wantStarted = props.started !== false;
70
+ const isEnabled =
71
+ step.status.unitFileState === 'enabled' || step.status.unitFileState === 'enabled-runtime';
72
+ const undo = async (detail: string): Promise<never> => {
73
+ // ⚠️ A create that never came up leaves nothing behind; an update keeps the new file and the
74
+ // old stored digest, so the next deploy retries.
75
+ if (step.before === undefined) {
76
+ await runner.removeFile(step.path).catch(() => undefined);
77
+ await daemonReload(runner).catch(() => undefined);
78
+ }
79
+ throw refuse(props.name, `${detail} The unit is NOT running.`);
80
+ };
81
+ try {
82
+ if (wantEnabled && (!isEnabled || step.changed)) await enableUnit(runner, props.name);
83
+ if (!wantEnabled && isEnabled) await disableUnit(runner, props.name);
84
+ const active = step.status.activeState === 'active' || step.status.activeState === 'activating';
85
+ if (!wantStarted) {
86
+ if (active) await stopUnit(runner, props.name);
87
+ } else if (!active) await startUnit(runner, props.name);
88
+ else if (step.changed || step.configChanged) await restartUnit(runner, props.name);
89
+ } catch (cause) {
90
+ return undo(cause instanceof Error ? cause.message : String(cause));
91
+ }
92
+ /**
93
+ * ⚠️ READ BACK, never echo the declaration. Returning `enabled: wantEnabled` because we called
94
+ * `enable` would put a CLAIM in state instead of a reading, and the next plan would compare
95
+ * the declaration against itself — so a link systemd did not actually make would never show up
96
+ * as drift, in either direction.
97
+ */
98
+ const after = await showUnit(runner, props.name);
99
+ if (wantStarted && after.activeState !== 'active' && after.activeState !== 'activating') {
100
+ return undo(`systemctl reported success but the unit is ${after.activeState}.`);
101
+ }
102
+ const settled = attributesOf(props, step.desired, after);
103
+ if (wantEnabled && !settled.enabled) {
104
+ return undo(
105
+ `systemctl enable reported success but systemd calls the unit ` +
106
+ `${JSON.stringify(after.unitFileState ?? '')}, not enabled.`,
107
+ );
108
+ }
109
+ return settled;
110
+ };
@@ -0,0 +1,81 @@
1
+ /**
2
+ * `Systemd.Unit` and `Systemd.Timer` — one systemd unit file and systemd's view of it, declared.
3
+ *
4
+ * - create / update — write the unit file atomically, `daemon-reload`, enable or disable, then
5
+ * start, stop, or RESTART ONLY when something actually changed (unit-lifecycle.ts settle).
6
+ * - read — the unit file's SHA-256 plus `systemctl show` (load state, active state, enablement).
7
+ * - diff — the rendered digest against the stored AND the on-disk one, the declared enablement and
8
+ * running state against systemd's, and systemd's own `NeedDaemonReload`.
9
+ * - replace — only when the unit's name or directory changes, and DELETE FIRST: two unit files for
10
+ * one name cannot both be the one systemd reads.
11
+ * - delete — stop, disable, remove the file, reload.
12
+ *
13
+ * ⛔ A DEPLOY NEVER MASS-RESTARTS — see the ⛔ at the top of unit-lifecycle.ts. It is the property
14
+ * that lets this family reach a host holding a vault, a clock or a UPS handler.
15
+ * ⛔ THE UNIT FILE IS NEVER A SECRET (unit-form.ts): it is 0644 and its props sit in state.
16
+ * ⚠️ NOTHING IS ADOPTED WITHOUT `--adopt`. A unit already on the host reads as `Unowned`; adopting
17
+ * one that already matches the declaration does not restart it.
18
+ * ★ TWO RESOURCES, ONE LIFECYCLE. A timer is a unit whose file ends `.timer` and carries a
19
+ * `[Timer]` section; splitting the TYPE rather than the code means a plan says which of the two
20
+ * a row is, and a `.service` declared as a timer is a refusal instead of a puzzle.
21
+ */
22
+ import { Resource } from 'alchemy';
23
+ import * as Provider from 'alchemy/Provider';
24
+ import * as Effect from 'effect/Effect';
25
+ import { HostRunnerService } from '../launchd/runner.ts';
26
+ import type { SystemdUnitAttributes, SystemdUnitProps } from './unit-form.ts';
27
+ import { deleteHandler, diffHandler, readHandler, reconcileHandler } from './unit-handlers.ts';
28
+
29
+ export type { SystemdUnitAttributes, SystemdUnitProps, UnitSection } from './unit-form.ts';
30
+
31
+ export interface SystemdUnit extends Resource<
32
+ 'Systemd.Unit',
33
+ SystemdUnitProps,
34
+ SystemdUnitAttributes
35
+ > {}
36
+
37
+ export interface SystemdTimer extends Resource<
38
+ 'Systemd.Timer',
39
+ SystemdUnitProps,
40
+ SystemdUnitAttributes
41
+ > {}
42
+
43
+ export const SystemdUnit = Resource<SystemdUnit>('Systemd.Unit');
44
+ export const SystemdTimer = Resource<SystemdTimer>('Systemd.Timer');
45
+
46
+ export const SystemdUnitProvider = () =>
47
+ Provider.effect(
48
+ SystemdUnit,
49
+ Effect.gen(function* () {
50
+ const runner = yield* HostRunnerService;
51
+ return SystemdUnit.Provider.of({
52
+ /**
53
+ * ⛔ systemd's unit list is not a list of things this stack owns — it is every vendor,
54
+ * package and hand-written unit on the host. Returning them would invite a nuke to stop
55
+ * services nothing here declared.
56
+ */
57
+ list: () => Effect.succeed([]),
58
+ read: ({ olds, output }) => readHandler(runner, olds, output),
59
+ diff: ({ instanceId, news, output }) => diffHandler(runner, instanceId, news, output),
60
+ reconcile: (args) => reconcileHandler(runner, args),
61
+ delete: ({ output }) => deleteHandler(runner, output),
62
+ });
63
+ }),
64
+ );
65
+
66
+ export const SystemdTimerProvider = () =>
67
+ Provider.effect(
68
+ SystemdTimer,
69
+ Effect.gen(function* () {
70
+ const runner = yield* HostRunnerService;
71
+ return SystemdTimer.Provider.of({
72
+ list: () => Effect.succeed([]),
73
+ read: ({ olds, output }) => readHandler(runner, olds, output),
74
+ // ★ `'timer'` is the whole difference: the same lifecycle, refusing a name that is not one.
75
+ diff: ({ instanceId, news, output }) =>
76
+ diffHandler(runner, instanceId, news, output, 'timer'),
77
+ reconcile: (args) => reconcileHandler(runner, args, 'timer'),
78
+ delete: ({ output }) => deleteHandler(runner, output),
79
+ });
80
+ }),
81
+ );
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Identity resolution, and the two ways it must refuse.
3
+ *
4
+ * ⛔ THE FAILURE THIS GUARDS IS SILENT AND SELF-RENEWING. If an ambiguous match took the first
5
+ * row, adopt would bind to it, the next plan would bind to the other, and every plan after that
6
+ * would report drift that is not there — while "fixing" it PATCHed one object with the other's
7
+ * declaration. Failing loudly is the only outcome an operator can act on.
8
+ */
9
+ import { describe, expect, test } from 'bun:test';
10
+ import { NetboxError, apiBase, soleMatch } from './client.ts';
11
+
12
+ describe('soleMatch', () => {
13
+ test('no candidate is absent, not an error — that is what plans a create', () => {
14
+ expect(soleMatch([], 'ipam/prefixes 10.0.0.0/24')).toBeUndefined();
15
+ });
16
+
17
+ test('exactly one candidate is the object', () => {
18
+ expect(soleMatch([{ id: 7 }], 'ipam/prefixes 10.0.0.0/24')).toEqual({ id: 7 });
19
+ });
20
+
21
+ /** ⛔ Two rows means the identity rule is wrong, and the message says which object it was. */
22
+ test('more than one candidate throws and names the object', () => {
23
+ expect(() => soleMatch([{ id: 7 }, { id: 8 }], 'ipam/prefixes 10.0.0.0/24')).toThrow(
24
+ /ipam\/prefixes 10\.0\.0\.0\/24 matched 2 NetBox objects/,
25
+ );
26
+ });
27
+ });
28
+
29
+ describe('the instance origin', () => {
30
+ /**
31
+ * ⛔ NO DEFAULT HOSTNAME. A published package that fell back to a loopback address would let a
32
+ * consumer who forgot the variable watch every call fail against a machine that is not
33
+ * theirs, rather than being told which variable is missing.
34
+ */
35
+ test('an unset or blank NETBOX_URL names the variable', () => {
36
+ const before = process.env['NETBOX_URL'];
37
+ try {
38
+ delete process.env['NETBOX_URL'];
39
+ expect(() => apiBase()).toThrow(/NETBOX_URL is not set/);
40
+ process.env['NETBOX_URL'] = ' ';
41
+ expect(() => apiBase()).toThrow(/NETBOX_URL is not set/);
42
+ } finally {
43
+ if (before === undefined) delete process.env['NETBOX_URL'];
44
+ else process.env['NETBOX_URL'] = before;
45
+ }
46
+ });
47
+
48
+ test('a trailing slash is stripped so paths do not double up', () => {
49
+ const before = process.env['NETBOX_URL'];
50
+ try {
51
+ process.env['NETBOX_URL'] = 'https://netbox.example.com/';
52
+ expect(apiBase()).toBe('https://netbox.example.com');
53
+ } finally {
54
+ if (before === undefined) delete process.env['NETBOX_URL'];
55
+ else process.env['NETBOX_URL'] = before;
56
+ }
57
+ });
58
+ });
59
+
60
+ describe('NetboxError', () => {
61
+ /** ⚠️ The status is on the instance, because `absentOn404` folds ONLY 404 and nothing else. */
62
+ test('it carries the status a caller has to branch on', () => {
63
+ const error = new NetboxError(404, 'GET', 'ipam/prefixes/', 'Not found.');
64
+ expect(error.status).toBe(404);
65
+ expect(error.message).toContain('NetBox GET ipam/prefixes/ -> 404');
66
+ });
67
+ });