@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,244 @@
1
+ /**
2
+ * `Proxmox.CephOsd` — one OSD in the cluster's CRUSH tree.
3
+ *
4
+ * ⛔ CREATE AND DELETE ARE IMPLEMENTED AND `root@pam` IS THE ONLY IDENTITY THAT CAN RUN THEM.
5
+ * That is PVE's restriction, not this package's caution, and it is MEASURED from the cluster's
6
+ * own schema (`/usr/share/pve-docs/api-viewer/apidoc.js`, 2026-09-13):
7
+ *
8
+ * GET /nodes/{node}/ceph/osd -> {"check":["perm","/",["Sys.Audit","Datastore.Audit"],"any",1]}
9
+ * POST /nodes/{node}/ceph/osd -> *** no permissions block at all ***
10
+ * DELETE /nodes/{node}/ceph/osd/{osdid} -> *** no permissions block at all ***
11
+ *
12
+ * A PVE method with no permissions block is refused to every identity except `root@pam`, so an
13
+ * OpenBao-minted `hf-provision@pve!…` token is answered 403 however wide its role — widening
14
+ * `LXCProvisioner` cannot fix it. Compare the sibling families, which DO carry a check and
15
+ * therefore do work with a token: `ceph/fs`, `ceph/mon` and `disks/zfs` all want `Sys.Modify`.
16
+ *
17
+ * ⚠️ SO THE WRITE PATH IS HONEST RATHER THAN USEFUL. It is written out properly in
18
+ * `ceph-osd-write.ts` — read that file for what each verb destroys — and it will work for an
19
+ * operator running as root@pam. Through this package's credential it will 403, and a 403 is a
20
+ * better answer than a `delete` that silently does nothing while the plan claims otherwise.
21
+ *
22
+ * ★ WHAT IT IS ACTUALLY FOR, THEN: ASSERTION. It turns "n2, n3 and n4 each hold two ssd OSDs"
23
+ * into a line a plan checks, and it gives Ceph-backed resources something real to depend on —
24
+ * read `osd.osdid` into an `rbd` storage's props and Alchemy orders that storage after the
25
+ * assertion, so a stack can no longer report a healthy Ceph storage on a cluster whose OSDs are
26
+ * gone. A replica that disappears becomes work in `plan` and a loud failure on the deploy.
27
+ *
28
+ * ⛔ THE WRITE VERBS MAY NOT EVEN BE REACHABLE FOR THIS PACKAGE'S CREDENTIAL. MEASURED from the
29
+ * cluster's own schema (`/usr/share/pve-docs/api-viewer/apidoc.js`, 2026-09-13): `index`,
30
+ * `osdindex`, `metadata` and `lv-info` each declare a `permissions` block, while `createosd`
31
+ * and `destroyosd` declare NONE — the key is absent, not empty. REASONED, NOT MEASURED: PVE
32
+ * refuses a method with no permissions block to everyone but `root@pam`, so an OpenBao-minted
33
+ * `hf-provision@pve!…` token would likely be answered 403 however wide its role. That is a
34
+ * reason to leave the write lane closed, not something to confirm by trying: if the reasoning
35
+ * is wrong, the confirmation costs a disk.
36
+ */
37
+ import { Resource } from 'alchemy';
38
+ import { isResolved } from 'alchemy/Diff';
39
+ import type { Input } from 'alchemy/Input';
40
+ import * as Provider from 'alchemy/Provider';
41
+ import * as Effect from 'effect/Effect';
42
+ import type { CephOsdAttributes } from './ceph-osd-tree.ts';
43
+ import { readOsd } from './ceph-osd-tree.ts';
44
+ import { createOsd, destroyOsd } from './ceph-osd-write.ts';
45
+ import type { PveRequirements } from './resource.ts';
46
+ import type { WithTarget } from './resource.ts';
47
+
48
+ /**
49
+ * ⚠️ RE-EXPORTED, NOT DECLARED HERE — it is built in ceph-osd-tree.ts, next to the walk that fills
50
+ * it in and to the reasoning for which of its fields may be compared. One name, one import.
51
+ */
52
+ export type { CephOsdAttributes };
53
+
54
+ export interface CephOsdProps extends WithTarget {
55
+ /**
56
+ * Which node ANSWERS the read — not where the OSD lives. MEASURED: the tree n2 returns and the
57
+ * tree n3 returns are the same six leaves, n4's included. Point this at any node that is up;
58
+ * `host` is the field that says where the disk is.
59
+ */
60
+ node: string;
61
+ /** `2` for `osd.2`. Identity, and an INTEGER — see the ⛔ on `findOsd` in ceph-osd-tree.ts. */
62
+ osdid: number;
63
+ /**
64
+ * The CRUSH host bucket this OSD must sit in. Undeclared means unmanaged, as in storage.ts.
65
+ *
66
+ * ⚠️ DECLARING IT IS AN ASSERTION WITH NO REPAIR. If osd.2 turns up under n3, the plan reports
67
+ * work every time and the deploy fails, because moving an OSD between failure domains is a
68
+ * `ceph osd crush move` this provider will not perform. That is the intent: a replica in the
69
+ * wrong failure domain is not something to converge quietly past.
70
+ */
71
+ host?: string;
72
+ /**
73
+ * `ssd` | `hdd` | `nvme`, asserted the same way as `host`.
74
+ *
75
+ * ⚠️ THE READ'S SPELLING IS KEPT rather than the create parameter's (`crush-device-class`).
76
+ * This resource never writes, so the tree's name is the only one it has, and inventing a
77
+ * second name for one field is exactly what values.ts exists to stop.
78
+ */
79
+ device_class?: string;
80
+ /**
81
+ * The block device to build this OSD from, e.g. `/dev/nvme1n1`. Declaring it makes the resource
82
+ * able to CREATE; leaving it out keeps the resource a pure assertion over an OSD that exists.
83
+ *
84
+ * ⛔ EVERYTHING ON THIS DEVICE IS DESTROYED BY A CREATE. `POST /nodes/{node}/ceph/osd` hands it
85
+ * to `ceph-volume lvm create`, which zaps it. Declare it only for a disk that is genuinely
86
+ * blank, and read the ⛔ about id allocation below before assuming the create will land as the
87
+ * `osdid` written above.
88
+ */
89
+ dev?: string;
90
+ /**
91
+ * Zap the OSD's logical volumes on destroy (`cleanup=1`). Default false: the LVs are left behind
92
+ * so the disk can be re-added without a rebuild, which is the recoverable choice.
93
+ */
94
+ cleanup?: boolean;
95
+ }
96
+
97
+ export interface ProxmoxCephOsd extends Resource<
98
+ 'Proxmox.CephOsd',
99
+ CephOsdProps,
100
+ CephOsdAttributes,
101
+ never,
102
+ PveRequirements
103
+ > {}
104
+
105
+ /**
106
+ * ★ `retain` BY DEFAULT, FOLLOWING ALCHEMY'S OWN PRECEDENT. `GitHub.Repository` and
107
+ * `Cloudflare.Zone` default to retain "because their contents are irreplaceable"; an OSD is a
108
+ * replica of live data and qualifies twice over. Retain means DELETING THE DECLARATION DOES NOT
109
+ * DELETE THE OSD — Alchemy drops its state row and leaves the disk alone. `destroyOsd` is fully
110
+ * implemented in ceph-osd-write.ts and runs when the caller opts in with
111
+ * `.pipe(RemovalPolicy.destroy())`, so this is the Terraform `prevent_destroy` shape rather than
112
+ * a missing operation — and through a minted token it 403s anyway, per the ⛔ at the top.
113
+ */
114
+ export const ProxmoxCephOsd = Resource<ProxmoxCephOsd>('Proxmox.CephOsd', {
115
+ defaultRemovalPolicy: 'retain',
116
+ });
117
+
118
+ /**
119
+ * The live OSD, or undefined when the tree has no leaf with that id.
120
+ *
121
+ * ⛔ NO `Effect.orElseSucceed(() => undefined)`, AND THAT IS THE ONE DELIBERATE DEPARTURE FROM
122
+ * `pveOperations.read`. The factory folds every failure into "absent" because a 404 is a
123
+ * legitimate answer on a per-object path — but this path is a COLLECTION that always exists
124
+ * while Ceph is installed, so a failure here is never "the OSD is gone". It is an unreachable
125
+ * node, an expired lease, a role without `Sys.Audit`, or a cluster with no Ceph at all. Folding
126
+ * those into "absent" would turn every one of them into the sentence "osd.2 is missing", which
127
+ * is the most alarming thing this provider can say and would be a lie in all four cases. Left
128
+ * to fail, the plan shows `PVE GET nodes/n2/ceph/osd -> 500: …` and names the real problem.
129
+ */
130
+
131
+ /**
132
+ * Whether the live OSD is the one that was declared.
133
+ *
134
+ * ⚠️ AN UNDECLARED FIELD IS NOT COMPARED, as in storage.ts: undeclared means unmanaged, and there
135
+ * is no default to fall back on — the whole point of a CRUSH placement is that it is a choice.
136
+ * Declaring nothing but `osdid` therefore asserts only presence, which is a perfectly good
137
+ * thing to assert about a replica and the safest declaration this resource accepts.
138
+ */
139
+ const settled = (live: CephOsdAttributes, props: CephOsdProps) =>
140
+ (props.host === undefined || props.host === live.host) &&
141
+ (props.device_class === undefined || props.device_class === live.device_class);
142
+
143
+ const describe = (props: CephOsdProps) =>
144
+ `osd.${String(props.osdid)} (read via nodes/${props.node}/ceph/osd)`;
145
+
146
+ /**
147
+ * ⛔ HAND-WRITTEN RATHER THAN `pveHandlers(spec)`, and the reason is not that the shape does not
148
+ * fit — it fits perfectly, which is the danger. Given a spec, the factory's `reconcile` POSTs
149
+ * `createForm` at `collection` whenever the read says absent, and `destroy` DELETEs `path`.
150
+ * Wired to this family that is: build an OSD over whatever block device the props named, and
151
+ * remove a replica when somebody deletes a line from a stack file. acl.ts is the precedent for
152
+ * overriding the factory because PVE lacks a verb; this file overrides it because PVE HAS the
153
+ * verbs and they must not be reachable from a declaration.
154
+ *
155
+ * ⚠️ THERE IS NO `replace` PATH HERE AND THERE CANNOT BE ONE. Alchemy implements a replace as
156
+ * create-then-delete (or the reverse); with both of those refusing, `replace` could only ever
157
+ * be a failure wearing a different word. A changed `osdid` is simply a different assertion.
158
+ */
159
+ const handlers = {
160
+ /**
161
+ * ⛔ EMPTY, as everywhere in this package, and here the stakes are at their highest. The tree is
162
+ * the whole cluster's — adopting it would hand Alchemy six resources it never created, each
163
+ * with a `delete` in its future. Adoption stays an explicit act, and for this family it stays
164
+ * an act with no teeth: see `delete` below.
165
+ */
166
+ list: () => Effect.succeed([]),
167
+
168
+ read: Effect.fn(function* ({ olds }: { olds: CephOsdProps }) {
169
+ return yield* readOsd(olds);
170
+ }),
171
+
172
+ /**
173
+ * ⚠️ `update` IS THE LOUDEST WORD AVAILABLE, NOT A PROMISE TO FIX ANYTHING. Alchemy's Diff
174
+ * admits only noop/update/replace, so a missing or misplaced OSD has to be spelled `update`
175
+ * — and the deploy that follows fails on purpose in `reconcile`. A resource that answered
176
+ * `noop` over a replica that is not there would be the exact lie this package keeps writing
177
+ * guards against; a permanent entry in `plan` until a human puts a disk back is the cost, and
178
+ * it is the right cost for a degraded pool.
179
+ */
180
+ diff: Effect.fn(function* ({
181
+ news,
182
+ output,
183
+ }: {
184
+ news: Input<CephOsdProps>;
185
+ output: CephOsdAttributes | undefined;
186
+ }) {
187
+ if (output === undefined || !isResolved(news)) return undefined;
188
+ const live = yield* readOsd(news);
189
+ if (live === undefined) return { action: 'update' } as const;
190
+ /**
191
+ * ⛔ THE RECORDED STATE MUST DESCRIBE THE OSD THE DECLARATION NOW NAMES. `settled` compares
192
+ * only host and device_class, so changing `osdid: 2` to `3` reads osd.3, finds it on the
193
+ * same host with the same class, and answers noop — which records nothing, leaving OSD 2's
194
+ * crush_weight, pgs and name in state under a declaration that says 3. It is the OUTPUT's id
195
+ * that must be checked: the LIVE id is found BY `news.osdid`, so comparing that is vacuous.
196
+ */
197
+ if (output.osdid !== news.osdid) return { action: 'update' } as const;
198
+ return settled(live, news) ? ({ action: 'noop' } as const) : ({ action: 'update' } as const);
199
+ }),
200
+
201
+ /**
202
+ * ⛔ THE ASSERTION, AND THE WHOLE REASON THIS PROVIDER IS HAND-WRITTEN. It reads, and then it
203
+ * either records what it found or dies. It never POSTs. The first deploy of a declaration is
204
+ * an adoption of an OSD that already exists; every later one is a check that it still does.
205
+ */
206
+ reconcile: Effect.fn(function* ({ news }: { news: CephOsdProps }) {
207
+ const live = yield* readOsd(news);
208
+ if (live === undefined) {
209
+ /**
210
+ * ⛔ CEPH ALLOCATES THE OSD ID; THE DECLARATION CANNOT CHOOSE IT. Measured from the schema:
211
+ * `POST /nodes/{node}/ceph/osd` requires `dev` and takes `crush-device-class`, `db_dev`,
212
+ * `wal_dev`, `encrypted` and `osds-per-device` — and NO id. The cluster assigns the next
213
+ * free number. So a create only lands on the declared `osdid` when that number happens to
214
+ * be the one Ceph picks, which for a fresh disk on a contiguous cluster it usually is.
215
+ * The read-back below is what keeps that from being a silent mismatch: if Ceph assigned a
216
+ * different id, the OSD this resource names is still absent and reconcile refuses.
217
+ */
218
+ return yield* createOsd(news);
219
+ }
220
+ if (!settled(live, news)) {
221
+ return yield* Effect.die(
222
+ new Error(
223
+ `${describe(news)}: exists but not as declared -- live host=${live.host} ` +
224
+ `device_class=${live.device_class}. Nothing here moves an OSD between hosts or ` +
225
+ 'reclassifies it; fix the declaration, or move it deliberately with `ceph osd crush ' +
226
+ 'move` / `ceph osd crush set-device-class` and deploy again.',
227
+ ),
228
+ );
229
+ }
230
+ return live;
231
+ }),
232
+
233
+ /**
234
+ * ⛔ REMOVING THE DECLARATION REMOVES THE ASSERTION, NOT THE REPLICA — the resource defaults
235
+ * to `retain`, so this runs only on `.pipe(RemovalPolicy.destroy())`. What the DELETE actually
236
+ * does to a live pool is documented on `destroyOsd` in ceph-osd-write.ts.
237
+ */
238
+ delete: Effect.fn(function* ({ olds }: { olds: CephOsdProps }) {
239
+ yield* destroyOsd(olds);
240
+ }),
241
+ };
242
+
243
+ export const ProxmoxCephOsdProvider = () =>
244
+ Provider.effect(ProxmoxCephOsd, Effect.succeed(ProxmoxCephOsd.Provider.of(handlers)));
@@ -0,0 +1,249 @@
1
+ /**
2
+ * What a Ceph pool IS as a declaration, and the two different forms PVE wants it in.
3
+ *
4
+ * ★ SPLIT OUT OF ceph-pool.ts TO KEEP BOTH FILES UNDER THE 250-LINE CAP, and the seam is
5
+ * "does this touch the cluster" — the same one metric-server-form.ts draws, just with more on
6
+ * this side because this family needs it. Here: the declared shape, the reported shape, and the
7
+ * coercions that turn one into the other. There: reading TB4, deciding a diff, and the four
8
+ * handlers. Nothing in this file makes a call or decides an action.
9
+ * ⚠️ THE PROPS LIVE HERE RATHER THAN IN THE RESOURCE FILE, which is the one place this pair
10
+ * differs from metric-server / notification-target. There is no `import type` cycle back as a
11
+ * result; ceph-pool.ts re-exports both interfaces so the public surface is unchanged.
12
+ *
13
+ * ⛔ POST AND PUT DO NOT TAKE THE SAME PARAMETERS, AND BOTH EXTRAS ARE DESTRUCTIVE. MEASURED from
14
+ * the cluster's own schema (/usr/share/pve-docs/api-viewer/apidoc.js on n2, pve-manager 9.2.11,
15
+ * 2026-09-13): POST adds `add_storages` and `erasure-coding` to the shared set, PUT takes
16
+ * neither. Both are refused below, so what is left between the two bodies is `name`, which PUT
17
+ * reads from the path, and `pg_num`.
18
+ *
19
+ * ⛔ `pg_num` IS IN THE CREATE BODY AND NOT IN THE UPDATE BODY, AND THAT IS THE MOST IMPORTANT
20
+ * LINE IN THIS FILE. MEASURED in PVE::API2::Ceph::Pool::createpool on n2: a POST carrying no
21
+ * `pg_num` gets 128 substituted (`$param->{pg_num} //= 128`), and PVE::Ceph::Tools::create_pool
22
+ * then runs `osd pool create` — which Ceph answers with SUCCESS for a pool that already exists,
23
+ * before applying the rest of the body to it. So a create fired at a live pool SETS its pg_num,
24
+ * and lowering pg_num starts a PG MERGE: hours of backfill across every OSD, on a cluster whose
25
+ * Ceph traffic shares vmbr1.11 with everything else. ceph-pool.ts guards the create for exactly
26
+ * that reason; this half simply never offers pg_num to an update.
27
+ */
28
+ import type { WithTarget } from './resource.ts';
29
+ import { csv } from './values.ts';
30
+
31
+ export interface CephPoolProps extends WithTarget {
32
+ /**
33
+ * ⚠️ WHICH NODE THE CALL GOES THROUGH, NOT WHERE THE POOL LIVES. A pool is cluster-wide: n2 and
34
+ * n3 answer byte-identically for `cephtb4` (measured). The endpoint is node-scoped only
35
+ * because PVE talks to the local RADOS socket, so this is a door, not a location — it is never
36
+ * compared, and moving a declaration from n2 to n3 plans noop. Point it at a node you expect
37
+ * to be up: a node that is down makes the read fail, and a failed read reads as "absent".
38
+ */
39
+ node: string;
40
+ /**
41
+ * Ceph's primary key, cluster-wide.
42
+ * ⛔ CHANGING IT DOES NOT RENAME ANYTHING. There is no rename in this API: the plan reads the new
43
+ * name, finds nothing, and CREATES AN EMPTY POOL, leaving the old one and its data behind,
44
+ * untracked. `replace` was considered and rejected — it would DELETE the old pool and every
45
+ * image in it to satisfy an edited string. Rename by hand, deliberately, or not at all.
46
+ */
47
+ name: string;
48
+ /** Replicas per object, 1-7. ⚠️ Frozen when the pool carries Ceph's `nosizechange`. */
49
+ size?: number;
50
+ /** Replicas required to accept writes, 1-7. ⚠️ Frozen by `nosizechange` too. */
51
+ min_size?: number;
52
+ /**
53
+ * ⛔ CREATE-TIME ONLY, AND NEVER COMPARED — the single biggest noop hazard in this family. With
54
+ * `pg_autoscale_mode` on or warn the autoscaler owns this number: MEASURED on TB4, `cephtb4`
55
+ * sits at pg_num 128 while the autoscaler's own `pg_num_final` is 256, so a declaration
56
+ * diffing it reports an update the moment Ceph decides to act. A split is gradual even with
57
+ * the autoscaler off, and `nopgchange` can refuse the write outright — which kills the WHOLE
58
+ * PUT, not just this field. Declare it as the birth size; steer it afterwards with
59
+ * `pg_num_min`, or by hand.
60
+ */
61
+ pg_num?: number;
62
+ /** The floor the autoscaler may not go below. Operator-owned and stable, so it IS compared. */
63
+ pg_num_min?: number;
64
+ /** ⚠️ PVE's create default is `warn`; Ceph's own is `on`, which is what all four TB4 pools have. */
65
+ pg_autoscale_mode?: 'off' | 'on' | 'warn';
66
+ /**
67
+ * The CRUSH rule BY NAME, e.g. `replicated_rule`.
68
+ * ★ THE SUSPECTED INTEGER-VS-NAME TRAP IS REAL BUT NOT ON THE PATH THIS RESOURCE READS, AND THAT
69
+ * IS MEASURED BOTH WAYS. `GET .../ceph/pool` (the collection) reports `crush_rule: 0` with the
70
+ * name beside it in `crush_rule_name`; `GET .../ceph/pool/{name}/status` reports
71
+ * `crush_rule: "replicated_rule"` — the same spelling POST and PUT accept. Reading the
72
+ * collection instead would reintroduce the forever-diff in a single edit.
73
+ */
74
+ crush_rule?: string;
75
+ /**
76
+ * ⛔ CREATE-TIME ONLY, AND NEVER COMPARED. PVE's PUT turns this into
77
+ * `osd pool application enable` (Tools.pm:250-255), which ADDS a tag and never removes one,
78
+ * and Ceph refuses a second application without `--yes-i-really-mean-it` — so an update would
79
+ * either leave the pool tagged twice or die, and comparing one declared string against the
80
+ * LIST that comes back is the set-in-arbitrary-order forever-diff. The live list is reported
81
+ * as `applications` instead.
82
+ */
83
+ application?: 'cephfs' | 'rbd' | 'rgw';
84
+ /** Autoscaler size hint, ⚠️ IN BYTES — see the ⚠️ on `updateBody`. A `1T` string is not accepted. */
85
+ target_size?: number;
86
+ /**
87
+ * Autoscaler share of total capacity, 0-1.
88
+ * ⚠️ WRITTEN BUT NEVER DIFFED, because a float is not a safe equality: Ceph stores a double and
89
+ * PVE prints about 15 significant digits, so a ratio that is not exactly representable would
90
+ * report an update on every plan for a pool nobody touched.
91
+ */
92
+ target_size_ratio?: number;
93
+ /**
94
+ * ⛔ `never` ON PURPOSE, BOTH OF THEM, AND THE COMPILE ERROR IS THE FEATURE.
95
+ * `erasure-coding` does not configure this pool — it makes PVE build TWO pools and name
96
+ * neither of them what you declared: `<name>-data` for the EC data and `<name>-metadata` for
97
+ * the replicated half (Pool.pm:517-530). Nothing would then exist at `{name}`, so every plan
98
+ * would read absent and try to create it again, forever.
99
+ * `add_storages` writes a section into storage.cfg that this graph does not own; declare a
100
+ * `Proxmox.Storage` and pass this pool's `name` into it instead, which makes the link real.
101
+ */
102
+ 'erasure-coding'?: never;
103
+ add_storages?: never;
104
+ }
105
+
106
+ /**
107
+ * ⚠️ THE THREE `no*` FLAGS ARE REPORTED BECAUSE THEY EXPLAIN A REFUSAL. Ceph can freeze a pool's
108
+ * size or its PG shape, and `set_pool` dies on the whole PUT when any one parameter will not
109
+ * apply (Tools.pm:310). `matches` refuses to diff the fields they freeze, so these attributes
110
+ * are the only place a plan can show WHY a declared size is not being enforced. All three read
111
+ * false on all four TB4 pools.
112
+ * ⚠️ `pg_num`, `id` AND `applications` ARE REPORTED AND NEVER COMPARED — see the props above.
113
+ * ⛔ NO `statistics` AND NO `autoscale_status`, though the verbose read returns both. They change
114
+ * every few seconds; persisting them would rewrite this resource's state on every deploy and
115
+ * read like drift. `UNSET` means the pool carries no value for that field.
116
+ */
117
+ export interface CephPoolAttributes {
118
+ name: string;
119
+ /** The node the read went through. A debugging aid, not a property of the pool. */
120
+ node: string;
121
+ id: number;
122
+ size: number;
123
+ min_size: number;
124
+ pg_num: number;
125
+ pg_num_min: number;
126
+ pg_autoscale_mode: string;
127
+ /** ⚠️ Empty when PVE answered with a rule id instead of a name; `matches` then stops diffing it. */
128
+ crush_rule: string;
129
+ /** Sorted comma list, e.g. `rbd` or `cephfs`. Readable only under `?verbose=1`. */
130
+ applications: string;
131
+ target_size: number;
132
+ target_size_ratio: number;
133
+ nodelete: boolean;
134
+ nopgchange: boolean;
135
+ nosizechange: boolean;
136
+ }
137
+
138
+ /**
139
+ * Where one pool is WRITTEN, and where a new one is POSTed.
140
+ *
141
+ * ⚠️ NEITHER IS WHERE IT IS READ — the read path is a segment longer and lives in ceph-pool.ts
142
+ * next to the ⛔ that explains it. These two are here because a path is wire knowledge, and
143
+ * because the create guard needs the collection without needing anything else from the spec.
144
+ */
145
+ export const object = (props: CephPoolProps) => `nodes/${props.node}/ceph/pool/${props.name}`;
146
+ export const collection = (props: CephPoolProps) => `nodes/${props.node}/ceph/pool`;
147
+
148
+ /**
149
+ * ⚠️ "NOT SET" IS -1 AND NOT 0, for the reason metric-server-form.ts gives: `pg_num_min`,
150
+ * `target_size` and `target_size_ratio` each take 0 as a REAL value meaning "no floor, no hint",
151
+ * so a provider using 0 as its absent-marker cannot tell a declared zero from an undeclared
152
+ * field. Every one of these is >= 0, so -1 sits outside all of their ranges.
153
+ */
154
+ export const UNSET = -1;
155
+
156
+ /** ⚠️ PVE returns the tags as an ARRAY in Ceph's order, so `csv` sorts before anything sees them. */
157
+ export const applications = (value: unknown) =>
158
+ csv(Array.isArray(value) ? value.map((entry: unknown) => String(entry)) : []);
159
+
160
+ /**
161
+ * Declared? compare it. Undeclared, or frozen by a flag the pool carries? leave it alone.
162
+ *
163
+ * ⚠️ `frozen` IS THE WHOLE ANSWER TO "A FIELD PVE WILL NOT ACCEPT ON WRITE". Comparing one can only
164
+ * plan an update that no write can satisfy: work reported on every run, forever.
165
+ */
166
+ export const same = <T>(declared: T | undefined, live: T, frozen = false) =>
167
+ declared === undefined || frozen || declared === live;
168
+
169
+ /**
170
+ * A declared autoscaler hint, or nothing when it is zero.
171
+ *
172
+ * ⚠️ A DECLARED ZERO IS "NO FLOOR, NO HINT", AND IT IS DELIBERATELY LEFT UNCOMPARED. REASONED, NOT
173
+ * MEASURED: all four TB4 pools carry null for both `pg_num_min` and `target_size`, and finding
174
+ * out for certain would mean writing to the cluster. Ceph CLEARS both when they are set to 0 and
175
+ * `osd pool get all` then omits the key, which PVE reports as null and this pair reads as UNSET
176
+ * — so a declared 0 would compare 0 against -1 on every plan, forever. Mapping 0 to UNSET
177
+ * instead would be a guess in the OTHER direction, and wrong the same way if Ceph does report
178
+ * the zero. Neither guess is made: a zero is still written, and then left unmanaged exactly like
179
+ * an undeclared field.
180
+ */
181
+ export const hint = (value: number | undefined) => (value === 0 ? undefined : value);
182
+
183
+ /** One optional field, present in the form only when it was declared. */
184
+ const field = (name: string, value: string | undefined): Record<string, string> =>
185
+ value === undefined ? {} : { [name]: value };
186
+
187
+ /** ⚠️ Not named `digits`: `target_size_ratio` is a fraction, and `String` is right for both. */
188
+ const numeric = (value: number | undefined) => (value === undefined ? undefined : String(value));
189
+
190
+ /**
191
+ * The PUT body: everything POST and PUT both accept, `pg_num` excepted. `createBody` builds on it,
192
+ * the way storage.ts's `mutable` serves both of its forms. `name` is the path's last segment on an
193
+ * update and is not repeated here.
194
+ *
195
+ * ⚠️ SAFE TO RE-APPLY TO A POOL THAT ALREADY MATCHES, which reconcile relies on. MEASURED in
196
+ * PVE::Ceph::Tools::set_pool (Tools.pm:293-300): it reads `osd pool get all` first and SKIPS
197
+ * every setting whose value did not change, so re-sending the declared set costs one mon read.
198
+ * ⛔ BUT A SETTING THAT DID CHANGE AND CANNOT BE APPLIED KILLS THE WHOLE PUT. Tools.pm:310 dies
199
+ * with "Could not set: <fields>" if any parameter is left unapplied, so a pool carrying Ceph's
200
+ * `nosizechange` fails an entire update over one field. That is why the `no*` flags are reported
201
+ * and why `matches` refuses to diff what they freeze.
202
+ *
203
+ * ⚠️ AN UNDECLARED FIELD IS NEITHER SENT NOR COMPARED — storage.ts's rule, and the opposite of
204
+ * Proxmox.Pool's. Undeclared means UNMANAGED here, because a pool has no one set of defaults to
205
+ * fall back on: PVE's POST fills in size 3, min_size 2, application rbd and pg_autoscale_mode
206
+ * 'warn' (MEASURED in createpool), while Ceph's own defaults — which is what every pool on TB4
207
+ * was actually born with — put pg_autoscale_mode at 'on'. Guessing either set would rewrite a
208
+ * pool somebody tuned by hand.
209
+ *
210
+ * ⛔ THERE IS NO `delete=` HERE AND THERE MUST NOT BE ONE. `setpool` is not a SectionConfig write:
211
+ * it turns each parameter into an `osd pool set`, and Ceph has no "unset" for size, min_size or
212
+ * crush_rule. metric-server-form.ts clears an undeclared optional because status.cfg supports
213
+ * it; the same move here would send a parameter PVE has no verb for.
214
+ *
215
+ * ⚠️ `target_size` IS BYTES ON THE WIRE IN BOTH DIRECTIONS, WHICH IS WHY THE PROP IS A NUMBER AND
216
+ * NOT PVE's `1T` STRING. MEASURED: the API parses the string with PVE::JSONSchema::parse_size
217
+ * and stores Ceph's `target_size_bytes` (Pool.pm:474-477 for POST, 683-686 for PUT), and
218
+ * `GET .../status` hands that integer back under the name `target_size` (Pool.pm:878). Run on
219
+ * n2, `parse_size('1099511627776')` returns 1099511627776 — bare digits are bytes, no
220
+ * multiplier — so a byte count survives the round trip exactly, while a declared `1T` would read
221
+ * back as 1099511627776 and diff against itself on every plan, forever.
222
+ */
223
+ export const updateBody = (props: CephPoolProps): Record<string, string> => ({
224
+ ...field('crush_rule', props.crush_rule),
225
+ ...field('min_size', numeric(props.min_size)),
226
+ ...field('pg_autoscale_mode', props.pg_autoscale_mode),
227
+ ...field('pg_num_min', numeric(props.pg_num_min)),
228
+ ...field('size', numeric(props.size)),
229
+ ...field('target_size', numeric(props.target_size)),
230
+ ...field('target_size_ratio', numeric(props.target_size_ratio)),
231
+ });
232
+
233
+ /**
234
+ * The POST body.
235
+ *
236
+ * ⛔ `add_storages=0` IS SENT EXPLICITLY RATHER THAN LEFT OUT, AND THE DEFAULT IS NOT WHAT IT LOOKS
237
+ * LIKE. MEASURED at Pool.pm:481 — `$add_storages = 1 if $ec && !defined($add_storages)` — so for
238
+ * an erasure-coded pool PVE turns it ON when the parameter is absent. A pool created with it
239
+ * writes a NEW SECTION INTO storage.cfg that this graph does not own: `Proxmox.Storage` would
240
+ * not know about it, `alchemy destroy` would leave it behind, and the first person to notice
241
+ * would be whoever found a storage nobody declared.
242
+ */
243
+ export const createBody = (props: CephPoolProps): Record<string, string> => ({
244
+ ...updateBody(props),
245
+ ...field('application', props.application),
246
+ ...field('pg_num', numeric(props.pg_num)),
247
+ add_storages: '0',
248
+ name: props.name,
249
+ });
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Waiting for Ceph to finish, which is a concern of its own.
3
+ *
4
+ * ★ SPLIT OUT OF ceph-pool.ts FOR THE 250-LINE CAP, and the seam is real: every other PVE family
5
+ * in this package writes and reads back in one breath, because PVE's config endpoints are
6
+ * synchronous. Ceph's are not — a pool delete returns before the PGs are gone — so this file is
7
+ * the polling that difference forces, and ceph-pool.ts is the declaration.
8
+ */
9
+ import * as Effect from 'effect/Effect';
10
+ import type { CephPoolAttributes, CephPoolProps } from './ceph-pool-form.ts';
11
+ import { pve } from './client.ts';
12
+ import type { PveRequirements } from './resource.ts';
13
+
14
+ /**
15
+ * ⚠️ THE DEPENDENCIES ARE PASSED IN RATHER THAN IMPORTED, and that is what made this file
16
+ * separable at all. `collection`, `object` and `ops.read` are module-level bindings in
17
+ * ceph-pool.ts; importing them back would be a runtime cycle, and duplicating them would be two
18
+ * definitions of one path. Passing them is the pattern zfs-pool-write.ts already uses.
19
+ */
20
+ export type PoolRead = (
21
+ props: CephPoolProps,
22
+ ) => Effect.Effect<CephPoolAttributes | undefined, never, PveRequirements>;
23
+
24
+ /**
25
+ * ⛔ THE GUARD ON THE CREATE. `read` folds a 403, a timeout and a node that is down into the same
26
+ * `undefined` as a pool that is genuinely missing — so "absent" is not evidence. This asks a
27
+ * second, different question: does the cluster LIST this pool? A failure is left to propagate
28
+ * rather than folded, so "I could not ask" fails the deploy instead of creating over live data.
29
+ * An empty list is trusted: the collection 403s when the role is too narrow rather than
30
+ * filtering, so `[]` really does mean a cluster with no pools.
31
+ */
32
+ export const confirmAbsent = (props: CephPoolProps, collection: string, describe: string) =>
33
+ pve<{ pool_name?: string }[]>(props.target, 'read', 'GET', collection).pipe(
34
+ Effect.flatMap((rows) =>
35
+ (rows ?? []).some((row) => row.pool_name === props.name)
36
+ ? Effect.die(
37
+ new Error(
38
+ `${describe}: the cluster lists this pool but its status could not be read, ` +
39
+ 'so this deploy will NOT create over it. Check that the node is up and that the ' +
40
+ 'read role holds Sys.Audit or Datastore.Audit on / -- a create here would apply ' +
41
+ 'pg_num to a live pool and start a PG merge.',
42
+ ),
43
+ )
44
+ : Effect.void,
45
+ ),
46
+ );
47
+
48
+ /**
49
+ * Wait for the cluster to agree, then answer the last thing it said.
50
+ *
51
+ * ★ IT POLLS `matches`, THE SAME PREDICATE `diff` USES, so "settled" has exactly one definition in
52
+ * this file. Polling `/nodes/{node}/tasks/{upid}/status` instead would read the worker's own
53
+ * exit status, but it needs `Sys.Audit` on the node for a token that is NOT the one that started
54
+ * the task — every `pve` call mints a fresh token, so the task's owner check cannot pass.
55
+ * ⚠️ SHORT POLLS, LOW CAP: 2s apart, 30 tries, about a minute. A pool create takes seconds; one
56
+ * still unsettled after a minute is a fault to surface, not a wait to lengthen.
57
+ */
58
+ export const settle = (
59
+ props: CephPoolProps,
60
+ read: PoolRead,
61
+ done: (live: CephPoolAttributes | undefined) => boolean,
62
+ ) =>
63
+ Effect.gen(function* () {
64
+ for (let attempt = 0; attempt < 30; attempt += 1) {
65
+ const live = yield* read(props);
66
+ if (done(live)) return live;
67
+ yield* Effect.sleep('2 seconds');
68
+ }
69
+ return yield* read(props);
70
+ });