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,253 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pvectl
4
+ module Services
5
+ # Orchestrates volume resize operations for VMs and containers.
6
+ #
7
+ # Handles size parsing, preflight validation (current size comparison),
8
+ # and execution via the repository interface. Works polymorphically with
9
+ # both VM and Container repositories (they share #fetch_config and #resize).
10
+ #
11
+ # Two-phase operation:
12
+ # 1. {#preflight} — validates volume exists, computes new size, checks constraints
13
+ # 2. {#perform} — executes the actual resize via repository
14
+ #
15
+ # @example Basic resize flow
16
+ # parsed = ResizeVolume.parse_size("+10G")
17
+ # service = ResizeVolume.new(repository: vm_repo)
18
+ # info = service.preflight(100, "scsi0", parsed, node: "pve1")
19
+ # result = service.perform(100, "scsi0", parsed.raw, node: "pve1")
20
+ #
21
+ class ResizeVolume
22
+ # Raised when the specified volume key is not found in resource config.
23
+ class VolumeNotFoundError < StandardError; end
24
+
25
+ # Raised when absolute size is not larger than current disk size.
26
+ class SizeTooSmallError < StandardError; end
27
+
28
+ # Parsed size representation returned by {.parse_size}.
29
+ #
30
+ # @!attribute [r] relative
31
+ # @return [Boolean] true if size is relative (prefixed with +)
32
+ # @!attribute [r] value
33
+ # @return [String] clean size value without + prefix (e.g., "10G")
34
+ # @!attribute [r] raw
35
+ # @return [String] original size string for API (e.g., "+10G")
36
+ ParsedSize = Struct.new(:relative, :value, :raw, keyword_init: true) do
37
+ # Whether this is a relative size (increment).
38
+ #
39
+ # @return [Boolean]
40
+ def relative?
41
+ relative
42
+ end
43
+ end
44
+
45
+ # Size regex: optional +, digits with optional decimal, optional T/G/M/K suffix.
46
+ SIZE_PATTERN = /\A(\+)?(\d+(?:\.\d+)?)([TGMK])?\z/i
47
+
48
+ # Multipliers for converting units to megabytes (MB as base unit).
49
+ UNIT_MULTIPLIERS = {
50
+ "T" => 1024 * 1024,
51
+ "G" => 1024,
52
+ "M" => 1,
53
+ "K" => 1.0 / 1024
54
+ }.freeze
55
+
56
+ # Parses a size string into a {ParsedSize}.
57
+ #
58
+ # Accepts formats like "10G", "+10G", "1.5T", "512M", "+100".
59
+ # Suffix is uppercased. No suffix means raw number.
60
+ #
61
+ # @param size_str [String] size string to parse
62
+ # @return [ParsedSize] parsed size components
63
+ # @raise [ArgumentError] if format is invalid, empty, or negative
64
+ #
65
+ # @example Relative size
66
+ # ResizeVolume.parse_size("+10G")
67
+ # #=> ParsedSize(relative: true, value: "10G", raw: "+10G")
68
+ #
69
+ # @example Absolute size
70
+ # ResizeVolume.parse_size("50G")
71
+ # #=> ParsedSize(relative: false, value: "50G", raw: "50G")
72
+ def self.parse_size(size_str)
73
+ raise ArgumentError, "Size cannot be empty" if size_str.nil? || size_str.strip.empty?
74
+
75
+ match = SIZE_PATTERN.match(size_str.strip)
76
+ raise ArgumentError, "Invalid size format: #{size_str}" unless match
77
+
78
+ plus, number, suffix = match.captures
79
+ suffix = suffix&.upcase
80
+
81
+ raise ArgumentError, "Size must be positive: #{size_str}" if number.to_f <= 0
82
+
83
+ clean_value = "#{number}#{suffix}"
84
+ raw_value = "#{plus}#{number}#{suffix}"
85
+
86
+ ParsedSize.new(
87
+ relative: !plus.nil?,
88
+ value: clean_value,
89
+ raw: raw_value
90
+ )
91
+ end
92
+
93
+ # Creates a new ResizeVolume service.
94
+ #
95
+ # @param repository [Repositories::Vm, Repositories::Container] resource repository
96
+ def initialize(repository:)
97
+ @repository = repository
98
+ end
99
+
100
+ # Validates the resize operation and returns size information.
101
+ #
102
+ # Checks that the disk exists in the resource config, extracts current
103
+ # size, calculates new size, and validates constraints (absolute must
104
+ # be larger than current).
105
+ #
106
+ # @param id [Integer] resource identifier (VMID or CTID)
107
+ # @param disk [String] disk key (e.g., "scsi0", "rootfs", "mp0")
108
+ # @param parsed_size [ParsedSize] parsed size from {.parse_size}
109
+ # @param node [String] node name
110
+ # @return [Hash] preflight info with :disk, :current_size, :new_size
111
+ # @raise [VolumeNotFoundError] if volume not in config or size not extractable
112
+ # @raise [SizeTooSmallError] if absolute size <= current size
113
+ def preflight(id, disk, parsed_size, node:)
114
+ config = @repository.fetch_config(node, id)
115
+ current_size = extract_disk_size(config, disk, id)
116
+ new_size = calculate_new_size(current_size, parsed_size)
117
+ validate_new_size!(current_size, new_size, parsed_size)
118
+
119
+ { disk: disk, current_size: current_size, new_size: new_size }
120
+ end
121
+
122
+ # Executes the disk resize via repository.
123
+ #
124
+ # @param id [Integer] resource identifier (VMID or CTID)
125
+ # @param disk [String] disk key
126
+ # @param raw_size [String] size string for API (e.g., "+10G", "50G")
127
+ # @param node [String] node name
128
+ # @return [Models::OperationResult] operation result
129
+ def perform(id, disk, raw_size, node:)
130
+ @repository.resize(id, node, disk: disk, size: raw_size)
131
+ Models::OperationResult.new(
132
+ operation: :resize_volume,
133
+ success: true,
134
+ resource: { id: id, node: node, disk: disk, size: raw_size }
135
+ )
136
+ rescue StandardError => e
137
+ Models::OperationResult.new(
138
+ operation: :resize_volume,
139
+ success: false,
140
+ error: e.message,
141
+ resource: { id: id, node: node, disk: disk }
142
+ )
143
+ end
144
+
145
+ private
146
+
147
+ # Extracts the disk size from a config value string.
148
+ #
149
+ # Config values have formats like:
150
+ # - VM: "local-lvm:vm-100-disk-0,size=32G"
151
+ # - Container: "local-lvm:subvol-100-disk-0,size=8G"
152
+ # - Rootfs: "local-lvm:subvol-100-disk-0,size=8G"
153
+ #
154
+ # @param config [Hash] resource configuration
155
+ # @param disk [String] disk key to look up
156
+ # @param id [Integer] resource ID (for error messages)
157
+ # @return [String] current size (e.g., "32G")
158
+ # @raise [VolumeNotFoundError] if volume not found or size not extractable
159
+ def extract_disk_size(config, disk, id)
160
+ disk_value = config[disk.to_sym]
161
+ raise VolumeNotFoundError, "Volume '#{disk}' not found in config for resource #{id}" unless disk_value
162
+
163
+ size_match = disk_value.to_s.match(/size=(\d+(?:\.\d+)?[TGMK]?)/i)
164
+ raise VolumeNotFoundError, "Cannot determine size for volume '#{disk}' on resource #{id}" unless size_match
165
+
166
+ size_match[1]
167
+ end
168
+
169
+ # Calculates the new size after resize.
170
+ #
171
+ # For relative sizes, adds the increment to current size (converting
172
+ # units as needed). For absolute sizes, returns the parsed value directly.
173
+ #
174
+ # @param current_size [String] current size (e.g., "32G")
175
+ # @param parsed_size [ParsedSize] parsed size
176
+ # @return [String] new size (e.g., "42G")
177
+ def calculate_new_size(current_size, parsed_size)
178
+ if parsed_size.relative?
179
+ current_num, current_suffix = parse_size_components(current_size)
180
+ add_num, add_suffix = parse_size_components(parsed_size.value)
181
+
182
+ converted_add = convert_to_unit(add_num, add_suffix, current_suffix)
183
+ new_num = current_num + converted_add
184
+
185
+ format_size(new_num, current_suffix)
186
+ else
187
+ parsed_size.value
188
+ end
189
+ end
190
+
191
+ # Validates that the new size is larger than current for absolute resizes.
192
+ #
193
+ # Relative sizes always pass (Proxmox enforces positive increments).
194
+ # Absolute sizes must be strictly larger than current.
195
+ #
196
+ # @param current_size [String] current size
197
+ # @param new_size [String] new size
198
+ # @param parsed_size [ParsedSize] parsed size (to check if relative)
199
+ # @raise [SizeTooSmallError] if absolute size <= current
200
+ def validate_new_size!(current_size, new_size, parsed_size)
201
+ return if parsed_size.relative?
202
+
203
+ if size_to_bytes(new_size) <= size_to_bytes(current_size)
204
+ raise SizeTooSmallError,
205
+ "New size #{new_size} must be larger than current size #{current_size}"
206
+ end
207
+ end
208
+
209
+ # Parses size string into numeric value and unit suffix.
210
+ #
211
+ # @param size [String] size string (e.g., "32G", "1.5T", "100")
212
+ # @return [Array(Float, String)] number and suffix (defaults to "G")
213
+ def parse_size_components(size)
214
+ match = size.to_s.match(/\A(\d+(?:\.\d+)?)([TGMK])?\z/i)
215
+ return [0.0, "G"] unless match
216
+
217
+ [match[1].to_f, (match[2] || "G").upcase]
218
+ end
219
+
220
+ # Converts a value from one unit to another using MB as base.
221
+ #
222
+ # @param value [Float] numeric value
223
+ # @param from_unit [String] source unit (T, G, M, K)
224
+ # @param to_unit [String] target unit (T, G, M, K)
225
+ # @return [Float] converted value
226
+ def convert_to_unit(value, from_unit, to_unit)
227
+ mb_value = value * UNIT_MULTIPLIERS.fetch(from_unit, 1024)
228
+ mb_value / UNIT_MULTIPLIERS.fetch(to_unit, 1024)
229
+ end
230
+
231
+ # Formats a numeric value and suffix into a size string.
232
+ #
233
+ # Produces integer format when possible (e.g., "42G" not "42.0G").
234
+ #
235
+ # @param value [Float] numeric value
236
+ # @param suffix [String] unit suffix
237
+ # @return [String] formatted size (e.g., "42G")
238
+ def format_size(value, suffix)
239
+ formatted = value == value.to_i ? value.to_i.to_s : format("%.1f", value)
240
+ "#{formatted}#{suffix}"
241
+ end
242
+
243
+ # Converts a size string to bytes for comparison.
244
+ #
245
+ # @param size [String] size string (e.g., "32G")
246
+ # @return [Float] size in bytes
247
+ def size_to_bytes(size)
248
+ num, suffix = parse_size_components(size)
249
+ num * UNIT_MULTIPLIERS.fetch(suffix, 1024) * 1024 * 1024 # MB to bytes
250
+ end
251
+ end
252
+ end
253
+ end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pvectl
4
+ module Services
5
+ # Orchestrates deletion of VMs and containers.
6
+ #
7
+ # Handles validation, force-stop of running resources, and async/sync modes.
8
+ #
9
+ # @example Delete stopped VMs
10
+ # service = ResourceDelete.new(vm_repository: vm_repo, container_repository: ct_repo, task_repository: task_repo)
11
+ # results = service.execute(:vm, [vm1, vm2])
12
+ #
13
+ # @example Delete with force (stops running VMs first)
14
+ # service = ResourceDelete.new(..., options: { force: true })
15
+ # results = service.execute(:vm, [running_vm])
16
+ #
17
+ class ResourceDelete
18
+ DEFAULT_TIMEOUT = 60
19
+
20
+ # Creates a new ResourceDelete service.
21
+ #
22
+ # @param vm_repository [Repositories::Vm] VM repository
23
+ # @param container_repository [Repositories::Container] Container repository
24
+ # @param task_repository [Repositories::Task] Task repository
25
+ # @param options [Hash] Options (force, keep_disks, purge, timeout, async, fail_fast)
26
+ def initialize(vm_repository:, container_repository:, task_repository:, options: {})
27
+ @vm_repository = vm_repository
28
+ @container_repository = container_repository
29
+ @task_repository = task_repository
30
+ @options = options
31
+ end
32
+
33
+ # Executes delete operation on resources.
34
+ #
35
+ # @param resource_type [Symbol] :vm or :container
36
+ # @param resources [Array<Models::Vm, Models::Container>] Resources to delete
37
+ # @return [Array<Models::OperationResult>] Results for each resource
38
+ def execute(resource_type, resources)
39
+ @resource_type = resource_type
40
+ results = []
41
+
42
+ resources.each do |resource|
43
+ result = delete_single(resource)
44
+ results << result
45
+
46
+ break if @options[:fail_fast] && result.failed?
47
+ end
48
+
49
+ results
50
+ end
51
+
52
+ private
53
+
54
+ # Deletes a single resource.
55
+ #
56
+ # @param resource [Models::Vm, Models::Container] Resource to delete
57
+ # @return [Models::OperationResult] Result
58
+ def delete_single(resource)
59
+ # Check if running
60
+ if resource.status == "running"
61
+ return running_error(resource) unless @options[:force]
62
+
63
+ stop_result = stop_resource(resource)
64
+ return stop_result if stop_result.failed?
65
+ end
66
+
67
+ # Perform delete
68
+ perform_delete(resource)
69
+ rescue StandardError => e
70
+ build_result(resource,
71
+ operation: :delete,
72
+ success: false,
73
+ error: e.message
74
+ )
75
+ end
76
+
77
+ # Returns error for running resource.
78
+ #
79
+ # @param resource [Models::Vm, Models::Container] Resource
80
+ # @return [Models::OperationResult] Failed result
81
+ def running_error(resource)
82
+ type_name = @resource_type == :vm ? "VM" : "Container"
83
+ build_result(resource,
84
+ operation: :delete,
85
+ success: false,
86
+ error: "#{type_name} #{resource.vmid} is running. Stop it first or use --force"
87
+ )
88
+ end
89
+
90
+ # Stops a running resource.
91
+ #
92
+ # @param resource [Models::Vm, Models::Container] Resource
93
+ # @return [Models::OperationResult] Result
94
+ def stop_resource(resource)
95
+ repo = repository_for(@resource_type)
96
+ upid = repo.stop(resource.vmid, resource.node)
97
+ task = @task_repository.wait(upid, timeout: timeout)
98
+
99
+ if task.successful?
100
+ build_result(resource, operation: :stop, task: task, success: true)
101
+ else
102
+ build_result(resource,
103
+ operation: :delete,
104
+ success: false,
105
+ error: "Failed to stop: #{task.exitstatus}"
106
+ )
107
+ end
108
+ end
109
+
110
+ # Performs the actual delete operation.
111
+ #
112
+ # @param resource [Models::Vm, Models::Container] Resource
113
+ # @return [Models::OperationResult] Result
114
+ def perform_delete(resource)
115
+ repo = repository_for(@resource_type)
116
+ delete_opts = {
117
+ destroy_disks: !@options[:keep_disks],
118
+ purge: @options[:purge] || false,
119
+ force: false
120
+ }
121
+
122
+ upid = repo.delete(resource.vmid, resource.node, **delete_opts)
123
+
124
+ if @options[:async]
125
+ build_result(resource,
126
+ operation: :delete,
127
+ task_upid: upid,
128
+ success: :pending
129
+ )
130
+ else
131
+ task = @task_repository.wait(upid, timeout: timeout)
132
+ build_result(resource,
133
+ operation: :delete,
134
+ task: task,
135
+ success: task.successful?
136
+ )
137
+ end
138
+ end
139
+
140
+ # Builds typed OperationResult for the current resource type.
141
+ #
142
+ # @param resource [Models::Vm, Models::Container] Resource
143
+ # @param attrs [Hash] Result attributes
144
+ # @return [Models::VmOperationResult, Models::ContainerOperationResult] Typed result
145
+ def build_result(resource, **attrs)
146
+ if @resource_type == :vm
147
+ Models::VmOperationResult.new(vm: resource, **attrs)
148
+ else
149
+ Models::ContainerOperationResult.new(container: resource, **attrs)
150
+ end
151
+ end
152
+
153
+ # Returns the appropriate repository for resource type.
154
+ #
155
+ # @param type [Symbol] :vm or :container
156
+ # @return [Repositories::Vm, Repositories::Container] Repository
157
+ def repository_for(type)
158
+ type == :vm ? @vm_repository : @container_repository
159
+ end
160
+
161
+ # Returns configured timeout.
162
+ #
163
+ # @return [Integer] Timeout in seconds
164
+ def timeout
165
+ @options[:timeout] || DEFAULT_TIMEOUT
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pvectl
4
+ module Services
5
+ # Orchestrates migration of VMs and containers between cluster nodes.
6
+ #
7
+ # Single service for both resource types, parameterized by resource_type.
8
+ # Async mode (default): returns UPID immediately, no blocking.
9
+ # Sync mode (--wait): polls Task until completion or timeout.
10
+ #
11
+ # @example Migrate VMs async (default)
12
+ # service = ResourceMigration.new(vm_repository: vm_repo, container_repository: ct_repo, task_repository: task_repo)
13
+ # results = service.execute(:vm, [vm1, vm2], target: "pve2")
14
+ #
15
+ # @example Migrate with sync wait
16
+ # service = ResourceMigration.new(..., options: { wait: true })
17
+ # results = service.execute(:vm, [vm], target: "pve2")
18
+ #
19
+ class ResourceMigration
20
+ DEFAULT_TIMEOUT = 600
21
+
22
+ # Creates a new ResourceMigration service.
23
+ #
24
+ # @param vm_repository [Repositories::Vm] VM repository
25
+ # @param container_repository [Repositories::Container] Container repository
26
+ # @param task_repository [Repositories::Task] Task repository
27
+ # @param options [Hash] Options (online, restart, target_storage, timeout, wait, fail_fast)
28
+ def initialize(vm_repository:, container_repository:, task_repository:, options: {})
29
+ @vm_repository = vm_repository
30
+ @container_repository = container_repository
31
+ @task_repository = task_repository
32
+ @options = options
33
+ end
34
+
35
+ # Executes migration operation on resources.
36
+ #
37
+ # @param resource_type [Symbol] :vm or :container
38
+ # @param resources [Array<Models::Vm, Models::Container>] Resources to migrate
39
+ # @param target [String] Target node name
40
+ # @return [Array<Models::OperationResult>] Results for each resource
41
+ def execute(resource_type, resources, target:)
42
+ @resource_type = resource_type
43
+
44
+ migratable, skipped = partition_by_target(resources, target)
45
+ report_skipped(skipped, target)
46
+ return all_on_target_results(target) if migratable.empty?
47
+
48
+ results = []
49
+ migratable.each do |resource|
50
+ result = migrate_single(resource, target)
51
+ results << result
52
+
53
+ break if @options[:fail_fast] && result.failed?
54
+ end
55
+
56
+ results
57
+ end
58
+
59
+ private
60
+
61
+ # Partitions resources into migratable and already-on-target groups.
62
+ #
63
+ # @param resources [Array] Resources to partition
64
+ # @param target [String] Target node name
65
+ # @return [Array<Array, Array>] [migratable, skipped]
66
+ def partition_by_target(resources, target)
67
+ resources.partition { |r| r.node != target }
68
+ end
69
+
70
+ # Reports skipped resources to stderr.
71
+ #
72
+ # @param skipped [Array] Resources already on target
73
+ # @param target [String] Target node name
74
+ # @return [void]
75
+ def report_skipped(skipped, target)
76
+ type_name = @resource_type == :vm ? "VM" : "container"
77
+ skipped.each do |r|
78
+ $stderr.puts "Skipping #{type_name} #{r.vmid} (already on #{target})"
79
+ end
80
+ end
81
+
82
+ # Handles case when all resources are already on target.
83
+ #
84
+ # @param target [String] Target node name
85
+ # @return [Array] Empty results array
86
+ def all_on_target_results(target)
87
+ $stderr.puts "All resources are already on target node #{target}"
88
+ []
89
+ end
90
+
91
+ # Migrates a single resource.
92
+ #
93
+ # @param resource [Models::Vm, Models::Container] Resource to migrate
94
+ # @param target [String] Target node name
95
+ # @return [Models::OperationResult] Result
96
+ def migrate_single(resource, target)
97
+ repo = repository_for(@resource_type)
98
+ params = build_migrate_params(target)
99
+ upid = repo.migrate(resource.vmid, resource.node, params)
100
+
101
+ if @options[:wait]
102
+ task = @task_repository.wait(upid, timeout: timeout)
103
+ build_result(resource,
104
+ operation: :migrate,
105
+ task: task,
106
+ success: task.successful?
107
+ )
108
+ else
109
+ build_result(resource,
110
+ operation: :migrate,
111
+ task_upid: upid,
112
+ success: :pending
113
+ )
114
+ end
115
+ rescue StandardError => e
116
+ build_result(resource,
117
+ operation: :migrate,
118
+ success: false,
119
+ error: e.message
120
+ )
121
+ end
122
+
123
+ # Builds migration parameters for the API call.
124
+ #
125
+ # @param target [String] Target node name
126
+ # @return [Hash] Migration parameters
127
+ def build_migrate_params(target)
128
+ params = { target: target }
129
+
130
+ if @options[:online]
131
+ params[:online] = 1
132
+ params[:"with-local-disks"] = 1 if @resource_type == :vm
133
+ end
134
+
135
+ params[:restart] = 1 if @options[:restart] && @resource_type == :container
136
+ params[:targetstorage] = @options[:target_storage] if @options[:target_storage]
137
+
138
+ params
139
+ end
140
+
141
+ # Builds typed OperationResult for the current resource type.
142
+ #
143
+ # @param resource [Models::Vm, Models::Container] Resource
144
+ # @param attrs [Hash] Result attributes
145
+ # @return [Models::VmOperationResult, Models::ContainerOperationResult] Typed result
146
+ def build_result(resource, **attrs)
147
+ if @resource_type == :vm
148
+ Models::VmOperationResult.new(vm: resource, **attrs)
149
+ else
150
+ Models::ContainerOperationResult.new(container: resource, **attrs)
151
+ end
152
+ end
153
+
154
+ # Returns the appropriate repository for resource type.
155
+ #
156
+ # @param type [Symbol] :vm or :container
157
+ # @return [Repositories::Vm, Repositories::Container] Repository
158
+ def repository_for(type)
159
+ type == :vm ? @vm_repository : @container_repository
160
+ end
161
+
162
+ # Returns configured timeout.
163
+ #
164
+ # @return [Integer] Timeout in seconds
165
+ def timeout
166
+ @options[:timeout] || DEFAULT_TIMEOUT
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pvectl
4
+ module Services
5
+ # Orchestrates QEMU monitor key-send operations for a single VM.
6
+ #
7
+ # Resolves the VM (and its node) via the repository, validates that the
8
+ # target VM is running, and forwards the key sequence verbatim to the
9
+ # Proxmox API. The key string is passed through unmodified — interpretation
10
+ # is delegated to QEMU's qcode parser.
11
+ #
12
+ # @example Sending Ctrl+Alt+Delete to a running VM
13
+ # service = Sendkey.new(vm_repository: vm_repo)
14
+ # result = service.execute(vmid: 100, key: "ctrl-alt-delete")
15
+ # result.successful? #=> true
16
+ #
17
+ class Sendkey
18
+ # Creates a new Sendkey service.
19
+ #
20
+ # @param vm_repository [Repositories::Vm] VM repository for lookup and key send
21
+ def initialize(vm_repository:)
22
+ @vm_repository = vm_repository
23
+ end
24
+
25
+ # Sends a QEMU key sequence to a VM.
26
+ #
27
+ # Looks up the VM (the +node+ kwarg is honored when provided, otherwise
28
+ # the node is taken from the resolved VM). Returns a failed
29
+ # +VmOperationResult+ when the VM cannot be found, is not running, or
30
+ # the underlying API call raises.
31
+ #
32
+ # @param vmid [Integer, String] VM identifier
33
+ # @param key [String] QEMU qcode key sequence (e.g., "ctrl-alt-delete")
34
+ # @param node [String, nil] optional node override
35
+ # @return [Models::VmOperationResult] result of the operation
36
+ def execute(vmid:, key:, node: nil)
37
+ vmid_int = vmid.to_i
38
+
39
+ vm = @vm_repository.get(vmid_int)
40
+ return not_found_result(vmid_int) unless vm
41
+
42
+ target_node = node || vm.node
43
+ return not_running_result(vm, target_node, key) unless vm.status == "running"
44
+
45
+ @vm_repository.sendkey(vmid_int, target_node, key)
46
+
47
+ success_result(vm, target_node, key)
48
+ rescue StandardError => e
49
+ failure_result(vm, target_node || vm&.node, key, e.message)
50
+ end
51
+
52
+ private
53
+
54
+ # @param vmid [Integer]
55
+ # @return [Models::VmOperationResult]
56
+ def not_found_result(vmid)
57
+ Models::VmOperationResult.new(
58
+ operation: :sendkey,
59
+ success: false,
60
+ error: "VM #{vmid} not found",
61
+ resource: { vmid: vmid }
62
+ )
63
+ end
64
+
65
+ # @param vm [Models::Vm]
66
+ # @param node [String]
67
+ # @param key [String]
68
+ # @return [Models::VmOperationResult]
69
+ def not_running_result(vm, node, key)
70
+ Models::VmOperationResult.new(
71
+ vm: vm,
72
+ operation: :sendkey,
73
+ success: false,
74
+ error: "VM #{vm.vmid} is not running (status: #{vm.status})",
75
+ resource: { vmid: vm.vmid, node: node, key: key }
76
+ )
77
+ end
78
+
79
+ # @param vm [Models::Vm]
80
+ # @param node [String]
81
+ # @param key [String]
82
+ # @return [Models::VmOperationResult]
83
+ def success_result(vm, node, key)
84
+ Models::VmOperationResult.new(
85
+ vm: vm,
86
+ operation: :sendkey,
87
+ success: true,
88
+ resource: { vmid: vm.vmid, node: node, key: key }
89
+ )
90
+ end
91
+
92
+ # @param vm [Models::Vm, nil]
93
+ # @param node [String, nil]
94
+ # @param key [String]
95
+ # @param message [String]
96
+ # @return [Models::VmOperationResult]
97
+ def failure_result(vm, node, key, message)
98
+ Models::VmOperationResult.new(
99
+ vm: vm,
100
+ operation: :sendkey,
101
+ success: false,
102
+ error: message,
103
+ resource: { vmid: vm&.vmid, node: node, key: key }
104
+ )
105
+ end
106
+ end
107
+ end
108
+ end