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,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pvectl
4
+ module Services
5
+ # Orchestrates non-interactive volume property updates.
6
+ #
7
+ # Handles two types of changes:
8
+ # 1. Size changes — delegates to ResizeVolume service (irreversible)
9
+ # 2. Config changes (cache, discard, ssd, etc.) — rebuilds volume config string
10
+ #
11
+ # @example Resize only
12
+ # service = SetVolume.new(repository: vm_repo, resource_type: :vm)
13
+ # result = service.execute(id: 100, disk: "scsi0", params: { "size" => "+10G" }, node: "pve1")
14
+ #
15
+ # @example Config change
16
+ # service.execute(id: 100, disk: "scsi0", params: { "cache" => "writeback" }, node: "pve1")
17
+ #
18
+ # @example Mixed (size + config)
19
+ # service.execute(id: 100, disk: "scsi0",
20
+ # params: { "size" => "+10G", "cache" => "writeback" }, node: "pve1")
21
+ #
22
+ class SetVolume
23
+ # @param repository [Repositories::Vm, Repositories::Container] resource repository
24
+ # @param resource_type [Symbol] :vm or :container
25
+ def initialize(repository:, resource_type:)
26
+ @repository = repository
27
+ @resource_type = resource_type
28
+ end
29
+
30
+ # Executes the volume property update.
31
+ #
32
+ # Separates size param (delegated to ResizeVolume) from config params
33
+ # (rebuilt into the disk config string). Both can be applied in a single call.
34
+ #
35
+ # @param id [Integer] resource ID (VMID or CTID)
36
+ # @param disk [String] disk name (e.g., "scsi0", "rootfs")
37
+ # @param params [Hash] key-value pairs to set
38
+ # @param node [String] node name
39
+ # @return [Models::VolumeOperationResult] operation result
40
+ def execute(id:, disk:, params:, node:)
41
+ config = @repository.fetch_config(node, id)
42
+ disk_value = config[disk.to_sym]
43
+
44
+ unless disk_value
45
+ return build_result(id, disk, node, success: false,
46
+ error: "Volume '#{disk}' not found in config for resource #{id}")
47
+ end
48
+
49
+ # Separate size from config params (dup to avoid mutating caller's hash)
50
+ params = params.dup
51
+ size_param = params.delete("size") || params.delete(:size)
52
+ config_params = params
53
+
54
+ # Handle size change (resize)
55
+ if size_param
56
+ parsed_size = ResizeVolume.parse_size(size_param)
57
+ resize_service = ResizeVolume.new(repository: @repository)
58
+ resize_service.preflight(id, disk, parsed_size, node: node)
59
+ resize_service.perform(id, disk, parsed_size.raw, node: node)
60
+ end
61
+
62
+ # Handle config changes (cache, discard, ssd, iothread, backup)
63
+ unless config_params.empty?
64
+ new_disk_value = rebuild_disk_config(disk_value, config_params)
65
+ @repository.update(id, node, { disk.to_sym => new_disk_value })
66
+ end
67
+
68
+ build_result(id, disk, node, success: true)
69
+ rescue ResizeVolume::VolumeNotFoundError, ResizeVolume::SizeTooSmallError, ArgumentError => e
70
+ build_result(id, disk, node, success: false, error: e.message)
71
+ rescue StandardError => e
72
+ build_result(id, disk, node, success: false, error: e.message)
73
+ end
74
+
75
+ private
76
+
77
+ # Rebuilds a disk config string with updated properties.
78
+ #
79
+ # Config format: "storage:volume-id,key1=val1,key2=val2"
80
+ # Replaces existing keys and appends new ones.
81
+ #
82
+ # @param current_value [String] current disk config string
83
+ # @param updates [Hash] key-value pairs to update
84
+ # @return [String] updated config string
85
+ def rebuild_disk_config(current_value, updates)
86
+ parts = current_value.to_s.split(",")
87
+ base = parts.shift # "storage:volume-id"
88
+
89
+ # Parse existing key=value pairs
90
+ existing = {}
91
+ parts.each do |part|
92
+ key, value = part.split("=", 2)
93
+ existing[key] = value
94
+ end
95
+
96
+ # Apply updates
97
+ updates.each do |key, value|
98
+ existing[key.to_s] = value.to_s
99
+ end
100
+
101
+ # Rebuild string
102
+ config_parts = existing.map { |k, v| "#{k}=#{v}" }
103
+ ([base] + config_parts).join(",")
104
+ end
105
+
106
+ # Builds a VolumeOperationResult with volume metadata.
107
+ #
108
+ # @param id [Integer] resource ID
109
+ # @param disk [String] disk name
110
+ # @param node [String] node name
111
+ # @param attrs [Hash] result attributes (:success, :error)
112
+ # @return [Models::VolumeOperationResult]
113
+ def build_result(id, disk, node, **attrs)
114
+ volume = Models::Volume.new(
115
+ name: disk,
116
+ resource_type: @resource_type.to_s,
117
+ resource_id: id,
118
+ node: node
119
+ )
120
+ Models::VolumeOperationResult.new(
121
+ operation: :set, volume: volume, **attrs
122
+ )
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,261 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pvectl
4
+ module Services
5
+ # Orchestrates snapshot operations.
6
+ #
7
+ # Handles listing, creating, deleting and rolling back snapshots
8
+ # for VMs and containers with unified interface.
9
+ #
10
+ # @example Basic usage
11
+ # service = Snapshot.new(
12
+ # snapshot_repo: snapshot_repo,
13
+ # resource_resolver: resolver,
14
+ # task_repo: task_repo
15
+ # )
16
+ # snapshots = service.list([100, 101])
17
+ #
18
+ class Snapshot
19
+ DEFAULT_TIMEOUT = 60
20
+
21
+ # Creates a new Snapshot service.
22
+ #
23
+ # @param snapshot_repo [Repositories::Snapshot] Snapshot repository
24
+ # @param resource_resolver [Utils::ResourceResolver] Resource resolver
25
+ # @param task_repo [Repositories::Task] Task repository
26
+ # @param options [Hash] Options (timeout, async, fail_fast)
27
+ def initialize(snapshot_repo:, resource_resolver:, task_repo:, options: {})
28
+ @snapshot_repo = snapshot_repo
29
+ @resolver = resource_resolver
30
+ @task_repo = task_repo
31
+ @options = options
32
+ end
33
+
34
+ # Lists snapshots for given VMIDs.
35
+ #
36
+ # When vmids is empty, lists snapshots for all resources in the cluster.
37
+ #
38
+ # @param vmids [Array<Integer>] VM/container IDs (empty = all)
39
+ # @param node [String, nil] filter by node name
40
+ # @return [Array<Models::Snapshot>] all snapshots
41
+ def list(vmids, node: nil)
42
+ resources = resolve_resources(vmids)
43
+ resources = filter_by_node(resources, node)
44
+ return [] if resources.empty?
45
+
46
+ resources.flat_map do |r|
47
+ @snapshot_repo.list(r[:vmid], r[:node], r[:type])
48
+ end
49
+ end
50
+
51
+ # Describes a snapshot by name across given VMIDs.
52
+ #
53
+ # When vmids is empty, searches all resources in the cluster.
54
+ # Returns a SnapshotDescription with entries for each VM/CT that has the snapshot.
55
+ #
56
+ # @param vmids [Array<Integer>] VM/container IDs (empty = search all)
57
+ # @param name [String] snapshot name to find
58
+ # @param node [String, nil] filter by node name
59
+ # @return [Models::SnapshotDescription] description with entries per VM
60
+ # @raise [ResourceNotFoundError] when snapshot not found
61
+ def describe(vmids, name, node: nil)
62
+ resources = resolve_resources(vmids)
63
+ resources = filter_by_node(resources, node)
64
+
65
+ if resources.empty?
66
+ message = vmids.empty? ? "no resources found in cluster" : "resource #{vmids.first} not found"
67
+ raise ResourceNotFoundError, message
68
+ end
69
+
70
+ entries = build_describe_entries(resources, name)
71
+
72
+ if entries.empty?
73
+ message = vmids.empty? ? "snapshot '#{name}' not found in cluster" : "snapshot '#{name}' not found on VM #{vmids.join(', ')}"
74
+ raise ResourceNotFoundError, message
75
+ end
76
+
77
+ Models::SnapshotDescription.new(entries: entries)
78
+ end
79
+
80
+ # Creates snapshots for given VMIDs, or all cluster resources when vmids is empty.
81
+ #
82
+ # When vmids is empty, creates snapshots for all resources in the cluster.
83
+ #
84
+ # @param vmids [Array<Integer>] VM/container IDs (empty = all)
85
+ # @param name [String] snapshot name
86
+ # @param description [String, nil] optional description
87
+ # @param vmstate [Boolean] save VM memory state
88
+ # @param node [String, nil] filter by node name
89
+ # @return [Array<Models::OperationResult>] results for each resource
90
+ def create(vmids, name:, description: nil, vmstate: false, node: nil)
91
+ resources = resolve_resources(vmids)
92
+ resources = filter_by_node(resources, node)
93
+ return [] if resources.empty?
94
+
95
+ execute_multi(resources, :create) do |r|
96
+ @snapshot_repo.create(r[:vmid], r[:node], r[:type], name: name, description: description, vmstate: vmstate)
97
+ end
98
+ end
99
+
100
+ # Deletes snapshots from given VMIDs, or all cluster resources when vmids is empty.
101
+ #
102
+ # When vmids is empty, deletes snapshots from all resources in the cluster.
103
+ #
104
+ # @param vmids [Array<Integer>] VM/container IDs (empty = all)
105
+ # @param snapname [String] snapshot name to delete
106
+ # @param force [Boolean] force removal even if disk snapshot fails
107
+ # @param node [String, nil] filter by node name
108
+ # @return [Array<Models::OperationResult>] results for each resource
109
+ def delete(vmids, snapname, force: false, node: nil)
110
+ resources = resolve_resources(vmids)
111
+ resources = filter_by_node(resources, node)
112
+ return [] if resources.empty?
113
+
114
+ execute_multi(resources, :delete) do |r|
115
+ @snapshot_repo.delete(r[:vmid], r[:node], r[:type], snapname, force: force)
116
+ end
117
+ end
118
+
119
+ # Deletes ALL snapshots from given VMIDs.
120
+ #
121
+ # When vmids is empty, deletes all snapshots from all resources in the cluster.
122
+ # Skips the "current" pseudo-snapshot.
123
+ #
124
+ # @param vmids [Array<Integer>] VM/container IDs (empty = all)
125
+ # @param node [String, nil] filter by node name
126
+ # @param force [Boolean] force removal even if disk snapshot fails
127
+ # @return [Array<Models::OperationResult>] results for each snapshot
128
+ def delete_all(vmids, node: nil, force: false)
129
+ resources = resolve_resources(vmids)
130
+ resources = filter_by_node(resources, node)
131
+ return [] if resources.empty?
132
+
133
+ results = []
134
+
135
+ resources.each do |r|
136
+ snapshots = @snapshot_repo.list(r[:vmid], r[:node], r[:type])
137
+ snapshots.reject! { |s| s.name == "current" }
138
+
139
+ snapshots.each do |snap|
140
+ result = execute_single(r, :delete) do
141
+ @snapshot_repo.delete(r[:vmid], r[:node], r[:type], snap.name, force: force)
142
+ end
143
+ results << result
144
+
145
+ break if @options[:fail_fast] && result.failed?
146
+ end
147
+
148
+ break if @options[:fail_fast] && results.last&.failed?
149
+ end
150
+
151
+ results
152
+ end
153
+
154
+ # Rolls back to a snapshot.
155
+ #
156
+ # @param vmid [Integer] VM/container ID
157
+ # @param snapname [String] snapshot name to rollback to
158
+ # @param start [Boolean] start after rollback (LXC only)
159
+ # @return [Models::OperationResult] result
160
+ def rollback(vmid, snapname, start: false)
161
+ resource = @resolver.resolve(vmid)
162
+
163
+ if resource.nil?
164
+ return Models::OperationResult.new(
165
+ resource: { vmid: vmid },
166
+ operation: :rollback,
167
+ success: false,
168
+ error: "Resource #{vmid} not found"
169
+ )
170
+ end
171
+
172
+ execute_single(resource, :rollback) do
173
+ @snapshot_repo.rollback(resource[:vmid], resource[:node], resource[:type], snapname, start: start)
174
+ end
175
+ end
176
+
177
+ private
178
+
179
+ # Resolves resources from VMIDs or returns all cluster resources.
180
+ #
181
+ # @param vmids [Array<Integer>] VM/container IDs (empty = resolve all)
182
+ # @return [Array<Hash>] resolved resources
183
+ def resolve_resources(vmids)
184
+ vmids.empty? ? @resolver.resolve_all : @resolver.resolve_multiple(vmids)
185
+ end
186
+
187
+ # Filters resources by node name.
188
+ #
189
+ # @param resources [Array<Hash>] resolved resources
190
+ # @param node [String, nil] node name to filter by (nil = no filter)
191
+ # @return [Array<Hash>] filtered resources
192
+ def filter_by_node(resources, node)
193
+ return resources unless node
194
+
195
+ resources.select { |r| r[:node] == node }
196
+ end
197
+
198
+ def build_describe_entries(resources, name)
199
+ entries = []
200
+
201
+ resources.each do |r|
202
+ siblings = @snapshot_repo.list(r[:vmid], r[:node], r[:type])
203
+ target = siblings.find { |s| s.name == name }
204
+ next unless target
205
+
206
+ entries << Models::SnapshotDescription::Entry.new(
207
+ snapshot: target,
208
+ siblings: siblings
209
+ )
210
+ end
211
+
212
+ entries
213
+ end
214
+
215
+ def execute_multi(resources, operation)
216
+ results = []
217
+
218
+ resources.each do |r|
219
+ result = execute_single(r, operation) { yield(r) }
220
+ results << result
221
+
222
+ break if @options[:fail_fast] && result.failed?
223
+ end
224
+
225
+ results
226
+ end
227
+
228
+ def execute_single(resource, operation)
229
+ upid = yield
230
+
231
+ if @options[:async]
232
+ Models::OperationResult.new(
233
+ resource: resource,
234
+ operation: operation,
235
+ task_upid: upid,
236
+ success: :pending
237
+ )
238
+ else
239
+ task = @task_repo.wait(upid, timeout: timeout)
240
+ Models::OperationResult.new(
241
+ resource: resource,
242
+ operation: operation,
243
+ task: task,
244
+ success: task.successful?
245
+ )
246
+ end
247
+ rescue StandardError => e
248
+ Models::OperationResult.new(
249
+ resource: resource,
250
+ operation: operation,
251
+ success: false,
252
+ error: e.message
253
+ )
254
+ end
255
+
256
+ def timeout
257
+ @options[:timeout] || DEFAULT_TIMEOUT
258
+ end
259
+ end
260
+ end
261
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pvectl
4
+ module Services
5
+ # Service for listing tasks across one or all cluster nodes.
6
+ #
7
+ # Encapsulates multi-node iteration, merge, sort, and limit logic.
8
+ # Used by both Get::Handlers::Tasks and Logs::Handlers::TaskLogs.
9
+ #
10
+ # @example List all tasks cluster-wide
11
+ # service = TaskListing.new(task_list_repository: repo, node_repository: node_repo)
12
+ # tasks = service.list(limit: 20)
13
+ #
14
+ # @example List tasks on a specific node
15
+ # tasks = service.list(node: "pve1", type_filter: "vzdump")
16
+ #
17
+ class TaskListing
18
+ # Creates a new TaskListing service.
19
+ #
20
+ # @param task_list_repository [Repositories::TaskList] task list repository
21
+ # @param node_repository [Repositories::Node] node repository for cluster discovery
22
+ def initialize(task_list_repository:, node_repository:)
23
+ @task_list_repository = task_list_repository
24
+ @node_repository = node_repository
25
+ end
26
+
27
+ # Lists tasks, optionally filtered.
28
+ #
29
+ # When node is nil, iterates all cluster nodes and merges results
30
+ # sorted by starttime descending.
31
+ #
32
+ # @param node [String, nil] specific node or nil for all nodes
33
+ # @param vmid [Integer, nil] filter by VMID
34
+ # @param limit [Integer] max entries (default 50)
35
+ # @param since [String, nil] start time filter
36
+ # @param until_time [String, nil] end time filter
37
+ # @param type_filter [String, nil] task type filter
38
+ # @param status_filter [String, nil] status filter
39
+ # @return [Array<Models::TaskEntry>] task entries
40
+ def list(node: nil, vmid: nil, limit: 50, since: nil, until_time: nil,
41
+ type_filter: nil, status_filter: nil)
42
+ if node
43
+ @task_list_repository.list(
44
+ node: node, vmid: vmid, limit: limit, since: since,
45
+ until_time: until_time, type_filter: type_filter,
46
+ status_filter: status_filter
47
+ )
48
+ else
49
+ list_all_nodes(
50
+ vmid: vmid, limit: limit, since: since,
51
+ until_time: until_time, type_filter: type_filter,
52
+ status_filter: status_filter
53
+ )
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ # Iterates all cluster nodes and merges results.
60
+ #
61
+ # @return [Array<Models::TaskEntry>] merged and sorted entries
62
+ def list_all_nodes(vmid:, limit:, since:, until_time:, type_filter:, status_filter:)
63
+ nodes = @node_repository.list.map(&:name)
64
+ entries = nodes.flat_map do |node_name|
65
+ @task_list_repository.list(
66
+ node: node_name, vmid: vmid, limit: limit, since: since,
67
+ until_time: until_time, type_filter: type_filter,
68
+ status_filter: status_filter
69
+ )
70
+ end
71
+ entries.sort_by { |e| -(e.starttime || 0) }.first(limit)
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pvectl
4
+ module Services
5
+ # Orchestrates disk unlinking from a VM configuration.
6
+ #
7
+ # Delegates to the VM repository to issue the unlink PUT request and
8
+ # wraps the outcome in a {Models::VmOperationResult}. The underlying
9
+ # Proxmox endpoint is synchronous and returns no UPID, so the result
10
+ # captures success/failure synchronously.
11
+ #
12
+ # @example Soft unlink (keeps volume as unused[n])
13
+ # service = UnlinkDisk.new(repository: vm_repo)
14
+ # result = service.execute(vmid: 100, node: "pve1", disk_ids: "scsi1")
15
+ #
16
+ # @example Hard delete via force
17
+ # service = UnlinkDisk.new(repository: vm_repo)
18
+ # result = service.execute(vmid: 100, node: "pve1", disk_ids: %w[scsi1 scsi2], force: true)
19
+ #
20
+ class UnlinkDisk
21
+ # Creates a new UnlinkDisk service.
22
+ #
23
+ # @param repository [Repositories::Vm] VM repository
24
+ def initialize(repository:)
25
+ @repository = repository
26
+ end
27
+
28
+ # Unlinks one or more disks from the VM configuration.
29
+ #
30
+ # @param vmid [Integer] VM identifier
31
+ # @param node [String] node name
32
+ # @param disk_ids [Array<String>, String] disk identifiers
33
+ # @param force [Boolean] physically remove the underlying volume(s)
34
+ # @return [Models::VmOperationResult] operation result
35
+ def execute(vmid:, node:, disk_ids:, force: false)
36
+ @repository.unlink_disks(node, vmid, disk_ids, force: force)
37
+ build_result(vmid, node, disk_ids, force, success: true)
38
+ rescue StandardError => e
39
+ build_result(vmid, node, disk_ids, force, success: false, error: e.message)
40
+ end
41
+
42
+ private
43
+
44
+ # Builds a VmOperationResult.
45
+ #
46
+ # @param vmid [Integer] VM identifier
47
+ # @param node [String] node name
48
+ # @param disk_ids [Array<String>, String] disk identifiers
49
+ # @param force [Boolean] force flag
50
+ # @param attrs [Hash] additional result attributes (:success, :error)
51
+ # @return [Models::VmOperationResult]
52
+ def build_result(vmid, node, disk_ids, force, **attrs)
53
+ vm = fetch_vm(vmid) || Models::Vm.new(vmid: vmid, node: node)
54
+ Models::VmOperationResult.new(
55
+ operation: :unlink_disk,
56
+ vm: vm,
57
+ resource: {
58
+ vmid: vmid,
59
+ node: node,
60
+ disk_ids: format_disk_ids(disk_ids),
61
+ force: force
62
+ },
63
+ **attrs
64
+ )
65
+ end
66
+
67
+ # Fetches the VM model for the result, swallowing any lookup errors.
68
+ #
69
+ # @param vmid [Integer] VM identifier
70
+ # @return [Models::Vm, nil] VM model or nil if unavailable
71
+ def fetch_vm(vmid)
72
+ @repository.get(vmid)
73
+ rescue StandardError
74
+ nil
75
+ end
76
+
77
+ # Formats disk_ids into a stable comma-separated representation.
78
+ #
79
+ # @param disk_ids [Array<String>, String] disk identifiers
80
+ # @return [String] comma-separated disk ID list
81
+ def format_disk_ids(disk_ids)
82
+ Array(disk_ids).flat_map { |id| id.to_s.split(",") }.map(&:strip).reject(&:empty?).join(",")
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pvectl
4
+ module Services
5
+ # Orchestrates VM lifecycle operations.
6
+ #
7
+ # Handles execution of start/stop/shutdown/restart/reset/suspend/resume
8
+ # operations with sync/async modes, error handling, and result collection.
9
+ #
10
+ # @example Basic usage
11
+ # service = VmLifecycle.new(vm_repo, task_repo)
12
+ # results = service.execute(:start, [vm1, vm2])
13
+ # results.each { |r| puts "#{r.vm.vmid}: #{r.status_text}" }
14
+ #
15
+ class VmLifecycle
16
+ SYNC_OPERATIONS = %i[start stop reset resume].freeze
17
+ ASYNC_OPERATIONS = %i[shutdown restart suspend].freeze
18
+ ALL_OPERATIONS = (SYNC_OPERATIONS + ASYNC_OPERATIONS).freeze
19
+
20
+ DEFAULT_TIMEOUT = 60
21
+
22
+ # Creates a new VmLifecycle service.
23
+ #
24
+ # @param vm_repository [Repositories::Vm] VM repository
25
+ # @param task_repository [Repositories::Task] Task repository
26
+ # @param options [Hash] Options (timeout, async, wait, fail_fast)
27
+ def initialize(vm_repository, task_repository, options = {})
28
+ @vm_repository = vm_repository
29
+ @task_repository = task_repository
30
+ @options = options
31
+ end
32
+
33
+ # Executes a lifecycle operation on a list of VMs.
34
+ #
35
+ # @param operation [Symbol] Operation to execute
36
+ # @param vms [Array<Models::Vm>] VMs to operate on
37
+ # @return [Array<Models::OperationResult>] Results for each VM
38
+ def execute(operation, vms)
39
+ validate_operation!(operation)
40
+
41
+ results = []
42
+ vms.each do |vm|
43
+ result = execute_single(operation, vm)
44
+ results << result
45
+
46
+ break if @options[:fail_fast] && result.failed?
47
+ end
48
+ results
49
+ end
50
+
51
+ private
52
+
53
+ # Executes operation on a single VM.
54
+ #
55
+ # @param operation [Symbol] Operation
56
+ # @param vm [Models::Vm] VM
57
+ # @return [Models::OperationResult] Result
58
+ def execute_single(operation, vm)
59
+ task_upid = call_api(operation, vm)
60
+
61
+ if sync_mode?(operation)
62
+ task = @task_repository.wait(task_upid, timeout: timeout)
63
+ Models::VmOperationResult.new(
64
+ vm: vm,
65
+ operation: operation,
66
+ task: task,
67
+ success: task.successful?
68
+ )
69
+ else
70
+ Models::VmOperationResult.new(
71
+ vm: vm,
72
+ operation: operation,
73
+ task_upid: task_upid,
74
+ success: :pending
75
+ )
76
+ end
77
+ rescue StandardError => e
78
+ Models::VmOperationResult.new(
79
+ vm: vm,
80
+ operation: operation,
81
+ success: false,
82
+ error: e.message
83
+ )
84
+ end
85
+
86
+ # Calls the appropriate API method.
87
+ #
88
+ # @param operation [Symbol] Operation
89
+ # @param vm [Models::Vm] VM
90
+ # @return [String] Task UPID
91
+ def call_api(operation, vm)
92
+ @vm_repository.send(operation, vm.vmid, vm.node)
93
+ end
94
+
95
+ # Determines if operation should run in sync mode.
96
+ #
97
+ # @param operation [Symbol] Operation
98
+ # @return [Boolean] true if sync mode
99
+ def sync_mode?(operation)
100
+ return false if @options[:async]
101
+ return true if @options[:wait]
102
+
103
+ SYNC_OPERATIONS.include?(operation)
104
+ end
105
+
106
+ # Returns configured timeout.
107
+ #
108
+ # @return [Integer] Timeout in seconds
109
+ def timeout
110
+ @options[:timeout] || DEFAULT_TIMEOUT
111
+ end
112
+
113
+ # Validates operation is supported.
114
+ #
115
+ # @param operation [Symbol] Operation
116
+ # @raise [ArgumentError] if operation is not supported
117
+ def validate_operation!(operation)
118
+ return if ALL_OPERATIONS.include?(operation)
119
+
120
+ raise ArgumentError, "Unknown operation: #{operation}. Valid: #{ALL_OPERATIONS.join(', ')}"
121
+ end
122
+ end
123
+ end
124
+ end