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,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pvectl
4
+ module Services
5
+ # Orchestrates systemd service lifecycle operations (start/stop/restart/reload)
6
+ # on a Proxmox node.
7
+ #
8
+ # Wraps Repositories::Service to provide a uniform return shape suitable
9
+ # for table/JSON/YAML formatting (Models::NodeOperationResult) and to
10
+ # convert API errors into structured results instead of raising.
11
+ #
12
+ # @example Restarting a service
13
+ # service = ServiceLifecycle.new(service_repository: repo)
14
+ # result = service.execute(operation: :restart, node: "pve1", service: "pveproxy")
15
+ # result.successful? # => true
16
+ #
17
+ class ServiceLifecycle
18
+ # Operations supported by the Proxmox services API.
19
+ OPERATIONS = %i[start stop restart reload].freeze
20
+
21
+ # Creates a new ServiceLifecycle orchestrator.
22
+ #
23
+ # @param service_repository [Repositories::Service] service repository
24
+ def initialize(service_repository:)
25
+ @service_repository = service_repository
26
+ end
27
+
28
+ # Executes a lifecycle operation on a single service.
29
+ #
30
+ # @param operation [Symbol] one of :start, :stop, :restart, :reload
31
+ # @param node [String] node name
32
+ # @param service [String] service identifier (e.g., "pveproxy")
33
+ # @return [Models::NodeOperationResult] result with task UPID or error
34
+ # @raise [ArgumentError] when operation is unknown
35
+ def execute(operation:, node:, service:)
36
+ validate_operation!(operation)
37
+
38
+ begin
39
+ upid = @service_repository.public_send(operation, node, service)
40
+ build_result(
41
+ operation: operation,
42
+ node: node,
43
+ service: service,
44
+ task_upid: upid,
45
+ success: :pending
46
+ )
47
+ rescue StandardError => e
48
+ build_result(
49
+ operation: operation,
50
+ node: node,
51
+ service: service,
52
+ success: false,
53
+ error: e.message
54
+ )
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ # Validates operation is one of the supported actions.
61
+ #
62
+ # @param operation [Symbol] operation
63
+ # @raise [ArgumentError] when not in OPERATIONS
64
+ def validate_operation!(operation)
65
+ return if OPERATIONS.include?(operation)
66
+
67
+ raise ArgumentError,
68
+ "Unknown service operation: #{operation}. Valid: #{OPERATIONS.join(', ')}"
69
+ end
70
+
71
+ # Builds a NodeOperationResult for the operation.
72
+ #
73
+ # @param operation [Symbol] operation
74
+ # @param node [String] node name
75
+ # @param service [String] service identifier
76
+ # @param attrs [Hash] additional result attributes (task_upid, success, error)
77
+ # @return [Models::NodeOperationResult]
78
+ def build_result(operation:, node:, service:, **attrs)
79
+ node_model = Models::Node.new(name: node)
80
+ Models::NodeOperationResult.new(
81
+ operation: operation,
82
+ node_model: node_model,
83
+ resource: { service: service, node: node },
84
+ **attrs
85
+ )
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pvectl
4
+ module Services
5
+ # Orchestrates non-interactive container configuration updates.
6
+ #
7
+ # Takes key-value pairs directly (no editor), computes diff against
8
+ # current config, and applies changes via the API.
9
+ # Supports dry-run mode and optimistic locking via digest.
10
+ #
11
+ # @example Basic usage
12
+ # service = SetContainer.new(container_repository: repo)
13
+ # result = service.execute(ctid: 200, params: { memory: "8192", cores: "4" })
14
+ #
15
+ # @example Dry run
16
+ # service = SetContainer.new(container_repository: repo, options: { dry_run: true })
17
+ # result = service.execute(ctid: 200, params: { memory: "8192" })
18
+ #
19
+ class SetContainer
20
+ # Creates a new SetContainer service.
21
+ #
22
+ # @param container_repository [Repositories::Container] Container repository
23
+ # @param options [Hash] options (dry_run)
24
+ def initialize(container_repository:, options: {})
25
+ @container_repository = container_repository
26
+ @options = options
27
+ end
28
+
29
+ # Executes the non-interactive container config update.
30
+ #
31
+ # Fetches current config, computes diff against requested params,
32
+ # and applies changes via the API (unless dry-run).
33
+ #
34
+ # @param ctid [Integer] Container identifier
35
+ # @param params [Hash] key-value pairs to set
36
+ # @return [Models::ContainerOperationResult, nil] result, or nil if no changes
37
+ def execute(ctid:, params:)
38
+ container = @container_repository.get(ctid)
39
+ return not_found_result(ctid) unless container
40
+
41
+ config = @container_repository.fetch_config(container.node, ctid)
42
+ resource_info = { vmid: ctid, node: container.node, status: container.status }
43
+
44
+ changes = compute_diff(config, params)
45
+
46
+ if changes[:changed].empty? && changes[:added].empty? && changes[:removed].empty?
47
+ return nil
48
+ end
49
+
50
+ resource_info[:diff] = changes
51
+
52
+ if @options[:dry_run]
53
+ return build_result(resource_info, success: true)
54
+ end
55
+
56
+ update_params = build_update_params(changes, config)
57
+ @container_repository.update(ctid, container.node, update_params)
58
+ build_result(resource_info, success: true)
59
+ rescue StandardError => e
60
+ build_result({ vmid: ctid }, success: false, error: e.message)
61
+ end
62
+
63
+ private
64
+
65
+ # Computes diff between current config and requested params.
66
+ #
67
+ # Categorizes each requested param as :changed (value differs),
68
+ # :added (key not in current config), or unchanged (skipped).
69
+ #
70
+ # @param config [Hash] current configuration
71
+ # @param params [Hash] requested key-value changes
72
+ # @return [Hash] diff with :changed, :added, :removed keys
73
+ def compute_diff(config, params)
74
+ changed = {}
75
+ added = {}
76
+
77
+ params.each do |key, value|
78
+ sym_key = key.to_sym
79
+ current = config[sym_key]
80
+
81
+ if current.nil?
82
+ added[sym_key] = value
83
+ elsif current.to_s != value.to_s
84
+ changed[sym_key] = [current.to_s, value.to_s]
85
+ end
86
+ end
87
+
88
+ { changed: changed, added: added, removed: [] }
89
+ end
90
+
91
+ # Builds API update parameters from diff.
92
+ #
93
+ # Maps changed/added keys to their new values and includes
94
+ # digest from original config for optimistic locking.
95
+ #
96
+ # @param changes [Hash] diff hash with :changed, :added
97
+ # @param original_config [Hash] original config (for digest)
98
+ # @return [Hash] API parameters
99
+ def build_update_params(changes, original_config)
100
+ params = {}
101
+ changes[:changed].each { |key, (_old, new_val)| params[key] = new_val }
102
+ changes[:added].each { |key, val| params[key] = val }
103
+ params[:digest] = original_config[:digest] if original_config[:digest]
104
+ params
105
+ end
106
+
107
+ # Builds a ContainerOperationResult with the :set operation.
108
+ #
109
+ # @param resource_info [Hash] resource info (vmid, node, status)
110
+ # @param attrs [Hash] additional result attributes
111
+ # @return [Models::ContainerOperationResult]
112
+ def build_result(resource_info, **attrs)
113
+ container = Models::Container.new(vmid: resource_info[:vmid], node: resource_info[:node])
114
+ Models::ContainerOperationResult.new(
115
+ operation: :set, container: container, resource: resource_info, **attrs
116
+ )
117
+ end
118
+
119
+ # Builds a not-found error result.
120
+ #
121
+ # @param ctid [Integer] Container identifier
122
+ # @return [Models::ContainerOperationResult]
123
+ def not_found_result(ctid)
124
+ build_result({ vmid: ctid }, success: false, error: "Container #{ctid} not found")
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,236 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pvectl
4
+ module Services
5
+ # Orchestrates non-interactive node configuration updates.
6
+ #
7
+ # Takes key-value pairs directly (no editor), computes diff against
8
+ # current config, and applies changes via the API.
9
+ # Supports dry-run mode and optimistic locking via digest.
10
+ #
11
+ # The `timezone` key is special: it lives at `PUT /nodes/{node}/time`
12
+ # rather than `PUT /nodes/{node}/config`, so the service splits it off
13
+ # and routes it to a separate `time_repository`.
14
+ #
15
+ # @example Basic usage
16
+ # service = SetNode.new(node_repository: repo)
17
+ # result = service.execute(node_name: "pve1", params: { description: "updated" })
18
+ #
19
+ # @example Setting timezone
20
+ # service = SetNode.new(node_repository: node_repo, time_repository: time_repo)
21
+ # result = service.execute(node_name: "pve1", params: { timezone: "Europe/Warsaw" })
22
+ #
23
+ # @example Dry run
24
+ # service = SetNode.new(node_repository: repo, options: { dry_run: true })
25
+ # result = service.execute(node_name: "pve1", params: { description: "updated" })
26
+ #
27
+ class SetNode
28
+ # Key recognized by this service as the node timezone (handled by the
29
+ # `/time` endpoint rather than `/config`). Accepted as Symbol or String.
30
+ TIMEZONE_KEY = :timezone
31
+
32
+ # Creates a new SetNode service.
33
+ #
34
+ # @param node_repository [Repositories::Node] Node repository
35
+ # @param time_repository [Repositories::TimeConfig, nil] Time repository (optional;
36
+ # required only when `params` includes `:timezone`)
37
+ # @param options [Hash] options (dry_run)
38
+ def initialize(node_repository:, time_repository: nil, options: {})
39
+ @node_repository = node_repository
40
+ @time_repository = time_repository
41
+ @options = options
42
+ end
43
+
44
+ # Executes the non-interactive node config update.
45
+ #
46
+ # Fetches current config and (if timezone is requested) current timezone,
47
+ # computes diffs against the requested params, and applies changes via
48
+ # the appropriate API endpoint (unless dry-run).
49
+ #
50
+ # @param node_name [String] Node name
51
+ # @param params [Hash] key-value pairs to set
52
+ # @return [Models::NodeOperationResult, nil] result, or nil if no changes
53
+ def execute(node_name:, params:)
54
+ node = @node_repository.get(node_name)
55
+ return not_found_result(node_name) unless node
56
+
57
+ config = @node_repository.fetch_config(node_name)
58
+ config_params, requested_tz = split_timezone(params)
59
+ current_tz = requested_tz ? current_timezone(node_name) : nil
60
+
61
+ changes = compute_diff(config, config_params)
62
+ tz_change = compute_timezone_change(current_tz, requested_tz)
63
+ merge_timezone_into_diff(changes, tz_change) if tz_change
64
+
65
+ return nil if no_changes?(changes)
66
+
67
+ resource_info = { node_name: node_name, status: node.status, diff: changes }
68
+ return build_result(resource_info, success: true) if @options[:dry_run]
69
+
70
+ apply_config_changes(node_name, changes, config) unless config_changes_empty?(changes, tz_change)
71
+ @time_repository.set_timezone(node_name, tz_change[:to]) if tz_change
72
+
73
+ build_result(resource_info, success: true)
74
+ rescue StandardError => e
75
+ build_result({ node_name: node_name }, success: false, error: e.message)
76
+ end
77
+
78
+ private
79
+
80
+ # Separates the `:timezone` key (accepted as Symbol or String) from
81
+ # other config params.
82
+ #
83
+ # @param params [Hash] requested params
84
+ # @return [Array(Hash, String?)] pair of [remaining_params, requested_timezone_or_nil]
85
+ def split_timezone(params)
86
+ remaining = {}
87
+ requested_tz = nil
88
+
89
+ params.each do |key, value|
90
+ if key.to_sym == TIMEZONE_KEY
91
+ requested_tz = value.to_s
92
+ else
93
+ remaining[key] = value
94
+ end
95
+ end
96
+
97
+ [remaining, requested_tz]
98
+ end
99
+
100
+ # Fetches the current timezone for the node.
101
+ #
102
+ # @param node_name [String]
103
+ # @return [String, nil]
104
+ def current_timezone(node_name)
105
+ return nil unless @time_repository
106
+
107
+ @time_repository.fetch(node_name).timezone
108
+ end
109
+
110
+ # Builds a timezone-change descriptor when requested != current.
111
+ #
112
+ # @param current [String, nil]
113
+ # @param requested [String, nil]
114
+ # @return [Hash{from: String, to: String}, nil] descriptor or nil when unchanged/missing
115
+ def compute_timezone_change(current, requested)
116
+ return nil if requested.nil?
117
+ return nil if current.to_s == requested.to_s
118
+
119
+ { from: current.to_s, to: requested.to_s }
120
+ end
121
+
122
+ # Folds a timezone change into the diff structure so it shows up in
123
+ # the operation result alongside config-key changes.
124
+ #
125
+ # @param changes [Hash] diff hash (mutated in place)
126
+ # @param tz_change [Hash] `{from:, to:}`
127
+ # @return [void]
128
+ def merge_timezone_into_diff(changes, tz_change)
129
+ if tz_change[:from].nil? || tz_change[:from].empty?
130
+ changes[:added][TIMEZONE_KEY] = tz_change[:to]
131
+ else
132
+ changes[:changed][TIMEZONE_KEY] = [tz_change[:from], tz_change[:to]]
133
+ end
134
+ end
135
+
136
+ # Computes diff between current config and requested params.
137
+ #
138
+ # @param config [Hash] current configuration
139
+ # @param params [Hash] requested key-value changes
140
+ # @return [Hash] diff with :changed, :added, :removed keys
141
+ def compute_diff(config, params)
142
+ changed = {}
143
+ added = {}
144
+
145
+ params.each do |key, value|
146
+ sym_key = key.to_sym
147
+ current = config[sym_key]
148
+
149
+ if current.nil?
150
+ added[sym_key] = value
151
+ elsif current.to_s != value.to_s
152
+ changed[sym_key] = [current.to_s, value.to_s]
153
+ end
154
+ end
155
+
156
+ { changed: changed, added: added, removed: [] }
157
+ end
158
+
159
+ # Returns true when the diff contains no actionable change.
160
+ #
161
+ # @param changes [Hash] diff hash
162
+ # @return [Boolean]
163
+ def no_changes?(changes)
164
+ changes[:changed].empty? && changes[:added].empty? && changes[:removed].empty?
165
+ end
166
+
167
+ # Applies non-timezone diff entries via the node config endpoint.
168
+ #
169
+ # @param node_name [String]
170
+ # @param changes [Hash] diff hash
171
+ # @param original_config [Hash] for digest
172
+ # @return [void]
173
+ def apply_config_changes(node_name, changes, original_config)
174
+ update_params = build_update_params(changes, original_config)
175
+ return if update_params.empty? || update_params.keys == [:digest]
176
+
177
+ @node_repository.update(node_name, update_params)
178
+ end
179
+
180
+ # True when the only changes in the diff are timezone-related.
181
+ #
182
+ # @param changes [Hash] diff hash
183
+ # @param tz_change [Hash, nil]
184
+ # @return [Boolean]
185
+ def config_changes_empty?(changes, tz_change)
186
+ return false if tz_change.nil?
187
+
188
+ non_tz_changed = changes[:changed].keys - [TIMEZONE_KEY]
189
+ non_tz_added = changes[:added].keys - [TIMEZONE_KEY]
190
+ non_tz_changed.empty? && non_tz_added.empty?
191
+ end
192
+
193
+ # Builds API update parameters from diff (excluding timezone, which is
194
+ # routed elsewhere).
195
+ #
196
+ # @param changes [Hash] diff hash with :changed, :added
197
+ # @param original_config [Hash] original config (for digest)
198
+ # @return [Hash] API parameters
199
+ def build_update_params(changes, original_config)
200
+ params = {}
201
+ changes[:changed].each do |key, (_old, new_val)|
202
+ next if key == TIMEZONE_KEY
203
+
204
+ params[key] = new_val
205
+ end
206
+ changes[:added].each do |key, val|
207
+ next if key == TIMEZONE_KEY
208
+
209
+ params[key] = val
210
+ end
211
+ params[:digest] = original_config[:digest] if original_config[:digest] && !params.empty?
212
+ params
213
+ end
214
+
215
+ # Builds a NodeOperationResult with the :set operation.
216
+ #
217
+ # @param resource_info [Hash] resource info (node_name, status)
218
+ # @param attrs [Hash] additional result attributes
219
+ # @return [Models::NodeOperationResult]
220
+ def build_result(resource_info, **attrs)
221
+ node_model = Models::Node.new(name: resource_info[:node_name])
222
+ Models::NodeOperationResult.new(
223
+ operation: :set, node_model: node_model, resource: resource_info, **attrs
224
+ )
225
+ end
226
+
227
+ # Builds a not-found error result.
228
+ #
229
+ # @param node_name [String] Node name
230
+ # @return [Models::NodeOperationResult]
231
+ def not_found_result(node_name)
232
+ build_result({ node_name: node_name }, success: false, error: "Node #{node_name} not found")
233
+ end
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pvectl
4
+ module Services
5
+ # Orchestrates non-interactive VM configuration updates.
6
+ #
7
+ # Takes key-value pairs directly (no editor), computes diff against
8
+ # current config, and applies changes via the API.
9
+ # Supports dry-run mode and optimistic locking via digest.
10
+ #
11
+ # @example Basic usage
12
+ # service = SetVm.new(vm_repository: repo)
13
+ # result = service.execute(vmid: 100, params: { memory: "8192", cores: "4" })
14
+ #
15
+ # @example Dry run
16
+ # service = SetVm.new(vm_repository: repo, options: { dry_run: true })
17
+ # result = service.execute(vmid: 100, params: { memory: "8192" })
18
+ #
19
+ class SetVm
20
+ # Creates a new SetVm service.
21
+ #
22
+ # @param vm_repository [Repositories::Vm] VM repository
23
+ # @param options [Hash] options (dry_run)
24
+ def initialize(vm_repository:, options: {})
25
+ @vm_repository = vm_repository
26
+ @options = options
27
+ end
28
+
29
+ # Executes the non-interactive VM config update.
30
+ #
31
+ # Fetches current config, computes diff against requested params,
32
+ # and applies changes via the API (unless dry-run).
33
+ #
34
+ # @param vmid [Integer] VM identifier
35
+ # @param params [Hash] key-value pairs to set
36
+ # @return [Models::VmOperationResult, nil] result, or nil if no changes
37
+ def execute(vmid:, params:)
38
+ vm = @vm_repository.get(vmid)
39
+ return not_found_result(vmid) unless vm
40
+
41
+ config = @vm_repository.fetch_config(vm.node, vmid)
42
+ resource_info = { vmid: vmid, node: vm.node, status: vm.status }
43
+
44
+ changes = compute_diff(config, params)
45
+
46
+ if changes[:changed].empty? && changes[:added].empty? && changes[:removed].empty?
47
+ return nil
48
+ end
49
+
50
+ resource_info[:diff] = changes
51
+
52
+ if @options[:dry_run]
53
+ return build_result(resource_info, success: true)
54
+ end
55
+
56
+ update_params = build_update_params(changes, config)
57
+ @vm_repository.update(vmid, vm.node, update_params)
58
+ build_result(resource_info, success: true)
59
+ rescue StandardError => e
60
+ build_result({ vmid: vmid }, success: false, error: e.message)
61
+ end
62
+
63
+ private
64
+
65
+ # Computes diff between current config and requested params.
66
+ #
67
+ # Categorizes each requested param as :changed (value differs),
68
+ # :added (key not in current config), or unchanged (skipped).
69
+ #
70
+ # @param config [Hash] current configuration
71
+ # @param params [Hash] requested key-value changes
72
+ # @return [Hash] diff with :changed, :added, :removed keys
73
+ def compute_diff(config, params)
74
+ changed = {}
75
+ added = {}
76
+
77
+ params.each do |key, value|
78
+ sym_key = key.to_sym
79
+ current = config[sym_key]
80
+
81
+ if current.nil?
82
+ added[sym_key] = value
83
+ elsif current.to_s != value.to_s
84
+ changed[sym_key] = [current.to_s, value.to_s]
85
+ end
86
+ end
87
+
88
+ { changed: changed, added: added, removed: [] }
89
+ end
90
+
91
+ # Builds API update parameters from diff.
92
+ #
93
+ # Maps changed/added keys to their new values and includes
94
+ # digest from original config for optimistic locking.
95
+ #
96
+ # @param changes [Hash] diff hash with :changed, :added
97
+ # @param original_config [Hash] original config (for digest)
98
+ # @return [Hash] API parameters
99
+ def build_update_params(changes, original_config)
100
+ params = {}
101
+ changes[:changed].each { |key, (_old, new_val)| params[key] = new_val }
102
+ changes[:added].each { |key, val| params[key] = val }
103
+ params[:digest] = original_config[:digest] if original_config[:digest]
104
+ params
105
+ end
106
+
107
+ # Builds a VmOperationResult with the :set operation.
108
+ #
109
+ # @param resource_info [Hash] resource info (vmid, node, status)
110
+ # @param attrs [Hash] additional result attributes
111
+ # @return [Models::VmOperationResult]
112
+ def build_result(resource_info, **attrs)
113
+ vm = Models::Vm.new(vmid: resource_info[:vmid], node: resource_info[:node])
114
+ Models::VmOperationResult.new(
115
+ operation: :set, vm: vm, resource: resource_info, **attrs
116
+ )
117
+ end
118
+
119
+ # Builds a not-found error result.
120
+ #
121
+ # @param vmid [Integer] VM identifier
122
+ # @return [Models::VmOperationResult]
123
+ def not_found_result(vmid)
124
+ build_result({ vmid: vmid }, success: false, error: "VM #{vmid} not found")
125
+ end
126
+ end
127
+ end
128
+ end