pvectl 0.2.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 (558) hide show
  1. checksums.yaml +7 -0
  2. data/.claude/rules/branch-before-changes.md +52 -0
  3. data/.claude/rules/documentation-updates.md +104 -0
  4. data/.claude/rules/git-workflow.md +84 -0
  5. data/.claude/rules/proxmox-api-docs.md +58 -0
  6. data/.claude/rules/rbs-signatures.md +80 -0
  7. data/.claude/rules/refactoring-as-design-option.md +35 -0
  8. data/.claude/scheduled_tasks.lock +1 -0
  9. data/.claude/settings.json +51 -0
  10. data/.mcp.json +8 -0
  11. data/.ruby-gemset +1 -0
  12. data/.ruby-version +1 -0
  13. data/CHANGELOG.md +138 -0
  14. data/CLAUDE.md +211 -0
  15. data/CODE_OF_CONDUCT.md +132 -0
  16. data/LICENSE.txt +21 -0
  17. data/README.md +143 -0
  18. data/Rakefile +8 -0
  19. data/docs/proxmox-api-update.sh +96 -0
  20. data/exe/pvectl +5 -0
  21. data/lib/pvectl/argv_preprocessor.rb +334 -0
  22. data/lib/pvectl/cli.rb +102 -0
  23. data/lib/pvectl/commands/apt.rb +389 -0
  24. data/lib/pvectl/commands/clone_container.rb +230 -0
  25. data/lib/pvectl/commands/clone_vm.rb +331 -0
  26. data/lib/pvectl/commands/cloudinit/command.rb +122 -0
  27. data/lib/pvectl/commands/cloudinit/dump.rb +94 -0
  28. data/lib/pvectl/commands/cloudinit/pending.rb +137 -0
  29. data/lib/pvectl/commands/cloudinit/regenerate.rb +79 -0
  30. data/lib/pvectl/commands/config/command.rb +65 -0
  31. data/lib/pvectl/commands/config/get_contexts.rb +68 -0
  32. data/lib/pvectl/commands/config/set_cluster.rb +103 -0
  33. data/lib/pvectl/commands/config/set_context.rb +136 -0
  34. data/lib/pvectl/commands/config/set_credentials.rb +181 -0
  35. data/lib/pvectl/commands/config/use_context.rb +69 -0
  36. data/lib/pvectl/commands/config/view.rb +67 -0
  37. data/lib/pvectl/commands/console.rb +93 -0
  38. data/lib/pvectl/commands/console_ct.rb +187 -0
  39. data/lib/pvectl/commands/console_vm.rb +187 -0
  40. data/lib/pvectl/commands/container_lifecycle_command.rb +77 -0
  41. data/lib/pvectl/commands/create_backup.rb +173 -0
  42. data/lib/pvectl/commands/create_container.rb +141 -0
  43. data/lib/pvectl/commands/create_resource_command.rb +244 -0
  44. data/lib/pvectl/commands/create_snapshot.rb +242 -0
  45. data/lib/pvectl/commands/create_vm.rb +267 -0
  46. data/lib/pvectl/commands/delete_backup.rb +139 -0
  47. data/lib/pvectl/commands/delete_command.rb +119 -0
  48. data/lib/pvectl/commands/delete_container.rb +30 -0
  49. data/lib/pvectl/commands/delete_snapshot.rb +248 -0
  50. data/lib/pvectl/commands/delete_vm.rb +127 -0
  51. data/lib/pvectl/commands/describe/command.rb +251 -0
  52. data/lib/pvectl/commands/edit_container.rb +56 -0
  53. data/lib/pvectl/commands/edit_dns.rb +149 -0
  54. data/lib/pvectl/commands/edit_hosts.rb +135 -0
  55. data/lib/pvectl/commands/edit_node.rb +54 -0
  56. data/lib/pvectl/commands/edit_resource_command.rb +180 -0
  57. data/lib/pvectl/commands/edit_vm.rb +154 -0
  58. data/lib/pvectl/commands/edit_volume.rb +189 -0
  59. data/lib/pvectl/commands/feature_command.rb +230 -0
  60. data/lib/pvectl/commands/feature_container.rb +21 -0
  61. data/lib/pvectl/commands/feature_vm.rb +94 -0
  62. data/lib/pvectl/commands/get/command.rb +360 -0
  63. data/lib/pvectl/commands/get/handlers/backups.rb +76 -0
  64. data/lib/pvectl/commands/get/handlers/capabilities.rb +107 -0
  65. data/lib/pvectl/commands/get/handlers/containers.rb +148 -0
  66. data/lib/pvectl/commands/get/handlers/disks.rb +107 -0
  67. data/lib/pvectl/commands/get/handlers/dns.rb +94 -0
  68. data/lib/pvectl/commands/get/handlers/hosts.rb +94 -0
  69. data/lib/pvectl/commands/get/handlers/nodes.rb +162 -0
  70. data/lib/pvectl/commands/get/handlers/services.rb +81 -0
  71. data/lib/pvectl/commands/get/handlers/snapshots.rb +97 -0
  72. data/lib/pvectl/commands/get/handlers/storage.rb +118 -0
  73. data/lib/pvectl/commands/get/handlers/subscription.rb +69 -0
  74. data/lib/pvectl/commands/get/handlers/tasks.rb +89 -0
  75. data/lib/pvectl/commands/get/handlers/templates.rb +175 -0
  76. data/lib/pvectl/commands/get/handlers/time.rb +118 -0
  77. data/lib/pvectl/commands/get/handlers/vms.rb +145 -0
  78. data/lib/pvectl/commands/get/handlers/volume.rb +134 -0
  79. data/lib/pvectl/commands/get/resource_handler.rb +63 -0
  80. data/lib/pvectl/commands/get/resource_registry.rb +18 -0
  81. data/lib/pvectl/commands/get/watch_loop.rb +129 -0
  82. data/lib/pvectl/commands/irreversible_command.rb +265 -0
  83. data/lib/pvectl/commands/logs/command.rb +275 -0
  84. data/lib/pvectl/commands/logs/handlers/journal.rb +46 -0
  85. data/lib/pvectl/commands/logs/handlers/syslog.rb +53 -0
  86. data/lib/pvectl/commands/logs/handlers/task_detail.rb +52 -0
  87. data/lib/pvectl/commands/logs/handlers/task_logs.rb +115 -0
  88. data/lib/pvectl/commands/logs/resource_handler.rb +46 -0
  89. data/lib/pvectl/commands/logs/resource_registry.rb +22 -0
  90. data/lib/pvectl/commands/migrate_command.rb +282 -0
  91. data/lib/pvectl/commands/migrate_container.rb +23 -0
  92. data/lib/pvectl/commands/migrate_vm.rb +122 -0
  93. data/lib/pvectl/commands/move_disk_command.rb +239 -0
  94. data/lib/pvectl/commands/move_disk_container.rb +21 -0
  95. data/lib/pvectl/commands/move_disk_vm.rb +127 -0
  96. data/lib/pvectl/commands/ping.rb +249 -0
  97. data/lib/pvectl/commands/pull.rb +342 -0
  98. data/lib/pvectl/commands/push.rb +352 -0
  99. data/lib/pvectl/commands/reset.rb +64 -0
  100. data/lib/pvectl/commands/resource_lifecycle_command.rb +277 -0
  101. data/lib/pvectl/commands/resource_registry.rb +73 -0
  102. data/lib/pvectl/commands/restart.rb +70 -0
  103. data/lib/pvectl/commands/restart_container.rb +18 -0
  104. data/lib/pvectl/commands/restore_backup.rb +236 -0
  105. data/lib/pvectl/commands/resume.rb +57 -0
  106. data/lib/pvectl/commands/rollback_snapshot.rb +228 -0
  107. data/lib/pvectl/commands/sendkey_vm.rb +205 -0
  108. data/lib/pvectl/commands/service.rb +293 -0
  109. data/lib/pvectl/commands/set_container.rb +50 -0
  110. data/lib/pvectl/commands/set_node.rb +52 -0
  111. data/lib/pvectl/commands/set_resource_command.rb +185 -0
  112. data/lib/pvectl/commands/set_vm.rb +136 -0
  113. data/lib/pvectl/commands/set_volume.rb +212 -0
  114. data/lib/pvectl/commands/shared_config_parsers.rb +126 -0
  115. data/lib/pvectl/commands/shared_flags.rb +155 -0
  116. data/lib/pvectl/commands/shutdown.rb +73 -0
  117. data/lib/pvectl/commands/shutdown_container.rb +18 -0
  118. data/lib/pvectl/commands/start.rb +79 -0
  119. data/lib/pvectl/commands/start_container.rb +18 -0
  120. data/lib/pvectl/commands/stop.rb +75 -0
  121. data/lib/pvectl/commands/stop_container.rb +18 -0
  122. data/lib/pvectl/commands/suspend.rb +64 -0
  123. data/lib/pvectl/commands/template_command.rb +205 -0
  124. data/lib/pvectl/commands/template_container.rb +27 -0
  125. data/lib/pvectl/commands/template_vm.rb +106 -0
  126. data/lib/pvectl/commands/top/command.rb +206 -0
  127. data/lib/pvectl/commands/top/handlers/containers.rb +61 -0
  128. data/lib/pvectl/commands/top/handlers/nodes.rb +61 -0
  129. data/lib/pvectl/commands/top/handlers/vms.rb +61 -0
  130. data/lib/pvectl/commands/top/resource_handler.rb +46 -0
  131. data/lib/pvectl/commands/top/resource_registry.rb +22 -0
  132. data/lib/pvectl/commands/unlink_disk_vm.rb +232 -0
  133. data/lib/pvectl/commands/vm_lifecycle_command.rb +77 -0
  134. data/lib/pvectl/commands/wakeonlan_node.rb +153 -0
  135. data/lib/pvectl/config/errors.rb +62 -0
  136. data/lib/pvectl/config/models/cluster.rb +180 -0
  137. data/lib/pvectl/config/models/context.rb +100 -0
  138. data/lib/pvectl/config/models/resolved_config.rb +171 -0
  139. data/lib/pvectl/config/models/user.rb +133 -0
  140. data/lib/pvectl/config/provider.rb +297 -0
  141. data/lib/pvectl/config/service.rb +300 -0
  142. data/lib/pvectl/config/store.rb +161 -0
  143. data/lib/pvectl/config/wizard.rb +309 -0
  144. data/lib/pvectl/config_serializer.rb +1034 -0
  145. data/lib/pvectl/connection/retry_handler.rb +161 -0
  146. data/lib/pvectl/connection.rb +157 -0
  147. data/lib/pvectl/console/terminal_session.rb +449 -0
  148. data/lib/pvectl/editor_session.rb +157 -0
  149. data/lib/pvectl/exit_codes.rb +43 -0
  150. data/lib/pvectl/formatters/base.rb +55 -0
  151. data/lib/pvectl/formatters/color_support.rb +90 -0
  152. data/lib/pvectl/formatters/json.rb +45 -0
  153. data/lib/pvectl/formatters/output_helper.rb +77 -0
  154. data/lib/pvectl/formatters/registry.rb +72 -0
  155. data/lib/pvectl/formatters/table.rb +235 -0
  156. data/lib/pvectl/formatters/wide.rb +93 -0
  157. data/lib/pvectl/formatters/yaml.rb +49 -0
  158. data/lib/pvectl/manifest_serializer.rb +142 -0
  159. data/lib/pvectl/models/apt_package.rb +107 -0
  160. data/lib/pvectl/models/backup.rb +173 -0
  161. data/lib/pvectl/models/base.rb +49 -0
  162. data/lib/pvectl/models/capability.rb +62 -0
  163. data/lib/pvectl/models/container.rb +205 -0
  164. data/lib/pvectl/models/container_operation_result.rb +27 -0
  165. data/lib/pvectl/models/dns_config.rb +54 -0
  166. data/lib/pvectl/models/hosts_file.rb +47 -0
  167. data/lib/pvectl/models/journal_entry.rb +16 -0
  168. data/lib/pvectl/models/network_interface.rb +85 -0
  169. data/lib/pvectl/models/node.rb +195 -0
  170. data/lib/pvectl/models/node_operation_result.rb +45 -0
  171. data/lib/pvectl/models/operation_result.rb +110 -0
  172. data/lib/pvectl/models/physical_disk.rb +193 -0
  173. data/lib/pvectl/models/service.rb +80 -0
  174. data/lib/pvectl/models/snapshot.rb +101 -0
  175. data/lib/pvectl/models/snapshot_description.rb +39 -0
  176. data/lib/pvectl/models/storage.rb +180 -0
  177. data/lib/pvectl/models/subscription.rb +87 -0
  178. data/lib/pvectl/models/syslog_entry.rb +17 -0
  179. data/lib/pvectl/models/task.rb +95 -0
  180. data/lib/pvectl/models/task_entry.rb +52 -0
  181. data/lib/pvectl/models/task_log_line.rb +17 -0
  182. data/lib/pvectl/models/time_config.rb +47 -0
  183. data/lib/pvectl/models/vm.rb +137 -0
  184. data/lib/pvectl/models/vm_operation_result.rb +27 -0
  185. data/lib/pvectl/models/volume.rb +133 -0
  186. data/lib/pvectl/models/volume_operation_result.rb +26 -0
  187. data/lib/pvectl/parsers/cloud_init_config.rb +92 -0
  188. data/lib/pvectl/parsers/disk_config.rb +97 -0
  189. data/lib/pvectl/parsers/lxc_mount_config.rb +98 -0
  190. data/lib/pvectl/parsers/lxc_net_config.rb +97 -0
  191. data/lib/pvectl/parsers/net_config.rb +95 -0
  192. data/lib/pvectl/parsers/smart_text.rb +42 -0
  193. data/lib/pvectl/plugin_loader.rb +157 -0
  194. data/lib/pvectl/presenters/apt_package.rb +99 -0
  195. data/lib/pvectl/presenters/backup.rb +128 -0
  196. data/lib/pvectl/presenters/base.rb +283 -0
  197. data/lib/pvectl/presenters/capability.rb +104 -0
  198. data/lib/pvectl/presenters/config/context.rb +80 -0
  199. data/lib/pvectl/presenters/container.rb +574 -0
  200. data/lib/pvectl/presenters/container_operation_result.rb +109 -0
  201. data/lib/pvectl/presenters/disk.rb +184 -0
  202. data/lib/pvectl/presenters/dns_config.rb +68 -0
  203. data/lib/pvectl/presenters/hosts_file.rb +61 -0
  204. data/lib/pvectl/presenters/journal_entry.rb +20 -0
  205. data/lib/pvectl/presenters/node.rb +762 -0
  206. data/lib/pvectl/presenters/node_operation_result.rb +50 -0
  207. data/lib/pvectl/presenters/operation_result.rb +61 -0
  208. data/lib/pvectl/presenters/service.rb +76 -0
  209. data/lib/pvectl/presenters/snapshot.rb +239 -0
  210. data/lib/pvectl/presenters/snapshot_operation_result.rb +125 -0
  211. data/lib/pvectl/presenters/storage.rb +329 -0
  212. data/lib/pvectl/presenters/subscription.rb +189 -0
  213. data/lib/pvectl/presenters/syslog_entry.rb +20 -0
  214. data/lib/pvectl/presenters/task_entry.rb +69 -0
  215. data/lib/pvectl/presenters/task_log_line.rb +20 -0
  216. data/lib/pvectl/presenters/template.rb +76 -0
  217. data/lib/pvectl/presenters/time_config.rb +86 -0
  218. data/lib/pvectl/presenters/top_container.rb +112 -0
  219. data/lib/pvectl/presenters/top_node.rb +115 -0
  220. data/lib/pvectl/presenters/top_presenter.rb +59 -0
  221. data/lib/pvectl/presenters/top_vm.rb +105 -0
  222. data/lib/pvectl/presenters/vm.rb +853 -0
  223. data/lib/pvectl/presenters/vm_operation_result.rb +109 -0
  224. data/lib/pvectl/presenters/volume.rb +136 -0
  225. data/lib/pvectl/presenters/volume_operation_result.rb +58 -0
  226. data/lib/pvectl/repositories/apt.rb +93 -0
  227. data/lib/pvectl/repositories/backup.rb +186 -0
  228. data/lib/pvectl/repositories/base.rb +110 -0
  229. data/lib/pvectl/repositories/capabilities.rb +96 -0
  230. data/lib/pvectl/repositories/container.rb +503 -0
  231. data/lib/pvectl/repositories/disk.rb +87 -0
  232. data/lib/pvectl/repositories/dns.rb +54 -0
  233. data/lib/pvectl/repositories/hosts.rb +63 -0
  234. data/lib/pvectl/repositories/journal.rb +23 -0
  235. data/lib/pvectl/repositories/node.rb +537 -0
  236. data/lib/pvectl/repositories/service.rb +139 -0
  237. data/lib/pvectl/repositories/snapshot.rb +133 -0
  238. data/lib/pvectl/repositories/storage.rb +302 -0
  239. data/lib/pvectl/repositories/subscription.rb +77 -0
  240. data/lib/pvectl/repositories/syslog.rb +25 -0
  241. data/lib/pvectl/repositories/task.rb +82 -0
  242. data/lib/pvectl/repositories/task_list.rb +30 -0
  243. data/lib/pvectl/repositories/task_log.rb +31 -0
  244. data/lib/pvectl/repositories/time_config.rb +53 -0
  245. data/lib/pvectl/repositories/vm.rb +616 -0
  246. data/lib/pvectl/repositories/volume.rb +306 -0
  247. data/lib/pvectl/selectors/base.rb +201 -0
  248. data/lib/pvectl/selectors/container.rb +116 -0
  249. data/lib/pvectl/selectors/disk.rb +59 -0
  250. data/lib/pvectl/selectors/vm.rb +116 -0
  251. data/lib/pvectl/selectors/volume.rb +59 -0
  252. data/lib/pvectl/services/backup.rb +209 -0
  253. data/lib/pvectl/services/clone_container.rb +260 -0
  254. data/lib/pvectl/services/clone_vm.rb +265 -0
  255. data/lib/pvectl/services/cloudinit.rb +96 -0
  256. data/lib/pvectl/services/console.rb +152 -0
  257. data/lib/pvectl/services/container_lifecycle.rb +124 -0
  258. data/lib/pvectl/services/create_container.rb +179 -0
  259. data/lib/pvectl/services/create_vm.rb +191 -0
  260. data/lib/pvectl/services/edit_container.rb +125 -0
  261. data/lib/pvectl/services/edit_dns.rb +159 -0
  262. data/lib/pvectl/services/edit_hosts.rb +78 -0
  263. data/lib/pvectl/services/edit_node.rb +147 -0
  264. data/lib/pvectl/services/edit_vm.rb +125 -0
  265. data/lib/pvectl/services/edit_volume.rb +224 -0
  266. data/lib/pvectl/services/get/resource_service.rb +98 -0
  267. data/lib/pvectl/services/move_disk.rb +132 -0
  268. data/lib/pvectl/services/pull_config.rb +94 -0
  269. data/lib/pvectl/services/push_config.rb +524 -0
  270. data/lib/pvectl/services/resize_volume.rb +253 -0
  271. data/lib/pvectl/services/resource_delete.rb +169 -0
  272. data/lib/pvectl/services/resource_migration.rb +170 -0
  273. data/lib/pvectl/services/sendkey.rb +108 -0
  274. data/lib/pvectl/services/service_lifecycle.rb +89 -0
  275. data/lib/pvectl/services/set_container.rb +128 -0
  276. data/lib/pvectl/services/set_node.rb +236 -0
  277. data/lib/pvectl/services/set_vm.rb +128 -0
  278. data/lib/pvectl/services/set_volume.rb +126 -0
  279. data/lib/pvectl/services/snapshot.rb +261 -0
  280. data/lib/pvectl/services/task_listing.rb +75 -0
  281. data/lib/pvectl/services/unlink_disk.rb +86 -0
  282. data/lib/pvectl/services/vm_lifecycle.rb +124 -0
  283. data/lib/pvectl/services/wakeonlan.rb +79 -0
  284. data/lib/pvectl/utils/resource_resolver.rb +80 -0
  285. data/lib/pvectl/version.rb +13 -0
  286. data/lib/pvectl/wizards/create_container.rb +105 -0
  287. data/lib/pvectl/wizards/create_vm.rb +98 -0
  288. data/lib/pvectl.rb +439 -0
  289. data/sig/external/gli.rbs +16 -0
  290. data/sig/external/proxmox_api.rbs +10 -0
  291. data/sig/pvectl/argv_preprocessor.rbs +53 -0
  292. data/sig/pvectl/cli.rbs +26 -0
  293. data/sig/pvectl/commands/apt.rbs +47 -0
  294. data/sig/pvectl/commands/clone_container.rbs +31 -0
  295. data/sig/pvectl/commands/clone_vm.rbs +33 -0
  296. data/sig/pvectl/commands/cloudinit/command.rbs +13 -0
  297. data/sig/pvectl/commands/cloudinit/dump.rbs +13 -0
  298. data/sig/pvectl/commands/cloudinit/pending.rbs +17 -0
  299. data/sig/pvectl/commands/cloudinit/regenerate.rbs +11 -0
  300. data/sig/pvectl/commands/config/command.rbs +9 -0
  301. data/sig/pvectl/commands/config/get_contexts.rbs +11 -0
  302. data/sig/pvectl/commands/config/set_cluster.rbs +11 -0
  303. data/sig/pvectl/commands/config/set_context.rbs +15 -0
  304. data/sig/pvectl/commands/config/set_credentials.rbs +15 -0
  305. data/sig/pvectl/commands/config/use_context.rbs +11 -0
  306. data/sig/pvectl/commands/config/view.rbs +11 -0
  307. data/sig/pvectl/commands/console.rbs +9 -0
  308. data/sig/pvectl/commands/console_ct.rbs +27 -0
  309. data/sig/pvectl/commands/console_vm.rbs +27 -0
  310. data/sig/pvectl/commands/container_lifecycle_command.rbs +25 -0
  311. data/sig/pvectl/commands/create_backup.rbs +29 -0
  312. data/sig/pvectl/commands/create_container.rbs +30 -0
  313. data/sig/pvectl/commands/create_resource_command.rbs +53 -0
  314. data/sig/pvectl/commands/create_snapshot.rbs +35 -0
  315. data/sig/pvectl/commands/create_vm.rbs +30 -0
  316. data/sig/pvectl/commands/delete_backup.rbs +25 -0
  317. data/sig/pvectl/commands/delete_command.rbs +45 -0
  318. data/sig/pvectl/commands/delete_container.rbs +11 -0
  319. data/sig/pvectl/commands/delete_snapshot.rbs +35 -0
  320. data/sig/pvectl/commands/delete_vm.rbs +13 -0
  321. data/sig/pvectl/commands/describe/command.rbs +27 -0
  322. data/sig/pvectl/commands/edit_container.rbs +17 -0
  323. data/sig/pvectl/commands/edit_dns.rbs +25 -0
  324. data/sig/pvectl/commands/edit_hosts.rbs +23 -0
  325. data/sig/pvectl/commands/edit_node.rbs +17 -0
  326. data/sig/pvectl/commands/edit_resource_command.rbs +35 -0
  327. data/sig/pvectl/commands/edit_vm.rbs +19 -0
  328. data/sig/pvectl/commands/edit_volume.rbs +24 -0
  329. data/sig/pvectl/commands/feature_command.rbs +43 -0
  330. data/sig/pvectl/commands/feature_container.rbs +10 -0
  331. data/sig/pvectl/commands/feature_vm.rbs +12 -0
  332. data/sig/pvectl/commands/get/command.rbs +42 -0
  333. data/sig/pvectl/commands/get/handlers/backups.rbs +23 -0
  334. data/sig/pvectl/commands/get/handlers/capabilities.rbs +29 -0
  335. data/sig/pvectl/commands/get/handlers/containers.rbs +35 -0
  336. data/sig/pvectl/commands/get/handlers/disks.rbs +27 -0
  337. data/sig/pvectl/commands/get/handlers/dns.rbs +25 -0
  338. data/sig/pvectl/commands/get/handlers/hosts.rbs +25 -0
  339. data/sig/pvectl/commands/get/handlers/nodes.rbs +33 -0
  340. data/sig/pvectl/commands/get/handlers/services.rbs +23 -0
  341. data/sig/pvectl/commands/get/handlers/snapshots.rbs +27 -0
  342. data/sig/pvectl/commands/get/handlers/storage.rbs +25 -0
  343. data/sig/pvectl/commands/get/handlers/subscription.rbs +25 -0
  344. data/sig/pvectl/commands/get/handlers/tasks.rbs +28 -0
  345. data/sig/pvectl/commands/get/handlers/templates.rbs +35 -0
  346. data/sig/pvectl/commands/get/handlers/time.rbs +29 -0
  347. data/sig/pvectl/commands/get/handlers/vms.rbs +35 -0
  348. data/sig/pvectl/commands/get/handlers/volume.rbs +27 -0
  349. data/sig/pvectl/commands/get/resource_handler.rbs +13 -0
  350. data/sig/pvectl/commands/get/resource_registry.rbs +8 -0
  351. data/sig/pvectl/commands/get/watch_loop.rbs +33 -0
  352. data/sig/pvectl/commands/irreversible_command.rbs +32 -0
  353. data/sig/pvectl/commands/logs/command.rbs +35 -0
  354. data/sig/pvectl/commands/logs/handlers/journal.rbs +21 -0
  355. data/sig/pvectl/commands/logs/handlers/syslog.rbs +21 -0
  356. data/sig/pvectl/commands/logs/handlers/task_detail.rbs +21 -0
  357. data/sig/pvectl/commands/logs/handlers/task_logs.rbs +35 -0
  358. data/sig/pvectl/commands/logs/resource_handler.rbs +11 -0
  359. data/sig/pvectl/commands/logs/resource_registry.rbs +8 -0
  360. data/sig/pvectl/commands/migrate_command.rbs +45 -0
  361. data/sig/pvectl/commands/migrate_container.rbs +11 -0
  362. data/sig/pvectl/commands/migrate_vm.rbs +13 -0
  363. data/sig/pvectl/commands/move_disk_command.rbs +43 -0
  364. data/sig/pvectl/commands/move_disk_container.rbs +11 -0
  365. data/sig/pvectl/commands/move_disk_vm.rbs +13 -0
  366. data/sig/pvectl/commands/ping.rbs +39 -0
  367. data/sig/pvectl/commands/pull.rbs +33 -0
  368. data/sig/pvectl/commands/push.rbs +32 -0
  369. data/sig/pvectl/commands/reset.rbs +11 -0
  370. data/sig/pvectl/commands/resource_lifecycle_command.rbs +55 -0
  371. data/sig/pvectl/commands/resource_registry.rbs +19 -0
  372. data/sig/pvectl/commands/restart.rbs +11 -0
  373. data/sig/pvectl/commands/restart_container.rbs +9 -0
  374. data/sig/pvectl/commands/restore_backup.rbs +27 -0
  375. data/sig/pvectl/commands/resume.rbs +11 -0
  376. data/sig/pvectl/commands/rollback_snapshot.rbs +31 -0
  377. data/sig/pvectl/commands/sendkey_vm.rbs +25 -0
  378. data/sig/pvectl/commands/service.rbs +38 -0
  379. data/sig/pvectl/commands/set_container.rbs +13 -0
  380. data/sig/pvectl/commands/set_node.rbs +13 -0
  381. data/sig/pvectl/commands/set_resource_command.rbs +25 -0
  382. data/sig/pvectl/commands/set_vm.rbs +15 -0
  383. data/sig/pvectl/commands/set_volume.rbs +24 -0
  384. data/sig/pvectl/commands/shared_config_parsers.rbs +19 -0
  385. data/sig/pvectl/commands/shared_flags.rbs +10 -0
  386. data/sig/pvectl/commands/shutdown.rbs +11 -0
  387. data/sig/pvectl/commands/shutdown_container.rbs +9 -0
  388. data/sig/pvectl/commands/start.rbs +11 -0
  389. data/sig/pvectl/commands/start_container.rbs +9 -0
  390. data/sig/pvectl/commands/stop.rbs +11 -0
  391. data/sig/pvectl/commands/stop_container.rbs +9 -0
  392. data/sig/pvectl/commands/suspend.rbs +11 -0
  393. data/sig/pvectl/commands/template_command.rbs +21 -0
  394. data/sig/pvectl/commands/template_container.rbs +10 -0
  395. data/sig/pvectl/commands/template_vm.rbs +12 -0
  396. data/sig/pvectl/commands/top/command.rbs +31 -0
  397. data/sig/pvectl/commands/top/handlers/containers.rbs +21 -0
  398. data/sig/pvectl/commands/top/handlers/nodes.rbs +21 -0
  399. data/sig/pvectl/commands/top/handlers/vms.rbs +21 -0
  400. data/sig/pvectl/commands/top/resource_handler.rbs +11 -0
  401. data/sig/pvectl/commands/top/resource_registry.rbs +8 -0
  402. data/sig/pvectl/commands/unlink_disk_vm.rbs +27 -0
  403. data/sig/pvectl/commands/vm_lifecycle_command.rbs +25 -0
  404. data/sig/pvectl/commands/wakeonlan_node.rbs +21 -0
  405. data/sig/pvectl/config/errors.rbs +24 -0
  406. data/sig/pvectl/config/models/cluster.rbs +39 -0
  407. data/sig/pvectl/config/models/context.rbs +23 -0
  408. data/sig/pvectl/config/models/resolved_config.rbs +51 -0
  409. data/sig/pvectl/config/models/user.rbs +31 -0
  410. data/sig/pvectl/config/provider.rbs +40 -0
  411. data/sig/pvectl/config/service.rbs +65 -0
  412. data/sig/pvectl/config/store.rbs +14 -0
  413. data/sig/pvectl/config/wizard.rbs +48 -0
  414. data/sig/pvectl/config_serializer.rbs +121 -0
  415. data/sig/pvectl/connection/retry_handler.rbs +31 -0
  416. data/sig/pvectl/connection.rbs +35 -0
  417. data/sig/pvectl/console/terminal_session.rbs +63 -0
  418. data/sig/pvectl/editor_session.rbs +33 -0
  419. data/sig/pvectl/exit_codes.rbs +19 -0
  420. data/sig/pvectl/formatters/base.rbs +13 -0
  421. data/sig/pvectl/formatters/color_support.rbs +13 -0
  422. data/sig/pvectl/formatters/json.rbs +7 -0
  423. data/sig/pvectl/formatters/output_helper.rbs +9 -0
  424. data/sig/pvectl/formatters/registry.rbs +13 -0
  425. data/sig/pvectl/formatters/table.rbs +25 -0
  426. data/sig/pvectl/formatters/wide.rbs +15 -0
  427. data/sig/pvectl/formatters/yaml.rbs +7 -0
  428. data/sig/pvectl/manifest_serializer.rbs +18 -0
  429. data/sig/pvectl/models/apt_package.rbs +26 -0
  430. data/sig/pvectl/models/backup.rbs +31 -0
  431. data/sig/pvectl/models/base.rbs +11 -0
  432. data/sig/pvectl/models/capability.rbs +16 -0
  433. data/sig/pvectl/models/container.rbs +44 -0
  434. data/sig/pvectl/models/container_operation_result.rbs +9 -0
  435. data/sig/pvectl/models/dns_config.rbs +15 -0
  436. data/sig/pvectl/models/hosts_file.rbs +13 -0
  437. data/sig/pvectl/models/journal_entry.rbs +10 -0
  438. data/sig/pvectl/models/network_interface.rbs +20 -0
  439. data/sig/pvectl/models/node.rbs +47 -0
  440. data/sig/pvectl/models/node_operation_result.rbs +12 -0
  441. data/sig/pvectl/models/operation_result.rbs +21 -0
  442. data/sig/pvectl/models/physical_disk.rbs +35 -0
  443. data/sig/pvectl/models/service.rbs +18 -0
  444. data/sig/pvectl/models/snapshot.rbs +21 -0
  445. data/sig/pvectl/models/snapshot_description.rbs +18 -0
  446. data/sig/pvectl/models/storage.rbs +39 -0
  447. data/sig/pvectl/models/subscription.rbs +24 -0
  448. data/sig/pvectl/models/syslog_entry.rbs +10 -0
  449. data/sig/pvectl/models/task.rbs +22 -0
  450. data/sig/pvectl/models/task_entry.rbs +24 -0
  451. data/sig/pvectl/models/task_log_line.rbs +10 -0
  452. data/sig/pvectl/models/time_config.rbs +12 -0
  453. data/sig/pvectl/models/vm.rbs +32 -0
  454. data/sig/pvectl/models/vm_operation_result.rbs +9 -0
  455. data/sig/pvectl/models/volume.rbs +29 -0
  456. data/sig/pvectl/models/volume_operation_result.rbs +9 -0
  457. data/sig/pvectl/parsers/cloud_init_config.rbs +15 -0
  458. data/sig/pvectl/parsers/disk_config.rbs +19 -0
  459. data/sig/pvectl/parsers/lxc_mount_config.rbs +19 -0
  460. data/sig/pvectl/parsers/lxc_net_config.rbs +19 -0
  461. data/sig/pvectl/parsers/net_config.rbs +19 -0
  462. data/sig/pvectl/parsers/smart_text.rbs +7 -0
  463. data/sig/pvectl/plugin_loader.rbs +25 -0
  464. data/sig/pvectl/presenters/apt_package.rbs +19 -0
  465. data/sig/pvectl/presenters/backup.rbs +25 -0
  466. data/sig/pvectl/presenters/base.rbs +41 -0
  467. data/sig/pvectl/presenters/capability.rbs +19 -0
  468. data/sig/pvectl/presenters/config/context.rbs +17 -0
  469. data/sig/pvectl/presenters/container.rbs +78 -0
  470. data/sig/pvectl/presenters/container_operation_result.rbs +19 -0
  471. data/sig/pvectl/presenters/disk.rbs +31 -0
  472. data/sig/pvectl/presenters/dns_config.rbs +13 -0
  473. data/sig/pvectl/presenters/hosts_file.rbs +13 -0
  474. data/sig/pvectl/presenters/journal_entry.rbs +11 -0
  475. data/sig/pvectl/presenters/node.rbs +118 -0
  476. data/sig/pvectl/presenters/node_operation_result.rbs +11 -0
  477. data/sig/pvectl/presenters/operation_result.rbs +17 -0
  478. data/sig/pvectl/presenters/service.rbs +15 -0
  479. data/sig/pvectl/presenters/snapshot.rbs +35 -0
  480. data/sig/pvectl/presenters/snapshot_operation_result.rbs +27 -0
  481. data/sig/pvectl/presenters/storage.rbs +59 -0
  482. data/sig/pvectl/presenters/subscription.rbs +36 -0
  483. data/sig/pvectl/presenters/syslog_entry.rbs +11 -0
  484. data/sig/pvectl/presenters/task_entry.rbs +21 -0
  485. data/sig/pvectl/presenters/task_log_line.rbs +11 -0
  486. data/sig/pvectl/presenters/template.rbs +15 -0
  487. data/sig/pvectl/presenters/time_config.rbs +19 -0
  488. data/sig/pvectl/presenters/top_container.rbs +17 -0
  489. data/sig/pvectl/presenters/top_node.rbs +17 -0
  490. data/sig/pvectl/presenters/top_presenter.rbs +13 -0
  491. data/sig/pvectl/presenters/top_vm.rbs +17 -0
  492. data/sig/pvectl/presenters/vm.rbs +91 -0
  493. data/sig/pvectl/presenters/vm_operation_result.rbs +19 -0
  494. data/sig/pvectl/presenters/volume.rbs +23 -0
  495. data/sig/pvectl/presenters/volume_operation_result.rbs +11 -0
  496. data/sig/pvectl/repositories/apt.rbs +17 -0
  497. data/sig/pvectl/repositories/backup.rbs +27 -0
  498. data/sig/pvectl/repositories/base.rbs +23 -0
  499. data/sig/pvectl/repositories/capabilities.rbs +20 -0
  500. data/sig/pvectl/repositories/container.rbs +63 -0
  501. data/sig/pvectl/repositories/disk.rbs +17 -0
  502. data/sig/pvectl/repositories/dns.rbs +13 -0
  503. data/sig/pvectl/repositories/hosts.rbs +13 -0
  504. data/sig/pvectl/repositories/journal.rbs +7 -0
  505. data/sig/pvectl/repositories/node.rbs +68 -0
  506. data/sig/pvectl/repositories/service.rbs +27 -0
  507. data/sig/pvectl/repositories/snapshot.rbs +19 -0
  508. data/sig/pvectl/repositories/storage.rbs +37 -0
  509. data/sig/pvectl/repositories/subscription.rbs +17 -0
  510. data/sig/pvectl/repositories/syslog.rbs +7 -0
  511. data/sig/pvectl/repositories/task.rbs +19 -0
  512. data/sig/pvectl/repositories/task_list.rbs +7 -0
  513. data/sig/pvectl/repositories/task_log.rbs +11 -0
  514. data/sig/pvectl/repositories/time_config.rbs +13 -0
  515. data/sig/pvectl/repositories/vm.rbs +85 -0
  516. data/sig/pvectl/repositories/volume.rbs +43 -0
  517. data/sig/pvectl/selectors/base.rbs +37 -0
  518. data/sig/pvectl/selectors/container.rbs +19 -0
  519. data/sig/pvectl/selectors/disk.rbs +13 -0
  520. data/sig/pvectl/selectors/vm.rbs +19 -0
  521. data/sig/pvectl/selectors/volume.rbs +13 -0
  522. data/sig/pvectl/services/backup.rbs +27 -0
  523. data/sig/pvectl/services/clone_container.rbs +35 -0
  524. data/sig/pvectl/services/clone_vm.rbs +35 -0
  525. data/sig/pvectl/services/cloudinit.rbs +19 -0
  526. data/sig/pvectl/services/console.rbs +23 -0
  527. data/sig/pvectl/services/container_lifecycle.rbs +26 -0
  528. data/sig/pvectl/services/create_container.rbs +64 -0
  529. data/sig/pvectl/services/create_vm.rbs +72 -0
  530. data/sig/pvectl/services/edit_container.rbs +17 -0
  531. data/sig/pvectl/services/edit_dns.rbs +23 -0
  532. data/sig/pvectl/services/edit_hosts.rbs +13 -0
  533. data/sig/pvectl/services/edit_node.rbs +21 -0
  534. data/sig/pvectl/services/edit_vm.rbs +17 -0
  535. data/sig/pvectl/services/edit_volume.rbs +18 -0
  536. data/sig/pvectl/services/get/resource_service.rbs +23 -0
  537. data/sig/pvectl/services/move_disk.rbs +21 -0
  538. data/sig/pvectl/services/pull_config.rbs +18 -0
  539. data/sig/pvectl/services/push_config.rbs +37 -0
  540. data/sig/pvectl/services/resize_volume.rbs +47 -0
  541. data/sig/pvectl/services/resource_delete.rbs +27 -0
  542. data/sig/pvectl/services/resource_migration.rbs +29 -0
  543. data/sig/pvectl/services/sendkey.rbs +19 -0
  544. data/sig/pvectl/services/service_lifecycle.rbs +17 -0
  545. data/sig/pvectl/services/set_container.rbs +14 -0
  546. data/sig/pvectl/services/set_node.rbs +23 -0
  547. data/sig/pvectl/services/set_vm.rbs +14 -0
  548. data/sig/pvectl/services/set_volume.rbs +12 -0
  549. data/sig/pvectl/services/snapshot.rbs +35 -0
  550. data/sig/pvectl/services/task_listing.rbs +13 -0
  551. data/sig/pvectl/services/unlink_disk.rbs +17 -0
  552. data/sig/pvectl/services/vm_lifecycle.rbs +26 -0
  553. data/sig/pvectl/services/wakeonlan.rbs +17 -0
  554. data/sig/pvectl/utils/resource_resolver.rbs +17 -0
  555. data/sig/pvectl/wizards/create_container.rbs +21 -0
  556. data/sig/pvectl/wizards/create_vm.rbs +21 -0
  557. data/sig/pvectl.rbs +9 -0
  558. metadata +675 -0
