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,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pvectl
4
+ module Selectors
5
+ # Selector for filtering VMs.
6
+ #
7
+ # Extends Base with VM-specific field extraction.
8
+ # Supports: status, tags, pool, name, template.
9
+ #
10
+ # @example Filter running VMs
11
+ # selector = Vm.parse("status=running")
12
+ # running_vms = selector.apply(all_vms)
13
+ #
14
+ # @example Filter by multiple criteria
15
+ # selector = Vm.parse("status=running,tags=prod")
16
+ # filtered = selector.apply(all_vms)
17
+ #
18
+ # @example Filter by name pattern
19
+ # selector = Vm.parse("name=~web-*")
20
+ # web_vms = selector.apply(all_vms)
21
+ #
22
+ class Vm < Base
23
+ SUPPORTED_FIELDS = %w[status tags pool name template].freeze
24
+
25
+ # Applies selector to VM collection.
26
+ #
27
+ # @param vms [Array<Models::Vm>] VMs to filter
28
+ # @return [Array<Models::Vm>] Filtered VMs
29
+ def apply(vms)
30
+ return vms if empty?
31
+
32
+ vms.select { |vm| matches?(vm) }
33
+ end
34
+
35
+ protected
36
+
37
+ # Extracts field value from VM model.
38
+ #
39
+ # @param vm [Models::Vm] VM model
40
+ # @param field [String] Field name (status, tags, pool, name, template)
41
+ # @return [String, nil] Field value
42
+ # @raise [ArgumentError] if field is not supported
43
+ def extract_value(vm, field)
44
+ case field
45
+ when "status"
46
+ vm.status
47
+ when "tags"
48
+ vm.tags
49
+ when "pool"
50
+ vm.pool
51
+ when "name"
52
+ vm.name
53
+ when "template"
54
+ vm.template? ? "yes" : "no"
55
+ else
56
+ raise ArgumentError, "Unknown field: #{field}. Supported: #{SUPPORTED_FIELDS.join(', ')}"
57
+ end
58
+ end
59
+
60
+ # Override to handle tags specially.
61
+ # Tags in Proxmox are semicolon-separated: "tag1;tag2;tag3"
62
+ # Selector "tags=prod" should match if "prod" is one of the tags.
63
+ #
64
+ # @param vm [Models::Vm] VM model
65
+ # @param condition [Hash] Condition
66
+ # @return [Boolean] true if matches
67
+ def match_condition?(vm, condition)
68
+ return match_tags_condition?(vm, condition) if condition[:field] == "tags"
69
+ return super(vm, normalize_boolean_condition(condition)) if condition[:field] == "template"
70
+
71
+ super
72
+ end
73
+
74
+ private
75
+
76
+ # Normalizes boolean condition values for template field.
77
+ # Accepts: yes/no, true/false, 1/0
78
+ #
79
+ # @param condition [Hash] Condition with :value key
80
+ # @return [Hash] Condition with normalized value
81
+ def normalize_boolean_condition(condition)
82
+ normalized = case condition[:value]
83
+ when "true", "1" then "yes"
84
+ when "false", "0" then "no"
85
+ else condition[:value]
86
+ end
87
+ condition.merge(value: normalized)
88
+ end
89
+
90
+ # Special matching for tags field.
91
+ # Proxmox tags are semicolon-separated, so we check if the value
92
+ # is contained in the tag list.
93
+ #
94
+ # @param vm [Models::Vm] VM model
95
+ # @param condition [Hash] Condition
96
+ # @return [Boolean] true if matches
97
+ def match_tags_condition?(vm, condition)
98
+ tags_string = vm.tags || ""
99
+ tag_list = tags_string.split(";").map(&:strip)
100
+
101
+ case condition[:operator]
102
+ when :eq
103
+ tag_list.include?(condition[:value])
104
+ when :neq
105
+ !tag_list.include?(condition[:value])
106
+ when :match
107
+ tag_list.any? { |tag| wildcard_match?(tag, condition[:value]) }
108
+ when :in
109
+ (tag_list & condition[:value]).any?
110
+ else
111
+ false
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pvectl
4
+ module Selectors
5
+ # Selector for filtering virtual disk volumes.
6
+ #
7
+ # Extends Base with volume-specific field extraction.
8
+ # Supports: format, storage, node, content, resource_type, name.
9
+ #
10
+ # @example Filter raw volumes only
11
+ # selector = Volume.parse("format=raw")
12
+ # raw_vols = selector.apply(all_volumes)
13
+ #
14
+ # @example Filter volumes on a specific storage and node
15
+ # selector = Volume.parse("storage=local-lvm,node=pve1")
16
+ # filtered = selector.apply(all_volumes)
17
+ #
18
+ class Volume < Base
19
+ SUPPORTED_FIELDS = %w[format storage node content resource_type name].freeze
20
+
21
+ # Applies selector to volume collection.
22
+ #
23
+ # @param volumes [Array<Models::Volume>] volumes to filter
24
+ # @return [Array<Models::Volume>] filtered volumes
25
+ def apply(volumes)
26
+ return volumes if empty?
27
+
28
+ volumes.select { |vol| matches?(vol) }
29
+ end
30
+
31
+ protected
32
+
33
+ # Extracts field value from Volume model.
34
+ #
35
+ # @param vol [Models::Volume] volume model
36
+ # @param field [String] field name
37
+ # @return [String, nil] field value
38
+ # @raise [ArgumentError] if field is not supported
39
+ def extract_value(vol, field)
40
+ case field
41
+ when "format"
42
+ vol.format
43
+ when "storage"
44
+ vol.storage
45
+ when "node"
46
+ vol.node
47
+ when "content"
48
+ vol.content
49
+ when "resource_type"
50
+ vol.resource_type
51
+ when "name"
52
+ vol.name
53
+ else
54
+ raise ArgumentError, "Unknown field: #{field}. Supported: #{SUPPORTED_FIELDS.join(', ')}"
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pvectl
4
+ module Services
5
+ # Orchestrates backup operations.
6
+ #
7
+ # Handles creating, listing, deleting, and restoring backups
8
+ # for VMs and containers with multi-ID support and error handling.
9
+ #
10
+ # @example Basic usage
11
+ # service = Backup.new(
12
+ # backup_repo: backup_repo,
13
+ # resource_resolver: resolver,
14
+ # task_repo: task_repo
15
+ # )
16
+ # backups = service.list(vmid: 100)
17
+ #
18
+ class Backup
19
+ DEFAULT_TIMEOUT = 300 # 5 minutes for backups
20
+
21
+ # Creates a new Backup service.
22
+ #
23
+ # @param backup_repo [Repositories::Backup] Backup repository
24
+ # @param resource_resolver [Utils::ResourceResolver] Resource resolver
25
+ # @param task_repo [Repositories::Task] Task repository
26
+ # @param options [Hash] Options (timeout, async, fail_fast)
27
+ def initialize(backup_repo:, resource_resolver:, task_repo:, options: {})
28
+ @backup_repo = backup_repo
29
+ @resolver = resource_resolver
30
+ @task_repo = task_repo
31
+ @options = options
32
+ end
33
+
34
+ # Lists backups with optional filtering.
35
+ #
36
+ # @param vmid [Integer, nil] filter by VM ID
37
+ # @param storage [String, nil] filter by storage
38
+ # @return [Array<Models::Backup>]
39
+ def list(vmid: nil, storage: nil)
40
+ @backup_repo.list(vmid: vmid, storage: storage)
41
+ end
42
+
43
+ # Creates backups for multiple VMs.
44
+ #
45
+ # @param vmids [Array<Integer>] VM IDs
46
+ # @param storage [String] target storage
47
+ # @param mode [String] backup mode (snapshot/suspend/stop)
48
+ # @param compress [String] compression (zstd/gzip/lzo/0)
49
+ # @param notes [String, nil] backup notes
50
+ # @param protected [Boolean] protect backup
51
+ # @return [Array<Models::OperationResult>]
52
+ def create(vmids, storage:, mode: "snapshot", compress: "zstd", notes: nil, protected: false)
53
+ resources = @resolver.resolve_multiple(vmids)
54
+ return [] if resources.empty?
55
+
56
+ execute_multi(resources, :create) do |resource|
57
+ @backup_repo.create(
58
+ resource[:vmid],
59
+ resource[:node],
60
+ storage: storage,
61
+ mode: mode,
62
+ compress: compress,
63
+ notes: notes,
64
+ protected: protected
65
+ )
66
+ end
67
+ end
68
+
69
+ # Deletes a backup.
70
+ #
71
+ # @param volid [String] backup volume ID
72
+ # @return [Models::OperationResult]
73
+ def delete(volid)
74
+ node = find_node_for_volid(volid)
75
+
76
+ upid = @backup_repo.delete(volid, node)
77
+
78
+ if @options[:async]
79
+ Models::OperationResult.new(
80
+ resource: { volid: volid, node: node },
81
+ operation: :delete,
82
+ task_upid: upid,
83
+ success: :pending
84
+ )
85
+ else
86
+ task = @task_repo.wait(upid, timeout: timeout)
87
+ Models::OperationResult.new(
88
+ resource: { volid: volid, node: node },
89
+ operation: :delete,
90
+ task: task,
91
+ success: task.successful?
92
+ )
93
+ end
94
+ rescue StandardError => e
95
+ Models::OperationResult.new(
96
+ resource: { volid: volid },
97
+ operation: :delete,
98
+ success: false,
99
+ error: e.message
100
+ )
101
+ end
102
+
103
+ # Restores a backup to a VM/container.
104
+ #
105
+ # @param volid [String] backup volume ID
106
+ # @param vmid [Integer] target VM ID
107
+ # @param storage [String, nil] target storage
108
+ # @param force [Boolean] overwrite existing
109
+ # @param start [Boolean] start after restore
110
+ # @param unique [Boolean] regenerate unique properties
111
+ # @return [Models::OperationResult]
112
+ def restore(volid, vmid:, storage: nil, force: false, start: false, unique: false)
113
+ node = find_node_for_volid(volid)
114
+
115
+ upid = @backup_repo.restore(
116
+ volid,
117
+ node,
118
+ vmid: vmid,
119
+ storage: storage,
120
+ force: force,
121
+ start: start,
122
+ unique: unique
123
+ )
124
+
125
+ if @options[:async]
126
+ Models::OperationResult.new(
127
+ resource: { volid: volid, vmid: vmid, node: node },
128
+ operation: :restore,
129
+ task_upid: upid,
130
+ success: :pending
131
+ )
132
+ else
133
+ task = @task_repo.wait(upid, timeout: timeout)
134
+ Models::OperationResult.new(
135
+ resource: { volid: volid, vmid: vmid, node: node },
136
+ operation: :restore,
137
+ task: task,
138
+ success: task.successful?
139
+ )
140
+ end
141
+ rescue StandardError => e
142
+ Models::OperationResult.new(
143
+ resource: { volid: volid, vmid: vmid },
144
+ operation: :restore,
145
+ success: false,
146
+ error: e.message
147
+ )
148
+ end
149
+
150
+ private
151
+
152
+ def execute_multi(resources, operation)
153
+ results = []
154
+
155
+ resources.each do |resource|
156
+ result = execute_single(resource, operation) { yield(resource) }
157
+ results << result
158
+
159
+ break if @options[:fail_fast] && result.failed?
160
+ end
161
+
162
+ results
163
+ end
164
+
165
+ def execute_single(resource, operation)
166
+ upid = yield
167
+
168
+ if @options[:async]
169
+ Models::OperationResult.new(
170
+ resource: resource,
171
+ operation: operation,
172
+ task_upid: upid,
173
+ success: :pending
174
+ )
175
+ else
176
+ task = @task_repo.wait(upid, timeout: timeout)
177
+ Models::OperationResult.new(
178
+ resource: resource,
179
+ operation: operation,
180
+ task: task,
181
+ success: task.successful?
182
+ )
183
+ end
184
+ rescue StandardError => e
185
+ Models::OperationResult.new(
186
+ resource: resource,
187
+ operation: operation,
188
+ success: false,
189
+ error: e.message
190
+ )
191
+ end
192
+
193
+ def timeout
194
+ @options[:timeout] || DEFAULT_TIMEOUT
195
+ end
196
+
197
+ # Finds the node for a backup by searching all backups.
198
+ #
199
+ # @param volid [String] backup volume ID
200
+ # @return [String] node name
201
+ # @raise [StandardError] if backup not found
202
+ def find_node_for_volid(volid)
203
+ backups = @backup_repo.list
204
+ backup = backups.find { |b| b.volid == volid }
205
+ backup&.node || raise("Backup not found: #{volid}")
206
+ end
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,260 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pvectl
4
+ module Services
5
+ # Orchestrates container clone operations.
6
+ #
7
+ # Handles validation, auto-generation of CTID/hostname, and sync/async modes.
8
+ # Supports both full clones and linked clones (templates only).
9
+ #
10
+ # @example Full clone with auto-generated CTID
11
+ # service = CloneContainer.new(container_repository: ct_repo, task_repository: task_repo)
12
+ # result = service.execute(ctid: 100)
13
+ #
14
+ # @example Linked clone to specific node
15
+ # service = CloneContainer.new(container_repository: ct_repo, task_repository: task_repo)
16
+ # result = service.execute(ctid: 100, linked: true, target_node: "pve2")
17
+ #
18
+ # @example Async clone with custom timeout
19
+ # service = CloneContainer.new(container_repository: ct_repo, task_repository: task_repo, options: { async: true })
20
+ # result = service.execute(ctid: 100, new_ctid: 200, hostname: "web-clone")
21
+ #
22
+ class CloneContainer
23
+ DEFAULT_TIMEOUT = 300
24
+
25
+ # @return [Integer] Default timeout for start operations (seconds)
26
+ START_TIMEOUT = 60
27
+
28
+ # Creates a new CloneContainer service.
29
+ #
30
+ # @param container_repository [Repositories::Container] Container repository
31
+ # @param task_repository [Repositories::Task] Task repository
32
+ # @param options [Hash] Options (timeout, async)
33
+ def initialize(container_repository:, task_repository:, options: {})
34
+ @container_repository = container_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 container first, then optionally apply
42
+ # config updates via PUT /nodes/{node}/lxc/{ctid}/config.
43
+ #
44
+ # @param ctid [Integer] Source container identifier
45
+ # @param node [String, nil] Source node (auto-detected from container if nil)
46
+ # @param new_ctid [Integer, nil] New CTID (auto-selected if nil)
47
+ # @param hostname [String, nil] Hostname 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] Container config parameters to apply after clone
54
+ # @return [Models::ContainerOperationResult] Clone result
55
+ def execute(ctid:, node: nil, new_ctid: nil, hostname: nil, target_node: nil,
56
+ storage: nil, linked: false, pool: nil, description: nil,
57
+ config_params: {})
58
+ source_ct = @container_repository.get(ctid)
59
+ return container_not_found_error(ctid) unless source_ct
60
+
61
+ if linked && !source_ct.template?
62
+ return linked_clone_error(source_ct)
63
+ end
64
+
65
+ node ||= source_ct.node
66
+ new_ctid ||= @container_repository.next_available_ctid
67
+ hostname ||= generate_hostname(source_ct)
68
+
69
+ clone_options = build_clone_options(
70
+ hostname: hostname, target_node: target_node, storage: storage,
71
+ linked: linked, pool: pool, description: description
72
+ )
73
+
74
+ upid = @container_repository.clone(ctid, node, new_ctid, clone_options)
75
+ resource_info = { new_ctid: new_ctid, hostname: hostname, node: target_node || node }
76
+
77
+ if @options[:async]
78
+ Models::ContainerOperationResult.new(
79
+ container: source_ct, 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::ContainerOperationResult.new(
88
+ container: source_ct, 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_ct, new_ctid, resource_info[:node], config_params, resource_info)
96
+ else
97
+ start_container(new_ctid, resource_info[:node]) if @options[:start]
98
+ Models::ContainerOperationResult.new(
99
+ container: source_ct, operation: :clone,
100
+ task: task, success: true,
101
+ resource: resource_info
102
+ )
103
+ end
104
+ end
105
+ rescue StandardError => e
106
+ Models::ContainerOperationResult.new(
107
+ container: source_ct, operation: :clone,
108
+ success: false, error: e.message
109
+ )
110
+ end
111
+
112
+ private
113
+
114
+ # Generates clone hostname from source container.
115
+ #
116
+ # @param source_ct [Models::Container] Source container
117
+ # @return [String] Generated hostname
118
+ def generate_hostname(source_ct)
119
+ if source_ct.name && !source_ct.name.empty?
120
+ "#{source_ct.name}-clone"
121
+ else
122
+ "ct-#{source_ct.vmid}-clone"
123
+ end
124
+ end
125
+
126
+ # Builds clone options hash for repository call.
127
+ #
128
+ # @param hostname [String] Clone hostname
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(hostname:, target_node:, storage:, linked:, pool:, description:)
136
+ opts = { hostname: hostname, 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 container.
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_ct [Models::Container] Source container
150
+ # @param new_ctid [Integer] Cloned container identifier
151
+ # @param node [String] Target node for the cloned container
152
+ # @param config_params [Hash] Config parameters to apply
153
+ # @param resource_info [Hash] Resource info for result
154
+ # @return [Models::ContainerOperationResult] Operation result
155
+ def apply_config_update(source_ct, new_ctid, node, config_params, resource_info)
156
+ api_params = build_ct_config_api_params(config_params)
157
+ @container_repository.update(new_ctid, node, api_params)
158
+ start_container(new_ctid, node) if @options[:start]
159
+ Models::ContainerOperationResult.new(
160
+ container: source_ct, operation: :clone,
161
+ success: true, resource: resource_info
162
+ )
163
+ rescue StandardError => e
164
+ Models::ContainerOperationResult.new(
165
+ container: source_ct, 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 container config options.
172
+ #
173
+ # Maps config keys to their Proxmox API equivalents. Does not include
174
+ # hostname, ostemplate, 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_ct_config_api_params(config_params)
179
+ params = {}
180
+ params[:cores] = config_params[:cores] if config_params[:cores]
181
+ params[:memory] = config_params[:memory] if config_params[:memory]
182
+ params[:swap] = config_params[:swap] if config_params[:swap]
183
+ params[:unprivileged] = config_params[:privileged] ? 0 : 1 unless config_params[:privileged].nil?
184
+ params[:rootfs] = Parsers::LxcMountConfig.to_proxmox(config_params[:rootfs]) if config_params[:rootfs]
185
+ add_mountpoint_params(params, config_params[:mountpoints]) if config_params[:mountpoints]
186
+ add_net_params(params, config_params[:nets]) if config_params[:nets]
187
+ params[:features] = config_params[:features] if config_params[:features]
188
+ params[:password] = config_params[:password] if config_params[:password]
189
+ params[:"ssh-public-keys"] = config_params[:ssh_public_keys] if config_params[:ssh_public_keys]
190
+ params[:onboot] = config_params[:onboot] ? 1 : 0 unless config_params[:onboot].nil?
191
+ params[:startup] = config_params[:startup] if config_params[:startup]
192
+ params[:tags] = config_params[:tags] if config_params[:tags]
193
+ params
194
+ end
195
+
196
+ # Adds mountpoint parameters mapped to mp0, mp1, etc.
197
+ #
198
+ # @param params [Hash] Parameters hash to modify
199
+ # @param mountpoints [Array<Hash>] Mountpoint configurations
200
+ # @return [void]
201
+ def add_mountpoint_params(params, mountpoints)
202
+ mountpoints.each_with_index do |mp, index|
203
+ params[:"mp#{index}"] = Parsers::LxcMountConfig.to_proxmox(mp)
204
+ end
205
+ end
206
+
207
+ # Adds network parameters mapped to net0, net1, etc.
208
+ #
209
+ # @param params [Hash] Parameters hash to modify
210
+ # @param nets [Array<Hash>] Network configurations
211
+ # @return [void]
212
+ def add_net_params(params, nets)
213
+ nets.each_with_index do |net, index|
214
+ params[:"net#{index}"] = Parsers::LxcNetConfig.to_proxmox(net)
215
+ end
216
+ end
217
+
218
+ # Starts a container after successful clone and config update.
219
+ #
220
+ # @param ctid [Integer] Container identifier
221
+ # @param node [String] Node name
222
+ # @return [void]
223
+ def start_container(ctid, node)
224
+ upid = @container_repository.start(ctid, node)
225
+ @task_repository.wait(upid, timeout: START_TIMEOUT)
226
+ end
227
+
228
+ # Returns configured timeout.
229
+ #
230
+ # @return [Integer] Timeout in seconds
231
+ def timeout
232
+ @options[:timeout] || DEFAULT_TIMEOUT
233
+ end
234
+
235
+ # Returns error for container not found.
236
+ #
237
+ # @param ctid [Integer] Container identifier
238
+ # @return [Models::ContainerOperationResult] Failed result
239
+ def container_not_found_error(ctid)
240
+ Models::ContainerOperationResult.new(
241
+ operation: :clone,
242
+ success: false,
243
+ error: "Container #{ctid} not found"
244
+ )
245
+ end
246
+
247
+ # Returns error for linked clone of non-template container.
248
+ #
249
+ # @param source_ct [Models::Container] Source container
250
+ # @return [Models::ContainerOperationResult] Failed result
251
+ def linked_clone_error(source_ct)
252
+ Models::ContainerOperationResult.new(
253
+ container: source_ct, operation: :clone,
254
+ success: false,
255
+ error: "Linked clone requires container to be a template. Container #{source_ct.vmid} is not a template"
256
+ )
257
+ end
258
+ end
259
+ end
260
+ end