@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,249 @@
1
+ /**
2
+ * `Proxmox.CephFlag` — one cluster-wide Ceph OSD flag, declared: `noout`, `pause`, `norebalance`.
3
+ *
4
+ * ★ ONE RESOURCE PER FLAG, NOT ONE CARRYING ALL ELEVEN, AND THE CLUSTER'S OWN API DECIDED IT.
5
+ * MEASURED on TB4 2026-09-13 (pve-manager 9.2.11, ceph tentacle 20.2.2) by reading
6
+ * `/usr/share/perl5/PVE/API2/Cluster/Ceph.pm` and the published schema in
7
+ * `/usr/share/pve-docs/api-viewer/apidoc.js`:
8
+ * · `PUT /cluster/ceph/flags` ends in `fork_worker('cephsetflags', ...)` and returns a STRING
9
+ * — a UPID. The bulk write is ASYNCHRONOUS, and this package has no task-polling machinery.
10
+ * · `PUT /cluster/ceph/flags/{flag}` calls `$rados->mon_command` inline and returns null. The
11
+ * per-flag write is SYNCHRONOUS.
12
+ * `pveOperations.reconcile` READS BACK after every write and refuses to record a value it did
13
+ * not see. Against the bulk endpoint that read-back races a worker which has not run yet, so it
14
+ * would record the OLD value as the new attributes — the one honesty mechanism in this package
15
+ * turned into a lie generator. Per-flag, the read-back means what it says.
16
+ * ★ THE SECOND REASON IS SMALLER AND STILL REAL. The bulk GET answers rows of
17
+ * `{name, description, value}`, and `description` is PVE's own English prose that no write
18
+ * accepts: compare it and you have a forever-update, report it and this provider is pretending
19
+ * to manage prose. `GET /cluster/ceph/flags/{flag}` answers a BARE BOOLEAN, so that trap
20
+ * cannot be written here at all. The brief's plural `Proxmox.CephFlags` became singular for
21
+ * those two reasons; the bulk endpoint is read by nothing in this package.
22
+ *
23
+ * ⛔ THESE ARE NOT CONFIGURATION. THEY ARE A HAND ON THE BRAKE, AND A DECLARATION CAN PULL IT OFF.
24
+ * `noout` is what a person sets before pulling a disk. A stack saying `value: false` reasserts
25
+ * that off on EVERY deploy — including a deploy somebody else runs, for an unrelated resource,
26
+ * twenty minutes into a maintenance window — and the plan line reads `Proxmox.CephFlag noout
27
+ * update`, which nobody reads as "ceph is about to start rebalancing 128 PGs across three nodes
28
+ * while a disk is out of the chassis".
29
+ * ★ SO `value` IS REQUIRED AND HAS NO DEFAULT, AND THAT IS THE MITIGATION. An optional `value`
30
+ * defaulting to false would make `CephFlag('noout', { flag: 'noout', target })` mean "clear
31
+ * it" — the dangerous direction, chosen by OMISSION. Here the dangerous sentence has to be
32
+ * typed out by a person. Do not add a default, and do not declare a flag merely to document
33
+ * that it is off: MEASURED 2026-09-13, all eleven flags on TB4 read 0, so eleven
34
+ * `value: false` lines would plan green forever and do nothing except take the brake off
35
+ * whenever somebody happens to apply them.
36
+ *
37
+ * ⛔ `delete` DOES NOTHING, ON PURPOSE, AND THE ASYMMETRY OF THE TWO FAILURES IS THE ARGUMENT.
38
+ * `flags/{flag}` has exactly two methods, GET and PUT, so the factory's `destroy` would answer
39
+ * "Method 'DELETE /cluster/ceph/flags/noout' not implemented" on every teardown — whatever this
40
+ * handler does is INVENTED. The obvious invention, "clear the flag", is refused: removing a line
41
+ * from a stack file, or Alchemy collecting an old generation after a replace, would silently
42
+ * take the brake off and nothing would warn. Leaving a flag set that nothing declares any more
43
+ * is the opposite kind of failure — ceph reports `HEALTH_WARN ... flag(s) set` for every one of
44
+ * these, on `ceph -s`, in the PVE UI and in the `pve_*` metrics this estate already scrapes.
45
+ * A silent destructive failure loses to a loud inert one.
46
+ * ⚠️ THE RESIDUAL RISK, NAMED RATHER THAN DENIED: deleting a `value: true` declaration LEAVES
47
+ * THE FLAG SET. Retire a maintenance window by flipping the line to `value: false` and
48
+ * deploying BEFORE the line is removed, or a `noout` outlives the window that needed it.
49
+ * ⚠️ REASONED, NOT MEASURED: that health warning was not observed, because observing it means
50
+ * setting a flag on a live cluster and this file was researched read-only. Undeclaring stops
51
+ * MANAGING a flag; to clear one, declare `value: false` and deploy, where a plan says so.
52
+ *
53
+ * ⛔ `pause` IS TWO CEPH FLAGS AND THIS RESOURCE READS ONLY ONE OF THEM. MEASURED in
54
+ * `PVE::Ceph::Tools::get_real_flag_name`, whose own comment reads "the 'pause' flag gets always
55
+ * set to both 'pauserd' and 'pausewr'": PVE writes both and then decides the flag is set by
56
+ * looking at `pauserd` alone. A cluster where somebody ran `ceph osd unset pauserd` by hand
57
+ * still has `pausewr` — writes blocked, guests hung — and this resource reports `value: false`
58
+ * and plans `noop` straight past it. It cannot express a half-pause, so never use it to prove
59
+ * one is gone; `ceph osd dump | head -1` can.
60
+ * ⛔ `pause` AND `noup` ARE ALSO THE TWO THAT TAKE THE CLUSTER DOWN FROM A TYPO. `pause: true`
61
+ * stops all reads and writes, so every guest on `cephtb4` — the cluster's only rbd pool,
62
+ * MEASURED 2026-09-13 — freezes on its root disk, and so does `cephfs-tb4`. `noup: true`
63
+ * stops a rebooted OSD from ever rejoining. Neither is undone by deleting the line.
64
+ *
65
+ * ⚠️ PRIVILEGES, AND THE WRITE ONE IS A BIG ASK. Both GETs check `Sys.Audit` on `/`; both PUTs
66
+ * check `Sys.Modify` on `/`, and all four carry `allowtoken => 1`, so an API token may make the
67
+ * call at all. There is no `/ceph` ACL object to scope to — the check names the ROOT path
68
+ * literally — so granting the provision role `Sys.Modify` here also buys datacenter options,
69
+ * metric servers, notification targets and every other cluster-wide write, exactly as
70
+ * metric-server.ts warns about its own family.
71
+ * ⚠️ REASONED, NOT MEASURED: nothing here was exercised under a minted lease, because a mint
72
+ * writes an API token into the cluster and the brief for this file was reads only. Expect
73
+ * "Permission check failed (/, Sys.Modify)" on the first deploy, and widen deliberately.
74
+ */
75
+ import { Resource } from 'alchemy';
76
+ import * as Provider from 'alchemy/Provider';
77
+ import * as Effect from 'effect/Effect';
78
+ import { type PveRequirements, type WithTarget, pveHandlers } from './resource.ts';
79
+ import { bool, flag } from './values.ts';
80
+
81
+ /**
82
+ * PVE's eleven flags, spelled as `PVE::Ceph::Tools::get_possible_osd_flags` spells them.
83
+ *
84
+ * ⚠️ THE ENUM IS CLOSED AND A TYPO IS A 400, WHICH IS WHY THIS IS A UNION AND NOT `string`. Both
85
+ * the GET and the PUT declare `additionalProperties => 0` over exactly this list.
86
+ * ⚠️ CEPH HAS FLAGS PVE DOES NOT MODEL, and four of them are always on. MEASURED on TB4:
87
+ * `ceph osd dump` reports `flags sortbitwise,recovery_deletes,purged_snapdirs,pglog_hardlimit`.
88
+ * PVE reads that same string and answers only about its own eleven, so the others can neither
89
+ * leak in here nor be set from here — `noautoscale` and `nosnaptrim` included.
90
+ * ⛔ IF SOMEBODY LATER "IMPROVES" THE READ BY PARSING `osd dump` DIRECTLY, those four become
91
+ * permanently-set flags that nothing declares: a forever-diff on a brand new cluster.
92
+ */
93
+ export type CephFlagName =
94
+ | 'nobackfill'
95
+ | 'nodeep-scrub'
96
+ | 'nodown'
97
+ | 'noin'
98
+ | 'noout'
99
+ | 'norebalance'
100
+ | 'norecover'
101
+ | 'noscrub'
102
+ | 'notieragent'
103
+ | 'noup'
104
+ | 'pause';
105
+
106
+ export interface CephFlagProps extends WithTarget {
107
+ /**
108
+ * Which flag. PVE's primary key here, and the last segment of the path.
109
+ *
110
+ * ⚠️ EDITING IT IN PLACE ORPHANS THE OLD FLAG RATHER THAN MOVING ANYTHING. `diff` reads the NEW
111
+ * path and reconcile writes it, while the old flag keeps whatever this stack last put there.
112
+ * There is deliberately no replace override for it — acl.ts needs one because its `delete`
113
+ * removes a real grant, and `delete` here is inert by design, so a replace would do exactly
114
+ * what an update already does. Declare a second resource and set the old one to `value: false`
115
+ * rather than renaming this one.
116
+ * ⛔ TWO RESOURCES DECLARING THE SAME FLAG ARE ONE CLUSTER OBJECT, and Alchemy sees two ids
117
+ * rather than a collision — the acl.ts hazard exactly. Disagreeing, they take turns winning
118
+ * and BOTH plan `update` for ever; agreeing, deleting either leaves the flag where the other
119
+ * put it. One declaration per flag per cluster.
120
+ */
121
+ flag: CephFlagName;
122
+ /**
123
+ * Set the flag (`true`) or clear it (`false`).
124
+ *
125
+ * ⛔ REQUIRED, WITH NO DEFAULT — see the second ⛔ in the header, which is the whole safety
126
+ * argument for this family. It is also the only field PVE accepts on this endpoint, and it is
127
+ * not optional in the schema either: omitting it from the form is a 400, not an untouched
128
+ * flag. (The BULK endpoint is the one where omission means "leave it alone"; this is not it.)
129
+ */
130
+ value: boolean;
131
+ }
132
+
133
+ export interface CephFlagAttributes {
134
+ /**
135
+ * ⚠️ REPORTED, NEVER COMPARED. It is the path key the read was made WITH, copied back out of
136
+ * props, so comparing it against props would be true by construction — the same reasoning
137
+ * acl.ts gives for the four identity fields it also declines to diff.
138
+ */
139
+ flag: CephFlagName;
140
+ /** Whether ceph has the flag set right now. The only field `matches` looks at. */
141
+ value: boolean;
142
+ }
143
+
144
+ export interface ProxmoxCephFlag extends Resource<
145
+ 'Proxmox.CephFlag',
146
+ CephFlagProps,
147
+ CephFlagAttributes,
148
+ never,
149
+ PveRequirements
150
+ > {}
151
+
152
+ export const ProxmoxCephFlag = Resource<ProxmoxCephFlag>('Proxmox.CephFlag');
153
+
154
+ const handlers = pveHandlers<CephFlagProps, CephFlagAttributes>({
155
+ /**
156
+ * ⚠️ `live` IS A BARE BOOLEAN HERE, NOT AN OBJECT, AND IT IS HANDED TO `bool` WHOLE ON PURPOSE.
157
+ * `get_flag` returns perl `1` or `0` under a `type => 'boolean'` return schema, so the wire is
158
+ * `{"data":0}` or `{"data":false}` depending on how the REST layer renders it. `bool` accepts
159
+ * every one of those spellings, which is exactly why it is a shared coercion. The factory
160
+ * types this parameter `Record<string, unknown>` because every OTHER PVE read is an object;
161
+ * reaching for `live['value']` here would answer undefined on every plan and diff `false`
162
+ * forever against a set flag.
163
+ * ⚠️ THE SCALAR IS A MEASURED PROPERTY OF THIS PVE, NOT A PROMISE. A later version wrapping
164
+ * the answer in an object would make `bool` read a SET flag as false, and `value: false`
165
+ * would then plan `noop` over a live flag — silently. Re-measure
166
+ * `GET /cluster/ceph/flags/noout` after a major upgrade; cheaper than a second parse path.
167
+ *
168
+ * ⛔ IT NEVER ANSWERS undefined, WHICH MAKES THE CREATE BRANCH UNREACHABLE, AND ITS ERROR WILL
169
+ * MISLEAD YOU. All eleven flags always exist; there is nothing for "absent" to mean. So a POST
170
+ * to `collection` means the READ failed — an expired 300s lease, n2 down, or ceph simply not
171
+ * configured on the cluster (every one of these four handlers opens with
172
+ * `check_ceph_configured()`, which dies). Read the resulting "Method 'POST /cluster/ceph/flags'
173
+ * not implemented" as "the read failed" and go and look at the credential, not at ceph. Same
174
+ * shape as the unreachable POST in acl.ts, same advice.
175
+ */
176
+ attributes: (live, props) => ({ flag: props.flag, value: bool(live) }),
177
+ collection: () => 'cluster/ceph/flags',
178
+ /**
179
+ * ⛔ UNREACHABLE — see the second ⛔ in `attributes`. It is written in the BULK endpoint's shape
180
+ * (the flag name is the parameter, not `value`) rather than left empty, because that is the
181
+ * only body `cluster/ceph/flags` has ever documented. Nothing sends it today.
182
+ */
183
+ createForm: (props) => ({ [props.flag]: flag(props.value) ?? '0' }),
184
+ /**
185
+ * ⚠️ `value` IS THE ONLY THING COMPARED, AND THE LIST OF THINGS DELIBERATELY NOT COMPARED IS THE
186
+ * point of this resource. `flag` is props-derived (see `CephFlagAttributes`). `description`
187
+ * and `name` exist only on the bulk GET, which this file does not read, so neither can be
188
+ * compared by accident. Nothing PVE returns here is rewritten, re-ordered or re-typed by the
189
+ * cluster, because all PVE returns here is one boolean.
190
+ * ★ MEASURED: on TB4, `GET /cluster/ceph/flags` answers value 0 for all eleven flags, so a
191
+ * declaration of `value: false` reads back false and plans `noop`.
192
+ */
193
+ matches: (attributes, props) => attributes.value === props.value,
194
+ path: (props) => `cluster/ceph/flags/${props.flag}`,
195
+ /**
196
+ * ⚠️ ONLY `value` GOES IN THE BODY. `flag` is already bound by the path and the PUT declares
197
+ * `additionalProperties => 0`, so a second copy in the form could only disagree with the URL.
198
+ * ⚠️ THE `?? '0'` IS UNREACHABLE, AND IS WRITTEN RATHER THAN CAST AWAY. `flag()` types its
199
+ * answer `string | undefined` because it serves OPTIONAL props, and `value` here is required,
200
+ * so the branch cannot be taken; a cast would claim a proof the type system has not made.
201
+ */
202
+ updateForm: (props) => ({ value: flag(props.value) ?? '0' }),
203
+ });
204
+
205
+ export const ProxmoxCephFlagProvider = () =>
206
+ Provider.effect(
207
+ ProxmoxCephFlag,
208
+ Effect.succeed(
209
+ ProxmoxCephFlag.Provider.of({
210
+ /**
211
+ * ⚠️ NOT THE ONE-LINE `Provider.of(handlers)` EVERY OTHER FILE HERE USES, AND THE TWO
212
+ * REASONS ARE BOTH acl.ts's. PVE implements no DELETE on this path, and the factory's
213
+ * read-back guard is dead whenever `attributes` cannot answer undefined. Unlike acl.ts
214
+ * this SPREADS the factory's handlers instead of retyping them, so `list`, `read` and
215
+ * `diff` cannot drift from the shared ones — only the two that genuinely differ are
216
+ * written out below.
217
+ */
218
+ ...handlers,
219
+ /** ⛔ Inert, deliberately. The third ⛔ in the header is the whole argument; read it. */
220
+ delete: () => Effect.void,
221
+ /**
222
+ * ⚠️ THE READ-BACK GUARD, RESTORED FOR A FAMILY WHOSE "ABSENT" DOES NOT EXIST.
223
+ * `ops.reconcile` refuses when the object is still missing after a write, and a flag is
224
+ * never missing, so that check can never fire here — exactly the hole acl.ts patches
225
+ * with `bound`. PVE answers 200 with `{"data":null}` on calls that did nothing, and this
226
+ * PUT's documented return IS null, so the status code carries no evidence at all.
227
+ * ⚠️ THE PER-FLAG PUT IS SYNCHRONOUS (`$rados->mon_command` inline, MEASURED in Ceph.pm),
228
+ * which is what makes this check fair rather than a race against a worker task. If it
229
+ * ever fires spuriously the honest fix is to look at the mon, not to delete the guard.
230
+ */
231
+ reconcile: Effect.fn(function* ({ news }) {
232
+ const after = yield* handlers.reconcile({ news });
233
+ if (after.value !== news.value) {
234
+ return yield* Effect.die(
235
+ new Error(
236
+ `cluster/ceph/flags/${news.flag}: the PUT returned no error but ceph still ` +
237
+ `reports the flag ${after.value ? 'set' : 'clear'} rather than ` +
238
+ `${news.value ? 'set' : 'clear'}. This endpoint's success is documented as a ` +
239
+ 'null body, so the status code proves nothing -- check `ceph osd dump | ' +
240
+ 'head -1` and the cluster log. `pause` in particular is two ceph flags read ' +
241
+ 'as one; see the header.',
242
+ ),
243
+ );
244
+ }
245
+ return after;
246
+ }),
247
+ }),
248
+ ),
249
+ );
@@ -0,0 +1,246 @@
1
+ /**
2
+ * The wire lane of `Proxmox.CephFs`: how a PVE answer becomes attributes, how a declaration becomes
3
+ * a PVE call, and how that call becomes a fact.
4
+ *
5
+ * ★ SPLIT OUT OF ceph-fs.ts TO KEEP BOTH FILES UNDER THE 250-LINE CAP, and the seam is a real one
6
+ * rather than a convenient line number. ceph-fs.ts answers "what is a CephFS, and when has it
7
+ * changed"; this file answers "what does PVE say, what does it want, and has it finished". On
8
+ * this family that is where all the strangeness lives — the read is an index and not an object,
9
+ * the paths are inverted, the delete takes its arguments in the query string, and both writes
10
+ * fork a worker — so keeping it in one place is what stops the next reader assuming the ordinary
11
+ * shape from the other nineteen resources. ⚠️ NOTHING HERE DECIDES A DIFF.
12
+ *
13
+ * ⚠️ THE `import type` BACK TO ceph-fs.ts IS A CYCLE ON PAPER ONLY. It is type-only and erased
14
+ * before anything runs, exactly as metric-server-form.ts imports `MetricServerProps`. The props
15
+ * stay the resource's public shape, declared in the file that declares the resource.
16
+ *
17
+ * ⛔ POST AND DELETE ANSWER WITH A UPID, NOT A RESULT — THE WRITE IS ASYNCHRONOUS AND A FAILED ONE
18
+ * IS STILL HTTP 200. Read from the cluster's own source, /usr/share/perl5/PVE/API2/Ceph/FS.pm on
19
+ * n2: `createfs` and `destroyfs` both end `return $rpcenv->fork_worker(...)`. Two different lies
20
+ * come out of trusting that status code. A read-back straight after the POST reports "still
21
+ * absent" about a filesystem that is being built as it says so — which is precisely the message
22
+ * `pveOperations.reconcile` would print, pointing at the wrong cause. And a worker that dies
23
+ * INSIDE — the pool create, or the add-storage step, both of which are wrapped in `eval` — is
24
+ * never reported to the caller at all. `settle` below is the answer to both.
25
+ * ⚠️ REASONED FROM THAT SOURCE, NOT MEASURED. Nothing was written to this cluster, so the race
26
+ * has not been observed; only the `fork_worker` that guarantees it is there.
27
+ *
28
+ * ⛔ A DELETE BODY IS SILENTLY DISCARDED BY PVE, SO `remove-pools` GOES IN THE QUERY STRING.
29
+ * MEASURED in /usr/share/perl5/PVE/APIServer/AnyEvent.pm on n2: line 928 reads the request
30
+ * content into params only `if ($method eq 'PUT' || $method eq 'POST')`, and line 1653 sends
31
+ * every other method down a branch that parses `$request->url->query()` and nothing else. So
32
+ * `pve(target, role, 'DELETE', path, form)` would send a body PVE never looks at: the flags would
33
+ * read as unset, the pools would quietly survive a delete that asked for them, and no error would
34
+ * be raised anywhere. sdn-apply.ts already puts `?pending=1` in the path for the same reason.
35
+ *
36
+ * ⚠️ THE TASK POLL RUNS ON THE `read` ROLE THOUGH IT SITS INSIDE reconcile, FOR TWO REASONS. It is
37
+ * a read, and resource.ts already mints `read` for its own read-back inside reconcile. And the
38
+ * leases differ where it matters: `provision` is 300s and explicitly non-renewable, so a create
39
+ * that waits out a slow worker on that lease can expire mid-poll, while `read` is 3600s.
40
+ *
41
+ * ⛔ THE POLLER IS NEVER THE TASK'S OWNER, WHICH COSTS A PRIVILEGE, AND THIS ONE IS MEASURED
42
+ * RATHER THAN FEARED. client.ts mints a fresh credential per call and the mount vends a NEW
43
+ * token id each time, so the token reading the status is never the token recorded in the UPID —
44
+ * and PVE compares them exactly. /usr/share/perl5/PVE/API2/Tasks.pm on n2, `$check_task_user`:
45
+ * `return $user eq $fulltoken || $user eq $task->{user};`, above it the comment "token only sees
46
+ * token tasks, user sees user + token tasks". Token B of the same user matches neither branch.
47
+ * So the fallback is the schema's other clause for `GET /nodes/{node}/tasks/{upid}/status`: "The
48
+ * user needs 'Sys.Audit' permissions on '/nodes/<node>' if they are not the owner of the task."
49
+ * Grant it on the `read` role, or every create burns the full cap below and reports the wrong
50
+ * cause.
51
+ */
52
+ import * as Effect from 'effect/Effect';
53
+ import type { CephFsAttributes, CephFsProps } from './ceph-fs.ts';
54
+ import { pve } from './client.ts';
55
+ import type { PveTarget } from './credentials.ts';
56
+ import { csv, flag, int, text } from './values.ts';
57
+
58
+ /**
59
+ * This filesystem's row of the index, as attributes.
60
+ *
61
+ * ⚠️ `GET /nodes/{node}/ceph/fs` ANSWERS AN ARRAY, while the factory hands `attributes` the
62
+ * `Record<string, unknown>` every other PVE read is shaped like. Rows are narrowed rather than
63
+ * trusted, the same way acl.ts narrows its one flat grant list.
64
+ *
65
+ * ⛔ RETURNING undefined IS WHAT MAKES "ABSENT" MEAN ABSENT HERE, unlike acl.ts where the object is
66
+ * always present-but-unbound. A filesystem missing from the index has not been created, and
67
+ * `reconcile` takes the create branch on exactly that signal.
68
+ */
69
+ export const readRow = (
70
+ live: Record<string, unknown>,
71
+ props: CephFsProps,
72
+ ): CephFsAttributes | undefined => {
73
+ const row = (Array.isArray(live) ? live : [])
74
+ .filter(
75
+ (entry): entry is Record<string, unknown> => typeof entry === 'object' && entry !== null,
76
+ )
77
+ .find((entry) => entry['name'] === props.name);
78
+ if (row === undefined) return undefined;
79
+ return {
80
+ data_pool: text(row['data_pool']),
81
+ data_pools: csv(
82
+ Array.isArray(row['data_pools'])
83
+ ? row['data_pools'].map((entry: unknown) => String(entry))
84
+ : [],
85
+ ),
86
+ metadata_pool: text(row['metadata_pool']),
87
+ metadata_pool_id: int(row['metadata_pool_id'], -1),
88
+ name: props.name,
89
+ };
90
+ };
91
+
92
+ /** ⚠️ `exitstatus` is ABSENT while a task runs, and is the exact string `OK` only on success. */
93
+ type TaskStatus = { readonly status?: string; readonly exitstatus?: string };
94
+
95
+ /**
96
+ * ⚠️ SHORT POLLS, LOW CAP. Ninety seconds covers two pool creates plus the ten seconds `createfs`
97
+ * itself spends waiting for an MDS to go active, and is short enough that a stuck task fails the
98
+ * deploy rather than parking it for the afternoon.
99
+ */
100
+ const POLL_SECONDS = 2;
101
+ const POLL_ATTEMPTS = 45;
102
+
103
+ /** ⚠️ `nodes/{n}/ceph/fs/{name}` — the POST and DELETE target, and NOT where a read goes. */
104
+ export const objectPath = (props: CephFsProps) => `nodes/${props.node}/ceph/fs/${props.name}`;
105
+
106
+ /** Only the declared flags. ⚠️ An omitted one is PVE's own default, not a value to send. */
107
+ const field = (name: string, value: string | undefined): Record<string, string> =>
108
+ value === undefined ? {} : { [name]: value };
109
+
110
+ /**
111
+ * ⚠️ `name` IS DELIBERATELY ABSENT FROM THE FORM. It is already the last segment of the path being
112
+ * POSTed to — the metric-server.ts case again — and a second copy can only ever disagree with it.
113
+ */
114
+ export const createForm = (props: CephFsProps): Record<string, string> => ({
115
+ ...field('add-storage', flag(props['add-storage'])),
116
+ ...field('pg_num', props.pg_num === undefined ? undefined : String(props.pg_num)),
117
+ });
118
+
119
+ /** The delete target with its flags in the query string — see the ⛔ in the header. */
120
+ const destroyPath = (props: CephFsProps) => {
121
+ const query = [
122
+ props['remove-pools'] === true ? 'remove-pools=1' : undefined,
123
+ props['remove-storages'] === true ? 'remove-storages=1' : undefined,
124
+ ].filter((part) => part !== undefined);
125
+ return query.length === 0 ? objectPath(props) : `${objectPath(props)}?${query.join('&')}`;
126
+ };
127
+
128
+ /**
129
+ * Wait for a forked PVE task, and fail loudly rather than quietly.
130
+ *
131
+ * ⚠️ THE UPID IS PERCENT-ENCODED BECAUSE IT IS FULL OF COLONS — `UPID:n2:00396D3A:…:root@pam:`
132
+ * (MEASURED shape, from `GET /nodes/n2/tasks`) is ONE path segment, not seven. Encoding is what
133
+ * guarantees it arrives as one; PVE's router decodes each segment before matching. REASONED from
134
+ * the URI handling rather than measured — no task of this provider's has been polled yet.
135
+ */
136
+ const settle = (target: PveTarget, node: string, upid: string, what: string) =>
137
+ Effect.gen(function* () {
138
+ for (let attempt = 0; attempt < POLL_ATTEMPTS; attempt += 1) {
139
+ const status = yield* pve<TaskStatus>(
140
+ target,
141
+ 'read',
142
+ 'GET',
143
+ `nodes/${node}/tasks/${encodeURIComponent(upid)}/status`,
144
+ ).pipe(
145
+ // ⚠️ AN UNREADABLE STATUS IS "NOT SETTLED YET", NOT A FAILURE. It is a missing Sys.Audit
146
+ // (see the header) or a node mid-restart; either way the task is still the cluster's
147
+ // business, and the cap is what ends the wait rather than one unlucky GET.
148
+ Effect.orElseSucceed(() => undefined),
149
+ );
150
+ if (status?.status === 'stopped') {
151
+ const exit = text(status.exitstatus);
152
+ if (exit === 'OK') return;
153
+ return yield* Effect.die(
154
+ new Error(
155
+ `${what}: PVE task ${upid} finished with "${exit === '' ? 'no exit status' : exit}". ` +
156
+ 'The write returned HTTP 200 because it only forked the worker -- the real error is ' +
157
+ `in \`pvesh get /nodes/${node}/tasks/${upid}/log\`.`,
158
+ ),
159
+ );
160
+ }
161
+ // ⚠️ A BARE NUMBER IS MILLISECONDS to Effect's Duration, not seconds. The cap in the
162
+ // message below is derived from the same constant so the two cannot drift apart.
163
+ yield* Effect.sleep(POLL_SECONDS * 1000);
164
+ }
165
+ return yield* Effect.die(
166
+ new Error(
167
+ `${what}: PVE task ${upid} was still running after ` +
168
+ `${String(POLL_ATTEMPTS * POLL_SECONDS)}s, or its ` +
169
+ 'status could not be read. Check the task log, and check that the `read` role holds ' +
170
+ `Sys.Audit on /nodes/${node} -- each call mints a new token, so the poller is never the ` +
171
+ 'task owner and is answered 403 rather than told to wait.',
172
+ ),
173
+ );
174
+ });
175
+
176
+ /**
177
+ * POST the create, then wait for the worker. ⛔ The caller still reads back; see ceph-fs.ts.
178
+ *
179
+ * ⚠️ A CREATE NEEDS A RUNNING *AND* A STANDBY MDS, and refuses BEFORE it forks -- "no running
180
+ * Metadata Server (MDS) found!" / "no standby Metadata Server (MDS) found!" are synchronous, so
181
+ * they surface as a failed POST rather than as a silent worker. TB4 has three (MEASURED: n3
182
+ * `up:active` for cephfs-tb4, n2 and n4 `up:standby`), so this is a note for a smaller cluster.
183
+ * ⚠️ BOTH WRITES CHECK `Sys.Modify` ON `/` -- the root, with the breadth metric-server.ts warns
184
+ * about: granting it buys datacenter options and every other cluster-wide config write too.
185
+ */
186
+ export const createFs = (props: CephFsProps) =>
187
+ Effect.gen(function* () {
188
+ const upid = text(
189
+ yield* pve<string>(props.target, 'provision', 'POST', objectPath(props), createForm(props)),
190
+ );
191
+ // ⛔ NO UPID MEANS NO WORKER. PVE answers 200 with `{"data":null}` on calls that did nothing,
192
+ // and here that is indistinguishable from success by status code alone. Refusing now is the
193
+ // difference between a clear failure and a state entry for a filesystem nobody built.
194
+ if (upid === '') {
195
+ return yield* Effect.die(
196
+ new Error(
197
+ `${objectPath(props)}: POST returned no task id. PVE wraps every answer in ` +
198
+ '{"data":...} and can report success on a call that did nothing.',
199
+ ),
200
+ );
201
+ }
202
+ yield* settle(props.target, props.node, upid, objectPath(props));
203
+ });
204
+
205
+ /**
206
+ * DELETE the filesystem, then wait for the worker.
207
+ *
208
+ * ⛔ WHAT THIS TAKES AWAY DEPENDS ENTIRELY ON `remove-pools`, AND THE DEFAULT IS THE SAFE ONE. With
209
+ * it unset PVE removes the filesystem from the MDS map and LEAVES the two pools on disk: every
210
+ * mount breaks, but the bytes are still there for an operator to recover. With it set the pools
211
+ * go too and nothing is recoverable. Leaving it unset also means a later create of the SAME name
212
+ * fails with "ceph pools '<name>_data' and/or '<name>_metadata' already exist" -- that refusal is
213
+ * a BRAKE, not a bug, and it is the last thing standing between a mis-typed rename and the data.
214
+ */
215
+ export const destroyFs = (props: CephFsProps) =>
216
+ Effect.gen(function* () {
217
+ const upid = text(yield* pve<string>(props.target, 'provision', 'DELETE', destroyPath(props)));
218
+ // ⚠️ A DESTROY THAT ANSWERS WITHOUT A UPID IS NOT REFUSED HERE. `destroyfs` dies synchronously
219
+ // with "no such cephfs" when the filesystem is already gone, which surfaces as a failed
220
+ // DELETE; an empty answer that is not an error is best treated as already-absent, and the
221
+ // caller's read-back is what proves it either way.
222
+ if (upid !== '') yield* settle(props.target, props.node, upid, destroyPath(props));
223
+ });
224
+
225
+ /**
226
+ * The two read-back failures, as messages rather than as guesses.
227
+ *
228
+ * ⛔ WAITING PROVES THE TASK ENDED; ONLY A READ PROVES THE FILESYSTEM DID OR DID NOT. `createfs`
229
+ * wraps its pool creates in an `eval` and CLEANS THEM UP on error before dying, so a worker can
230
+ * plausibly end having built and then removed everything it made. `destroyfs` refuses outright
231
+ * while a non-disabled `cephfs` storage still references the filesystem. Neither outcome is
232
+ * visible in a status code, which is why ceph-fs.ts reads back after both and dies with these.
233
+ */
234
+ export const notCreated = (props: CephFsProps) =>
235
+ new Error(
236
+ `${objectPath(props)}: the create task finished but the filesystem is not in ` +
237
+ `GET nodes/${props.node}/ceph/fs. Check the task log -- a PVE worker can exit having ` +
238
+ 'rolled back the pools it had just made.',
239
+ );
240
+
241
+ export const notDestroyed = (props: CephFsProps) =>
242
+ new Error(
243
+ `${objectPath(props)}: the destroy task finished but the filesystem is still in ` +
244
+ `GET nodes/${props.node}/ceph/fs. PVE refuses to destroy a CephFS while a PVE storage entry ` +
245
+ 'of type cephfs still references it and is not disabled.',
246
+ );