@@ -0,0 +1,524 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pvectl
4
+ module Services
5
+ # Orchestrates pushing YAML manifests to the Proxmox cluster.
6
+ # Implements a two-phase approach: prepare (validate + diff) then apply.
7
+ #
8
+ # @example Push a single manifest
9
+ # service = PushConfig.new(vm_repository: vm_repo, container_repository: ct_repo)
10
+ # result = service.prepare(yaml_string)
11
+ # service.apply(result[:plans]) unless result[:plans].empty?
12
+ class PushConfig
13
+ DEFAULT_TASK_TIMEOUT = 120
14
+
15
+ # @param vm_repository [Repositories::Vm] VM repository
16
+ # @param container_repository [Repositories::Container] container repository
17
+ # @param task_repository [Repositories::Task, nil] task repository for tracking async operations
18
+ def initialize(vm_repository:, container_repository:, task_repository: nil)
19
+ @vm_repository = vm_repository
20
+ @container_repository = container_repository
21
+ @task_repository = task_repository
22
+ end
23
+
24
+ # Prepares a push plan from a single YAML manifest string.
25
+ # Validates the manifest, determines update vs create, computes diff.
26
+ #
27
+ # @param yaml_string [String] YAML manifest content
28
+ # @return [Hash] { plans: Array<Hash>, errors: Array<String> }
29
+ def prepare(yaml_string)
30
+ errors = ManifestSerializer.validate(yaml_string)
31
+ return { plans: [], errors: errors } unless errors.empty?
32
+
33
+ manifest = ManifestSerializer.from_yaml(yaml_string)
34
+ type = manifest[:type]
35
+ metadata = manifest[:metadata]
36
+ spec = manifest[:spec]
37
+ vmid = metadata[:vmid]
38
+ repo = repository_for(type)
39
+
40
+ # Convert nested spec to flat config
41
+ flat_from_manifest = ConfigSerializer.from_nested(spec, type: type)
42
+
43
+ # No VMID → always create with auto-allocated ID
44
+ unless vmid
45
+ return prepare_create(type, metadata, flat_from_manifest, repo, auto_id: true)
46
+ end
47
+
48
+ # Check if resource exists (update) or not (create)
49
+ resource = repo.get(vmid)
50
+
51
+ if resource
52
+ # UPDATE path: fetch current config, compute diff
53
+ current_config = repo.fetch_config(resource.node, vmid)
54
+ original_flat = ConfigSerializer.from_nested(
55
+ ConfigSerializer.to_nested(current_config, type: type), type: type
56
+ )
57
+
58
+ # Filter nil/empty values from manifest (treated as "not specified").
59
+ # YAML null or empty strings mean the user didn't set the value.
60
+ flat_from_manifest = flat_from_manifest.reject { |_, v| v.nil? || (v.is_a?(String) && v.empty?) }
61
+
62
+ # Complete manifest's complex values with sub-properties from API.
63
+ # When a manifest omits sub-properties (volume, MAC, size) the API
64
+ # values fill them in, preventing false diffs from partial specs.
65
+ flat_from_manifest = ConfigSerializer.complete_from_api(flat_from_manifest, original_flat, type: type)
66
+
67
+ # Collect readonly keys and strip them from both sides.
68
+ readonly_keys = collect_readonly_keys(flat_from_manifest.merge(original_flat), type)
69
+ comparable_manifest = flat_from_manifest.reject { |k, _| readonly_keys.include?(k) }
70
+
71
+ # Only compare API keys that are also present in manifest.
72
+ # Keys only in API are "not specified" and should not generate diffs.
73
+ comparable_original = original_flat.select { |k, _| comparable_manifest.key?(k) }
74
+
75
+ diff = ConfigSerializer.diff(comparable_original, comparable_manifest)
76
+
77
+ if diff[:changed].empty? && diff[:added].empty? && diff[:removed].empty?
78
+ return { plans: [], errors: [], no_changes: true, vmid: vmid, type: type }
79
+ end
80
+
81
+ update_result = build_update_params(diff, current_config, type)
82
+
83
+ plan = {
84
+ action: :update,
85
+ type: type,
86
+ vmid: vmid,
87
+ node: resource.node,
88
+ diff: diff,
89
+ params: update_result[:params],
90
+ resize_ops: update_result[:resize_ops]
91
+ }
92
+
93
+ { plans: [plan], errors: [] }
94
+ else
95
+ prepare_create(type, metadata, flat_from_manifest, repo, vmid: vmid)
96
+ end
97
+ rescue StandardError => e
98
+ { plans: [], errors: [e.message] }
99
+ end
100
+
101
+ # Prepares push plans from multiple YAML contents.
102
+ #
103
+ # @param yaml_contents [Array<Hash>] array of { filename: String, content: String }
104
+ # @param filter_type [Symbol, nil] optional type filter (:vm or :container)
105
+ # @return [Hash] { plans: Array<Hash>, errors: Array<String>, skipped: Array<String> }
106
+ def prepare_batch(yaml_contents, filter_type: nil)
107
+ plans = []
108
+ errors = []
109
+ skipped = []
110
+ unchanged = []
111
+
112
+ yaml_contents.each do |entry|
113
+ filename = entry[:filename]
114
+ content = entry[:content]
115
+
116
+ # Pre-check kind filter before full prepare
117
+ if filter_type
118
+ begin
119
+ parsed = YAML.safe_load(content)
120
+ kind = ManifestSerializer::KINDS_REVERSE[parsed&.dig("kind")]
121
+ if kind && kind != filter_type
122
+ skipped << "#{filename}: skipped (kind #{parsed['kind']} doesn't match filter)"
123
+ next
124
+ end
125
+ rescue Psych::SyntaxError
126
+ # Will be caught by prepare
127
+ end
128
+ end
129
+
130
+ result = prepare(content)
131
+
132
+ if result[:no_changes]
133
+ skipped << "#{filename}: no changes"
134
+ unchanged << { vmid: result[:vmid], type: result[:type], source_path: entry[:path] }
135
+ next
136
+ end
137
+
138
+ result[:plans].each do |p|
139
+ p[:filename] = filename
140
+ p[:source_path] = entry[:path]
141
+ end
142
+ plans.concat(result[:plans])
143
+ errors.concat(result[:errors].map { |e| "#{filename}: #{e}" })
144
+ end
145
+
146
+ { plans: plans, errors: errors, skipped: skipped, unchanged: unchanged }
147
+ end
148
+
149
+ # Applies prepared plans (executes API calls).
150
+ # Tracks async task completion for resize and create operations.
151
+ #
152
+ # @param plans [Array<Hash>] plans from prepare/prepare_batch
153
+ # @return [Hash] { results: Array<Hash>, errors: Array<String> }
154
+ def apply(plans)
155
+ results = []
156
+ errors = []
157
+
158
+ plans.each do |plan|
159
+ begin
160
+ repo = repository_for(plan[:type])
161
+
162
+ if plan[:action] == :update
163
+ config_params = plan[:params].reject { |k, _| k == :digest }
164
+ unless config_params.empty?
165
+ repo.update(plan[:vmid], plan[:node], plan[:params])
166
+ end
167
+
168
+ resize_errors = apply_resize_ops(repo, plan)
169
+ if resize_errors.any?
170
+ resize_errors.each { |e| errors << "Error resizing #{type_label(plan[:type])} #{plan[:vmid]}: #{e}" }
171
+ results << { action: :update, vmid: plan[:vmid], type: plan[:type], success: false, error: resize_errors.join("; ") }
172
+ else
173
+ results << { action: :update, vmid: plan[:vmid], type: plan[:type], success: true }
174
+ end
175
+ elsif plan[:action] == :create
176
+ upid = repo.create(plan[:node], plan[:vmid], plan[:params])
177
+ task = wait_for_task(upid)
178
+
179
+ if task&.failed?
180
+ error_msg = task.exitstatus
181
+ errors << "Error creating #{type_label(plan[:type])} #{plan[:vmid]}: #{error_msg}"
182
+ results << { action: :create, vmid: plan[:vmid], type: plan[:type], success: false, error: error_msg }
183
+ else
184
+ results << {
185
+ action: :create, vmid: plan[:vmid], type: plan[:type], success: true,
186
+ auto_id: plan[:auto_id], source_path: plan[:source_path]
187
+ }
188
+ end
189
+ end
190
+ rescue ProxmoxAPI::ApiException => e
191
+ detail = extract_api_error(e)
192
+ errors << "Error applying #{plan[:action]} for #{type_label(plan[:type])} #{plan[:vmid]}: #{detail}"
193
+ results << { action: plan[:action], vmid: plan[:vmid], type: plan[:type], success: false, error: detail }
194
+ rescue StandardError => e
195
+ errors << "Error applying #{plan[:action]} for #{type_label(plan[:type])} #{plan[:vmid]}: #{e.message}"
196
+ results << { action: plan[:action], vmid: plan[:vmid], type: plan[:type], success: false, error: e.message }
197
+ end
198
+ end
199
+
200
+ { results: results, errors: errors }
201
+ end
202
+
203
+ private
204
+
205
+ # Prepares a create plan, optionally allocating a VMID.
206
+ #
207
+ # @param type [Symbol] :vm or :container
208
+ # @param metadata [Hash] manifest metadata
209
+ # @param flat_config [Hash] flat config from manifest spec
210
+ # @param repo [Repositories::Vm, Repositories::Container] repository
211
+ # @param vmid [Integer, nil] explicit VMID (nil when auto_id)
212
+ # @param auto_id [Boolean] whether to auto-allocate a VMID
213
+ # @return [Hash] { plans: Array<Hash>, errors: Array<String> }
214
+ def prepare_create(type, metadata, flat_config, repo, vmid: nil, auto_id: false)
215
+ node = metadata[:node]
216
+ unless node
217
+ label = vmid ? "VMID #{vmid}" : "new resource"
218
+ return { plans: [], errors: ["Node is required for creating #{label}"] }
219
+ end
220
+
221
+ if auto_id
222
+ vmid = allocate_vmid(repo, type)
223
+ end
224
+
225
+ create_config = transform_disks_for_create(flat_config, type)
226
+ create_config = create_config.reject { |_, v| v.nil? || (v.is_a?(String) && v.empty?) }
227
+
228
+ plan = {
229
+ action: :create,
230
+ type: type,
231
+ vmid: vmid,
232
+ node: node,
233
+ params: create_config,
234
+ auto_id: auto_id
235
+ }
236
+
237
+ { plans: [plan], errors: [] }
238
+ end
239
+
240
+ # Allocates the next available VMID from the repository.
241
+ #
242
+ # @param repo [Repositories::Vm, Repositories::Container] repository
243
+ # @param type [Symbol] :vm or :container
244
+ # @return [Integer] next available VMID
245
+ def allocate_vmid(repo, type)
246
+ if type == :container
247
+ repo.next_available_ctid
248
+ else
249
+ repo.next_available_vmid
250
+ end
251
+ end
252
+
253
+ # Returns the appropriate repository for the given resource type.
254
+ #
255
+ # @param type [Symbol] :vm or :container
256
+ # @return [Repositories::Vm, Repositories::Container]
257
+ def repository_for(type)
258
+ type == :container ? @container_repository : @vm_repository
259
+ end
260
+
261
+ # Builds flat update params from a diff and original config.
262
+ # Includes changed keys, added keys, and a delete list for removed keys.
263
+ # Preserves the digest from original config for optimistic locking.
264
+ # Extracts disk resize operations into a separate list (Proxmox requires
265
+ # the dedicated /resize endpoint for actual disk size changes).
266
+ #
267
+ # @param diff [Hash] diff from ConfigSerializer.diff
268
+ # @param original_config [Hash] original flat config from API
269
+ # @param type [Symbol] resource type (:vm or :container)
270
+ # @return [Hash] { params: Hash, resize_ops: Array<Hash> }
271
+ def build_update_params(diff, original_config, type)
272
+ params = {}
273
+ resize_ops = []
274
+
275
+ diff[:changed].each do |key, (old_val, new_val)|
276
+ if vm_disk_key?(key) && type == :vm
277
+ old_size = extract_disk_size(old_val.to_s)
278
+ new_size = extract_disk_size(new_val.to_s)
279
+
280
+ if old_size && new_size && old_size != new_size
281
+ resize_ops << { disk: key.to_s, size: new_size }
282
+ # Check if other disk options changed besides size
283
+ if disk_value_without_size(old_val.to_s) != disk_value_without_size(new_val.to_s)
284
+ params[key] = replace_disk_size(new_val.to_s, old_size)
285
+ end
286
+ next
287
+ end
288
+ end
289
+ params[key] = new_val
290
+ end
291
+
292
+ diff[:added].each { |key, val| params[key] = val }
293
+ unless diff[:removed].empty?
294
+ params[:delete] = diff[:removed].map(&:to_s).join(",")
295
+ end
296
+ params[:digest] = original_config[:digest] if original_config[:digest]
297
+
298
+ { params: params, resize_ops: resize_ops }
299
+ end
300
+
301
+ # Applies disk resize operations from a plan.
302
+ # Waits for each resize task to complete and returns errors.
303
+ #
304
+ # @param repo [Repositories::Vm, Repositories::Container] repository
305
+ # @param plan [Hash] update plan with optional :resize_ops
306
+ # @return [Array<String>] list of error messages (empty if all succeeded)
307
+ def apply_resize_ops(repo, plan)
308
+ errors = []
309
+ return errors unless plan[:resize_ops]&.any?
310
+
311
+ plan[:resize_ops].each do |op|
312
+ upid = repo.resize(plan[:vmid], plan[:node], disk: op[:disk], size: op[:size])
313
+ task = wait_for_task(upid)
314
+ if task&.failed?
315
+ errors << "#{op[:disk]}: #{task.exitstatus}"
316
+ end
317
+ end
318
+
319
+ errors
320
+ end
321
+
322
+ # Waits for an async Proxmox task to complete.
323
+ # Returns nil when task_repository is not configured (fire-and-forget mode).
324
+ #
325
+ # @param upid [String, nil] task UPID
326
+ # @return [Models::Task, nil] completed task or nil
327
+ def wait_for_task(upid)
328
+ return nil unless @task_repository && upid
329
+
330
+ @task_repository.wait(upid, timeout: DEFAULT_TASK_TIMEOUT)
331
+ end
332
+
333
+ # Checks if a key is a VM disk key (scsi, ide, virtio, sata, efidisk, tpmstate).
334
+ #
335
+ # @param key [Symbol, String] config key
336
+ # @return [Boolean]
337
+ def vm_disk_key?(key)
338
+ ConfigSerializer::VM_COMPLEX_KEYS[:disk][:pattern].match?(key.to_s)
339
+ end
340
+
341
+ # Checks if a key is a disk key that needs create-format transformation.
342
+ # For VMs: scsi*, ide*, virtio*, sata*, efidisk*, tpmstate*
343
+ # For containers: rootfs, mp*
344
+ #
345
+ # @param key [Symbol, String] config key
346
+ # @param type [Symbol] :vm or :container
347
+ # @return [Boolean]
348
+ def create_disk_key?(key, type)
349
+ key_str = key.to_s
350
+ if type == :container
351
+ ConfigSerializer::CT_COMPLEX_KEYS[:rootfs][:pattern].match?(key_str) ||
352
+ ConfigSerializer::CT_COMPLEX_KEYS[:mp][:pattern].match?(key_str)
353
+ else
354
+ vm_disk_key?(key)
355
+ end
356
+ end
357
+
358
+ # Transforms disk values in flat config to Proxmox create API format.
359
+ # Replaces volume names with STORAGE_ID:SIZE_IN_GiB syntax.
360
+ #
361
+ # @param flat_config [Hash] flat config from manifest
362
+ # @param type [Symbol] :vm or :container
363
+ # @return [Hash] config with disk values in create format
364
+ def transform_disks_for_create(flat_config, type)
365
+ flat_config.each_with_object({}) do |(key, value), result|
366
+ if create_disk_key?(key, type) && value.is_a?(String)
367
+ result[key] = disk_value_for_create(value)
368
+ else
369
+ result[key] = value
370
+ end
371
+ end
372
+ end
373
+
374
+ # Converts a single disk config string to Proxmox create API format.
375
+ # Handles: regular disks, cloud-init, EFI/TPM, and empty CD-ROMs.
376
+ #
377
+ # @param value [String] disk config string (e.g. "local-lvm:vm-100-disk-0,size=8G,iothread=1")
378
+ # @return [String] create format (e.g. "local-lvm:8,iothread=1")
379
+ def disk_value_for_create(value)
380
+ parts = value.split(",")
381
+ first = parts.first.strip
382
+ storage, volume = first.split(":", 2)
383
+
384
+ # "none" storage = keep as-is (empty CD-ROM: "none,media=cdrom")
385
+ return value if storage == "none"
386
+
387
+ # Cloud-init disk (volume contains "cloudinit")
388
+ return "#{storage}:cloudinit" if volume&.include?("cloudinit")
389
+
390
+ # Already in create format (volume is a plain number)
391
+ return value if volume&.match?(/\A\d+(\.\d+)?\z/)
392
+
393
+ # Extract size from size= option
394
+ size_str = extract_disk_size(value)
395
+
396
+ if size_str
397
+ gib = size_to_gib(size_str)
398
+ elsif volume
399
+ # Has volume name but no size (e.g., efidisk, tpmstate) — use default
400
+ gib = "1"
401
+ elsif parts.any? { |p| p.strip.start_with?("media=") }
402
+ # No volume, no size, but has media= (e.g., cloud-init on real storage)
403
+ # The only valid case for storage + media=cdrom without volume is cloud-init
404
+ return "#{storage}:cloudinit"
405
+ else
406
+ # No volume, no size, no media — can't determine create format
407
+ return value
408
+ end
409
+
410
+ # Build create format: storage:size,options (without size= key)
411
+ options = parts[1..].map(&:strip).reject { |p| p.start_with?("size=") }
412
+ create_parts = ["#{storage}:#{gib}"]
413
+ create_parts.concat(options) if options.any?
414
+ create_parts.join(",")
415
+ end
416
+
417
+ # Converts a Proxmox size string to GiB number for create API.
418
+ #
419
+ # @param size_str [String] e.g. "8G", "32G", "1T"
420
+ # @return [String] size in GiB as a string number
421
+ def size_to_gib(size_str)
422
+ if size_str.end_with?("G")
423
+ size_str.chomp("G")
424
+ elsif size_str.end_with?("T")
425
+ (size_str.chomp("T").to_i * 1024).to_s
426
+ elsif size_str.end_with?("M")
427
+ (size_str.chomp("M").to_f / 1024).to_s
428
+ elsif size_str.end_with?("K")
429
+ "1"
430
+ else
431
+ size_str
432
+ end
433
+ end
434
+
435
+ # Extracts the size value from a Proxmox disk config string.
436
+ #
437
+ # @param disk_value [String] e.g. "local-lvm:vm-100-disk-0,size=8G,iothread=1"
438
+ # @return [String, nil] size value or nil if not found
439
+ def extract_disk_size(disk_value)
440
+ match = disk_value.match(/(?:^|,)size=([^,]+)/)
441
+ match ? match[1] : nil
442
+ end
443
+
444
+ # Returns a disk config string with the size= part removed.
445
+ #
446
+ # @param value [String] disk config string
447
+ # @return [String] string without size= component
448
+ def disk_value_without_size(value)
449
+ value.split(",").reject { |p| p.strip.start_with?("size=") }.join(",")
450
+ end
451
+
452
+ # Replaces the size value in a disk config string.
453
+ #
454
+ # @param value [String] disk config string
455
+ # @param size [String] new size value
456
+ # @return [String] string with replaced size
457
+ def replace_disk_size(value, size)
458
+ value.split(",").map { |p| p.strip.start_with?("size=") ? "size=#{size}" : p }.join(",")
459
+ end
460
+
461
+ # Collects read-only keys present in the given flat config.
462
+ # Uses ConfigSerializer section definitions to identify readonly fields.
463
+ #
464
+ # @param flat_config [Hash] flat config hash
465
+ # @param type [Symbol] :vm or :container
466
+ # @return [Array<Symbol>] read-only keys found in the config
467
+ def collect_readonly_keys(flat_config, type)
468
+ sections = type == :container ? ConfigSerializer::CONTAINER_SECTIONS : ConfigSerializer::VM_SECTIONS
469
+ readonly = []
470
+
471
+ each_leaf_section(sections) do |section_def|
472
+ section_def[:readonly].each do |ro|
473
+ if ro.is_a?(Regexp)
474
+ flat_config.each_key { |k| readonly << k if ro.match?(k.to_s) }
475
+ else
476
+ readonly << ro if flat_config.key?(ro)
477
+ end
478
+ end
479
+ end
480
+
481
+ readonly.uniq
482
+ end
483
+
484
+ # Yields each leaf section definition (non-wrapper) from the sections hash.
485
+ #
486
+ # @param sections [Hash] section mapping
487
+ # @yield [Hash] leaf section definition
488
+ # @return [void]
489
+ def each_leaf_section(sections)
490
+ sections.each_value do |section_def|
491
+ if section_def.key?(:static)
492
+ yield section_def
493
+ else
494
+ section_def.each_value { |sub_def| yield sub_def }
495
+ end
496
+ end
497
+ end
498
+
499
+ # Returns a human-readable label for the resource type.
500
+ #
501
+ # @param type [Symbol] :vm or :container
502
+ # @return [String] "VM" or "Container"
503
+ def type_label(type)
504
+ type == :container ? "Container" : "VM"
505
+ end
506
+
507
+ # Extracts detailed error info from a Proxmox API exception.
508
+ # Parses the JSON response body to find field-level error messages.
509
+ #
510
+ # @param exception [ProxmoxAPI::ApiException] API exception with response
511
+ # @return [String] human-readable error detail
512
+ def extract_api_error(exception)
513
+ body = JSON.parse(exception.response.body)
514
+ if body["errors"]
515
+ body["errors"].map { |k, v| "#{k}: #{v}" }.join("; ")
516
+ else
517
+ exception.message
518
+ end
519
+ rescue StandardError
520
+ exception.message
521
+ end
522
+ end
523
+ end
524
+ end