@homeflare/alchemy 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (417) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -0
  3. package/dist/cloudflare/client.d.ts +45 -0
  4. package/dist/cloudflare/client.d.ts.map +1 -0
  5. package/dist/cloudflare/index.d.ts +11 -0
  6. package/dist/cloudflare/index.d.ts.map +1 -0
  7. package/dist/cloudflare/index.js +129 -0
  8. package/dist/cloudflare/index.js.map +13 -0
  9. package/dist/cloudflare/lock-rules.d.ts +50 -0
  10. package/dist/cloudflare/lock-rules.d.ts.map +1 -0
  11. package/dist/cloudflare/providers.d.ts +19 -0
  12. package/dist/cloudflare/providers.d.ts.map +1 -0
  13. package/dist/cloudflare/r2-bucket-lock.d.ts +100 -0
  14. package/dist/cloudflare/r2-bucket-lock.d.ts.map +1 -0
  15. package/dist/forgejo/branch-protection-form.d.ts +19 -0
  16. package/dist/forgejo/branch-protection-form.d.ts.map +1 -0
  17. package/dist/forgejo/branch-protection.d.ts +47 -0
  18. package/dist/forgejo/branch-protection.d.ts.map +1 -0
  19. package/dist/forgejo/client.d.ts +37 -0
  20. package/dist/forgejo/client.d.ts.map +1 -0
  21. package/dist/forgejo/index.d.ts +18 -0
  22. package/dist/forgejo/index.d.ts.map +1 -0
  23. package/dist/forgejo/index.js +479 -0
  24. package/dist/forgejo/index.js.map +21 -0
  25. package/dist/forgejo/org-actions-secrets.d.ts +45 -0
  26. package/dist/forgejo/org-actions-secrets.d.ts.map +1 -0
  27. package/dist/forgejo/org-label.d.ts +46 -0
  28. package/dist/forgejo/org-label.d.ts.map +1 -0
  29. package/dist/forgejo/org-team.d.ts +42 -0
  30. package/dist/forgejo/org-team.d.ts.map +1 -0
  31. package/dist/forgejo/repo-webhook-form.d.ts +27 -0
  32. package/dist/forgejo/repo-webhook-form.d.ts.map +1 -0
  33. package/dist/forgejo/repo-webhook.d.ts +46 -0
  34. package/dist/forgejo/repo-webhook.d.ts.map +1 -0
  35. package/dist/forgejo/repository.d.ts +45 -0
  36. package/dist/forgejo/repository.d.ts.map +1 -0
  37. package/dist/forgejo/resource.d.ts +83 -0
  38. package/dist/forgejo/resource.d.ts.map +1 -0
  39. package/dist/forgejo/team-member.d.ts +39 -0
  40. package/dist/forgejo/team-member.d.ts.map +1 -0
  41. package/dist/forgejo/values.d.ts +24 -0
  42. package/dist/forgejo/values.d.ts.map +1 -0
  43. package/dist/index.d.ts +18 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +3 -0
  46. package/dist/index.js.map +9 -0
  47. package/dist/openbao/auth-role-form.d.ts +38 -0
  48. package/dist/openbao/auth-role-form.d.ts.map +1 -0
  49. package/dist/openbao/auth-role.d.ts +20 -0
  50. package/dist/openbao/auth-role.d.ts.map +1 -0
  51. package/dist/openbao/bao-address.d.ts +72 -0
  52. package/dist/openbao/bao-address.d.ts.map +1 -0
  53. package/dist/openbao/bao-http.d.ts +71 -0
  54. package/dist/openbao/bao-http.d.ts.map +1 -0
  55. package/dist/openbao/bao-status.d.ts +74 -0
  56. package/dist/openbao/bao-status.d.ts.map +1 -0
  57. package/dist/openbao/cloudflare-group-scope.d.ts +73 -0
  58. package/dist/openbao/cloudflare-group-scope.d.ts.map +1 -0
  59. package/dist/openbao/cloudflare-parity-catalog.d.ts +28 -0
  60. package/dist/openbao/cloudflare-parity-catalog.d.ts.map +1 -0
  61. package/dist/openbao/cloudflare-parity-snapshot.d.ts +31 -0
  62. package/dist/openbao/cloudflare-parity-snapshot.d.ts.map +1 -0
  63. package/dist/openbao/cloudflare-parity.d.ts +14 -0
  64. package/dist/openbao/cloudflare-parity.d.ts.map +1 -0
  65. package/dist/openbao/cloudflare-permission-groups-solve.d.ts +85 -0
  66. package/dist/openbao/cloudflare-permission-groups-solve.d.ts.map +1 -0
  67. package/dist/openbao/cloudflare-permission-groups.d.ts +87 -0
  68. package/dist/openbao/cloudflare-permission-groups.d.ts.map +1 -0
  69. package/dist/openbao/cloudflare-policy.d.ts +65 -0
  70. package/dist/openbao/cloudflare-policy.d.ts.map +1 -0
  71. package/dist/openbao/cloudflare-role-form.d.ts +110 -0
  72. package/dist/openbao/cloudflare-role-form.d.ts.map +1 -0
  73. package/dist/openbao/cloudflare-role-wire.d.ts +27 -0
  74. package/dist/openbao/cloudflare-role-wire.d.ts.map +1 -0
  75. package/dist/openbao/cloudflare-role.d.ts +35 -0
  76. package/dist/openbao/cloudflare-role.d.ts.map +1 -0
  77. package/dist/openbao/cloudflare-roles-config.d.ts +67 -0
  78. package/dist/openbao/cloudflare-roles-config.d.ts.map +1 -0
  79. package/dist/openbao/cloudflare-roles-expand.d.ts +76 -0
  80. package/dist/openbao/cloudflare-roles-expand.d.ts.map +1 -0
  81. package/dist/openbao/digest.d.ts +24 -0
  82. package/dist/openbao/digest.d.ts.map +1 -0
  83. package/dist/openbao/fake-bao.d.ts +40 -0
  84. package/dist/openbao/fake-bao.d.ts.map +1 -0
  85. package/dist/openbao/forgejo-bootstrap-parse.d.ts +46 -0
  86. package/dist/openbao/forgejo-bootstrap-parse.d.ts.map +1 -0
  87. package/dist/openbao/forgejo-bootstrap.d.ts +29 -0
  88. package/dist/openbao/forgejo-bootstrap.d.ts.map +1 -0
  89. package/dist/openbao/index.d.ts +18 -0
  90. package/dist/openbao/index.d.ts.map +1 -0
  91. package/dist/openbao/index.js +1158 -0
  92. package/dist/openbao/index.js.map +33 -0
  93. package/dist/openbao/mount-form.d.ts +60 -0
  94. package/dist/openbao/mount-form.d.ts.map +1 -0
  95. package/dist/openbao/mount-wire.d.ts +24 -0
  96. package/dist/openbao/mount-wire.d.ts.map +1 -0
  97. package/dist/openbao/mount.d.ts +20 -0
  98. package/dist/openbao/mount.d.ts.map +1 -0
  99. package/dist/openbao/pki-role-form.d.ts +118 -0
  100. package/dist/openbao/pki-role-form.d.ts.map +1 -0
  101. package/dist/openbao/pki-role.d.ts +50 -0
  102. package/dist/openbao/pki-role.d.ts.map +1 -0
  103. package/dist/openbao/policy-assembly.d.ts +13 -0
  104. package/dist/openbao/policy-assembly.d.ts.map +1 -0
  105. package/dist/openbao/policy-wire.d.ts +41 -0
  106. package/dist/openbao/policy-wire.d.ts.map +1 -0
  107. package/dist/openbao/policy.d.ts +83 -0
  108. package/dist/openbao/policy.d.ts.map +1 -0
  109. package/dist/openbao/proxmox-role-form.d.ts +92 -0
  110. package/dist/openbao/proxmox-role-form.d.ts.map +1 -0
  111. package/dist/openbao/proxmox-role.d.ts +47 -0
  112. package/dist/openbao/proxmox-role.d.ts.map +1 -0
  113. package/dist/openbao/ssh-role-form.d.ts +99 -0
  114. package/dist/openbao/ssh-role-form.d.ts.map +1 -0
  115. package/dist/openbao/ssh-role.d.ts +43 -0
  116. package/dist/openbao/ssh-role.d.ts.map +1 -0
  117. package/dist/proxmox/acl.d.ts +72 -0
  118. package/dist/proxmox/acl.d.ts.map +1 -0
  119. package/dist/proxmox/api-token-form.d.ts +59 -0
  120. package/dist/proxmox/api-token-form.d.ts.map +1 -0
  121. package/dist/proxmox/api-token.d.ts +159 -0
  122. package/dist/proxmox/api-token.d.ts.map +1 -0
  123. package/dist/proxmox/backup-job.d.ts +112 -0
  124. package/dist/proxmox/backup-job.d.ts.map +1 -0
  125. package/dist/proxmox/ceph-daemon-form.d.ts +70 -0
  126. package/dist/proxmox/ceph-daemon-form.d.ts.map +1 -0
  127. package/dist/proxmox/ceph-daemon.d.ts +162 -0
  128. package/dist/proxmox/ceph-daemon.d.ts.map +1 -0
  129. package/dist/proxmox/ceph-flag.d.ts +131 -0
  130. package/dist/proxmox/ceph-flag.d.ts.map +1 -0
  131. package/dist/proxmox/ceph-fs-wire.d.ts +106 -0
  132. package/dist/proxmox/ceph-fs-wire.d.ts.map +1 -0
  133. package/dist/proxmox/ceph-fs.d.ts +124 -0
  134. package/dist/proxmox/ceph-fs.d.ts.map +1 -0
  135. package/dist/proxmox/ceph-osd-tree.d.ts +154 -0
  136. package/dist/proxmox/ceph-osd-tree.d.ts.map +1 -0
  137. package/dist/proxmox/ceph-osd-write.d.ts +36 -0
  138. package/dist/proxmox/ceph-osd-write.d.ts.map +1 -0
  139. package/dist/proxmox/ceph-osd.d.ts +102 -0
  140. package/dist/proxmox/ceph-osd.d.ts.map +1 -0
  141. package/dist/proxmox/ceph-pool-form.d.ts +217 -0
  142. package/dist/proxmox/ceph-pool-form.d.ts.map +1 -0
  143. package/dist/proxmox/ceph-pool-settle.d.ts +39 -0
  144. package/dist/proxmox/ceph-pool-settle.d.ts.map +1 -0
  145. package/dist/proxmox/ceph-pool.d.ts +48 -0
  146. package/dist/proxmox/ceph-pool.d.ts.map +1 -0
  147. package/dist/proxmox/client.d.ts +64 -0
  148. package/dist/proxmox/client.d.ts.map +1 -0
  149. package/dist/proxmox/credentials.d.ts +153 -0
  150. package/dist/proxmox/credentials.d.ts.map +1 -0
  151. package/dist/proxmox/firewall-alias-form.d.ts +98 -0
  152. package/dist/proxmox/firewall-alias-form.d.ts.map +1 -0
  153. package/dist/proxmox/firewall-alias.d.ts +141 -0
  154. package/dist/proxmox/firewall-alias.d.ts.map +1 -0
  155. package/dist/proxmox/generated/pbs.d.ts +388 -0
  156. package/dist/proxmox/generated/pbs.d.ts.map +1 -0
  157. package/dist/proxmox/generated/pve.d.ts +4713 -0
  158. package/dist/proxmox/generated/pve.d.ts.map +1 -0
  159. package/dist/proxmox/group.d.ts +104 -0
  160. package/dist/proxmox/group.d.ts.map +1 -0
  161. package/dist/proxmox/ha-resource.d.ts +104 -0
  162. package/dist/proxmox/ha-resource.d.ts.map +1 -0
  163. package/dist/proxmox/ha-rule-form.d.ts +103 -0
  164. package/dist/proxmox/ha-rule-form.d.ts.map +1 -0
  165. package/dist/proxmox/ha-rule.d.ts +156 -0
  166. package/dist/proxmox/ha-rule.d.ts.map +1 -0
  167. package/dist/proxmox/index.d.ts +43 -0
  168. package/dist/proxmox/index.d.ts.map +1 -0
  169. package/dist/proxmox/index.js +2322 -0
  170. package/dist/proxmox/index.js.map +70 -0
  171. package/dist/proxmox/lease-cache.d.ts +98 -0
  172. package/dist/proxmox/lease-cache.d.ts.map +1 -0
  173. package/dist/proxmox/lxc.d.ts +59 -0
  174. package/dist/proxmox/lxc.d.ts.map +1 -0
  175. package/dist/proxmox/members.d.ts +56 -0
  176. package/dist/proxmox/members.d.ts.map +1 -0
  177. package/dist/proxmox/metric-server-form.d.ts +53 -0
  178. package/dist/proxmox/metric-server-form.d.ts.map +1 -0
  179. package/dist/proxmox/metric-server-otel.d.ts +49 -0
  180. package/dist/proxmox/metric-server-otel.d.ts.map +1 -0
  181. package/dist/proxmox/metric-server.d.ts +123 -0
  182. package/dist/proxmox/metric-server.d.ts.map +1 -0
  183. package/dist/proxmox/network-apply-read.d.ts +93 -0
  184. package/dist/proxmox/network-apply-read.d.ts.map +1 -0
  185. package/dist/proxmox/network-apply.d.ts +102 -0
  186. package/dist/proxmox/network-apply.d.ts.map +1 -0
  187. package/dist/proxmox/node-network-form.d.ts +128 -0
  188. package/dist/proxmox/node-network-form.d.ts.map +1 -0
  189. package/dist/proxmox/node-network.d.ts +173 -0
  190. package/dist/proxmox/node-network.d.ts.map +1 -0
  191. package/dist/proxmox/notification-target-form.d.ts +44 -0
  192. package/dist/proxmox/notification-target-form.d.ts.map +1 -0
  193. package/dist/proxmox/notification-target.d.ts +146 -0
  194. package/dist/proxmox/notification-target.d.ts.map +1 -0
  195. package/dist/proxmox/pbs-datastore-form.d.ts +147 -0
  196. package/dist/proxmox/pbs-datastore-form.d.ts.map +1 -0
  197. package/dist/proxmox/pbs-datastore-guard.d.ts +55 -0
  198. package/dist/proxmox/pbs-datastore-guard.d.ts.map +1 -0
  199. package/dist/proxmox/pbs-datastore.d.ts +140 -0
  200. package/dist/proxmox/pbs-datastore.d.ts.map +1 -0
  201. package/dist/proxmox/pbs-prune-job-form.d.ts +97 -0
  202. package/dist/proxmox/pbs-prune-job-form.d.ts.map +1 -0
  203. package/dist/proxmox/pbs-prune-job.d.ts +168 -0
  204. package/dist/proxmox/pbs-prune-job.d.ts.map +1 -0
  205. package/dist/proxmox/pbs-sync-job-form.d.ts +83 -0
  206. package/dist/proxmox/pbs-sync-job-form.d.ts.map +1 -0
  207. package/dist/proxmox/pbs-sync-job.d.ts +246 -0
  208. package/dist/proxmox/pbs-sync-job.d.ts.map +1 -0
  209. package/dist/proxmox/pbs-verify-job-form.d.ts +114 -0
  210. package/dist/proxmox/pbs-verify-job-form.d.ts.map +1 -0
  211. package/dist/proxmox/pbs-verify-job.d.ts +145 -0
  212. package/dist/proxmox/pbs-verify-job.d.ts.map +1 -0
  213. package/dist/proxmox/pool.d.ts +42 -0
  214. package/dist/proxmox/pool.d.ts.map +1 -0
  215. package/dist/proxmox/qemu.d.ts +46 -0
  216. package/dist/proxmox/qemu.d.ts.map +1 -0
  217. package/dist/proxmox/replication-job-form.d.ts +71 -0
  218. package/dist/proxmox/replication-job-form.d.ts.map +1 -0
  219. package/dist/proxmox/replication-job.d.ts +162 -0
  220. package/dist/proxmox/replication-job.d.ts.map +1 -0
  221. package/dist/proxmox/resource.d.ts +157 -0
  222. package/dist/proxmox/resource.d.ts.map +1 -0
  223. package/dist/proxmox/role.d.ts +97 -0
  224. package/dist/proxmox/role.d.ts.map +1 -0
  225. package/dist/proxmox/sdn-apply-read.d.ts +80 -0
  226. package/dist/proxmox/sdn-apply-read.d.ts.map +1 -0
  227. package/dist/proxmox/sdn-apply.d.ts +63 -0
  228. package/dist/proxmox/sdn-apply.d.ts.map +1 -0
  229. package/dist/proxmox/sdn-subnet-form.d.ts +111 -0
  230. package/dist/proxmox/sdn-subnet-form.d.ts.map +1 -0
  231. package/dist/proxmox/sdn-subnet.d.ts +161 -0
  232. package/dist/proxmox/sdn-subnet.d.ts.map +1 -0
  233. package/dist/proxmox/sdn-vnet.d.ts +113 -0
  234. package/dist/proxmox/sdn-vnet.d.ts.map +1 -0
  235. package/dist/proxmox/sdn-zone.d.ts +126 -0
  236. package/dist/proxmox/sdn-zone.d.ts.map +1 -0
  237. package/dist/proxmox/storage.d.ts +120 -0
  238. package/dist/proxmox/storage.d.ts.map +1 -0
  239. package/dist/proxmox/user.d.ts +107 -0
  240. package/dist/proxmox/user.d.ts.map +1 -0
  241. package/dist/proxmox/values.d.ts +134 -0
  242. package/dist/proxmox/values.d.ts.map +1 -0
  243. package/dist/proxmox/zfs-pool-write.d.ts +76 -0
  244. package/dist/proxmox/zfs-pool-write.d.ts.map +1 -0
  245. package/dist/proxmox/zfs-pool.d.ts +146 -0
  246. package/dist/proxmox/zfs-pool.d.ts.map +1 -0
  247. package/dist/talos/credentials.d.ts +30 -0
  248. package/dist/talos/credentials.d.ts.map +1 -0
  249. package/dist/talos/index.d.ts +15 -0
  250. package/dist/talos/index.d.ts.map +1 -0
  251. package/dist/talos/index.js +392 -0
  252. package/dist/talos/index.js.map +16 -0
  253. package/dist/talos/kubeconfig.d.ts +53 -0
  254. package/dist/talos/kubeconfig.d.ts.map +1 -0
  255. package/dist/talos/resource.d.ts +32 -0
  256. package/dist/talos/resource.d.ts.map +1 -0
  257. package/dist/talos/talos-bootstrap.d.ts +27 -0
  258. package/dist/talos/talos-bootstrap.d.ts.map +1 -0
  259. package/dist/talos/talos-cluster-health.d.ts +36 -0
  260. package/dist/talos/talos-cluster-health.d.ts.map +1 -0
  261. package/dist/talos/talos-machine-config.d.ts +41 -0
  262. package/dist/talos/talos-machine-config.d.ts.map +1 -0
  263. package/dist/talos/talosctl.d.ts +29 -0
  264. package/dist/talos/talosctl.d.ts.map +1 -0
  265. package/dist/talos/values.d.ts +20 -0
  266. package/dist/talos/values.d.ts.map +1 -0
  267. package/package.json +57 -0
  268. package/src/cloudflare/client.ts +76 -0
  269. package/src/cloudflare/index.ts +10 -0
  270. package/src/cloudflare/lock-rules.ts +60 -0
  271. package/src/cloudflare/providers.ts +23 -0
  272. package/src/cloudflare/r2-bucket-lock.ts +220 -0
  273. package/src/forgejo/branch-protection-form.ts +29 -0
  274. package/src/forgejo/branch-protection.ts +101 -0
  275. package/src/forgejo/client.ts +136 -0
  276. package/src/forgejo/index.ts +17 -0
  277. package/src/forgejo/org-actions-secrets.ts +99 -0
  278. package/src/forgejo/org-label.ts +100 -0
  279. package/src/forgejo/org-team.ts +105 -0
  280. package/src/forgejo/repo-webhook-form.ts +47 -0
  281. package/src/forgejo/repo-webhook.ts +118 -0
  282. package/src/forgejo/repository.ts +126 -0
  283. package/src/forgejo/resource.ts +171 -0
  284. package/src/forgejo/team-member.ts +97 -0
  285. package/src/forgejo/values.test.ts +48 -0
  286. package/src/forgejo/values.ts +61 -0
  287. package/src/index.ts +18 -0
  288. package/src/openbao/auth-role-form.ts +112 -0
  289. package/src/openbao/auth-role.ts +101 -0
  290. package/src/openbao/bao-address.test.ts +89 -0
  291. package/src/openbao/bao-address.ts +97 -0
  292. package/src/openbao/bao-http-gate.test.ts +88 -0
  293. package/src/openbao/bao-http-unix.test.ts +70 -0
  294. package/src/openbao/bao-http.test.ts +161 -0
  295. package/src/openbao/bao-http.ts +185 -0
  296. package/src/openbao/bao-status.test.ts +114 -0
  297. package/src/openbao/bao-status.ts +137 -0
  298. package/src/openbao/cloudflare-group-scope.test.ts +103 -0
  299. package/src/openbao/cloudflare-group-scope.ts +83 -0
  300. package/src/openbao/cloudflare-parity-catalog.ts +104 -0
  301. package/src/openbao/cloudflare-parity-snapshot.ts +84 -0
  302. package/src/openbao/cloudflare-parity.ts +152 -0
  303. package/src/openbao/cloudflare-permission-groups-solve.test.ts +171 -0
  304. package/src/openbao/cloudflare-permission-groups-solve.ts +197 -0
  305. package/src/openbao/cloudflare-permission-groups.test.ts +369 -0
  306. package/src/openbao/cloudflare-permission-groups.ts +299 -0
  307. package/src/openbao/cloudflare-policy.ts +180 -0
  308. package/src/openbao/cloudflare-role-form.test.ts +177 -0
  309. package/src/openbao/cloudflare-role-form.ts +183 -0
  310. package/src/openbao/cloudflare-role-wire.test.ts +132 -0
  311. package/src/openbao/cloudflare-role-wire.ts +41 -0
  312. package/src/openbao/cloudflare-role.ts +189 -0
  313. package/src/openbao/cloudflare-roles-config.ts +230 -0
  314. package/src/openbao/cloudflare-roles-expand-bucket.test.ts +134 -0
  315. package/src/openbao/cloudflare-roles-expand.ts +231 -0
  316. package/src/openbao/digest.ts +26 -0
  317. package/src/openbao/fake-bao.ts +84 -0
  318. package/src/openbao/forgejo-bootstrap-parse.test.ts +76 -0
  319. package/src/openbao/forgejo-bootstrap-parse.ts +90 -0
  320. package/src/openbao/forgejo-bootstrap.ts +181 -0
  321. package/src/openbao/index.ts +17 -0
  322. package/src/openbao/mount-form.ts +147 -0
  323. package/src/openbao/mount-wire.test.ts +108 -0
  324. package/src/openbao/mount-wire.ts +79 -0
  325. package/src/openbao/mount.ts +132 -0
  326. package/src/openbao/pki-role-form.ts +248 -0
  327. package/src/openbao/pki-role.ts +202 -0
  328. package/src/openbao/policy-assembly.test.ts +28 -0
  329. package/src/openbao/policy-assembly.ts +13 -0
  330. package/src/openbao/policy-wire.test.ts +90 -0
  331. package/src/openbao/policy-wire.ts +53 -0
  332. package/src/openbao/policy.ts +212 -0
  333. package/src/openbao/proxmox-role-form.ts +174 -0
  334. package/src/openbao/proxmox-role.ts +214 -0
  335. package/src/openbao/ssh-role-form.ts +248 -0
  336. package/src/openbao/ssh-role.ts +240 -0
  337. package/src/proxmox/acl.ts +267 -0
  338. package/src/proxmox/api-token-form.ts +122 -0
  339. package/src/proxmox/api-token.ts +222 -0
  340. package/src/proxmox/backup-job.ts +213 -0
  341. package/src/proxmox/ceph-daemon-form.ts +84 -0
  342. package/src/proxmox/ceph-daemon.ts +243 -0
  343. package/src/proxmox/ceph-flag.ts +249 -0
  344. package/src/proxmox/ceph-fs-wire.ts +246 -0
  345. package/src/proxmox/ceph-fs.ts +249 -0
  346. package/src/proxmox/ceph-osd-tree.ts +193 -0
  347. package/src/proxmox/ceph-osd-write.ts +76 -0
  348. package/src/proxmox/ceph-osd.ts +244 -0
  349. package/src/proxmox/ceph-pool-form.ts +249 -0
  350. package/src/proxmox/ceph-pool-settle.ts +70 -0
  351. package/src/proxmox/ceph-pool.ts +212 -0
  352. package/src/proxmox/client.test.ts +185 -0
  353. package/src/proxmox/client.ts +240 -0
  354. package/src/proxmox/credentials.test.ts +144 -0
  355. package/src/proxmox/credentials.ts +254 -0
  356. package/src/proxmox/firewall-alias-form.ts +112 -0
  357. package/src/proxmox/firewall-alias.ts +208 -0
  358. package/src/proxmox/generated/pbs.ts +461 -0
  359. package/src/proxmox/generated/pve.ts +7344 -0
  360. package/src/proxmox/group.ts +199 -0
  361. package/src/proxmox/ha-resource.ts +169 -0
  362. package/src/proxmox/ha-rule-form.ts +185 -0
  363. package/src/proxmox/ha-rule.ts +240 -0
  364. package/src/proxmox/index.ts +42 -0
  365. package/src/proxmox/lease-cache.test.ts +163 -0
  366. package/src/proxmox/lease-cache.ts +131 -0
  367. package/src/proxmox/lxc.ts +117 -0
  368. package/src/proxmox/members.test.ts +54 -0
  369. package/src/proxmox/members.ts +174 -0
  370. package/src/proxmox/metric-server-form.ts +128 -0
  371. package/src/proxmox/metric-server-otel.ts +99 -0
  372. package/src/proxmox/metric-server.ts +214 -0
  373. package/src/proxmox/network-apply-read.ts +228 -0
  374. package/src/proxmox/network-apply.ts +244 -0
  375. package/src/proxmox/node-network-form.ts +213 -0
  376. package/src/proxmox/node-network.ts +243 -0
  377. package/src/proxmox/notification-target-form.ts +104 -0
  378. package/src/proxmox/notification-target.ts +222 -0
  379. package/src/proxmox/pbs-datastore-form.ts +248 -0
  380. package/src/proxmox/pbs-datastore-guard.ts +105 -0
  381. package/src/proxmox/pbs-datastore.ts +271 -0
  382. package/src/proxmox/pbs-prune-job-form.ts +208 -0
  383. package/src/proxmox/pbs-prune-job.ts +227 -0
  384. package/src/proxmox/pbs-sync-job-form.ts +234 -0
  385. package/src/proxmox/pbs-sync-job.ts +258 -0
  386. package/src/proxmox/pbs-verify-job-form.ts +131 -0
  387. package/src/proxmox/pbs-verify-job.ts +227 -0
  388. package/src/proxmox/pool.test.ts +42 -0
  389. package/src/proxmox/pool.ts +80 -0
  390. package/src/proxmox/qemu.ts +89 -0
  391. package/src/proxmox/replication-job-form.ts +89 -0
  392. package/src/proxmox/replication-job.ts +248 -0
  393. package/src/proxmox/resource.ts +257 -0
  394. package/src/proxmox/role.ts +161 -0
  395. package/src/proxmox/sdn-apply-read.test.ts +152 -0
  396. package/src/proxmox/sdn-apply-read.ts +189 -0
  397. package/src/proxmox/sdn-apply.ts +154 -0
  398. package/src/proxmox/sdn-subnet-form.ts +156 -0
  399. package/src/proxmox/sdn-subnet.ts +238 -0
  400. package/src/proxmox/sdn-vnet.ts +228 -0
  401. package/src/proxmox/sdn-zone.ts +246 -0
  402. package/src/proxmox/storage.ts +245 -0
  403. package/src/proxmox/user.ts +214 -0
  404. package/src/proxmox/values.test.ts +193 -0
  405. package/src/proxmox/values.ts +203 -0
  406. package/src/proxmox/zfs-pool-write.ts +130 -0
  407. package/src/proxmox/zfs-pool.ts +253 -0
  408. package/src/talos/credentials.ts +136 -0
  409. package/src/talos/index.ts +14 -0
  410. package/src/talos/kubeconfig.ts +170 -0
  411. package/src/talos/resource.ts +31 -0
  412. package/src/talos/talos-bootstrap.ts +109 -0
  413. package/src/talos/talos-cluster-health.ts +119 -0
  414. package/src/talos/talos-machine-config.ts +146 -0
  415. package/src/talos/talosctl.ts +94 -0
  416. package/src/talos/values.test.ts +80 -0
  417. package/src/talos/values.ts +70 -0
