@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.CephFs` — a CephFS on the cluster's own Ceph. The filesystem a `cephfs` storage mounts.
3
+ *
4
+ * ⛔ A REPLACE OF THIS RESOURCE DESTROYS A LIVE FILESYSTEM, AND THERE IS NO PUT TO SAVE YOU FROM
5
+ * IT. MEASURED from the published schema on n2, 2026-09-13: `/nodes/{node}/ceph/fs/{name}` has
6
+ * exactly two methods, POST and DELETE. A CephFS cannot be edited in place, so `updateForm` is
7
+ * left undefined and anything the factory can see as changed becomes a REPLACE — and Alchemy's
8
+ * replace is create-then-delete, where the delete is `destroyfs`: the entry leaves the MDS map,
9
+ * every mount on every node breaks, and whether the bytes survive rests on `remove-pools` alone.
10
+ * ★ WHICH IS THE WHOLE REASON `matches` COMPARES NOTHING. Read its ⛔ before adding a field to
11
+ * it. On every other resource in this package a forever-diff is an annoying plan line; on this
12
+ * one it is a live filesystem destroyed on every deploy, forever, because a background daemon
13
+ * moved a number nobody declared.
14
+ *
15
+ * ⛔ THERE IS NO SINGLE-OBJECT READ, WHICH INVERTS `path` AND `collection`. MEASURED on n2:
16
+ * pvesh get /nodes/n2/ceph/fs/cephfs-tb4
17
+ * -> No 'get' handler defined for '/nodes/n2/ceph/fs/cephfs-tb4'
18
+ * `GET /nodes/{node}/ceph/fs` is a directory index and is the only read there is, so `path()`
19
+ * below returns the INDEX and `attributes` picks this filesystem out of the array client-side —
20
+ * the shape acl.ts uses for the one flat `GET /access/acl`. The POST goes to the `{name}` path,
21
+ * so `collection()` is the LONGER of the two strings, the mirror of metric-server.ts.
22
+ * ⚠️ AND THAT IS WHY `delete` AND `reconcile` ARE HAND-WRITTEN BELOW rather than taken from
23
+ * `pveHandlers`. `pveOperations.destroy` deletes `spec.path`, which here is the index:
24
+ * `DELETE /nodes/{node}/ceph/fs` is not a route, so a factory delete would fail every destroy
25
+ * while the filesystem stayed exactly where it was. `reconcile` is hand-written for a second,
26
+ * independent reason — the forked worker — set out in ceph-fs-wire.ts. acl.ts is the
27
+ * precedent for a family that genuinely does not fit; this is the second.
28
+ *
29
+ * ⛔ CREATING A CephFS CREATES TWO CEPH POOLS, AND A `Proxmox.CephPool` MUST NOT ALSO DECLARE THEM.
30
+ * `createfs` builds `<name>_data` and `<name>_metadata` itself and refuses outright if either
31
+ * exists ("ceph pools '…_data' and/or '…_metadata' already exist"). MEASURED on TB4: the live
32
+ * `cephfs-tb4` owns `cephfs-tb4_data` (id 6) and `cephfs-tb4_metadata` (id 7), both of which
33
+ * `GET /nodes/n2/ceph/pool` lists like any other pool — and `/nodes/{node}/ceph/pool/{name}` DOES
34
+ * have GET, PUT and DELETE, so a pool resource would be a clean `pveHandlers` fit and would
35
+ * adopt them without noticing. Declaring both is double management of one object: whichever
36
+ * reconciles first wins, and the loser either fails its create or PUTs `size`/`pg_num` onto pools
37
+ * this resource believes it owns. Declare the filesystem here and leave its two pools alone.
38
+ *
39
+ * ⚠️ `add-storage` IS A SECOND SIDE EFFECT AND IT LANDS IN storage.cfg. Set, `createfs` also calls
40
+ * `PVE::API2::Storage::Config->create` with type `cephfs`, content `backup,iso,vztmpl` and
41
+ * `fs-name <name>`. MEASURED: /etc/pve/storage.cfg on n2 holds exactly that — `cephfs:
42
+ * cephfs-tb4` / `content backup,vztmpl,iso` / `fs-name cephfs-tb4`. So a `Proxmox.Storage`
43
+ * declaring `cephfs-tb4` would be managing an entry this resource created. Declare ONE of them:
44
+ * either `add-storage` here and no storage resource, or `add-storage` off and a
45
+ * `Proxmox.Storage` that reads this resource's `name` attribute — which also gets the ordering
46
+ * right for free, because Alchemy orders by data flow.
47
+ *
48
+ * ⚠️ `node` IS A ROUTE, NOT IDENTITY, AND MUST NEVER BE DIFFED. A CephFS is cluster-wide; the node
49
+ * in the path only says which node PVE proxies the call to. MEASURED: `GET /nodes/n2/ceph/fs`,
50
+ * `…/n3/…` and `…/n4/…` returned the byte-identical array. So changing `node` plans `noop`, and
51
+ * two resources naming the same `name` on different nodes are the SAME filesystem — the vmid
52
+ * hazard from lxc.ts, with a filesystem on the end of it instead of a container.
53
+ *
54
+ * ⚠️ PRIVILEGES: the read lane needs `Sys.Audit` OR `Datastore.Audit` on `/`, which an auditor
55
+ * role already holds — unlike storage.ts, nothing needs widening before the first plan. The
56
+ * write side is in ceph-fs-wire.ts beside the calls, and in docs/privileges.md with the rest.
57
+ */
58
+ import { Resource } from 'alchemy';
59
+ import { isResolved } from 'alchemy/Diff';
60
+ import * as Provider from 'alchemy/Provider';
61
+ import * as Effect from 'effect/Effect';
62
+ import {
63
+ createForm,
64
+ createFs,
65
+ destroyFs,
66
+ notCreated,
67
+ notDestroyed,
68
+ objectPath,
69
+ readRow,
70
+ } from './ceph-fs-wire.ts';
71
+ import { type PveRequirements, type WithTarget, pveOperations } from './resource.ts';
72
+
73
+ export interface CephFsProps extends WithTarget {
74
+ /** ⚠️ Which node answers the call, NOT which node holds the filesystem. See the header. */
75
+ node: string;
76
+ /**
77
+ * The filesystem name, and the cluster-wide primary key. PVE's pattern is `^[^:/\s]+$`.
78
+ * ⛔ CHANGING IT IS A REPLACE, WHICH DESTROYS THE OLD FILESYSTEM. `diff` says so explicitly
79
+ * below rather than letting the factory report a create-shaped `update` and orphan the old one.
80
+ */
81
+ name: string;
82
+ /**
83
+ * Placement groups for the backing data pool; the metadata pool gets a quarter of it, or 8 when
84
+ * this is under 32. PVE's default is 128.
85
+ *
86
+ * ⛔ CREATE-TIME ONLY, UNREADABLE, AND OWNED BY THE AUTOSCALER FROM THE SECOND IT LANDS. It is
87
+ * never compared — see the ⛔ on `matches` — and this is not caution, it is measured. TB4's
88
+ * `cephfs-tb4` was created with the default 128 (the arithmetic proves it: `cephfs-tb4_metadata`
89
+ * sits at 32, which is 128/4), and `cephfs-tb4_data` reads pg_num 32 TODAY. The autoscaler moved
90
+ * it by a factor of four, and it is not finished: `pg_autoscale_mode` is `on` for every pool on
91
+ * this cluster and `cephfs-tb4_metadata` already reports `pg_num_final: 16` against its live 32.
92
+ */
93
+ pg_num?: number;
94
+ /**
95
+ * Also write a `cephfs` storage entry for this filesystem. ⚠️ Create-time only and never read
96
+ * back — it is an ACTION, not a field. See the header for what it does to `Proxmox.Storage`.
97
+ */
98
+ 'add-storage'?: boolean;
99
+ /** ⛔ DELETE-TIME ONLY, AND IT ERASES THE DATA. Read the ⛔ on `destroyFs` before setting it. */
100
+ 'remove-pools'?: boolean;
101
+ /**
102
+ * Delete the pveceph-managed storage entries too. ⚠️ PVE refuses unless they are already
103
+ * disabled ("storage '…' is not disabled, make sure to disable and unmount the storage first"),
104
+ * so this fails the destroy rather than unmounting anything out from under a running guest.
105
+ */
106
+ 'remove-storages'?: boolean;
107
+ }
108
+
109
+ /**
110
+ * ⚠️ EVERY FIELD HERE IS REPORTED AND NONE IS COMPARED. They exist so a plan, and the state entry
111
+ * behind it, can say what the declaration actually points at — which pools hold the bytes.
112
+ */
113
+ export interface CephFsAttributes {
114
+ name: string;
115
+ /** The first data pool. PVE keeps it for compatibility; `data_pools` is the real answer. */
116
+ data_pool: string;
117
+ /**
118
+ * ⚠️ SORTED, AND IT IS A SET RATHER THAN A LIST. A CephFS can hold several data pools — added
119
+ * with `ceph fs add_data_pool`, which this resource has no way to express — and PVE reports
120
+ * them in the MDS map's own order. Sorting is what keeps this attribute stable across reads.
121
+ */
122
+ data_pools: string;
123
+ metadata_pool: string;
124
+ /**
125
+ * ⚠️ AN INTEGER THAT NAMES SOMETHING WRITTEN AS A NAME, AND THEREFORE A CLASSIC FOREVER-DIFF IF
126
+ * IT EVER REACHED `matches`. PVE answers `metadata_pool_id: 7` while every write in this family
127
+ * spells a pool by name. It is here to be read, never to be compared. `-1` means absent: the
128
+ * field is optional in the schema and older PVE omits it.
129
+ */
130
+ metadata_pool_id: number;
131
+ }
132
+
133
+ export interface ProxmoxCephFs extends Resource<
134
+ 'Proxmox.CephFs',
135
+ CephFsProps,
136
+ CephFsAttributes,
137
+ never,
138
+ PveRequirements
139
+ > {}
140
+
141
+ /** ★ `retain` by default — a filesystem holding data cannot be rebuilt. See the ★ in resource.ts. */
142
+ export const ProxmoxCephFs = Resource<ProxmoxCephFs>('Proxmox.CephFs', {
143
+ defaultRemovalPolicy: 'retain',
144
+ });
145
+
146
+ const ops = pveOperations<CephFsProps, CephFsAttributes>({
147
+ attributes: readRow,
148
+ /** ⚠️ The `{name}` path — longer than `path()`, deliberately. See the second ⛔ in the header. */
149
+ collection: objectPath,
150
+ createForm,
151
+ /**
152
+ * ⛔ NOTHING IS COMPARED, AND THAT IS THE MOST DELIBERATE LINE IN THIS FILE. A CephFS has no
153
+ * readable, writable field: the index returns only the name and the pools behind it, and every
154
+ * prop this resource accepts is create-time or delete-time. So "present" IS "settled", and the
155
+ * only honest comparison is none at all.
156
+ * ⚠️ THE TEMPTING ADDITION IS `pg_num`, AND IT WOULD BE THE WORST BUG THIS PACKAGE HAS SHIPPED.
157
+ * It is not in the index at all, so it can only be read from the pool endpoint — where the
158
+ * autoscaler owns it. MEASURED on TB4 today: created at 128, live at 32, with the metadata
159
+ * pool already scheduled down to 16. Compared, that is an eternal mismatch; and because
160
+ * `updateForm` is undefined, the action it produces is REPLACE. The plan would destroy and
161
+ * rebuild a live filesystem on every deploy, triggered by a daemon on its own schedule.
162
+ * ⚠️ `data_pools` AND `metadata_pool_id` ARE OUT FOR THE ORDINARY REASONS: a set PVE returns
163
+ * in its own order, and an integer id for something written as a name. `name` is out because
164
+ * it is the FILTER that produced these attributes, so comparing it is true by construction —
165
+ * an identity change is handled in `diff`, not here. `node` is out because it is a route.
166
+ */
167
+ matches: () => true,
168
+ /** ⚠️ THE INDEX, NOT THE OBJECT. There is no GET on `{name}`; see the second ⛔ in the header. */
169
+ path: (props) => `nodes/${props.node}/ceph/fs`,
170
+ });
171
+
172
+ export const ProxmoxCephFsProvider = () =>
173
+ Provider.effect(
174
+ ProxmoxCephFs,
175
+ Effect.succeed(
176
+ ProxmoxCephFs.Provider.of({
177
+ /**
178
+ * ⛔ EMPTY, LIKE EVERY OTHER RESOURCE HERE. The index answers with every filesystem on the
179
+ * cluster, `cephfs-tb4` included — the one holding this estate's ISOs and templates.
180
+ * Adopting it would put Alchemy one `destroy` away from it. Adoption stays explicit.
181
+ */
182
+ list: () => Effect.succeed([]),
183
+ read: Effect.fn(function* ({ olds }) {
184
+ return yield* ops.read(olds);
185
+ }),
186
+ /**
187
+ * ⛔ A CHANGED `name` IS A REPLACE, AND THE FACTORY CANNOT SAY SO ALONE. Left to delegate,
188
+ * a rename reads the NEW name, finds nothing, and returns the drift-shaped `update` that
189
+ * `pveOperations.diff` uses for "Alchemy has state, the cluster does not" — so reconcile
190
+ * would build the new filesystem and LEAVE THE OLD ONE, with its pools, its storage entry
191
+ * and its bytes, owned by nobody and invisible in every later plan.
192
+ * ⛔ SAYING `replace` IS HONEST, AND IT IS ALSO WHAT MAKES A TYPO EXPENSIVE. Alchemy
193
+ * creates first and deletes second, so the old filesystem is destroyed AFTER the new
194
+ * one exists — with `remove-pools` unset the bytes are stranded rather than erased, and
195
+ * that default is the only thing between a mis-typed rename and an unrecoverable one.
196
+ * Do not "simplify" this back to the factory: the alternative is not safer, it is
197
+ * silent. ⚠️ `node` is NOT part of identity and must never join this comparison.
198
+ */
199
+ diff: Effect.fn(function* ({ news, output }) {
200
+ if (output !== undefined && isResolved(news) && news.name !== output.name) {
201
+ return { action: 'replace' } as const;
202
+ }
203
+ return yield* ops.diff(news, output);
204
+ }),
205
+ /**
206
+ * ⛔ NOT `ops.reconcile`, BECAUSE ITS READ-BACK RACES A FORKED WORKER. The POST returns a
207
+ * UPID and nothing else; `createFs` waits the task out and surfaces its real error. The
208
+ * read-back the factory does is kept here rather than dropped — waiting proves the task
209
+ * ENDED, reading proves the filesystem EXISTS, and PVE has shipped tasks that end OK
210
+ * having done nothing.
211
+ * ⚠️ AN EXISTING FILESYSTEM IS RETURNED UNTOUCHED. There is no update call to make, so the
212
+ * only write this handler can perform is a create; `diff` has already decided that a
213
+ * real change is a replace, and Alchemy calls delete for the old generation itself.
214
+ */
215
+ reconcile: Effect.fn(function* ({ news }) {
216
+ const live = yield* ops.read(news);
217
+ if (live !== undefined) return live;
218
+ yield* createFs(news);
219
+ const after = yield* ops.read(news);
220
+ if (after === undefined) return yield* Effect.die(notCreated(news));
221
+ return after;
222
+ }),
223
+ /**
224
+ * ⛔ NOT `ops.destroy`, AND NOT AN OVERSIGHT. It would DELETE `spec.path`, which on this
225
+ * family is the index — a route PVE does not implement — so every destroy would fail
226
+ * while the filesystem stayed exactly where it was. The read-back afterwards is what
227
+ * turns "the task said OK" into "it is actually gone": `destroyfs` refuses while a
228
+ * non-disabled `cephfs` storage still references the filesystem, and that refusal must
229
+ * fail the destroy rather than letting Alchemy drop the state entry for a live object.
230
+ */
231
+ delete: Effect.fn(function* ({ olds }) {
232
+ /**
233
+ * ⚠️ A FILESYSTEM SOMEBODY ALREADY REMOVED BY HAND IS NOT AN ERROR. `destroyfs` dies
234
+ * synchronously with "no such cephfs", which would fail every destroy of a stack whose
235
+ * filesystem was cleaned up outside Alchemy and leave the state entry unremovable
236
+ * without editing the store. ⚠️ THE COST IS STATED PLAINLY: `ops.read` folds a FAILED
237
+ * read into "absent" (the storage.ts trap), so a read this credential cannot perform
238
+ * turns this into a destroy that reports success having done nothing. That is not a
239
+ * new hazard — a broken read defeats the read-back below in exactly the same way — but
240
+ * it is the reason a 403 on this family must be fixed rather than lived with.
241
+ */
242
+ if ((yield* ops.read(olds)) === undefined) return;
243
+ yield* destroyFs(olds);
244
+ const after = yield* ops.read(olds);
245
+ if (after !== undefined) return yield* Effect.die(notDestroyed(olds));
246
+ }),
247
+ }),
248
+ ),
249
+ );
@@ -0,0 +1,193 @@
1
+ /**
2
+ * The CRUSH tree, walked down to one OSD leaf.
3
+ *
4
+ * ★ SPLIT OUT OF ceph-osd.ts TO KEEP BOTH FILES UNDER THE 250-LINE CAP, and the seam is the same
5
+ * one metric-server-form.ts cuts: this file answers "what does the cluster say about osd.N",
6
+ * ceph-osd.ts answers "what is an OSD and what may a plan do about it". Nothing here calls the
7
+ * API and nothing here decides a diff. `CephOsdAttributes` lives on this side of the seam
8
+ * because it is a description of what the TREE says — keeping each field's
9
+ * reported-never-compared reasoning against the line that builds it — and ceph-osd.ts re-exports
10
+ * it so a consumer still imports one name from one place. The `import type` back for
11
+ * `CephOsdProps` is a cycle on paper only: it is erased before anything runs.
12
+ *
13
+ * ⛔ `GET /nodes/{node}/ceph/osd` ANSWERS A TREE, NOT A LIST, AND IT IS THE WHOLE CLUSTER'S TREE.
14
+ * MEASURED 2026-09-13: asked of n2 it answers `{"flags":…,"root":{"children":[…]}}` whose
15
+ * leaves include osd.0 and osd.5 on n4; asked of n3 it answers the identical six leaves. The
16
+ * `{node}` in the path chooses WHO ANSWERS, not what is listed. A provider that read it as "the
17
+ * OSDs on this node" would find osd.0 through n2 and then report it as living there.
18
+ *
19
+ * ⛔ AND THE SINGLE-OBJECT PATH CANNOT BE USED INSTEAD — THIS WALK EXISTS BECAUSE IT LIES.
20
+ * MEASURED: `GET /nodes/n2/ceph/osd/99`, an id with no OSD behind it, answers HTTP 200 with
21
+ * `[{"name":"metadata"},{"name":"lv-info"}]` — byte for byte what `…/osd/2` answers for a real
22
+ * one. It is a directory index (its schema says `permissions: {"user":"all"}`), not a read of
23
+ * the OSD. Point `PveSpec.path` at it and every declared OSD is "present": `diff` reports
24
+ * `noop` over a dead replica forever, and `reconcile`'s read-back guard — the one thing that
25
+ * stops this package recording objects that do not exist — passes on an OSD that was never
26
+ * built. The only endpoint that can tell present from absent is the tree.
27
+ *
28
+ * ★ MEASURED ON TB4 ON 2026-09-13, `GET /nodes/n2/ceph/osd`. Six leaves, all `up` and `in`, Ceph
29
+ * 20.2.2 tentacle:
30
+ *
31
+ * id name host device_class crush_weight reweight pgs status
32
+ * 0 osd.0 n4 ssd 1.86299133300781 1 99 up
33
+ * 5 osd.5 n4 ssd 1.86299133300781 1 94 up
34
+ * 1 osd.1 n3 ssd 1.86299133300781 1 96 up
35
+ * 4 osd.4 n3 ssd 1.81939697265625 1 97 up
36
+ * 2 osd.2 n2 ssd 1.81939697265625 1 88 up
37
+ * 3 osd.3 n2 ssd 1.81939697265625 1 105 up
38
+ *
39
+ * `{ node: 'n2', osdid: 2, host: 'n2', device_class: 'ssd' }` plans `noop` against that row.
40
+ * That is the acceptance test this file was written to pass.
41
+ *
42
+ * ⚠️ EVERY OTHER COLUMN IS REPORTED AND NEVER COMPARED, FOR FOUR DIFFERENT REASONS. `pgs` is the
43
+ * autoscaler's placement rather than anybody's declaration — MEASURED, it spans 88 to 105 across
44
+ * six identical disks, and Ceph moves PGs whenever it rebalances. `crush_weight` is a FLOAT
45
+ * derived from device size (MEASURED: 1.86299133300781), so comparing it is float equality
46
+ * against a number Ceph recomputes on `crush reweight`. `reweight` is what
47
+ * `reweight-by-utilization` writes, unasked. `status` and `in` are daemon state: an OSD that
48
+ * flaps down while a node reboots would otherwise put work in the plan that no write this
49
+ * provider owns could ever clear. Each of those is the forever-update this package has already
50
+ * been bitten by, and each is out of `settled` below.
51
+ *
52
+ * ⚠️ THE CLUSTER'S OSD FLAGS ARE NOT AN ATTRIBUTE EITHER. The read carries
53
+ * `"flags":"sortbitwise,recovery_deletes,purged_snapdirs,pglog_hardlimit"` NEXT TO the root
54
+ * rather than inside any leaf: they describe the OSDMap, not this OSD. Recording them per-OSD
55
+ * would rewrite all six resources' state the moment somebody set `noout` for ten minutes of
56
+ * maintenance — the churn-that-reads-like-drift that keeps `digest` out of storage.ts.
57
+ *
58
+ */
59
+ import * as Effect from 'effect/Effect';
60
+ import type { CephOsdProps } from './ceph-osd.ts';
61
+ import { pve } from './client.ts';
62
+ import { bool, int, num, text } from './values.ts';
63
+
64
+ /** A CRUSH node. Buckets carry `children`; an OSD leaf carries `type: 'osd'`. */
65
+ type CrushNode = Record<string, unknown>;
66
+
67
+ const branches = (node: CrushNode): CrushNode[] =>
68
+ (Array.isArray(node['children']) ? node['children'] : []).filter(
69
+ (child): child is CrushNode => typeof child === 'object' && child !== null,
70
+ );
71
+
72
+ /**
73
+ * Every OSD leaf under the tree root, at any depth.
74
+ *
75
+ * ⚠️ RECURSIVE BECAUSE THE TREE IS NOT THREE LEVELS DEEP BY LAW. TB4 measures as
76
+ * root(`default`) -> host(n2|n3|n4) -> osd, but CRUSH admits datacenter, rack and chassis
77
+ * buckets between them, and an OSD created outside a host bucket hangs off the root. Reaching
78
+ * in as `root.children[].children[]` would read this cluster correctly and answer "absent" —
79
+ * i.e. "the replica is gone" — on the first cluster that has a rack in it.
80
+ *
81
+ * ⚠️ THE OUTERMOST OBJECT IS NOT ITSELF A BUCKET. MEASURED: `root` is `{"children":[…],"leaf":0}`
82
+ * with no `type` and no `name`; the bucket called `default` is its first child. So the walk
83
+ * starts at `tree.root` and selects on `type === 'osd'` rather than assuming a shape.
84
+ */
85
+ export const osdLeaves = (tree: CrushNode): CrushNode[] => {
86
+ const root = tree['root'];
87
+ const start: CrushNode = typeof root === 'object' && root !== null ? (root as CrushNode) : {};
88
+ const walk = (node: CrushNode): CrushNode[] => [
89
+ ...(node['type'] === 'osd' ? [node] : []),
90
+ ...branches(node).flatMap(walk),
91
+ ];
92
+ return walk(start);
93
+ };
94
+
95
+ /**
96
+ * The leaf for one OSD id, or undefined when nothing in the tree carries it.
97
+ *
98
+ * ⛔ `id` COMES BACK AS A STRING HERE AND AS AN INTEGER EVERYWHERE ELSE, which is the "an id
99
+ * returned one way and written another" forever-diff in its most expensive form. MEASURED: the
100
+ * leaf for osd.2 carries `"id":"2"` (and the host buckets carry `"id":"-7"`), while
101
+ * `GET …/osd/2/metadata` carries `"id":2` and the `osdid` parameter on POST and DELETE is typed
102
+ * integer. `'2' === 2` is false, so a provider comparing the raw value finds NOTHING, calls
103
+ * every declared OSD absent — and, with a create path wired up, offers to build six OSDs over
104
+ * six live disks. `int` from values.ts is the narrowing, and `Number.NaN` is the fallback
105
+ * precisely because it equals nothing, id 0 included.
106
+ *
107
+ * ⚠️ FIRST MATCH WINS. OSD ids are unique cluster-wide, and PVE does not put the per-device-class
108
+ * shadow buckets (`default~ssd`) in this tree — MEASURED: the whole tree is one `default` root,
109
+ * three hosts, six leaves. If a future PVE did include them, taking the first match keeps the
110
+ * answer stable instead of reporting drift depending on which copy was walked into first.
111
+ */
112
+ export const findOsd = (tree: CrushNode, osdid: number) =>
113
+ osdLeaves(tree).find((leaf) => int(leaf['id'], Number.NaN) === osdid);
114
+
115
+ export interface CephOsdAttributes {
116
+ /** Normalised to an integer here even though the tree spells it `"2"`. */
117
+ osdid: number;
118
+ /** `osd.2` — how Ceph, the UI and every log line name it. */
119
+ name: string;
120
+ /** COMPARED, but only when `props.host` is declared. */
121
+ host: string;
122
+ /** COMPARED, but only when `props.device_class` is declared. */
123
+ device_class: string;
124
+ /**
125
+ * ⚠️ REPORTED, NEVER COMPARED — `bluestore` on every live OSD and filestore has been gone for
126
+ * years, so declaring it would add a field that cannot disagree. It is recorded because a
127
+ * future objectstore change is exactly the kind of thing state should have caught in writing.
128
+ */
129
+ osdtype: string;
130
+ /** ⚠️ REPORTED, NEVER COMPARED. Daemon state — see the second ⚠️ in the header. */
131
+ status: string;
132
+ /** ⚠️ REPORTED, NEVER COMPARED. Ceph itself sets this to 0 after `mon_osd_down_out_interval`. */
133
+ in: boolean;
134
+ /** ⚠️ REPORTED, NEVER COMPARED. A float Ceph recomputes; comparing it is a permanent diff. */
135
+ crush_weight: number;
136
+ /** ⚠️ REPORTED, NEVER COMPARED. `reweight-by-utilization` owns this number, not a declaration. */
137
+ reweight: number;
138
+ /** ⚠️ REPORTED, NEVER COMPARED. The autoscaler moves PGs between OSDs whenever it likes. */
139
+ pgs: number;
140
+ /** Device capacity in bytes. Stable per disk, and the honest answer to "how big is this OSD". */
141
+ total_space: number;
142
+ /** `20.2.2`. ⚠️ Reported, never compared — an upgrade is not this resource's drift. */
143
+ ceph_version: string;
144
+ }
145
+
146
+ /**
147
+ * One leaf, as the attributes a plan and the state store get to see.
148
+ *
149
+ * ⚠️ THE TELEMETRY IS DELIBERATELY DROPPED, NOT FORGOTTEN. The leaf also carries `bytes_used`,
150
+ * `percent_used`, `apply_latency_ms` and `commit_latency_ms`. MEASURED, AND THE MEASUREMENT SAYS
151
+ * THE OPPOSITE OF WHAT YOU WOULD EXPECT: across two reads minutes apart, all four were identical
152
+ * on every OSD, and both latencies read 0 — TB4 was idle, so nothing was caught in the act. That
153
+ * these move is therefore REASONED, from what they count, not observed. Alchemy persists
154
+ * attributes; on a cluster doing work, keeping them would rewrite all six resources' state on
155
+ * every deploy and record a number that is stale by the time anybody reads it. What is kept is
156
+ * what a human asking "what is this replica, and is it healthy" needs; it is not a metrics feed.
157
+ *
158
+ * ⚠️ `osdid` AND `name` ARE THE NORMALISED ID AND ITS SPELLING, taken from the leaf rather than
159
+ * echoed from props, so that state records what the cluster said. They agree by construction
160
+ * here — the leaf was found BY id — but echoing props into attributes is how a resource ends up
161
+ * recording a declaration as though it were an observation.
162
+ */
163
+ export const osdAttributes = (leaf: CrushNode, props: CephOsdProps): CephOsdAttributes => ({
164
+ ceph_version: text(leaf['ceph_version_short']),
165
+ crush_weight: num(leaf['crush_weight'], 0),
166
+ device_class: text(leaf['device_class']),
167
+ host: text(leaf['host']),
168
+ in: bool(leaf['in']),
169
+ name: text(leaf['name'], `osd.${String(props.osdid)}`),
170
+ osdid: int(leaf['id'], props.osdid),
171
+ osdtype: text(leaf['osdtype']),
172
+ pgs: num(leaf['pgs'], 0),
173
+ reweight: num(leaf['reweight'], 0),
174
+ status: text(leaf['status']),
175
+ total_space: num(leaf['total_space'], 0),
176
+ });
177
+
178
+ /**
179
+ * The live OSD as attributes, or undefined.
180
+ *
181
+ * ★ IT LIVES HERE RATHER THAN IN ceph-osd.ts BECAUSE THE READ SIDE IS THIS FILE'S WHOLE JOB —
182
+ * `findOsd` and `osdAttributes` are the two halves it composes, and keeping the caller beside
183
+ * them is what let ceph-osd.ts come back under the 250-line cap when create and delete were
184
+ * added. ⚠️ A 404 or a 403 both arrive here as `undefined`: see the ⛔ on `readRole` in
185
+ * resource.ts for why that distinction is invisible and what it costs.
186
+ */
187
+ export const readOsd = (props: CephOsdProps) =>
188
+ pve<Record<string, unknown>>(props.target, 'read', 'GET', `nodes/${props.node}/ceph/osd`).pipe(
189
+ Effect.map((t) => {
190
+ const leaf = t === undefined ? undefined : findOsd(t, props.osdid);
191
+ return leaf === undefined ? undefined : osdAttributes(leaf, props);
192
+ }),
193
+ );
@@ -0,0 +1,76 @@
1
+ /**
2
+ * The two operations that change a disk: building an OSD and removing one.
3
+ *
4
+ * ★ SPLIT OUT OF ceph-osd.ts FOR THE 250-LINE CAP, and the seam is the honest one — this file is
5
+ * everything that WRITES, and ceph-osd.ts is the declaration and the assertion. A reader asking
6
+ * "can this destroy my data" has one file to read.
7
+ *
8
+ * ⛔ BOTH OF THESE ARE IRREVERSIBLE. `create` hands a block device to `ceph-volume lvm create`,
9
+ * which zaps it; `delete` takes a replica out of a live pool and Ceph immediately begins
10
+ * rewriting the missing copies across the remaining nodes. `Proxmox.CephOsd` therefore declares
11
+ * `defaultRemovalPolicy: 'retain'`, so removing a declaration does NOT reach `destroyOsd` — a
12
+ * caller opts in with `.pipe(RemovalPolicy.destroy())`.
13
+ */
14
+ import * as Effect from 'effect/Effect';
15
+ import { readOsd } from './ceph-osd-tree.ts';
16
+ import type { CephOsdProps } from './ceph-osd.ts';
17
+ import { pve } from './client.ts';
18
+
19
+ const describe = (props: CephOsdProps) => `osd.${String(props.osdid)} on ${props.node}`;
20
+
21
+ export const createOsd = Effect.fn(function* (news: CephOsdProps) {
22
+ if (news.dev === undefined) {
23
+ return yield* Effect.die(
24
+ new Error(
25
+ `${describe(news)}: no such OSD in the CRUSH tree, and no \`dev\` was declared. ` +
26
+ 'Without a device this resource is an ASSERTION over an OSD that must already ' +
27
+ 'exist. Declare `dev` to let it create one -- which WIPES that device -- or fix ' +
28
+ 'the declaration; if the disk was there yesterday, a replica is missing and Ceph ' +
29
+ 'is degraded.',
30
+ ),
31
+ );
32
+ }
33
+ yield* pve(news.target, 'provision', 'POST', `nodes/${news.node}/ceph/osd`, {
34
+ dev: news.dev,
35
+ ...(news.device_class === undefined ? {} : { 'crush-device-class': news.device_class }),
36
+ });
37
+ const created = yield* readOsd(news);
38
+ if (created === undefined) {
39
+ return yield* Effect.die(
40
+ new Error(
41
+ `${describe(news)}: the create returned no error but osd.${String(news.osdid)} is ` +
42
+ 'still absent. Ceph allocates OSD ids and cannot be told which to use, so it most ' +
43
+ 'likely assigned a different number -- check `ceph osd tree`, and declare the id it ' +
44
+ 'actually gave the device rather than deploying again.',
45
+ ),
46
+ );
47
+ }
48
+ return created;
49
+ });
50
+
51
+ /**
52
+ * ⛔ REMOVING THE DECLARATION REMOVES THE ASSERTION, NOT THE REPLICA, AND THAT ASYMMETRY IS THE
53
+ * POINT. `DELETE /nodes/{node}/ceph/osd/{osdid}` takes a replica out of a live pool — with
54
+ * `nobackfill`/`norecover` unset, Ceph immediately starts rewriting the missing copies across
55
+ * the two remaining nodes — and with `cleanup=1` it zaps the LVs as well. A stack file is
56
+ * edited by people in a hurry; a disk is not a thing a deleted line gets to destroy. Take an
57
+ * OSD out by hand, with the cluster healthy and `ceph -s` in front of you: `ceph osd out N`,
58
+ * wait for the backfill to finish, then `pveceph osd destroy N`.
59
+ *
60
+ * ★ SO IT IS IMPLEMENTED, AND THE RESOURCE DEFAULTS TO `retain` INSTEAD. Removing a declaration
61
+ * drops the state row and leaves the replica alone; a caller who really means it opts in with
62
+ * `.pipe(RemovalPolicy.destroy())`. That is the same shape Terraform gives `prevent_destroy`,
63
+ * and it beats a `delete` that silently does nothing — which is a lie to anyone reading the
64
+ * plan.
65
+ *
66
+ * ⚠️ `cleanup` DEFAULTS TO FALSE, so the logical volumes survive and the disk can be re-added
67
+ * without a rebuild. Ceph still starts backfilling the missing copies the moment the OSD goes.
68
+ */
69
+ export const destroyOsd = (olds: CephOsdProps) =>
70
+ pve(
71
+ olds.target,
72
+ 'provision',
73
+ 'DELETE',
74
+ `nodes/${olds.node}/ceph/osd/${String(olds.osdid)}`,
75
+ olds.cleanup === true ? { cleanup: '1' } : {},
76
+ );