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,265 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pvectl
4
+ module Services
5
+ # Orchestrates VM clone operations.
6
+ #
7
+ # Handles validation, auto-generation of VMID/name, and sync/async modes.
8
+ # Supports both full clones and linked clones (templates only).
9
+ #
10
+ # @example Full clone with auto-generated VMID
11
+ # service = CloneVm.new(vm_repository: vm_repo, task_repository: task_repo)
12
+ # result = service.execute(vmid: 100)
13
+ #
14
+ # @example Linked clone to specific node
15
+ # service = CloneVm.new(vm_repository: vm_repo, task_repository: task_repo)
16
+ # result = service.execute(vmid: 100, linked: true, target_node: "pve2")
17
+ #
18
+ # @example Async clone with custom timeout
19
+ # service = CloneVm.new(vm_repository: vm_repo, task_repository: task_repo, options: { async: true })
20
+ # result = service.execute(vmid: 100, new_vmid: 200, name: "web-clone")
21
+ #
22
+ class CloneVm
23
+ DEFAULT_TIMEOUT = 300
24
+
25
+ # @return [Integer] Default timeout for start operations (seconds)
26
+ START_TIMEOUT = 60
27
+
28
+ # Creates a new CloneVm service.
29
+ #
30
+ # @param vm_repository [Repositories::Vm] VM repository
31
+ # @param task_repository [Repositories::Task] Task repository
32
+ # @param options [Hash] Options (timeout, async)
33
+ def initialize(vm_repository:, task_repository:, options: {})
34
+ @vm_repository = vm_repository
35
+ @task_repository = task_repository
36
+ @options = options
37
+ end
38
+
39
+ # Executes clone operation.
40
+ #
41
+ # Performs a two-step flow: clone the VM first, then optionally apply
42
+ # config updates via PUT /nodes/{node}/qemu/{vmid}/config.
43
+ #
44
+ # @param vmid [Integer] Source VM identifier
45
+ # @param node [String, nil] Source node (auto-detected from VM if nil)
46
+ # @param new_vmid [Integer, nil] New VMID (auto-selected if nil)
47
+ # @param name [String, nil] Name for clone (auto-generated if nil)
48
+ # @param target_node [String, nil] Target node for clone
49
+ # @param storage [String, nil] Target storage
50
+ # @param linked [Boolean] Linked clone (default: false, requires template)
51
+ # @param pool [String, nil] Resource pool
52
+ # @param description [String, nil] Description
53
+ # @param config_params [Hash] VM config parameters to apply after clone
54
+ # @return [Models::OperationResult] Clone result
55
+ def execute(vmid:, node: nil, new_vmid: nil, name: nil, target_node: nil,
56
+ storage: nil, linked: false, pool: nil, description: nil,
57
+ config_params: {})
58
+ source_vm = @vm_repository.get(vmid)
59
+ return vm_not_found_error(vmid) unless source_vm
60
+
61
+ if linked && !source_vm.template?
62
+ return linked_clone_error(source_vm)
63
+ end
64
+
65
+ node ||= source_vm.node
66
+ new_vmid ||= @vm_repository.next_available_vmid
67
+ name ||= generate_name(source_vm)
68
+
69
+ clone_options = build_clone_options(
70
+ name: name, target_node: target_node, storage: storage,
71
+ linked: linked, pool: pool, description: description
72
+ )
73
+
74
+ upid = @vm_repository.clone(vmid, node, new_vmid, clone_options)
75
+ resource_info = { new_vmid: new_vmid, name: name, node: target_node || node }
76
+
77
+ if @options[:async]
78
+ Models::VmOperationResult.new(
79
+ vm: source_vm, operation: :clone,
80
+ task_upid: upid, success: :pending,
81
+ resource: resource_info
82
+ )
83
+ else
84
+ task = @task_repository.wait(upid, timeout: timeout)
85
+
86
+ unless task.successful?
87
+ return Models::VmOperationResult.new(
88
+ vm: source_vm, operation: :clone,
89
+ task: task, success: task.successful?,
90
+ resource: resource_info
91
+ )
92
+ end
93
+
94
+ if config_params.any?
95
+ apply_config_update(source_vm, new_vmid, resource_info[:node], config_params, resource_info)
96
+ else
97
+ start_vm(new_vmid, resource_info[:node]) if @options[:start]
98
+ Models::VmOperationResult.new(
99
+ vm: source_vm, operation: :clone,
100
+ task: task, success: true,
101
+ resource: resource_info
102
+ )
103
+ end
104
+ end
105
+ rescue StandardError => e
106
+ Models::VmOperationResult.new(
107
+ vm: source_vm, operation: :clone,
108
+ success: false, error: e.message
109
+ )
110
+ end
111
+
112
+ private
113
+
114
+ # Generates clone name from source VM.
115
+ #
116
+ # @param source_vm [Models::Vm] Source VM
117
+ # @return [String] Generated name
118
+ def generate_name(source_vm)
119
+ if source_vm.name && !source_vm.name.empty?
120
+ "#{source_vm.name}-clone"
121
+ else
122
+ "vm-#{source_vm.vmid}-clone"
123
+ end
124
+ end
125
+
126
+ # Builds clone options hash for repository call.
127
+ #
128
+ # @param name [String] Clone name
129
+ # @param target_node [String, nil] Target node
130
+ # @param storage [String, nil] Target storage
131
+ # @param linked [Boolean] Linked clone flag
132
+ # @param pool [String, nil] Resource pool
133
+ # @param description [String, nil] Description
134
+ # @return [Hash] Clone options
135
+ def build_clone_options(name:, target_node:, storage:, linked:, pool:, description:)
136
+ opts = { name: name, full: !linked }
137
+ opts[:target] = target_node if target_node
138
+ opts[:storage] = storage if storage
139
+ opts[:pool] = pool if pool
140
+ opts[:description] = description if description
141
+ opts
142
+ end
143
+
144
+ # Applies config update to the cloned VM.
145
+ #
146
+ # Converts user-friendly params to Proxmox API format and calls
147
+ # the repository update method. Returns partial result on failure.
148
+ #
149
+ # @param source_vm [Models::Vm] Source VM
150
+ # @param new_vmid [Integer] Cloned VM identifier
151
+ # @param node [String] Target node for the cloned VM
152
+ # @param config_params [Hash] Config parameters to apply
153
+ # @param resource_info [Hash] Resource info for result
154
+ # @return [Models::VmOperationResult] Operation result
155
+ def apply_config_update(source_vm, new_vmid, node, config_params, resource_info)
156
+ api_params = build_config_api_params(config_params)
157
+ @vm_repository.update(new_vmid, node, api_params)
158
+ start_vm(new_vmid, node) if @options[:start]
159
+ Models::VmOperationResult.new(
160
+ vm: source_vm, operation: :clone,
161
+ success: true, resource: resource_info
162
+ )
163
+ rescue StandardError => e
164
+ Models::VmOperationResult.new(
165
+ vm: source_vm, operation: :clone,
166
+ success: :partial, resource: resource_info,
167
+ error: "Cloned successfully, but config update failed: #{e.message}"
168
+ )
169
+ end
170
+
171
+ # Builds Proxmox API parameters from user-friendly config options.
172
+ #
173
+ # Maps config keys to their Proxmox API equivalents. Does not include
174
+ # name, description, or pool (those belong to the clone step).
175
+ #
176
+ # @param config_params [Hash] User-friendly config parameters
177
+ # @return [Hash] Proxmox API parameters
178
+ def build_config_api_params(config_params)
179
+ params = {}
180
+ params[:cores] = config_params[:cores] if config_params[:cores]
181
+ params[:sockets] = config_params[:sockets] if config_params[:sockets]
182
+ params[:cpu] = config_params[:cpu_type] if config_params[:cpu_type]
183
+ params[:numa] = config_params[:numa] ? 1 : 0 unless config_params[:numa].nil?
184
+ params[:memory] = config_params[:memory] if config_params[:memory]
185
+ params[:balloon] = config_params[:balloon] if config_params[:balloon]
186
+ add_disk_params(params, config_params[:disks]) if config_params[:disks]
187
+ params[:scsihw] = config_params[:scsihw] if config_params[:scsihw]
188
+ params[:cdrom] = config_params[:cdrom] if config_params[:cdrom]
189
+ add_net_params(params, config_params[:nets]) if config_params[:nets]
190
+ params[:bios] = config_params[:bios] if config_params[:bios]
191
+ params[:boot] = "order=#{config_params[:boot_order]}" if config_params[:boot_order]
192
+ params[:machine] = config_params[:machine] if config_params[:machine]
193
+ params[:efidisk0] = config_params[:efidisk] if config_params[:efidisk]
194
+ params.merge!(config_params[:cloud_init]) if config_params[:cloud_init]
195
+ params[:agent] = config_params[:agent] ? "1" : "0" unless config_params[:agent].nil?
196
+ params[:ostype] = config_params[:ostype] if config_params[:ostype]
197
+ params[:tags] = config_params[:tags] if config_params[:tags]
198
+ params
199
+ end
200
+
201
+ # Adds disk parameters mapped to scsi0, scsi1, etc.
202
+ #
203
+ # @param params [Hash] Parameters hash to modify
204
+ # @param disks [Array<Hash>] Disk configurations
205
+ # @return [void]
206
+ def add_disk_params(params, disks)
207
+ disks.each_with_index do |disk, index|
208
+ params[:"scsi#{index}"] = Parsers::DiskConfig.to_proxmox(disk)
209
+ end
210
+ end
211
+
212
+ # Adds network parameters mapped to net0, net1, etc.
213
+ #
214
+ # @param params [Hash] Parameters hash to modify
215
+ # @param nets [Array<Hash>] Network configurations
216
+ # @return [void]
217
+ def add_net_params(params, nets)
218
+ nets.each_with_index do |net, index|
219
+ params[:"net#{index}"] = Parsers::NetConfig.to_proxmox(net)
220
+ end
221
+ end
222
+
223
+ # Starts a VM after successful clone and config update.
224
+ #
225
+ # @param vmid [Integer] VM identifier
226
+ # @param node [String] Node name
227
+ # @return [void]
228
+ def start_vm(vmid, node)
229
+ upid = @vm_repository.start(vmid, node)
230
+ @task_repository.wait(upid, timeout: START_TIMEOUT)
231
+ end
232
+
233
+ # Returns configured timeout.
234
+ #
235
+ # @return [Integer] Timeout in seconds
236
+ def timeout
237
+ @options[:timeout] || DEFAULT_TIMEOUT
238
+ end
239
+
240
+ # Returns error for VM not found.
241
+ #
242
+ # @param vmid [Integer] VM identifier
243
+ # @return [Models::OperationResult] Failed result
244
+ def vm_not_found_error(vmid)
245
+ Models::VmOperationResult.new(
246
+ operation: :clone,
247
+ success: false,
248
+ error: "VM #{vmid} not found"
249
+ )
250
+ end
251
+
252
+ # Returns error for linked clone of non-template VM.
253
+ #
254
+ # @param source_vm [Models::Vm] Source VM
255
+ # @return [Models::OperationResult] Failed result
256
+ def linked_clone_error(source_vm)
257
+ Models::VmOperationResult.new(
258
+ vm: source_vm, operation: :clone,
259
+ success: false,
260
+ error: "Linked clone requires VM to be a template. VM #{source_vm.vmid} is not a template"
261
+ )
262
+ end
263
+ end
264
+ end
265
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pvectl
4
+ module Services
5
+ # Orchestrates cloud-init operations on VMs.
6
+ #
7
+ # Provides three operations:
8
+ # - +regenerate+ — rebuild the cloud-init ISO from current config
9
+ # - +pending+ — list configuration changes not yet applied
10
+ # - +dump+ — retrieve the auto-generated cloud-init YAML
11
+ #
12
+ # Operations are VM-only — LXC containers do not expose cloud-init
13
+ # endpoints. When the +vmid+ resolves to a non-QEMU resource, the
14
+ # service raises +Pvectl::ResourceNotFoundError+.
15
+ #
16
+ # @example Regenerate ISO for a VM
17
+ # service = Cloudinit.new(vm_repository: vm_repo, resource_resolver: resolver)
18
+ # service.regenerate(100)
19
+ #
20
+ # @example Dump generated user-data YAML
21
+ # yaml = service.dump(100, "user")
22
+ #
23
+ class Cloudinit
24
+ # Cloud-init config types supported by the +dump+ operation.
25
+ VALID_DUMP_TYPES = %w[user network meta].freeze
26
+
27
+ # Creates a new Cloudinit service.
28
+ #
29
+ # @param vm_repository [Repositories::Vm] VM repository
30
+ # @param resource_resolver [Utils::ResourceResolver] resolver for VMID -> node
31
+ def initialize(vm_repository:, resource_resolver:)
32
+ @vm_repository = vm_repository
33
+ @resolver = resource_resolver
34
+ end
35
+
36
+ # Regenerates the cloud-init ISO for the given VM.
37
+ #
38
+ # @param vmid [Integer] VM identifier
39
+ # @param node [String, nil] explicit node name (skips resolver)
40
+ # @return [Hash{Symbol => untyped}] +{ vmid: Integer, node: String }+
41
+ # @raise [Pvectl::ResourceNotFoundError] when VM does not exist or is not QEMU
42
+ def regenerate(vmid, node: nil)
43
+ node ||= resolve_node!(vmid)
44
+ @vm_repository.cloudinit_regenerate(node, vmid)
45
+ { vmid: vmid, node: node }
46
+ end
47
+
48
+ # Returns pending cloud-init configuration changes.
49
+ #
50
+ # @param vmid [Integer] VM identifier
51
+ # @param node [String, nil] explicit node name (skips resolver)
52
+ # @return [Array<Hash{Symbol => untyped}>] pending entries
53
+ # @raise [Pvectl::ResourceNotFoundError] when VM does not exist or is not QEMU
54
+ def pending(vmid, node: nil)
55
+ node ||= resolve_node!(vmid)
56
+ @vm_repository.cloudinit_pending(node, vmid)
57
+ end
58
+
59
+ # Dumps the auto-generated cloud-init configuration.
60
+ #
61
+ # @param vmid [Integer] VM identifier
62
+ # @param type [String] one of +"user"+, +"network"+, +"meta"+
63
+ # @param node [String, nil] explicit node name (skips resolver)
64
+ # @return [String] raw cloud-init YAML/text
65
+ # @raise [ArgumentError] when +type+ is not a valid dump type
66
+ # @raise [Pvectl::ResourceNotFoundError] when VM does not exist or is not QEMU
67
+ def dump(vmid, type, node: nil)
68
+ unless VALID_DUMP_TYPES.include?(type)
69
+ raise ArgumentError, "Invalid cloud-init dump type: #{type.inspect} " \
70
+ "(valid: #{VALID_DUMP_TYPES.join(', ')})"
71
+ end
72
+
73
+ node ||= resolve_node!(vmid)
74
+ @vm_repository.cloudinit_dump(node, vmid, type)
75
+ end
76
+
77
+ private
78
+
79
+ # Resolves a VMID to its node, ensuring the resource is a QEMU VM.
80
+ #
81
+ # @param vmid [Integer] VM identifier
82
+ # @return [String] node name
83
+ # @raise [Pvectl::ResourceNotFoundError] when not found or not a VM
84
+ def resolve_node!(vmid)
85
+ resolved = @resolver.resolve(vmid)
86
+ raise Pvectl::ResourceNotFoundError, "VM #{vmid} not found" if resolved.nil?
87
+
88
+ unless resolved[:type] == :qemu
89
+ raise Pvectl::ResourceNotFoundError, "Resource #{vmid} is not a VM (cloud-init is VM-only)"
90
+ end
91
+
92
+ resolved[:node]
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require "rest_client"
5
+ require "json"
6
+
7
+ module Pvectl
8
+ module Services
9
+ # Orchestrates an interactive console session to a VM or container.
10
+ #
11
+ # Handles: authentication (session ticket), termproxy setup,
12
+ # WebSocket URL construction, and terminal session lifecycle.
13
+ #
14
+ # @example Basic usage
15
+ # service = Console.new
16
+ # service.run(resource: vm, resource_path: "qemu/100", server: "https://pve1:8006",
17
+ # username: "root@pam", password: "secret", verify_ssl: true)
18
+ #
19
+ class Console
20
+ # Raised when the target resource is not in a running state.
21
+ class ResourceNotRunningError < Pvectl::Error; end
22
+
23
+ # Raised when authentication fails.
24
+ class AuthenticationError < Pvectl::Error; end
25
+
26
+ # Builds the WebSocket URL for vncwebsocket endpoint.
27
+ #
28
+ # @param server [String] Proxmox server URL (e.g. "https://pve1:8006")
29
+ # @param node [String] node name
30
+ # @param resource_path [String] resource path (e.g. "qemu/100" or "lxc/200")
31
+ # @param port [Integer] termproxy port
32
+ # @param ticket [String] PVEVNC ticket
33
+ # @return [String] full WebSocket URL
34
+ def build_websocket_url(server:, node:, resource_path:, port:, ticket:)
35
+ uri = URI.parse(server)
36
+ scheme = uri.scheme == "https" ? "wss" : "ws"
37
+ host = uri.host
38
+ ws_port = uri.port || 8006
39
+ encoded_ticket = URI.encode_www_form_component(ticket)
40
+
41
+ "#{scheme}://#{host}:#{ws_port}/api2/json/nodes/#{node}/#{resource_path}/vncwebsocket" \
42
+ "?port=#{port}&vncticket=#{encoded_ticket}"
43
+ end
44
+
45
+ # Validates that the resource is in a running state.
46
+ #
47
+ # @param resource [Models::Vm, Models::Container] resource to check
48
+ # @return [void]
49
+ # @raise [ResourceNotRunningError] if resource is not running
50
+ def validate_resource_running!(resource)
51
+ return if resource.status == "running"
52
+
53
+ raise ResourceNotRunningError,
54
+ "Resource #{resource.vmid} is not running (status: #{resource.status})"
55
+ end
56
+
57
+ # Runs a console session end-to-end.
58
+ #
59
+ # All API calls (authenticate, termproxy) use the same session-based
60
+ # REST client to ensure PVEVNC ticket and PVEAuthCookie share the
61
+ # same identity. Using a token-based connection for termproxy would
62
+ # generate a ticket bound to the token identity (e.g., "root@pam!pvectl"),
63
+ # which is rejected by the WebSocket endpoint expecting a session cookie
64
+ # for "root@pam".
65
+ #
66
+ # @param resource [Models::Vm, Models::Container] target resource
67
+ # @param resource_path [String] API path segment ("qemu/{vmid}" or "lxc/{ctid}")
68
+ # @param server [String] Proxmox server URL
69
+ # @param username [String] username for auth
70
+ # @param password [String] password for auth
71
+ # @param verify_ssl [Boolean] SSL verification flag
72
+ # @return [void]
73
+ def run(resource:, resource_path:, server:, username:, password:, verify_ssl:)
74
+ validate_resource_running!(resource)
75
+
76
+ # All operations use one session-authenticated REST client
77
+ api_url = "#{server}/api2/json/"
78
+ session_client = RestClient::Resource.new(api_url, verify_ssl: verify_ssl)
79
+
80
+ # 1. Authenticate
81
+ auth = authenticate_with_client(session_client, username, password)
82
+
83
+ # 2. Open termproxy (using session ticket, not API token)
84
+ termproxy_data = open_termproxy(
85
+ session_client, auth,
86
+ node: resource.node, resource_path: resource_path
87
+ )
88
+
89
+ # 3. Build websocket URL
90
+ url = build_websocket_url(
91
+ server: server,
92
+ node: resource.node,
93
+ resource_path: resource_path,
94
+ port: termproxy_data[:port],
95
+ ticket: termproxy_data[:ticket]
96
+ )
97
+
98
+ # 4. Run terminal session
99
+ session = Pvectl::Console::TerminalSession.new(
100
+ url: url,
101
+ cookie: "PVEAuthCookie=#{auth[:ticket]}",
102
+ user: termproxy_data[:user],
103
+ ticket: termproxy_data[:ticket],
104
+ verify_ssl: verify_ssl
105
+ )
106
+ session.run
107
+ end
108
+
109
+ private
110
+
111
+ # Authenticates using a provided REST client and returns session data.
112
+ #
113
+ # @param client [RestClient::Resource] clean REST client
114
+ # @param username [String] Proxmox username
115
+ # @param password [String] password
116
+ # @return [Hash] { ticket:, csrf_token: }
117
+ # @raise [AuthenticationError] on failure
118
+ def authenticate_with_client(client, username, password)
119
+ response = client["access/ticket"].post(username: username, password: password)
120
+ data = JSON.parse(response.body, symbolize_names: true)[:data]
121
+
122
+ {
123
+ ticket: data[:ticket],
124
+ csrf_token: data[:CSRFPreventionToken]
125
+ }
126
+ rescue StandardError => e
127
+ raise AuthenticationError, "Authentication failed: #{e.message}"
128
+ end
129
+
130
+ # Opens a termproxy session using session auth credentials.
131
+ #
132
+ # @param client [RestClient::Resource] REST client
133
+ # @param auth [Hash] session auth with :ticket and :csrf_token
134
+ # @param node [String] Proxmox node name
135
+ # @param resource_path [String] e.g. "qemu/100" or "lxc/200"
136
+ # @return [Hash] { port:, ticket:, user: }
137
+ def open_termproxy(client, auth, node:, resource_path:)
138
+ endpoint = "nodes/#{node}/#{resource_path}/termproxy"
139
+ response = client[endpoint].post(
140
+ {},
141
+ cookies: { PVEAuthCookie: auth[:ticket] },
142
+ CSRFPreventionToken: auth[:csrf_token]
143
+ )
144
+ data = JSON.parse(response.body, symbolize_names: true)[:data]
145
+
146
+ { port: data[:port], ticket: data[:ticket], user: data[:user] }
147
+ rescue StandardError => e
148
+ raise AuthenticationError, "Termproxy failed: #{e.message}"
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pvectl
4
+ module Services
5
+ # Orchestrates container lifecycle operations.
6
+ #
7
+ # Handles execution of start/stop/shutdown/restart operations
8
+ # with sync/async modes, error handling, and result collection.
9
+ #
10
+ # @example Basic usage
11
+ # service = ContainerLifecycle.new(ct_repo, task_repo)
12
+ # results = service.execute(:start, [ct1, ct2])
13
+ # results.each { |r| puts "#{r.container.vmid}: #{r.status_text}" }
14
+ #
15
+ class ContainerLifecycle
16
+ SYNC_OPERATIONS = %i[start stop].freeze
17
+ ASYNC_OPERATIONS = %i[shutdown restart].freeze
18
+ ALL_OPERATIONS = (SYNC_OPERATIONS + ASYNC_OPERATIONS).freeze
19
+
20
+ DEFAULT_TIMEOUT = 60
21
+
22
+ # Creates a new ContainerLifecycle service.
23
+ #
24
+ # @param container_repository [Repositories::Container] Container repository
25
+ # @param task_repository [Repositories::Task] Task repository
26
+ # @param options [Hash] Options (timeout, async, wait, fail_fast)
27
+ def initialize(container_repository, task_repository, options = {})
28
+ @container_repository = container_repository
29
+ @task_repository = task_repository
30
+ @options = options
31
+ end
32
+
33
+ # Executes a lifecycle operation on a list of containers.
34
+ #
35
+ # @param operation [Symbol] Operation to execute
36
+ # @param containers [Array<Models::Container>] Containers to operate on
37
+ # @return [Array<Models::ContainerOperationResult>] Results for each container
38
+ def execute(operation, containers)
39
+ validate_operation!(operation)
40
+
41
+ results = []
42
+ containers.each do |container|
43
+ result = execute_single(operation, container)
44
+ results << result
45
+
46
+ break if @options[:fail_fast] && result.failed?
47
+ end
48
+ results
49
+ end
50
+
51
+ private
52
+
53
+ # Executes operation on a single container.
54
+ #
55
+ # @param operation [Symbol] Operation
56
+ # @param container [Models::Container] Container
57
+ # @return [Models::ContainerOperationResult] Result
58
+ def execute_single(operation, container)
59
+ task_upid = call_api(operation, container)
60
+
61
+ if sync_mode?(operation)
62
+ task = @task_repository.wait(task_upid, timeout: timeout)
63
+ Models::ContainerOperationResult.new(
64
+ container: container,
65
+ operation: operation,
66
+ task: task,
67
+ success: task.successful?
68
+ )
69
+ else
70
+ Models::ContainerOperationResult.new(
71
+ container: container,
72
+ operation: operation,
73
+ task_upid: task_upid,
74
+ success: :pending
75
+ )
76
+ end
77
+ rescue StandardError => e
78
+ Models::ContainerOperationResult.new(
79
+ container: container,
80
+ operation: operation,
81
+ success: false,
82
+ error: e.message
83
+ )
84
+ end
85
+
86
+ # Calls the appropriate API method.
87
+ #
88
+ # @param operation [Symbol] Operation
89
+ # @param container [Models::Container] Container
90
+ # @return [String] Task UPID
91
+ def call_api(operation, container)
92
+ @container_repository.send(operation, container.vmid, container.node)
93
+ end
94
+
95
+ # Determines if operation should run in sync mode.
96
+ #
97
+ # @param operation [Symbol] Operation
98
+ # @return [Boolean] true if sync mode
99
+ def sync_mode?(operation)
100
+ return false if @options[:async]
101
+ return true if @options[:wait]
102
+
103
+ SYNC_OPERATIONS.include?(operation)
104
+ end
105
+
106
+ # Returns configured timeout.
107
+ #
108
+ # @return [Integer] Timeout in seconds
109
+ def timeout
110
+ @options[:timeout] || DEFAULT_TIMEOUT
111
+ end
112
+
113
+ # Validates operation is supported.
114
+ #
115
+ # @param operation [Symbol] Operation
116
+ # @raise [ArgumentError] if operation is not supported
117
+ def validate_operation!(operation)
118
+ return if ALL_OPERATIONS.include?(operation)
119
+
120
+ raise ArgumentError, "Unknown operation: #{operation}. Valid: #{ALL_OPERATIONS.join(', ')}"
121
+ end
122
+ end
123
+ end
124
+ end