@@ -0,0 +1,214 @@
1
+ /**
2
+ * `Proxmox.MetricServer` — where the cluster ships its metrics, declared.
3
+ *
4
+ * ★ THE FAILURE THIS PREVENTS IS A SILENT ONE, WHICH IS THE WHOLE ARGUMENT FOR DECLARING IT.
5
+ * Undeclared, the target exists only as a section of `/etc/pve/status.cfg`: rebuild a node,
6
+ * re-add one to the cluster, or restore a config from before it was configured, and the cluster
7
+ * stops shipping. Nothing fails, no task errors, no alert fires — the graphs go flat, and the
8
+ * first to find out is whoever went looking for a number that was not there.
9
+ *
10
+ * ⛔ CREATE IS `POST cluster/metrics/server/{id}` — THE OBJECT'S OWN PATH — SO `collection()`
11
+ * RETURNS THE SAME STRING AS `path()`, AND THAT IS NOT A TYPO. MEASURED: a POST to the
12
+ * collection answers "Method 'POST /cluster/metrics/server' not implemented", while the id path
13
+ * gets as far as a permission check (`/`, Sys.Modify); the published schema agrees, giving the
14
+ * collection a GET and nothing else. `createForm` omits `id` for the same reason — it is already
15
+ * the last segment of the path being POSTed to, and a second copy can only disagree with it.
16
+ *
17
+ * ⛔ THE `token` IS WRITE-ONLY AND MUST NEVER BECOME AN ATTRIBUTE. Alchemy persists attributes
18
+ * UNENCRYPTED, so one that reached state would outlive the change that set it, in the state
19
+ * store and in every backup of it. It is `never` in the props below — declaring one is a COMPILE
20
+ * ERROR rather than a leak found months later — and absent from `optional`, for the reason
21
+ * given there. Set it out of band; PVE never returns it on a read anyway. `otel-headers`, where an
22
+ * opentelemetry server's bearer token lives, gets the same treatment in metric-server-otel.ts.
23
+ *
24
+ * ⚠️ PRIVILEGES, AND THIS IS A BIGGER ASK THAN IT LOOKS. Read and diff are fine under an auditor
25
+ * lease — the single-object GET checks `Sys.Audit` on `/` — while create, update and delete all
26
+ * check `Sys.Modify` on `/` and answer "Permission check failed (/, Sys.Modify)" until the role
27
+ * is widened. Widen it knowingly: the ACL path is the ROOT and there is no `/metrics` object to
28
+ * scope to, so `Sys.Modify` also buys datacenter options, notification targets and every other
29
+ * cluster-wide config write. A separate role for metric writes is the narrower answer.
30
+ */
31
+ import { Resource } from 'alchemy';
32
+ import * as Provider from 'alchemy/Provider';
33
+ import * as Effect from 'effect/Effect';
34
+ import { UNSET, body, update } from './metric-server-form.ts';
35
+ import {
36
+ type MetricServerOtelAttributes,
37
+ type MetricServerOtelProps,
38
+ otelAttributes,
39
+ otelMatches,
40
+ } from './metric-server-otel.ts';
41
+ import { type PveRequirements, type WithTarget, pveHandlers } from './resource.ts';
42
+ import { bool, int, text } from './values.ts';
43
+
44
+ /**
45
+ * PVE's status plugins. Which one a server is decides which fields it will even accept.
46
+ *
47
+ * ★ `opentelemetry` IS OFFERED SINCE 2026-09-14, AND THE REASON IT WAS NOT STILL HOLDS FOR ANY FIELD
48
+ * LEFT OUT. Its whole configuration is the `otel-*` family; declared without managing that family
49
+ * it would build a target whose every meaningful setting sat outside the graph, and report `noop`
50
+ * over it. metric-server-otel.ts manages every `otel-*` field but two, and says why for each.
51
+ */
52
+ export type MetricServerType = 'graphite' | 'influxdb' | 'opentelemetry';
53
+
54
+ export interface MetricServerProps extends WithTarget, MetricServerOtelProps {
55
+ /** ⚠️ A `pve-configid` — a letter, then letters, digits, `-` and `_`. A rejected one fails with
56
+ * "invalid configuration ID", which reads as a broken request rather than as a naming rule. */
57
+ id: string;
58
+ /**
59
+ * ⛔ CREATE-TIME ONLY. The PUT schema has no `type` parameter at all: a section's type is fixed
60
+ * when it is written, so the ⛔ in `attributes` turns a changed type into a refusal rather than
61
+ * a silent no-op over the wrong target.
62
+ */
63
+ type: MetricServerType;
64
+ /** ⚠️ REQUIRED ON EVERY WRITE, UPDATE INCLUDED — see `required`. */
65
+ server: string;
66
+ port: number;
67
+ /** Keep the section but stop shipping. Unset is an ENABLED target, not an absent one. */
68
+ disable?: boolean;
69
+ /**
70
+ * graphite and influxdb only: UDP MTU (512-65536) and socket/HTTP timeout. Unset leaves PVE's
71
+ * defaults, 1500 and 1. ⚠️ opentelemetry has neither; its timeout is `otel-timeout`.
72
+ */
73
+ mtu?: number;
74
+ timeout?: number;
75
+ /** graphite only: the root path metrics are published under, e.g. `proxmox.mycluster`. */
76
+ path?: string;
77
+ /**
78
+ * influxdb only. `udp` is PVE's default and writes line protocol; `http`/`https` use the v2 API,
79
+ * the only place `organization` and `bucket` mean anything. An unset `verify-certificate` means
80
+ * VERIFY — PVE's default, and the safe one.
81
+ * ⚠️ PVE's hyphenated names are kept rather than camelCased, so nothing between here and the form
82
+ * body has to remember a mapping.
83
+ */
84
+ influxdbproto?: 'udp' | 'http' | 'https';
85
+ organization?: string;
86
+ bucket?: string;
87
+ 'api-path-prefix'?: string;
88
+ 'max-body-size'?: number;
89
+ 'verify-certificate'?: boolean;
90
+ /** ⛔ `never` ON PURPOSE — see the ⛔ in the header. The compile error is the feature. */
91
+ token?: never;
92
+ }
93
+
94
+ /**
95
+ * ⛔ NO `digest`, ON PURPOSE. PVE returns one with every read, but it is the digest of the WHOLE
96
+ * `status.cfg` rather than of this section — declaring a SECOND metric server would rewrite this
97
+ * one's stored attributes, and comparing it would report an update on a target nobody touched.
98
+ * ⛔ And no `token`, for the reason the header gives at length.
99
+ * ⚠️ `mtu`, `timeout` and `max-body-size` report `UNSET` when the section carries none, i.e. when
100
+ * PVE's own default is what the target will use; `verify-certificate` reports that default
101
+ * directly, because "absent" there means the target verifies. `port` reports 0 only for a
102
+ * malformed section — PVE requires a port — so a plan should say so loudly. The `otel-*` half
103
+ * follows the same rules; see metric-server-otel.ts.
104
+ */
105
+ export interface MetricServerAttributes extends MetricServerOtelAttributes {
106
+ id: string;
107
+ type: MetricServerType;
108
+ server: string;
109
+ port: number;
110
+ disable: boolean;
111
+ mtu: number;
112
+ timeout: number;
113
+ path: string;
114
+ influxdbproto: string;
115
+ organization: string;
116
+ bucket: string;
117
+ 'api-path-prefix': string;
118
+ 'max-body-size': number;
119
+ 'verify-certificate': boolean;
120
+ }
121
+
122
+ export interface ProxmoxMetricServer extends Resource<
123
+ 'Proxmox.MetricServer',
124
+ MetricServerProps,
125
+ MetricServerAttributes,
126
+ never,
127
+ PveRequirements
128
+ > {}
129
+
130
+ export const ProxmoxMetricServer = Resource<ProxmoxMetricServer>('Proxmox.MetricServer');
131
+
132
+ /**
133
+ * The type-specific half of `matches`.
134
+ *
135
+ * ⛔ ONLY FIELDS THIS TYPE ACTUALLY SENDS ARE COMPARED. `verify-certificate` is not in graphite's
136
+ * option set and `path` is not in influxdb's, so neither is ever sent to the wrong plugin — and
137
+ * diffing one anyway would report an update that the PUT it triggers cannot satisfy, forever.
138
+ * `mtu` and `timeout` are compared for graphite and influxdb only, because the opentelemetry
139
+ * plugin has neither and `optional` never sends them to it.
140
+ */
141
+ const matchesType = (attributes: MetricServerAttributes, props: MetricServerProps): boolean => {
142
+ if (props.type === 'opentelemetry') return otelMatches(attributes, props);
143
+ const socket =
144
+ attributes.mtu === (props.mtu ?? UNSET) && attributes.timeout === (props.timeout ?? UNSET);
145
+ if (props.type === 'graphite') return socket && attributes.path === (props.path ?? '');
146
+ return (
147
+ socket &&
148
+ attributes.influxdbproto === (props.influxdbproto ?? '') &&
149
+ attributes.organization === (props.organization ?? '') &&
150
+ attributes.bucket === (props.bucket ?? '') &&
151
+ attributes['api-path-prefix'] === (props['api-path-prefix'] ?? '') &&
152
+ attributes['max-body-size'] === (props['max-body-size'] ?? UNSET) &&
153
+ attributes['verify-certificate'] === (props['verify-certificate'] !== false)
154
+ );
155
+ };
156
+
157
+ const handlers = pveHandlers<MetricServerProps, MetricServerAttributes>({
158
+ attributes: (live, props) => {
159
+ /**
160
+ * ⛔ A SERVER OF ANOTHER TYPE IS ANOTHER OBJECT, AND "ABSENT" IS THE HONEST ANSWER. `type`
161
+ * cannot be changed by a PUT, so reporting the foreign section as missing makes reconcile
162
+ * POST instead and PVE refuses because the id is taken — a loud, accurate error rather than a
163
+ * PUT pushing influxdb fields at a graphite section. An older PVE that omits `type` from the
164
+ * read falls through and is trusted.
165
+ */
166
+ const liveType = text(live['type']);
167
+ if (liveType !== '' && liveType !== props.type) return undefined;
168
+ return {
169
+ ...otelAttributes(live),
170
+ 'api-path-prefix': text(live['api-path-prefix']),
171
+ bucket: text(live['bucket']),
172
+ disable: bool(live['disable'], false),
173
+ id: props.id,
174
+ influxdbproto: text(live['influxdbproto']),
175
+ 'max-body-size': int(live['max-body-size'], UNSET),
176
+ mtu: int(live['mtu'], UNSET),
177
+ organization: text(live['organization']),
178
+ path: text(live['path']),
179
+ port: int(live['port'], 0),
180
+ server: text(live['server']),
181
+ timeout: int(live['timeout'], UNSET),
182
+ type: props.type,
183
+ 'verify-certificate': bool(live['verify-certificate'], true),
184
+ };
185
+ },
186
+ /** ⛔ THE SAME STRING AS `path`, DELIBERATELY — see the ⛔ in the header. */
187
+ collection: (props) => `cluster/metrics/server/${props.id}`,
188
+ /** ⚠️ `type` IS REQUIRED ON CREATE; `id` IS NOT SENT, because the id is the path. */
189
+ createForm: (props) => ({ ...body(props), type: props.type }),
190
+ /**
191
+ * ⛔ `id` and `type` are absent: one is the key the object was read by, the other is refused
192
+ * rather than updated. Everything type-specific is `matchesType`'s, above.
193
+ */
194
+ matches: (attributes, props) =>
195
+ attributes.server === props.server &&
196
+ attributes.port === props.port &&
197
+ attributes.disable === (props.disable === true) &&
198
+ matchesType(attributes, props),
199
+ path: (props) => `cluster/metrics/server/${props.id}`,
200
+ /** The form and its clear-list, from the one table in metric-server-form.ts. */
201
+ updateForm: update,
202
+ });
203
+
204
+ /**
205
+ * ⛔ THE EMPTY `list` FROM `pveHandlers` EARNS ITS KEEP HERE. `GET cluster/metrics/server` answers
206
+ * with every target the cluster already ships to, including the one somebody configured by hand
207
+ * years ago; adopting that is how a later `alchemy destroy` silences a graph nobody declared.
208
+ * ⚠️ AND NOTHING BRAKES THIS DESTROY. A pool refuses while it holds guests, a container while it
209
+ * runs; a metric server just leaves `status.cfg`, the cluster stops shipping on the next
210
+ * interval, and no guest or task is affected. The plan diff is the only warning anyone gets —
211
+ * the top of this file again, this time caused by a deploy.
212
+ */
213
+ export const ProxmoxMetricServerProvider = () =>
214
+ Provider.effect(ProxmoxMetricServer, Effect.succeed(ProxmoxMetricServer.Provider.of(handlers)));
@@ -0,0 +1,228 @@
1
+ /**
2
+ * How this package finds out what a node has STAGED, and whether the cluster survived an apply.
3
+ *
4
+ * ★ SPLIT OUT OF network-apply.ts TO KEEP BOTH FILES UNDER THE 250-LINE CAP, and the seam is a real
5
+ * one rather than a convenient line number: this file answers "what is true on the cluster right
6
+ * now", network-apply.ts answers "what does the resource do about it". Nothing here decides a
7
+ * diff, and the only write in either file is the apply itself.
8
+ *
9
+ * ⛔ THE PENDING STATE IS NOT IN `data`, WHICH IS WHY THIS FILE DOES ITS OWN FETCH. MEASURED in the
10
+ * cluster's own source on n2, 2026-09-13 — not inferred from documentation:
11
+ * PVE/API2/Network.pm my $tmp = PVE::INotify::read_file('interfaces', 1);
12
+ * $rpcenv->set_result_attrib('changes', $changes) if $changes;
13
+ * PVE/HTTPServer.pm if (my $diff = $rpcenv->get_result_attrib('changes')) {
14
+ * $resp->{changes} = $diff;
15
+ * }
16
+ * So the answer is `{"data":[…interfaces…],"changes":"<unified diff>"}`: `changes` is a SIBLING
17
+ * of `data`, set only when the diff is non-empty. `client.ts`'s `pve()` returns `body.data` and
18
+ * drops the envelope, so the one readable pending signal is invisible to every other resource in
19
+ * this package. Without the fetch below, `Proxmox.NetworkApply` could not diff honestly at all.
20
+ * ★ `pveEnvelopeWith` in client.ts now carries the same member failover as every other call.
21
+ *
22
+ * ⛔ AND THE INTERFACE ROWS CANNOT TELL YOU EITHER. MEASURED in PVE/INotify.pm `read_file`: when
23
+ * `/etc/network/interfaces.new` exists, THAT is the file that gets parsed, so
24
+ * `GET /nodes/{node}/network` answers with the STAGED config and a node about to be reconfigured
25
+ * looks identical to one already running it. Same trap `sdn-apply.ts` documents for `?running=1`.
26
+ *
27
+ * ⚠️ `active` IS NOT A PENDING SIGNAL, AND THE OBVIOUS SHORTCUT IS MEASURABLY WRONG. On n2 and n3,
28
+ * with nothing staged anywhere (VERIFIED 2026-09-13: `/etc/network/interfaces.new` absent on n2,
29
+ * n3 and n4), `wlp91s0` already comes back with no `active` key at all. Counting interfaces that
30
+ * are not up as "pending" would report an update on two of the three nodes forever.
31
+ *
32
+ * ⚠️ ONE THING GENUINELY CANNOT BE SEEN FROM HERE, AND IT IS SMALL. The diff is computed as
33
+ * `diff -b -N -u`, so a staged file differing from the live one ONLY in whitespace produces no
34
+ * `changes` at all. This module reports that node as settled while `interfaces.new` still sits
35
+ * there waiting for the next reload anybody triggers. The file's existence is not exposed by the
36
+ * API in any other form, so the alternative is not a better read — it is SSH.
37
+ */
38
+ import * as Effect from 'effect/Effect';
39
+ import { pve, pveEnvelopeWith, pveWith } from './client.ts';
40
+ import { type PveCredential, type PveTarget } from './credentials.ts';
41
+ import { leased } from './lease-cache.ts';
42
+ import { bool, text } from './values.ts';
43
+
44
+ /** The `GET /nodes/{node}/network` envelope. ⛔ `changes` is beside `data`, never inside it. */
45
+ type NetworkEnvelope = { changes?: string };
46
+
47
+ /** One row of `GET /cluster/status` — a `cluster` row, or one `node` row per member. */
48
+ type StatusRow = { type?: string; name?: string; quorate?: unknown; online?: unknown };
49
+
50
+ /** One `GET /nodes/{node}/tasks/{upid}/status` answer. `status` is `running` or `stopped`. */
51
+ type TaskStatus = { status?: string; exitstatus?: string };
52
+
53
+ /** What the cluster looked like the moment it was asked. Never persisted — see network-apply.ts. */
54
+ export type ClusterHealth = { quorate: boolean; offline: readonly string[] };
55
+
56
+ /**
57
+ * The unified diff between a node's applied interfaces file and its staged one, or `''`.
58
+ *
59
+ * ⛔ THIS STRING NEVER LEAVES THIS MODULE, AND THAT IS A SECRET RULE, NOT A STYLE ONE. Alchemy
60
+ * persists attributes UNENCRYPTED (credentials.ts spells out why), and `/etc/network/interfaces`
61
+ * is a file that can legally contain `wpa-psk` — n2 and n3 both carry a `wlp91s0`, MEASURED — as
62
+ * well as any `pre-up` command somebody wrote. A diff of that file in a state store is a secret
63
+ * in a state store. Only the COUNT below ever escapes.
64
+ */
65
+ const stagedDiff = (target: PveTarget, node: string) =>
66
+ Effect.gen(function* () {
67
+ const path = `nodes/${node}/network`;
68
+ // ⚠️ THE `read` ROLE IS ENOUGH, AND DELIBERATELY SO. The schema's permission for this GET is
69
+ // `"user": "all"` — no privilege check at all — so the pending read never needs the
70
+ // provisioning lease, and a plan stays a plan even if the write role is missing entirely.
71
+ // ⛔ THROUGH THE LEASE CACHE, NOT `mint`. This read needs no particular identity — only the
72
+ // apply and its task poll do, and network-apply.ts mints for those itself. Minting here left
73
+ // SIX extra `hf-read@pve` tokens on n2 per TB4 plan (one per node, for read and for diff),
74
+ // MEASURED 2026-09-14 while the cache itself made one read mint for the whole run.
75
+ const credential = yield* leased(target, 'read');
76
+ const envelope = (yield* pveEnvelopeWith(target, credential, 'GET', path)) as NetworkEnvelope;
77
+ return text(envelope.changes);
78
+ });
79
+
80
+ /**
81
+ * Changed lines in a unified diff.
82
+ *
83
+ * ⚠️ A NON-EMPTY DIFF IS NEVER REPORTED AS ZERO, WHICH IS WHAT `Math.max` IS FOR. The `+++`/`---`
84
+ * headers have to be skipped, and a removed line whose own text starts with `---` is skipped with
85
+ * them; a staged change consisting of nothing else would otherwise count 0 and be reported as
86
+ * "settled", which is the exact lie this resource exists to prevent. The count is informational —
87
+ * the resource only ever compares it to zero — so an occasional undercount of 1 is harmless while
88
+ * an undercount to zero is not.
89
+ */
90
+ export const pendingLines = (diff: string) => {
91
+ if (diff === '') return 0;
92
+ const changed = diff
93
+ .split('\n')
94
+ .filter(
95
+ (line) =>
96
+ (line.startsWith('+') || line.startsWith('-')) &&
97
+ !line.startsWith('+++') &&
98
+ !line.startsWith('---'),
99
+ ).length;
100
+ return Math.max(changed, 1);
101
+ };
102
+
103
+ /** How many staged lines this node is carrying. Zero is the only settled value. */
104
+ export const pendingCount = (target: PveTarget, node: string) =>
105
+ stagedDiff(target, node).pipe(Effect.map(pendingLines));
106
+
107
+ /**
108
+ * Quorum, and every member that is not online.
109
+ *
110
+ * ⛔ QUORUM ALONE IS NOT THE CHECK, AND ON A THREE-NODE CLUSTER IT WOULD MISS THE ACCIDENT THIS
111
+ * RESOURCE IS AFRAID OF. Losing ONE of three leaves `quorate: 1` — MEASURED shape on TB4:
112
+ * `{"id":"cluster","name":"HF-TB4","nodes":3,"quorate":1,…}` plus one row per node with
113
+ * `online: 1`. So a reload that drops n2 off the network is invisible to `quorate` and visible
114
+ * only as `online: 0` on that row. Both are checked, and the node names come back so the failure
115
+ * message can say WHICH member went away.
116
+ *
117
+ * ⚠️ AN UNCLUSTERED NODE IS QUORATE BY DEFINITION. A standalone PVE answers with node rows and no
118
+ * `cluster` row; reading that absence as "not quorate" would make this resource undeclarable on
119
+ * every single-node install.
120
+ *
121
+ * ⚠️ THIS ONE NEEDS `Sys.Audit` ON `/` — the only privilege in this family beyond the apply itself.
122
+ * It is asked of the `read` role, which is auditor-shaped, rather than of `provision`.
123
+ */
124
+ const clusterHealth = (target: PveTarget) =>
125
+ pve<StatusRow[]>(target, 'read', 'GET', 'cluster/status').pipe(
126
+ Effect.map((rows) => {
127
+ const all = rows ?? [];
128
+ const cluster = all.find((row) => row.type === 'cluster');
129
+ return {
130
+ offline: all
131
+ .filter((row) => row.type === 'node' && !bool(row.online))
132
+ .map((row) => text(row.name, '<unnamed>')),
133
+ quorate: cluster === undefined ? true : bool(cluster.quorate),
134
+ } satisfies ClusterHealth;
135
+ }),
136
+ );
137
+
138
+ /**
139
+ * Why the cluster is not healthy, or `undefined` when it is.
140
+ *
141
+ * ★ A REASON RATHER THAN A BOOLEAN, BECAUSE THE CALLER'S ONLY USE FOR IT IS A REFUSAL MESSAGE.
142
+ * "The deploy stopped because the cluster was degraded" is not actionable at three in the
143
+ * morning; "quorate=true offline=[n3]" is. The two callers in network-apply.ts ask this before
144
+ * the reload and again after it, and the second answer is the one that stops the chain from
145
+ * reaching the next node.
146
+ *
147
+ * ⚠️ A MEMBER OFFLINE FOR AN UNRELATED REASON BLOCKS EVERY APPLY, AND THAT IS THE INTENDED
148
+ * BEHAVIOUR RATHER THAN AN OVERSIGHT. A node down for maintenance is one failure away from the
149
+ * pools losing min_size; reloading another node's networking while it is out is the move that
150
+ * completes the outage. Bring it back, or apply by hand with the cluster in front of you.
151
+ */
152
+ export const degradedReason = (target: PveTarget) =>
153
+ clusterHealth(target).pipe(
154
+ Effect.map((health) =>
155
+ health.quorate && health.offline.length === 0
156
+ ? undefined
157
+ : `quorate=${String(health.quorate)} offline=[${health.offline.join(', ')}]`,
158
+ ),
159
+ );
160
+
161
+ /**
162
+ * ⛔ NOT AN EXITSTATUS PVE CAN RETURN, AND THAT IS THE POINT. The caller compares against `'OK'`;
163
+ * these two say WHY the answer is not OK without needing a second branch for "we do not know".
164
+ */
165
+ export const UNREACHABLE =
166
+ 'the task could not be read -- the node stopped answering, or this lease cannot see it';
167
+ const STILL_RUNNING = 'still running when this provider stopped waiting';
168
+
169
+ /**
170
+ * How long to wait on `ifreload`, in one-second polls.
171
+ *
172
+ * ⚠️ THIRTY SECONDS IS A CEILING, NOT AN EXPECTATION. A reload of a handful of interfaces finishes
173
+ * in a second or two; past thirty, something is wrong in a way a deploy cannot fix by waiting,
174
+ * and the honest move is to stop and name the task rather than park the stack on it.
175
+ */
176
+ const ATTEMPTS = 30;
177
+
178
+ /**
179
+ * Wait for one `srvreload` task and answer its exitstatus.
180
+ *
181
+ * ⛔ THE APPLY IS ASYNCHRONOUS AND ITS 200 MEANS NOTHING. MEASURED: `reload_network_config` ends
182
+ * `return $rpcenv->fork_worker('srvreload', 'networking', $authuser, $worker)`, so the PUT
183
+ * returns a UPID the instant the worker is forked — before `ifreload -a` has run, let alone
184
+ * succeeded. Waiting for the task is the only way to learn the outcome.
185
+ *
186
+ * ⛔ AND IT MUST BE POLLED WITH THE CREDENTIAL THAT STARTED IT. The task-status schema says
187
+ * `"user": "all"` with the note "needs 'Sys.Audit' on '/nodes/<node>' if they are not the owner
188
+ * of the task". Every mint returns a NEW token id, so a second mint is a different identity and
189
+ * is not the owner — which is why this takes a `PveCredential` and the caller keeps one lease
190
+ * across both calls, instead of widening the provisioning role to read its own tasks.
191
+ */
192
+ export const awaitTask = (
193
+ target: PveTarget,
194
+ credential: PveCredential,
195
+ node: string,
196
+ upid: string,
197
+ ) =>
198
+ Effect.gen(function* () {
199
+ /**
200
+ * ⚠️ THE UPID GOES IN RAW, NOT PERCENT-ENCODED, AND THE CHOICE IS DELIBERATE. A UPID is
201
+ * `UPID:<node>:<hex>:<hex>:<hex>:<type>:<id>:<user>:` — its only punctuation is `:`, `@`, `!`
202
+ * and `-`, every one of which is a legal path character, so raw needs nothing from the
203
+ * router. Encoding them instead RELIES on PVE unescaping each path segment, which is
204
+ * plausible and was NOT measured here (measuring it means starting a task, i.e. writing). If
205
+ * that assumption were wrong the poll would 404, this would report the outcome as unknown,
206
+ * and a perfectly good reload would stop the deploy. Raw removes the assumption.
207
+ */
208
+ const path = `nodes/${node}/tasks/${upid}/status`;
209
+ for (let attempt = 0; attempt < ATTEMPTS; attempt += 1) {
210
+ /**
211
+ * ⛔ A FAILED POLL IS NOT A FAILED RELOAD, AND CALLING IT ONE WOULD BE THE WORST KIND OF
212
+ * WRONG. If the member serving the poll is the node being reloaded, `ifreload -a` can drop
213
+ * the very connection this poll rides on — see network-apply.ts.
214
+ * The reload may well have succeeded. So the call is collapsed to "we cannot see the task"
215
+ * and the caller reports the outcome as UNKNOWN, with the UPID, rather than guessing.
216
+ * ⚠️ A 403 LANDS HERE TOO, AND IT READS THE SAME. If the polling identity is somehow not the
217
+ * task's owner, the endpoint wants `Sys.Audit` on the node and this sees only "no answer" —
218
+ * which is why `UNREACHABLE` names both possibilities rather than blaming the network.
219
+ */
220
+ const status = yield* pveWith<TaskStatus>(target, credential, 'GET', path).pipe(
221
+ Effect.orElseSucceed(() => undefined),
222
+ );
223
+ if (status === undefined) return UNREACHABLE;
224
+ if (status.status === 'stopped') return text(status.exitstatus, 'stopped with no exitstatus');
225
+ yield* Effect.sleep('1 second');
226
+ }
227
+ return STILL_RUNNING;
228
+ });