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,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pvectl
4
+ module Services
5
+ # Orchestrates the interactive editing flow for /etc/hosts on a node.
6
+ #
7
+ # Fetches current /etc/hosts content + digest, opens it in an editor,
8
+ # and POSTs the new content back with the original digest for optimistic
9
+ # locking. If Proxmox returns a digest collision (concurrent modification)
10
+ # or any other error, the message is surfaced verbatim to the user.
11
+ #
12
+ # Unlike DNS (structured YAML), /etc/hosts is edited as raw text —
13
+ # there is no parsing or validation pvectl-side.
14
+ #
15
+ # @example Basic usage
16
+ # service = EditHosts.new(hosts_repository: repo)
17
+ # result = service.execute(node_name: "pve1")
18
+ #
19
+ # @example Dry run
20
+ # service = EditHosts.new(hosts_repository: repo, options: { dry_run: true })
21
+ # result = service.execute(node_name: "pve1")
22
+ #
23
+ class EditHosts
24
+ # Creates a new EditHosts service.
25
+ #
26
+ # @param hosts_repository [Repositories::Hosts] Hosts repository
27
+ # @param editor_session [EditorSession, nil] optional injected editor session
28
+ # @param options [Hash] options (dry_run)
29
+ def initialize(hosts_repository:, editor_session: nil, options: {})
30
+ @hosts_repository = hosts_repository
31
+ @editor_session = editor_session
32
+ @options = options
33
+ end
34
+
35
+ # Executes the interactive /etc/hosts edit flow.
36
+ #
37
+ # @param node_name [String] node name
38
+ # @return [Models::NodeOperationResult, nil] result, or nil if cancelled/no changes
39
+ def execute(node_name:)
40
+ hosts = @hosts_repository.fetch(node_name)
41
+ original = hosts.data || ""
42
+ digest = hosts.digest
43
+
44
+ session = @editor_session || EditorSession.new
45
+ edited = session.edit(original)
46
+
47
+ return nil if edited.nil?
48
+ return nil if edited == original
49
+
50
+ resource_info = {
51
+ node_name: node_name,
52
+ diff: { original: original, edited: edited }
53
+ }
54
+
55
+ return build_result(resource_info, success: true) if @options[:dry_run]
56
+
57
+ @hosts_repository.update(node_name, edited, digest)
58
+ build_result(resource_info, success: true)
59
+ rescue StandardError => e
60
+ build_result({ node_name: node_name }, success: false, error: e.message)
61
+ end
62
+
63
+ private
64
+
65
+ # Builds a NodeOperationResult with the :edit operation.
66
+ #
67
+ # @param resource_info [Hash] resource info (node_name, optional diff)
68
+ # @param attrs [Hash] additional result attributes
69
+ # @return [Models::NodeOperationResult]
70
+ def build_result(resource_info, **attrs)
71
+ node_model = Models::Node.new(name: resource_info[:node_name])
72
+ Models::NodeOperationResult.new(
73
+ operation: :edit, node_model: node_model, resource: resource_info, **attrs
74
+ )
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pvectl
4
+ module Services
5
+ # Orchestrates the interactive node configuration editing flow.
6
+ #
7
+ # Fetches current config, presents it as YAML in an editor,
8
+ # computes diff, and applies changes. Uses plain YAML (not
9
+ # ConfigSerializer) since node config is flat key-value.
10
+ #
11
+ # @example Basic usage
12
+ # service = EditNode.new(node_repository: repo)
13
+ # result = service.execute(node_name: "pve1")
14
+ #
15
+ # @example Dry run with injected editor session
16
+ # service = EditNode.new(node_repository: repo, editor_session: session,
17
+ # options: { dry_run: true })
18
+ # result = service.execute(node_name: "pve1")
19
+ #
20
+ class EditNode
21
+ # Read-only keys that should not be sent back to the API.
22
+ READONLY_KEYS = %i[digest].freeze
23
+
24
+ # Creates a new EditNode service.
25
+ #
26
+ # @param node_repository [Repositories::Node] Node repository
27
+ # @param editor_session [EditorSession, nil] optional injected editor session
28
+ # @param options [Hash] options (dry_run)
29
+ def initialize(node_repository:, editor_session: nil, options: {})
30
+ @node_repository = node_repository
31
+ @editor_session = editor_session
32
+ @options = options
33
+ end
34
+
35
+ # Executes the interactive node edit flow.
36
+ #
37
+ # @param node_name [String] node name
38
+ # @return [Models::NodeOperationResult, nil] result, or nil if cancelled/no changes
39
+ def execute(node_name:)
40
+ node = @node_repository.get(node_name)
41
+ return not_found_result(node_name) unless node
42
+
43
+ config = @node_repository.fetch_config(node_name)
44
+ resource_info = { node_name: node_name, status: node.status }
45
+
46
+ # Build editable YAML (exclude digest — it's for optimistic locking only)
47
+ editable = config.reject { |k, _| READONLY_KEYS.include?(k) }
48
+ # Convert symbol keys to strings for clean YAML output (avoids :key: format)
49
+ string_keyed = editable.transform_keys(&:to_s)
50
+ yaml_content = "# Node: #{node_name}\n# Edit configuration below. Save and close to apply.\n" +
51
+ string_keyed.to_yaml
52
+
53
+ session = @editor_session || EditorSession.new
54
+ edited = session.edit(yaml_content)
55
+
56
+ return nil unless edited
57
+
58
+ # Parse edited YAML (strip comment lines)
59
+ cleaned = edited.lines.reject { |l| l.strip.start_with?("#") }.join
60
+ edited_config = YAML.safe_load(cleaned, symbolize_names: true) || {}
61
+
62
+ # Compute diff against original editable config (with symbol keys)
63
+ changes = compute_diff(editable, edited_config)
64
+
65
+ if changes[:changed].empty? && changes[:added].empty? && changes[:removed].empty?
66
+ return nil
67
+ end
68
+
69
+ resource_info[:diff] = changes
70
+
71
+ if @options[:dry_run]
72
+ return build_result(resource_info, success: true)
73
+ end
74
+
75
+ update_params = build_update_params(changes, config)
76
+ @node_repository.update(node_name, update_params)
77
+ build_result(resource_info, success: true)
78
+ rescue StandardError => e
79
+ build_result({ node_name: node_name }, success: false, error: e.message)
80
+ end
81
+
82
+ private
83
+
84
+ # Computes diff between original and edited config.
85
+ #
86
+ # @param original [Hash] original config (without readonly keys)
87
+ # @param edited [Hash] edited config
88
+ # @return [Hash] diff with :changed, :added, :removed
89
+ def compute_diff(original, edited)
90
+ changed = {}
91
+ added = {}
92
+ removed = []
93
+
94
+ edited.each do |key, value|
95
+ orig_value = original[key]
96
+ if orig_value.nil?
97
+ added[key] = value
98
+ elsif orig_value.to_s != value.to_s
99
+ changed[key] = [orig_value.to_s, value.to_s]
100
+ end
101
+ end
102
+
103
+ original.each_key do |key|
104
+ removed << key unless edited.key?(key)
105
+ end
106
+
107
+ { changed: changed, added: added, removed: removed }
108
+ end
109
+
110
+ # Builds API update parameters from diff.
111
+ #
112
+ # @param changes [Hash] diff hash with :changed, :added, :removed
113
+ # @param original_config [Hash] original config (for digest)
114
+ # @return [Hash] API parameters
115
+ def build_update_params(changes, original_config)
116
+ params = {}
117
+ changes[:changed].each { |key, (_old, new_val)| params[key] = new_val }
118
+ changes[:added].each { |key, val| params[key] = val }
119
+ unless changes[:removed].empty?
120
+ params[:delete] = changes[:removed].map(&:to_s).join(",")
121
+ end
122
+ params[:digest] = original_config[:digest] if original_config[:digest]
123
+ params
124
+ end
125
+
126
+ # Builds a NodeOperationResult with the :edit operation.
127
+ #
128
+ # @param resource_info [Hash] resource info (node_name, status)
129
+ # @param attrs [Hash] additional result attributes
130
+ # @return [Models::NodeOperationResult]
131
+ def build_result(resource_info, **attrs)
132
+ node_model = Models::Node.new(name: resource_info[:node_name])
133
+ Models::NodeOperationResult.new(
134
+ operation: :edit, node_model: node_model, resource: resource_info, **attrs
135
+ )
136
+ end
137
+
138
+ # Builds a not-found error result.
139
+ #
140
+ # @param node_name [String] node name
141
+ # @return [Models::NodeOperationResult]
142
+ def not_found_result(node_name)
143
+ build_result({ node_name: node_name }, success: false, error: "Node #{node_name} not found")
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pvectl
4
+ module Services
5
+ # Orchestrates the interactive VM configuration editing flow.
6
+ #
7
+ # Fetches current config, opens it in an editor as structured YAML,
8
+ # validates changes, computes a diff, and applies updates via the API.
9
+ # Supports dry-run mode and optimistic locking via digest.
10
+ #
11
+ # @example Basic usage
12
+ # service = EditVm.new(vm_repository: repo)
13
+ # result = service.execute(vmid: 100)
14
+ #
15
+ # @example Dry run with injected editor session
16
+ # service = EditVm.new(vm_repository: repo, editor_session: session,
17
+ # options: { dry_run: true })
18
+ # result = service.execute(vmid: 100)
19
+ #
20
+ class EditVm
21
+ # Creates a new EditVm service.
22
+ #
23
+ # @param vm_repository [Repositories::Vm] VM repository
24
+ # @param editor_session [EditorSession, nil] optional injected editor session
25
+ # @param options [Hash] options (dry_run)
26
+ def initialize(vm_repository:, editor_session: nil, options: {})
27
+ @vm_repository = vm_repository
28
+ @editor_session = editor_session
29
+ @options = options
30
+ end
31
+
32
+ # Executes the interactive VM edit flow.
33
+ #
34
+ # @param vmid [Integer] VM identifier
35
+ # @return [Models::VmOperationResult, nil] operation result, or nil if cancelled/no changes
36
+ def execute(vmid:)
37
+ vm = @vm_repository.get(vmid)
38
+ return not_found_result(vmid) unless vm
39
+
40
+ config = @vm_repository.fetch_config(vm.node, vmid)
41
+ resource_info = { vmid: vmid, node: vm.node, status: vm.status }
42
+
43
+ yaml_content = ConfigSerializer.to_yaml(config, type: :vm, resource: resource_info)
44
+
45
+ validator = ->(content) { ConfigSerializer.validate(content, type: :vm) }
46
+ session = @editor_session || EditorSession.new(validator: validator)
47
+ edited = session.edit(yaml_content)
48
+
49
+ return nil unless edited
50
+
51
+ original_roundtrip = ConfigSerializer.from_yaml(yaml_content, type: :vm)
52
+ edited_flat = ConfigSerializer.from_yaml(edited, type: :vm)
53
+
54
+ violations = ConfigSerializer.readonly_violations(original_roundtrip, edited_flat, type: :vm)
55
+ unless violations.empty?
56
+ return build_result(resource_info, success: false,
57
+ error: "Read-only fields cannot be changed: #{violations.join(', ')}")
58
+ end
59
+
60
+ changes = ConfigSerializer.diff(original_roundtrip, edited_flat)
61
+
62
+ if changes[:changed].empty? && changes[:added].empty? && changes[:removed].empty?
63
+ return nil
64
+ end
65
+
66
+ params = build_update_params(changes, config)
67
+
68
+ resource_info[:diff] = changes
69
+
70
+ if @options[:dry_run]
71
+ return build_result(resource_info, success: true)
72
+ end
73
+
74
+ @vm_repository.update(vmid, vm.node, params)
75
+ build_result(resource_info, success: true)
76
+ rescue StandardError => e
77
+ build_result({ vmid: vmid }, success: false, error: e.message)
78
+ end
79
+
80
+ private
81
+
82
+ # Builds API update parameters from a diff hash.
83
+ #
84
+ # Maps changed/added keys to their new values, removed keys to the
85
+ # Proxmox `delete` parameter, and includes digest for optimistic locking.
86
+ #
87
+ # @param changes [Hash] diff hash with :changed, :added, :removed
88
+ # @param original_config [Hash] original flat config (for digest)
89
+ # @return [Hash] Proxmox API parameters
90
+ def build_update_params(changes, original_config)
91
+ params = {}
92
+ changes[:changed].each { |key, (_old, new_val)| params[key] = new_val }
93
+ changes[:added].each { |key, val| params[key] = val }
94
+ unless changes[:removed].empty?
95
+ params[:delete] = changes[:removed].map(&:to_s).join(",")
96
+ end
97
+ params[:digest] = original_config[:digest] if original_config[:digest]
98
+ params
99
+ end
100
+
101
+ # Builds a VmOperationResult with the :edit operation.
102
+ #
103
+ # @param resource_info [Hash] resource info (vmid, node, status)
104
+ # @param attrs [Hash] additional result attributes
105
+ # @return [Models::VmOperationResult]
106
+ def build_result(resource_info, **attrs)
107
+ vm = Models::Vm.new(
108
+ vmid: resource_info[:vmid],
109
+ node: resource_info[:node]
110
+ )
111
+ Models::VmOperationResult.new(
112
+ operation: :edit, vm: vm, resource: resource_info, **attrs
113
+ )
114
+ end
115
+
116
+ # Builds a not-found error result.
117
+ #
118
+ # @param vmid [Integer] VM identifier
119
+ # @return [Models::VmOperationResult]
120
+ def not_found_result(vmid)
121
+ build_result({ vmid: vmid }, success: false, error: "VM #{vmid} not found")
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pvectl
4
+ module Services
5
+ # Orchestrates the interactive volume property editing flow.
6
+ #
7
+ # Fetches the disk config string from VM/CT config, parses it into
8
+ # editable YAML (key-value pairs), opens editor, and applies changes.
9
+ # Size changes delegate to ResizeVolume; config changes rebuild the
10
+ # disk config string.
11
+ #
12
+ # @example Basic usage
13
+ # service = EditVolume.new(repository: vm_repo, resource_type: :vm)
14
+ # result = service.execute(id: 100, disk: "scsi0", node: "pve1")
15
+ #
16
+ # @example Dry run with injected editor session
17
+ # service = EditVolume.new(repository: vm_repo, resource_type: :vm,
18
+ # editor_session: session, options: { dry_run: true })
19
+ # result = service.execute(id: 100, disk: "scsi0", node: "pve1")
20
+ #
21
+ class EditVolume
22
+ # @param repository [Repositories::Vm, Repositories::Container] resource repository
23
+ # @param resource_type [Symbol] :vm or :container
24
+ # @param editor_session [EditorSession, nil] optional injected editor session
25
+ # @param options [Hash] options (dry_run)
26
+ def initialize(repository:, resource_type:, editor_session: nil, options: {})
27
+ @repository = repository
28
+ @resource_type = resource_type
29
+ @editor_session = editor_session
30
+ @options = options
31
+ end
32
+
33
+ # Executes the interactive volume edit flow.
34
+ #
35
+ # @param id [Integer] resource ID (VMID or CTID)
36
+ # @param disk [String] disk name (e.g., "scsi0", "rootfs")
37
+ # @param node [String] node name
38
+ # @return [Models::VolumeOperationResult, nil] result, or nil if cancelled/no changes
39
+ def execute(id:, disk:, node:)
40
+ config = @repository.fetch_config(node, id)
41
+ disk_value = config[disk.to_sym]
42
+
43
+ unless disk_value
44
+ return build_result(id, disk, node, success: false,
45
+ error: "Volume '#{disk}' not found in config for resource #{id}")
46
+ end
47
+
48
+ editable = parse_disk_config(disk_value)
49
+ yaml_content = build_yaml_content(editable, disk, id, node)
50
+
51
+ session = @editor_session || EditorSession.new
52
+ edited = session.edit(yaml_content)
53
+
54
+ return nil unless edited
55
+
56
+ edited_config = parse_edited_yaml(edited)
57
+ changes = compute_diff(editable, edited_config)
58
+
59
+ return nil if no_changes?(changes)
60
+
61
+ if @options[:dry_run]
62
+ return build_result(id, disk, node, success: true,
63
+ resource: { diff: changes })
64
+ end
65
+
66
+ apply_changes(id, disk, node, disk_value, changes)
67
+ build_result(id, disk, node, success: true)
68
+ rescue ResizeVolume::VolumeNotFoundError, ResizeVolume::SizeTooSmallError, ArgumentError => e
69
+ build_result(id, disk, node, success: false, error: e.message)
70
+ rescue StandardError => e
71
+ build_result(id, disk, node, success: false, error: e.message)
72
+ end
73
+
74
+ private
75
+
76
+ # Parses a disk config string into a hash of editable properties.
77
+ #
78
+ # Input: "local-lvm:vm-100-disk-0,size=32G,cache=none"
79
+ # Output: { "size" => "32G", "cache" => "none" }
80
+ #
81
+ # The base (storage:vol-id) is NOT included — it's read-only.
82
+ #
83
+ # @param disk_value [String] disk config string
84
+ # @return [Hash] editable properties with string keys
85
+ def parse_disk_config(disk_value)
86
+ parts = disk_value.to_s.split(",")
87
+ parts.shift # Remove base "storage:vol-id"
88
+
89
+ props = {}
90
+ parts.each do |part|
91
+ key, value = part.split("=", 2)
92
+ props[key] = value
93
+ end
94
+ props
95
+ end
96
+
97
+ # Builds YAML content for the editor with comment header.
98
+ #
99
+ # @param editable [Hash] editable properties
100
+ # @param disk [String] disk name
101
+ # @param id [Integer] resource ID
102
+ # @param node [String] node name
103
+ # @return [String] YAML content with comments
104
+ def build_yaml_content(editable, disk, id, node)
105
+ "# Volume: #{disk} (resource #{id} on #{node})\n" \
106
+ "# Edit properties below. Save and close to apply.\n" +
107
+ editable.to_yaml
108
+ end
109
+
110
+ # Parses edited YAML content, stripping comment lines.
111
+ #
112
+ # @param edited [String] raw editor content
113
+ # @return [Hash] parsed config with string keys
114
+ def parse_edited_yaml(edited)
115
+ cleaned = edited.lines.reject { |l| l.strip.start_with?("#") }.join
116
+ YAML.safe_load(cleaned) || {}
117
+ end
118
+
119
+ # Checks whether the diff contains any actual changes.
120
+ #
121
+ # @param changes [Hash] diff hash with :changed, :added, :removed
122
+ # @return [Boolean] true if no changes detected
123
+ def no_changes?(changes)
124
+ changes[:changed].empty? && changes[:added].empty? && changes[:removed].empty?
125
+ end
126
+
127
+ # Computes diff between original and edited config.
128
+ #
129
+ # @param original [Hash] original config (string keys)
130
+ # @param edited [Hash] edited config (string keys)
131
+ # @return [Hash] diff with :changed, :added, :removed
132
+ def compute_diff(original, edited)
133
+ changed = {}
134
+ added = {}
135
+ removed = []
136
+
137
+ edited.each do |key, value|
138
+ orig_value = original[key.to_s]
139
+ if orig_value.nil?
140
+ added[key.to_s] = value
141
+ elsif orig_value.to_s != value.to_s
142
+ changed[key.to_s] = [orig_value.to_s, value.to_s]
143
+ end
144
+ end
145
+
146
+ original.each_key do |key|
147
+ removed << key.to_s unless edited.key?(key.to_s)
148
+ end
149
+
150
+ { changed: changed, added: added, removed: removed }
151
+ end
152
+
153
+ # Applies changes — delegates size to ResizeVolume, config to rebuild.
154
+ #
155
+ # @param id [Integer] resource ID
156
+ # @param disk [String] disk name
157
+ # @param node [String] node name
158
+ # @param original_disk_value [String] original disk config string
159
+ # @param changes [Hash] diff hash
160
+ def apply_changes(id, disk, node, original_disk_value, changes)
161
+ size_change = changes[:changed].delete("size") || changes[:added].delete("size")
162
+
163
+ if size_change
164
+ new_size = size_change.is_a?(Array) ? size_change[1] : size_change
165
+ parsed_size = ResizeVolume.parse_size(new_size.to_s)
166
+ resize_service = ResizeVolume.new(repository: @repository)
167
+ resize_service.preflight(id, disk, parsed_size, node: node)
168
+ resize_service.perform(id, disk, parsed_size.raw, node: node)
169
+ end
170
+
171
+ config_updates = {}
172
+ changes[:changed].each { |key, (_old, new_val)| config_updates[key] = new_val }
173
+ changes[:added].each { |key, val| config_updates[key] = val }
174
+
175
+ if !config_updates.empty? || !changes[:removed].empty?
176
+ new_disk_value = rebuild_disk_config(original_disk_value, config_updates, changes[:removed])
177
+ @repository.update(id, node, { disk.to_sym => new_disk_value })
178
+ end
179
+ end
180
+
181
+ # Rebuilds a disk config string with updated/removed properties.
182
+ #
183
+ # @param current_value [String] current disk config string
184
+ # @param updates [Hash] key-value pairs to update
185
+ # @param removed_keys [Array<String>] keys to remove
186
+ # @return [String] updated config string
187
+ def rebuild_disk_config(current_value, updates, removed_keys = [])
188
+ parts = current_value.to_s.split(",")
189
+ base = parts.shift
190
+
191
+ existing = {}
192
+ parts.each do |part|
193
+ key, value = part.split("=", 2)
194
+ existing[key] = value
195
+ end
196
+
197
+ updates.each { |key, value| existing[key.to_s] = value.to_s }
198
+ removed_keys.each { |key| existing.delete(key.to_s) }
199
+
200
+ config_parts = existing.map { |k, v| "#{k}=#{v}" }
201
+ ([base] + config_parts).join(",")
202
+ end
203
+
204
+ # Builds a VolumeOperationResult.
205
+ #
206
+ # @param id [Integer] resource ID
207
+ # @param disk [String] disk name
208
+ # @param node [String] node name
209
+ # @param attrs [Hash] additional result attributes
210
+ # @return [Models::VolumeOperationResult]
211
+ def build_result(id, disk, node, **attrs)
212
+ volume = Models::Volume.new(
213
+ name: disk,
214
+ resource_type: @resource_type.to_s,
215
+ resource_id: id,
216
+ node: node
217
+ )
218
+ Models::VolumeOperationResult.new(
219
+ operation: :edit, volume: volume, **attrs
220
+ )
221
+ end
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pvectl
4
+ module Services
5
+ module Get
6
+ # Service for fetching and formatting resource data.
7
+ #
8
+ # Orchestrates the data flow between:
9
+ # - ResourceHandler (provides models and presenter)
10
+ # - Formatters::Registry (formats output)
11
+ #
12
+ # This follows ARCHITECTURE.md section 3.4:
13
+ # "Services orchestrate data flow between Repository, Models, and Formatters"
14
+ #
15
+ # @example Basic usage
16
+ # handler = ResourceRegistry.for("nodes")
17
+ # service = ResourceService.new(handler: handler, format: "table")
18
+ # output = service.list(node: "pve1")
19
+ # puts output
20
+ #
21
+ class ResourceService
22
+ # Creates a new ResourceService.
23
+ #
24
+ # @param handler [ResourceHandler] the resource handler for data fetching
25
+ # @param format [String] output format (table, json, yaml, wide)
26
+ # @param color_enabled [Boolean] whether to enable colored output
27
+ def initialize(handler:, format: "table", color_enabled: true)
28
+ @handler = handler
29
+ @format = format
30
+ @color_enabled = color_enabled
31
+ end
32
+
33
+ # Fetches and formats resources.
34
+ #
35
+ # @param node [String, nil] filter by node name
36
+ # @param name [String, nil] filter by resource name
37
+ # @param args [Array<String>] additional positional arguments (e.g., VMIDs for snapshots)
38
+ # @param storage [String, nil] filter by storage (for backups)
39
+ # @param vmid [Array<String>, nil] filter by VM/CT IDs
40
+ # @param selector [Selectors::Base, nil] client-side selector for filtering results
41
+ # @param options [Hash] additional options passed through to handler (e.g., limit, since, type_filter)
42
+ # @return [String] formatted output string
43
+ def list(node: nil, name: nil, args: [], storage: nil, vmid: nil, selector: nil, **options)
44
+ models = @handler.list(node: node, name: name, args: args, storage: storage, vmid: vmid, **options)
45
+ models = selector.apply(models) if selector && !selector.empty?
46
+ presenter = @handler.presenter
47
+ format_output(models, presenter)
48
+ end
49
+
50
+ # Describes and formats a single resource.
51
+ #
52
+ # For local storage with multiple instances, returns list of nodes
53
+ # when no node specified, or full describe when node is specified.
54
+ #
55
+ # @param name [String] resource name
56
+ # @param node [String, nil] filter by node name (for local storage)
57
+ # @param vmid [Array<String>, nil] filter by VM/CT IDs
58
+ # @return [String] formatted output string
59
+ def describe(name:, node: nil, args: [], vmid: nil)
60
+ result = @handler.describe(name: name, node: node, args: args, vmid: vmid)
61
+ presenter = @handler.presenter
62
+
63
+ if result.is_a?(Array)
64
+ # Multiple instances - format as list
65
+ format_output(result, presenter)
66
+ else
67
+ # Single model - format as describe
68
+ format_output_describe(result, presenter)
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ attr_reader :handler, :format, :color_enabled
75
+
76
+ # Formats models for output using the appropriate formatter.
77
+ #
78
+ # @param models [Array<Object>] collection of models
79
+ # @param presenter [Presenters::Base] presenter for the resource type
80
+ # @return [String] formatted output
81
+ def format_output(models, presenter)
82
+ formatter = Formatters::Registry.for(format)
83
+ formatter.format(models, presenter, color_enabled: color_enabled)
84
+ end
85
+
86
+ # Formats single model for describe output.
87
+ #
88
+ # @param model [Object] single model
89
+ # @param presenter [Presenters::Base] presenter for the resource type
90
+ # @return [String] formatted output
91
+ def format_output_describe(model, presenter)
92
+ formatter = Formatters::Registry.for(format)
93
+ formatter.format(model, presenter, color_enabled: color_enabled, describe: true)
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end