@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,213 @@
1
+ /**
2
+ * `Proxmox.BackupJob` — the vzdump schedule, declared. The one object whose drift nothing else on
3
+ * the cluster reveals.
4
+ *
5
+ * ★ WHY THIS ONE EARNS ITS PLACE. A hand-made vzdump job drifts silently: a schedule moved by an
6
+ * hour, a retention trimmed to `keep-last=1`, or `enabled 0` set during a maintenance window and
7
+ * never set back. All three look like a healthy cluster from every angle except a restore.
8
+ * Declared here, `diff` reads the LIVE job on every plan, so a hand edit surfaces as
9
+ * `1 to update` that day rather than on the day somebody needs last night's backup and finds a
10
+ * week-old one, or none.
11
+ *
12
+ * ⛔ PVE GENERATES `id` WHEN YOU OMIT IT, WHICH MAKES OMITTING IT THE WORST BUG THIS RESOURCE
13
+ * COULD CARRY. PVE's own API schema says so in one parenthesis — `id`: "Job ID (will be
14
+ * autogenerated)." — and a generated id looks like `backup-f56d8ab7-d524`. A resource that
15
+ * POSTed without an id would create a SECOND job beside the first on EVERY reconcile, and the
16
+ * read-back in `pveOperations` would still pass, because a job did appear: that check asks "is
17
+ * the object there", not "is it the one I meant". So `id` is a REQUIRED prop and is always sent
18
+ * on create. To adopt a job that already exists, declare its existing id; any other id is a new
19
+ * job beside it, not an adoption.
20
+ * ★ AN EXPLICIT id ALSO CLOSES A SECOND HOLE. `pveOperations.read` turns ANY failed GET into
21
+ * "absent" — an expired lease, a 403, a node that did not answer — and reconcile then POSTs.
22
+ * With an id, PVE refuses a duplicate section id and the deploy fails loudly; without one, that
23
+ * same swallowed error would have quietly doubled the cluster's backup jobs.
24
+ *
25
+ * ⚠️ `Sys.Modify` ON `/` IS THE GATE AND IT IS NOT A SMALL GRANT. PVE has no Backup.* privilege:
26
+ * create, update and delete of a vzdump job are all checked against `Sys.Modify` at `/` — the
27
+ * same privilege that covers node network configuration. A credential that can declare backup
28
+ * jobs can also rewrite the cluster's networking. That is PVE's granularity rather than a choice
29
+ * made here, and it is exactly why widening a role for this should be a decision rather than a
30
+ * reaction to a 403.
31
+ */
32
+ import { Resource } from 'alchemy';
33
+ import * as Provider from 'alchemy/Provider';
34
+ import * as Effect from 'effect/Effect';
35
+ import { type PveRequirements, type WithTarget, pveHandlers } from './resource.ts';
36
+ import { bool, guestList, propertyString, text } from './values.ts';
37
+
38
+ /**
39
+ * ⚠️ THE PROPS ARE PVE'S OWN KEY NAMES, DASHES INCLUDED. `notes-template` rather than
40
+ * `notesTemplate` is deliberate: `createForm` is then a copy rather than a translation table,
41
+ * and a translation table is one more place for a key to be renamed and silently never sent. A
42
+ * field that is never sent reads back as whatever it already was, which is indistinguishable
43
+ * from "PVE ignored me".
44
+ */
45
+ export interface BackupJobProps extends WithTarget {
46
+ /** ⛔ The primary key, REQUIRED. Never let PVE generate it — see the ⛔ above. */
47
+ id: string;
48
+ /** A subset of systemd calendar events, e.g. `01:00` or `mon..fri 02:30`. */
49
+ schedule: string;
50
+ /** Target storage id. That storage's own retention applies unless `prune-backups` overrides it. */
51
+ storage?: string;
52
+ mode?: 'snapshot' | 'suspend' | 'stop';
53
+ /** PVE's default is enabled. A disabled job is a backup that silently is not happening. */
54
+ enabled?: boolean;
55
+ /** Back up every guest. ⚠️ Pick exactly one of `all`, `vmid` and `pool` — see `shape` below. */
56
+ all?: boolean;
57
+ vmid?: readonly number[];
58
+ pool?: string;
59
+ /** Guest ids to skip. PVE reads this as "all guests except these", so it belongs with `all`. */
60
+ exclude?: readonly number[];
61
+ /** Retention, as a property string: `keep-daily=7,keep-weekly=4`. */
62
+ 'prune-backups'?: string;
63
+ /** Supports {{cluster}}, {{guestname}}, {{node}} and {{vmid}}. Single line only. */
64
+ 'notes-template'?: string;
65
+ 'notification-mode'?: 'auto' | 'legacy-sendmail' | 'notification-system';
66
+ /** Run a missed job as soon as the scheduler is back. */
67
+ 'repeat-missed'?: boolean;
68
+ /** VM-only fleecing, as a property string: `enabled=1,storage=<id>`. See `propertyString`. */
69
+ fleecing?: string;
70
+ comment?: string;
71
+ }
72
+
73
+ export interface BackupJobAttributes {
74
+ id: string;
75
+ schedule: string;
76
+ storage: string;
77
+ mode: string;
78
+ enabled: boolean;
79
+ all: boolean;
80
+ /** Sorted and comma-joined, so re-ordering a declaration is not a diff. See `guestList`. */
81
+ vmid: string;
82
+ pool: string;
83
+ exclude: string;
84
+ /** Flattened and sorted — never the raw value PVE returned. See `propertyString`. */
85
+ 'prune-backups': string;
86
+ 'notes-template': string;
87
+ 'notification-mode': string;
88
+ 'repeat-missed': boolean;
89
+ fleecing: string;
90
+ comment: string;
91
+ /**
92
+ * PVE's own estimate, epoch seconds. REPORTED, NEVER COMPARED: `next-run` in `matches` would
93
+ * report an update every time the clock passed a scheduled run. It is here so a plan can show
94
+ * that a job is still due — a `next-run` of 0 is a job the scheduler will not run.
95
+ */
96
+ 'next-run': number;
97
+ }
98
+
99
+ export interface ProxmoxBackupJob extends Resource<
100
+ 'Proxmox.BackupJob',
101
+ BackupJobProps,
102
+ BackupJobAttributes,
103
+ never,
104
+ PveRequirements
105
+ > {}
106
+
107
+ export const ProxmoxBackupJob = Resource<ProxmoxBackupJob>('Proxmox.BackupJob');
108
+
109
+ /**
110
+ * The form for both create and update.
111
+ *
112
+ * ⛔ A FIELD THE DECLARATION LEAVES OUT IS NOT SENT, AND THAT IS LOAD BEARING. PVE's PUT can only
113
+ * SET a key; clearing one needs `delete=<key>`, which this resource deliberately never sends.
114
+ * `updateForm` is handed the props and nothing else, so a `delete` list could only be "every
115
+ * field you did not declare" — and on a job adopted out of the UI that would silently strip
116
+ * settings this resource does not even model, `compress`, `mailto` and `bwlimit` among them, on
117
+ * the first deploy. Leaving undeclared fields alone is the smaller lie, and `matches` tells the
118
+ * same lie consistently: it does not compare what it cannot change.
119
+ *
120
+ * ⚠️ THE FIVE FIELDS PVE DOCUMENTS A DEFAULT FOR ARE ALWAYS SENT AND ALWAYS COMPARED — `enabled`
121
+ * (1), `all` (0), `mode` (snapshot), `notification-mode` (auto), `repeat-missed` (0). That is
122
+ * what makes a hand-flipped `enabled 0` show up as drift rather than as silence, and it is also
123
+ * what stops those five looping: a field compared against a default must be sent as that
124
+ * default, or every plan reports an update the PUT never performs. The cost is worth reading
125
+ * before the first deploy against an adopted job — an omitted `all` is a declaration that this
126
+ * job backs up NOTHING, and PVE accepts a job that selects nothing without a word. Read that
127
+ * first plan; do not deploy it unseen.
128
+ */
129
+ const shape = (props: BackupJobProps) => ({
130
+ all: props.all === true ? '1' : '0',
131
+ enabled: props.enabled === false ? '0' : '1',
132
+ mode: props.mode ?? 'snapshot',
133
+ 'notification-mode': props['notification-mode'] ?? 'auto',
134
+ 'repeat-missed': props['repeat-missed'] === true ? '1' : '0',
135
+ schedule: props.schedule,
136
+ ...(props.comment === undefined ? {} : { comment: props.comment }),
137
+ ...(props.exclude === undefined ? {} : { exclude: guestList(props.exclude) }),
138
+ ...(props.fleecing === undefined ? {} : { fleecing: props.fleecing }),
139
+ ...(props['notes-template'] === undefined ? {} : { 'notes-template': props['notes-template'] }),
140
+ ...(props.pool === undefined ? {} : { pool: props.pool }),
141
+ ...(props['prune-backups'] === undefined ? {} : { 'prune-backups': props['prune-backups'] }),
142
+ ...(props.storage === undefined ? {} : { storage: props.storage }),
143
+ ...(props.vmid === undefined ? {} : { vmid: guestList(props.vmid) }),
144
+ });
145
+
146
+ const handlers = pveHandlers<BackupJobProps, BackupJobAttributes>({
147
+ /**
148
+ * ⛔ NO "IS IT REALLY THERE" GUARD, ON PURPOSE. Returning undefined for a job whose JSON is
149
+ * missing some key would be a guess about which keys PVE echoes back, and a wrong guess here
150
+ * does not read as absent-and-harmless: `reconcile` would POST, and POST creates ANOTHER job.
151
+ * Absence is decided by the API declining to answer, which `pveOperations.read` already
152
+ * handles — not by a key being missing from an answer that did arrive.
153
+ */
154
+ attributes: (live, props) => ({
155
+ all: bool(live['all'], false),
156
+ comment: text(live['comment'], ''),
157
+ enabled: bool(live['enabled'], true),
158
+ exclude: guestList(live['exclude']),
159
+ fleecing: propertyString(live['fleecing'], 'enabled'),
160
+ id: props.id,
161
+ mode: text(live['mode'], 'snapshot'),
162
+ 'next-run': typeof live['next-run'] === 'number' ? live['next-run'] : 0,
163
+ 'notes-template': text(live['notes-template'], ''),
164
+ 'notification-mode': text(live['notification-mode'], 'auto'),
165
+ pool: text(live['pool'], ''),
166
+ 'prune-backups': propertyString(live['prune-backups']),
167
+ 'repeat-missed': bool(live['repeat-missed'], false),
168
+ schedule: text(live['schedule'], ''),
169
+ storage: text(live['storage'], ''),
170
+ vmid: guestList(live['vmid']),
171
+ }),
172
+ collection: () => 'cluster/backup',
173
+ /** ⛔ `id` IS SENT AND IS NOT OPTIONAL. Everything above depends on PVE not inventing one. */
174
+ createForm: (props) => ({ ...shape(props), id: props.id }),
175
+ /** Each line reads "not declared, or equal"; the five defaulted fields have no undeclared case. */
176
+ matches: (attributes, props) =>
177
+ attributes.schedule === props.schedule &&
178
+ attributes.enabled === (props.enabled !== false) &&
179
+ attributes.all === (props.all === true) &&
180
+ attributes.mode === (props.mode ?? 'snapshot') &&
181
+ attributes['notification-mode'] === (props['notification-mode'] ?? 'auto') &&
182
+ attributes['repeat-missed'] === (props['repeat-missed'] === true) &&
183
+ (props.storage === undefined || attributes.storage === props.storage) &&
184
+ (props.pool === undefined || attributes.pool === props.pool) &&
185
+ (props.comment === undefined || attributes.comment === props.comment) &&
186
+ (props.vmid === undefined || attributes.vmid === guestList(props.vmid)) &&
187
+ (props.exclude === undefined || attributes.exclude === guestList(props.exclude)) &&
188
+ (props['notes-template'] === undefined ||
189
+ attributes['notes-template'] === props['notes-template']) &&
190
+ (props['prune-backups'] === undefined ||
191
+ attributes['prune-backups'] === propertyString(props['prune-backups'])) &&
192
+ (props.fleecing === undefined ||
193
+ attributes.fleecing === propertyString(props.fleecing, 'enabled')),
194
+ path: (props) => `cluster/backup/${props.id}`,
195
+ updateForm: shape,
196
+ });
197
+
198
+ /**
199
+ * ⛔ Empty for the same reason as every other resource here: adoption must be explicit.
200
+ * `GET /cluster/backup` would hand back the job somebody made by hand, and adopting it
201
+ * implicitly would give Alchemy the right to DELETE a cluster's only backup schedule the
202
+ * first time a line left a stack file.
203
+
204
+ *
205
+ * ⚠️ DELETING THE JOB DOES NOT DELETE THE BACKUPS, AND THAT CUTS BOTH WAYS. The archives
206
+ * stay on the storage, so this looks harmless today; it is destructive in six months,
207
+ * when the schedule that should have replaced them has not run since. PVE will not
208
+ * refuse this the way it refuses to delete a pool still holding guests — nothing depends
209
+ * on a backup job — so the plan line is the only warning anyone gets.
210
+
211
+ */
212
+ export const ProxmoxBackupJobProvider = () =>
213
+ Provider.effect(ProxmoxBackupJob, Effect.succeed(ProxmoxBackupJob.Provider.of(handlers)));
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Where a Ceph daemon lives on the API, and what a declaration of one looks like as a PVE form.
3
+ *
4
+ * ★ SPLIT OUT OF ceph-daemon.ts TO KEEP BOTH FILES UNDER THE 250-LINE CAP, and the seam is a real
5
+ * one rather than a convenient line number — the same seam metric-server-form.ts cuts on.
6
+ * This file answers "how does a declaration address ONE daemon" — which URL, which body, and
7
+ * which row of the collection's answer is it. ceph-daemon.ts answers "what does the cluster
8
+ * say about that daemon, and when has it changed". Nothing here calls the API and nothing
9
+ * here decides a diff; `findRow` picks a row out of an answer its caller already fetched.
10
+ *
11
+ * ⛔ THE TWO PATHS ARE DIFFERENT AND THAT IS THE WHOLE REASON THIS FILE HAS TWO OF THEM. MEASURED
12
+ * on n2, 2026-09-13: there is no GET on the id path — `pvesh get /nodes/n2/ceph/mon/n2` answers
13
+ * "No 'get' handler defined" — while POST and DELETE are registered ONLY there. So the read uses
14
+ * `collectionPath` and the two writes use `daemonPath`, and swapping either one is a silent
15
+ * 501: a create that never lands, or a destroy that reports failure while the daemon runs on.
16
+ *
17
+ * ⚠️ THE `import type` BACK TO ceph-daemon.ts IS A CYCLE ON PAPER ONLY — type-only, so it is
18
+ * erased before anything runs. `CephDaemonProps` stays the resource's public shape, in the file
19
+ * that declares the resource.
20
+ *
21
+ * ⛔ POST AND DELETE RETURN A UPID, NOT AN OBJECT — both are `protected` task endpoints whose
22
+ * schema `returns` is a bare string. REASONED, NOT MEASURED: this cluster is read-only to me, so
23
+ * no create was ever run. The consequence to expect is that `reconcile` reads back immediately
24
+ * after the POST, and if the worker has not yet written ceph.conf the read-back still says
25
+ * absent and reconcile dies with "the write returned no error but the object is still absent".
26
+ * That refusal is honest — nothing is recorded for an object that is not there — and a second
27
+ * deploy converges once the task has finished, but it is a confusing FIRST-RUN failure. Watch
28
+ * the task in the PVE UI before concluding the create failed.
29
+ *
30
+ */
31
+ import type { CephDaemonProps } from './ceph-daemon.ts';
32
+ import { flag } from './values.ts';
33
+
34
+ /**
35
+ * PVE defaults the id to the nodename for all three kinds, and that default is resolved HERE
36
+ * rather than left to PVE, because the id is a PATH SEGMENT: create, delete and the row lookup in
37
+ * ceph-daemon.ts all need the same string, and a default resolved on the far side would leave
38
+ * three call sites guessing. Live on this cluster every daemon's name equals its node.
39
+ */
40
+ export const daemonId = (props: CephDaemonProps) => props.name ?? props.node;
41
+
42
+ /**
43
+ * The read path. ⚠️ `{node}` HERE IS ONLY THE NODE BEING ASKED, not a filter. MEASURED:
44
+ * `GET /nodes/n2/ceph/mon` returns n2, n3 AND n4, and `GET /nodes/n3/ceph/mon` returns the same
45
+ * three IN A DIFFERENT ORDER — [n4,n2,n3] from n2 against [n3,n2,n4] from n3. A caller that took
46
+ * row zero, or assumed the list was this node's daemons, would be wrong on both counts.
47
+ */
48
+ export const collectionPath = (props: CephDaemonProps) => `nodes/${props.node}/ceph/${props.kind}`;
49
+
50
+ /** The write path: POST creates here, DELETE destroys here. Never read from — see the ⛔ above. */
51
+ export const daemonPath = (props: CephDaemonProps) => `${collectionPath(props)}/${daemonId(props)}`;
52
+
53
+ /**
54
+ * The create body, which is EMPTY for a mgr and nearly empty for the other two.
55
+ *
56
+ * ⚠️ THE ID IS NOT SENT, because it is already the last segment of the path being POSTed to — the
57
+ * same reasoning as metric-server.ts, where a second copy of the key can only disagree with the
58
+ * first. PVE's schema does accept `monid`/`id`/`name` as body parameters; the path wins, so
59
+ * sending one would be a second source of truth for the object's identity and nothing else.
60
+ *
61
+ * ⚠️ AND THE BRANCHES CANNOT BE ONE SHARED BODY. `hotstandby` is an mds parameter and
62
+ * `mon-address` a mon one; each POST schema lists only its own, so the wrong field is a 400
63
+ * rather than an ignored hint. Both are create-only — the ⛔s on those props say why neither is
64
+ * ever compared afterwards.
65
+ */
66
+ export const createForm = (props: CephDaemonProps): Record<string, string> => {
67
+ if (props.kind === 'mon' && props['mon-address'] !== undefined) {
68
+ return { 'mon-address': props['mon-address'] };
69
+ }
70
+ const hotstandby = props.kind === 'mds' ? flag(props.hotstandby) : undefined;
71
+ return hotstandby === undefined ? {} : { hotstandby };
72
+ };
73
+
74
+ /**
75
+ * ⚠️ THE COLLECTION ANSWERS AN ARRAY while the factory types `attributes`' first parameter as the
76
+ * `Record<string, unknown>` every other PVE read is shaped like — the same mismatch acl.ts
77
+ * carries, handled the same way: narrow the rows, trust none of them, and match on `name`.
78
+ * ⚠️ MATCHED ON `name` ALONE, NEVER ON POSITION — the list comes back in a different order
79
+ * depending on which node was asked. See the ⚠️ on `collectionPath` above.
80
+ */
81
+ export const findRow = (live: unknown, props: CephDaemonProps) =>
82
+ (Array.isArray(live) ? live : [])
83
+ .filter((row): row is Record<string, unknown> => typeof row === 'object' && row !== null)
84
+ .find((row) => row['name'] === daemonId(props));
@@ -0,0 +1,243 @@
1
+ /**
2
+ * `Proxmox.CephDaemon` — a Ceph monitor, manager or metadata server on one node.
3
+ *
4
+ * ★ ONE RESOURCE FOR THREE KINDS, AND THE SCHEMAS WERE CHECKED BEFORE THAT WAS DECIDED. Parsed
5
+ * from `/usr/share/pve-docs/api-viewer/apidoc.js` on n2, 2026-09-13: mon, mgr and mds each
6
+ * expose GET on the collection and POST + DELETE on the id below it, each returns
7
+ * name/host/state/addr/ceph_version*, and the only divergence is the create parameter
8
+ * (`mon-address` / none / `hotstandby`) and the read-only extras (mon: quorum, rank; mds: rank,
9
+ * fs_name, standby_replay). Three near-identical files would have carried the traps below three
10
+ * times over and let them drift — the argument resource.ts already makes for the factory itself.
11
+ *
12
+ * ⛔ THE ONLY GET IS THE COLLECTION'S, AND IT ANSWERS FOR THE WHOLE CLUSTER. MEASURED:
13
+ * `GET /nodes/n2/ceph/mon` returns n2, n3 AND n4, and `GET /nodes/n3/ceph/mon` returns the same
14
+ * three IN A DIFFERENT ORDER — [n4,n2,n3] from n2, [n3,n2,n4] from n3 — so a row is found by
15
+ * `name` and NEVER by position, and the `{node}` in the path is only the node being ASKED.
16
+ * MEASURED too: there is no GET on the id path at all; `pvesh get /nodes/n2/ceph/mon/n2` answers
17
+ * "No 'get' handler defined for '/nodes/n2/ceph/mon/n2'", and mgr and mds answer the same.
18
+ * That is why `path()` below is the COLLECTION, and that in turn is why `delete` is the one
19
+ * handler this file writes by hand: `pveOperations.destroy` sends its DELETE to `spec.path`,
20
+ * which here would be the collection — a 501 reported as a failed destroy, with the daemon still
21
+ * running. Same shape and same rule as the override in acl.ts: do not "restore symmetry" by
22
+ * pointing it back at `ops.destroy`.
23
+ *
24
+ * ⛔ WHAT A DESTROY ACTUALLY DOES, BECAUSE THE PLAN LINE SAYS ONLY "delete". REASONED FROM PVE'S
25
+ * AND CEPH'S DOCUMENTED BEHAVIOUR, NOT MEASURED — nothing on this cluster was destroyed to find
26
+ * out, and nothing should be to check. `mon`: stops ceph-mon@id, removes it from the monmap and
27
+ * strips its section from ceph.conf. THREE MONS TOLERATE ONE LOSS; TWO TOLERATE NONE. Removing
28
+ * two in one deploy — or removing the mon on the node whose API is serving the call — loses
29
+ * quorum, and a cluster without mon quorum BLOCKS every RBD and CephFS I/O rather than erroring
30
+ * it, so on this cluster every guest on `cephtb4` hangs.
31
+ * `mds`: destroying a STANDBY is a non-event; destroying the ACTIVE one fails CephFS over,
32
+ * and with no standby left `cephfs-tb4` goes unavailable. `mgr`: no guest I/O depends on it, but
33
+ * the last one takes the PG autoscaler, the dashboard and PVE's own Ceph status with it.
34
+ * Live on 2026-09-13: mon, mgr AND mds on each of n2, n3, n4.
35
+ *
36
+ * ★ AND THE ONLY WAY TO REACH THAT DESTROY IS TO DELETE THE DECLARATION. `matches` is deliberately
37
+ * total (see its ⛔), so this resource CANNOT plan a `replace` — which is the one action that
38
+ * would tear down a live mon off the back of an edited field rather than a removed line.
39
+ *
40
+ * ⚠️ THE PRIVILEGES ARE ASYMMETRIC AND THE READ SIDE IS THE CHEAP ONE. The GET checks `any` of
41
+ * `Sys.Audit`/`Datastore.Audit` on `/`, and PVEAuditor holds both (MEASURED:
42
+ * `GET /access/roles/PVEAuditor`), so the mount's `read` role can see this family — unlike
43
+ * `Proxmox.Storage`, which 403s under that lease. POST and DELETE both check `Sys.Modify` on
44
+ * `/`, and MEASURED on this cluster `LXCProvisioner` already holds it, so the provision lane
45
+ * needs no widening for once. ⚠️ `Sys.Modify` on the ROOT path is broad — it also buys
46
+ * datacenter options and every other cluster-wide config write — so it is worth knowing that
47
+ * this credential already has it rather than discovering it the next time something is scoped.
48
+ */
49
+ import { Resource } from 'alchemy';
50
+ import * as Provider from 'alchemy/Provider';
51
+ import * as Effect from 'effect/Effect';
52
+ import { collectionPath, createForm, daemonId, daemonPath, findRow } from './ceph-daemon-form.ts';
53
+ import { pve } from './client.ts';
54
+ import { type PveRequirements, type WithTarget, pveHandlers } from './resource.ts';
55
+ import { bool, num, text } from './values.ts';
56
+
57
+ /**
58
+ * Which Ceph daemon this is. The discriminant picks the collection, the create parameters and
59
+ * which of the read-only extras below will even be populated.
60
+ *
61
+ * ⚠️ `osd` IS NOT HERE AND MUST NOT BE ADDED TO THIS UNION. `/nodes/{node}/ceph/osd` takes a disk
62
+ * device, wipes it, and its DELETE has a `cleanup` flag — a create that destroys data on the way
63
+ * in is a different resource with a different set of refusals, not a fourth case in this one.
64
+ */
65
+ export type CephDaemonKind = 'mon' | 'mgr' | 'mds';
66
+
67
+ export interface CephDaemonProps extends WithTarget {
68
+ kind: CephDaemonKind;
69
+ /**
70
+ * The node the daemon RUNS on. Identity — a daemon cannot be moved, only destroyed and rebuilt.
71
+ * ⚠️ It is also the node whose API is asked, and the read is only as available as that node: a
72
+ * node that is down — or has no Ceph installed, or answers 403 — reads as nothing at all,
73
+ * because `pveOperations.read` folds EVERY failure into "absent" (a 404 is a legitimate answer
74
+ * there and it cannot tell the two apart). Its daemons then plan as `update`, and reconcile
75
+ * POSTs a create for a mon that already exists. PVE refuses that with "monitor already
76
+ * exists", and the read-back refuses again — so it fails loudly rather than damaging anything,
77
+ * but the message will point at the daemon rather than at the node that would not answer.
78
+ */
79
+ node: string;
80
+ /**
81
+ * The daemon id. PVE defaults it to the nodename, and `daemonId` in ceph-daemon-form.ts resolves
82
+ * that default HERE rather than leaving it to PVE, because the id is a PATH SEGMENT: create,
83
+ * delete and the row lookup all need the same string, and a default resolved on the far side
84
+ * would leave three call sites guessing. Live on this cluster every daemon's name is its node.
85
+ */
86
+ name?: string;
87
+ /**
88
+ * mon only. Overrides the autodetected monitor IP; must sit in Ceph's public network — on this
89
+ * cluster that is 10.20.11.0/24, carried by `vmbr1.11`.
90
+ * ⛔ CREATE-ONLY AND NEVER COMPARED. PVE takes a bare ip-list on write and hands back `addr` as
91
+ * `10.20.11.12:6789/0` — an address, a port and a nonce. The two are not the same string and
92
+ * never will be, so diffing them would report a mismatch on every plan; with no PUT on this
93
+ * family that mismatch becomes a REPLACE, and a replace of a mon is the quorum loss in the
94
+ * header. Left out of `matches` on purpose.
95
+ */
96
+ 'mon-address'?: string;
97
+ /**
98
+ * mds only. Makes this standby replay the active MDS's journal for a faster failover.
99
+ * ⛔ CREATE-ONLY AND NEVER COMPARED, AND THE FIELD THAT LOOKS LIKE ITS READBACK IS NOT ONE.
100
+ * `standby_replay` in the list is the daemon's CURRENT state, not its configuration: MEASURED
101
+ * 2026-09-13, all three mds report `standby_replay: false`, n3 included — and n3 is the ACTIVE
102
+ * mds, which is not a standby at all and so can never report true however it was created.
103
+ * Comparing `hotstandby` to it would plan a replace against whichever mds Ceph happens to have
104
+ * elected, i.e. against a value no declaration controls.
105
+ * ⚠️ SET ON A mon OR mgr IT IS DROPPED RATHER THAN SENT, and `mon-address` likewise on an mds:
106
+ * each POST schema lists only its own parameter, so the wrong one is a 400 rather than an
107
+ * ignored hint. A field silently dropped is the lesser of the two, but it IS dropped.
108
+ */
109
+ hotstandby?: boolean;
110
+ }
111
+
112
+ /**
113
+ * Everything the cluster says about this daemon — reported, and (bar its identity) none of it
114
+ * compared. Read the ⛔ on `matches` for why that is the only safe split on this family.
115
+ */
116
+ export interface CephDaemonAttributes {
117
+ kind: CephDaemonKind;
118
+ node: string;
119
+ name: string;
120
+ /**
121
+ * The host Ceph says it runs on.
122
+ * ⚠️ REPORTED, NOT COMPARED, AND THAT IS A DELIBERATE BLIND SPOT. A daemon found under this name
123
+ * on a DIFFERENT node than `node` declares plans as `noop`, which does hide a misdeclaration.
124
+ * The alternative is worse: `host` is `optional` in the schema, so an absent one would read as
125
+ * a mismatch, and with no PUT a mismatch is a replace — i.e. a missing field in one API answer
126
+ * would destroy a live mon. When in doubt, out of `matches`; fix a wrong node by hand.
127
+ */
128
+ host: string;
129
+ /**
130
+ * mon `running`/`stopped`/`unknown`, mgr `active`/`standby`, mds `up:active`/`up:standby`/…
131
+ * ⛔ CEPH ELECTS THIS AND REWRITES IT WITHOUT ANYONE DECLARING ANYTHING. MEASURED: mgr n2 is
132
+ * `active` while n3 and n4 are `standby`; mds n3 is `up:active` while n2 and n4 are
133
+ * `up:standby`. Restart a daemon and the roles move. It is reported so a plan can show what is
134
+ * live and kept out of `matches` so that an election is never a diff.
135
+ * ⚠️ IT ALSO MEANS THIS RESOURCE DOES NOT MANAGE WHETHER THE DAEMON IS RUNNING. A configured but
136
+ * stopped daemon is still a row in the list, so it reads as present and plans `noop`.
137
+ */
138
+ state: string;
139
+ /** Ceph-formatted, e.g. `10.20.11.12:6789/0`. Reported; see `mon-address` for why never compared. */
140
+ addr: string;
141
+ /** `ceph_version_short`, e.g. `20.2.2`. Reported: a straggler after an upgrade is worth seeing. */
142
+ version: string;
143
+ /**
144
+ * mon rank in the monmap, mds rank in the filesystem, `-1` when the daemon has neither.
145
+ * ⛔ CEPH ASSIGNS IT AND NOTHING DECLARES IT. MEASURED: the mons are ranked n4=0, n3=1, n2=2 —
146
+ * monmap order, not declaration order — and the mds ranks are 0 for the active n3 and -1 for
147
+ * the two standbys. Rank moves when a daemon is added, removed or restarted.
148
+ */
149
+ rank: number;
150
+ /** mon only: in the current quorum. Reported. MEASURED: all three mons are in quorum. */
151
+ quorum: boolean;
152
+ /** mds only, and NOT the readback of `hotstandby` — see the ⛔ on that prop. */
153
+ standbyReplay: boolean;
154
+ /**
155
+ * mds only: the CephFS this daemon currently serves — `cephfs-tb4` here.
156
+ * ⛔ PRESENT ONLY ON THE ACTIVE MDS. MEASURED: n3 carries `fs_name`, n2 and n4 have no such key
157
+ * at all. So it is empty for two of three identical declarations, and comparing it would
158
+ * report work on exactly the daemons that are healthy standbys.
159
+ */
160
+ fsName: string;
161
+ /** A `ceph-<kind>@<id>` systemd unit is enabled on the host. Reported; absent reads as false. */
162
+ service: boolean;
163
+ }
164
+
165
+ export interface ProxmoxCephDaemon extends Resource<
166
+ 'Proxmox.CephDaemon',
167
+ CephDaemonProps,
168
+ CephDaemonAttributes,
169
+ never,
170
+ PveRequirements
171
+ > {}
172
+
173
+ /** ★ `retain` by default — a mon carrying quorum cannot be rebuilt. See the ★ in resource.ts. */
174
+ export const ProxmoxCephDaemon = Resource<ProxmoxCephDaemon>('Proxmox.CephDaemon', {
175
+ defaultRemovalPolicy: 'retain',
176
+ });
177
+
178
+ const handlers = pveHandlers<CephDaemonProps, CephDaemonAttributes>({
179
+ /** ⚠️ `undefined` when no row carries this name: that is how the factory learns to create. */
180
+ attributes: (live, props) => {
181
+ const row = findRow(live, props);
182
+ if (row === undefined) return undefined;
183
+ return {
184
+ addr: text(row['addr']),
185
+ fsName: text(row['fs_name']),
186
+ host: text(row['host']),
187
+ kind: props.kind,
188
+ name: daemonId(props),
189
+ node: props.node,
190
+ quorum: bool(row['quorum']),
191
+ rank: num(row['rank'], -1),
192
+ service: bool(row['service']),
193
+ standbyReplay: bool(row['standby_replay']),
194
+ state: text(row['state']),
195
+ version: text(row['ceph_version_short']),
196
+ };
197
+ },
198
+ /** ⛔ The ID path, not the collection — POST is registered on `{id}`, exactly like a metric server. */
199
+ collection: daemonPath,
200
+ createForm,
201
+ /**
202
+ * ⛔ TOTAL, AND THIS IS THE REASON THE FILE EXISTS RATHER THAN A SHORTCUT PAST WRITING IT. Every
203
+ * field a declaration can carry is CREATE-ONLY and unreadable — `mon-address` comes back as a
204
+ * different kind of string, `hotstandby` comes back as a state Ceph elects — and every field
205
+ * the read returns is assigned by Ceph: state, rank, quorum, fs_name, standby_replay, addr,
206
+ * version. Nothing is left that is both declared and readable. So the honest comparison is
207
+ * "does a daemon of this kind and name exist", which `attributes` has already answered by
208
+ * returning a value at all, and anything further would be a diff no write could ever satisfy.
209
+ * ⛔ AND ON THIS FAMILY THAT DIFF IS NOT MERELY NOISY. There is no PUT, so resource.ts answers
210
+ * `replace` rather than `update` whenever `matches` is false — and replace on a mon is
211
+ * delete-then-create against a live quorum. A forever-diff here is a forever-OUTAGE-RISK.
212
+ * ⚠️ THE PRICE, STATED PLAINLY: editing `hotstandby` or `mon-address` on a declared daemon
213
+ * plans as `noop` and never applies. Same bargain storage.ts strikes for a changed `type`.
214
+ * Change one by removing the declaration and re-adding it, ONE DAEMON AT A TIME, reading the
215
+ * ⛔ on destroy in the header first.
216
+ */
217
+ matches: () => true,
218
+ /** ⛔ The COLLECTION, because there is no GET on the id path. See the second ⛔ in the header. */
219
+ path: collectionPath,
220
+ /** ⚠️ No `updateForm`: none of the three has a PUT, so nothing about a daemon is editable. */
221
+ });
222
+
223
+ export const ProxmoxCephDaemonProvider = () =>
224
+ Provider.effect(
225
+ ProxmoxCephDaemon,
226
+ Effect.succeed(
227
+ ProxmoxCephDaemon.Provider.of({
228
+ ...handlers,
229
+ /**
230
+ * ⛔ THE ONE HANDLER NOT TAKEN FROM THE FACTORY, AND THE ONLY REASON IS THE PATH SPLIT.
231
+ * `ops.destroy` DELETEs `spec.path`, which this resource must point at the collection so
232
+ * that `read` works at all; DELETE on the collection is not implemented and would answer
233
+ * 501 while the daemon kept running — a failed destroy that reads like a permissions
234
+ * problem. Everything else here is the factory's.
235
+ * ⚠️ AND IT IS THE DANGEROUS ONE. Re-read the destroy ⛔ in the header before letting a
236
+ * plan that removes a mon run: two gone at once is a cluster with no quorum and every
237
+ * guest on `cephtb4` blocked on I/O.
238
+ */
239
+ delete: ({ olds }: { olds: CephDaemonProps }) =>
240
+ pve(olds.target, 'provision', 'DELETE', daemonPath(olds)),
241
+ }),
242
+ ),
243
+ );