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,306 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pvectl
4
+ module Repositories
5
+ # Repository for virtual disk volumes attached to VMs and containers.
6
+ #
7
+ # Aggregates volume data from two sources:
8
+ # - VM/CT config endpoints (parsed disk keys from configuration)
9
+ # - Storage content API (+/nodes/{node}/storage/{storage}/content+)
10
+ #
11
+ # Uses composition: delegates to VmRepository and ContainerRepository
12
+ # for config fetching and node resolution.
13
+ #
14
+ # @example Listing volumes from VM config
15
+ # repo = Volume.new(connection)
16
+ # volumes = repo.list_from_config(resource_type: "vm", ids: [100, 101])
17
+ # volumes.each { |v| puts "#{v.name}: #{v.storage}:#{v.volume_id} (#{v.size})" }
18
+ #
19
+ # @example Finding a specific disk
20
+ # volume = repo.find(resource_type: "vm", id: 100, disk_name: "scsi0")
21
+ # puts volume.size if volume
22
+ #
23
+ # @see Pvectl::Models::Volume Volume model
24
+ # @see Pvectl::Repositories::Vm VM repository
25
+ # @see Pvectl::Repositories::Container Container repository
26
+ #
27
+ class Volume < Base
28
+ # Pattern matching VM disk keys (scsi0, virtio1, ide2, sata3, efidisk0, tpmstate0)
29
+ VM_DISK_PATTERN = /\A(?:scsi|virtio|ide|sata|efidisk|tpmstate)\d+\z/
30
+
31
+ # Pattern matching container disk keys (rootfs, mp0, mp1, ...)
32
+ CT_DISK_PATTERN = /\A(?:rootfs|mp\d+)\z/
33
+
34
+ # Creates a new Volume repository.
35
+ #
36
+ # @param connection [Connection] API connection
37
+ # @param vm_repo [Repositories::Vm, nil] optional VM repository for DI
38
+ # @param container_repo [Repositories::Container, nil] optional container repository for DI
39
+ def initialize(connection, vm_repo: nil, container_repo: nil)
40
+ super(connection)
41
+ @vm_repo = vm_repo
42
+ @container_repo = container_repo
43
+ end
44
+
45
+ # Lists volumes from VM/CT configuration for given resource IDs.
46
+ #
47
+ # Fetches config from each VM/CT and extracts disk entries.
48
+ # Excludes CD-ROM entries (containing +media=cdrom+).
49
+ #
50
+ # @param resource_type [String] "vm" or "ct"
51
+ # @param ids [Array<Integer, String>] list of VMID/CTID values
52
+ # @param node [String, nil] filter results by node name
53
+ # @return [Array<Models::Volume>] collection of Volume models
54
+ def list_from_config(resource_type:, ids:, node: nil)
55
+ type = normalize_resource_type(resource_type)
56
+ repo = repo_for(type)
57
+ return [] unless repo
58
+
59
+ volumes = ids.flat_map do |id|
60
+ resource = repo.get(id)
61
+ next [] if resource.nil?
62
+ next [] if node && resource.node != node
63
+
64
+ config = repo.fetch_config(resource.node, id.to_i)
65
+ extract_volumes(config, type, id.to_i, resource.node)
66
+ end
67
+
68
+ volumes
69
+ end
70
+
71
+ # Lists volumes from storage content API.
72
+ #
73
+ # Queries +/nodes/{node}/storage/{storage}/content+ to list
74
+ # all volumes in the given storage.
75
+ #
76
+ # @param storage [String] storage name (e.g., "local-lvm")
77
+ # @param node [String, nil] node name (queries all online nodes if nil)
78
+ # @return [Array<Models::Volume>] collection of Volume models
79
+ def list_from_storage(storage:, node: nil)
80
+ nodes = node ? [node] : online_nodes
81
+ nodes.flat_map { |node_name| fetch_storage_volumes(node_name, storage) }
82
+ end
83
+
84
+ # Finds a specific volume by disk name in a VM/CT config.
85
+ #
86
+ # @param resource_type [String] "vm" or "ct"
87
+ # @param id [Integer, String] VMID or CTID
88
+ # @param disk_name [String] disk key name (e.g., "scsi0", "rootfs")
89
+ # @param node [String, nil] optional node override
90
+ # @return [Models::Volume, nil] Volume model or nil if not found
91
+ def find(resource_type:, id:, disk_name:, node: nil)
92
+ volumes = list_from_config(resource_type: resource_type, ids: [id], node: node)
93
+ volumes.find { |v| v.name == disk_name }
94
+ end
95
+
96
+ private
97
+
98
+ # Returns the appropriate repository for the given resource type.
99
+ #
100
+ # @param type [String] normalized resource type ("vm" or "ct")
101
+ # @return [Repositories::Vm, Repositories::Container, nil] repository instance
102
+ def repo_for(type)
103
+ case type
104
+ when "vm" then vm_repo
105
+ when "ct" then container_repo
106
+ end
107
+ end
108
+
109
+ # Returns VM repository instance.
110
+ # Uses injected repository if provided, otherwise creates new one.
111
+ #
112
+ # @return [Repositories::Vm] VM repository
113
+ def vm_repo
114
+ @vm_repo ||= Repositories::Vm.new(connection)
115
+ end
116
+
117
+ # Returns container repository instance.
118
+ # Uses injected repository if provided, otherwise creates new one.
119
+ #
120
+ # @return [Repositories::Container] container repository
121
+ def container_repo
122
+ @container_repo ||= Repositories::Container.new(connection)
123
+ end
124
+
125
+ # Normalizes resource type string to canonical form.
126
+ #
127
+ # @param type [String] resource type (e.g., "vm", "VM", "ct", "container")
128
+ # @return [String] normalized type ("vm" or "ct")
129
+ def normalize_resource_type(type)
130
+ case type.to_s.downcase
131
+ when "vm", "qemu" then "vm"
132
+ when "ct", "container", "lxc" then "ct"
133
+ else type.to_s.downcase
134
+ end
135
+ end
136
+
137
+ # Extracts volume models from a config hash.
138
+ #
139
+ # Iterates over config keys, selects disk-related entries,
140
+ # excludes CD-ROMs, and builds Volume models.
141
+ #
142
+ # @param config [Hash{Symbol => untyped}] VM/CT config hash
143
+ # @param resource_type [String] "vm" or "ct"
144
+ # @param resource_id [Integer] VMID or CTID
145
+ # @param node [String] node name
146
+ # @return [Array<Models::Volume>] extracted volumes
147
+ def extract_volumes(config, resource_type, resource_id, node)
148
+ pattern = resource_type == "vm" ? VM_DISK_PATTERN : CT_DISK_PATTERN
149
+
150
+ config.each_with_object([]) do |(key, value), volumes|
151
+ key_str = key.to_s
152
+ next unless key_str.match?(pattern)
153
+
154
+ value_str = value.to_s
155
+ next if value_str.include?("media=cdrom")
156
+
157
+ volumes << parse_config_value(key_str, value_str, resource_type, resource_id, node)
158
+ end
159
+ end
160
+
161
+ # Parses a config value string into a Volume model.
162
+ #
163
+ # Config values have the format:
164
+ # "storage:volume-id,key1=val1,key2=val2"
165
+ #
166
+ # @param name [String] disk key name (e.g., "scsi0")
167
+ # @param value [String] config value string
168
+ # @param resource_type [String] "vm" or "ct"
169
+ # @param resource_id [Integer] VMID or CTID
170
+ # @param node [String] node name
171
+ # @return [Models::Volume] parsed Volume model
172
+ def parse_config_value(name, value, resource_type, resource_id, node)
173
+ # Split "storage:volume-id,key=val,..." into storage_part and options
174
+ parts = value.split(",")
175
+ storage_spec = parts.shift || ""
176
+
177
+ storage, volume_id = storage_spec.split(":", 2)
178
+
179
+ # Parse key=value options
180
+ attrs = { name: name, storage: storage, volume_id: volume_id,
181
+ resource_type: resource_type, resource_id: resource_id, node: node }
182
+
183
+ parts.each do |part|
184
+ k, v = part.split("=", 2)
185
+ next unless k && v
186
+
187
+ case k
188
+ when "size" then attrs[:size] = v
189
+ when "format" then attrs[:format] = v
190
+ when "cache" then attrs[:cache] = v
191
+ when "discard" then attrs[:discard] = v
192
+ when "ssd" then attrs[:ssd] = parse_int(v)
193
+ when "iothread" then attrs[:iothread] = parse_int(v)
194
+ when "backup" then attrs[:backup] = parse_int(v)
195
+ when "mp" then attrs[:mp] = v
196
+ end
197
+ end
198
+
199
+ Models::Volume.new(attrs)
200
+ end
201
+
202
+ # Fetches volumes from a storage on a specific node.
203
+ #
204
+ # @param node_name [String] node name
205
+ # @param storage [String] storage name
206
+ # @return [Array<Models::Volume>] volumes from storage
207
+ def fetch_storage_volumes(node_name, storage)
208
+ response = connection.client["nodes/#{node_name}/storage/#{storage}/content"].get
209
+ data = unwrap(response)
210
+ data.map { |item| build_storage_volume(item, node_name, storage) }
211
+ rescue StandardError
212
+ []
213
+ end
214
+
215
+ # Builds a Volume model from storage content API data.
216
+ #
217
+ # @param data [Hash{Symbol => untyped}] API response item
218
+ # @param node [String] node name
219
+ # @param storage [String] storage name
220
+ # @return [Models::Volume] Volume model
221
+ def build_storage_volume(data, node, storage)
222
+ resource_type, resource_id = extract_resource_from_volume_id(data[:volid], data[:content])
223
+
224
+ Models::Volume.new(
225
+ volid: data[:volid],
226
+ volume_id: data[:volid]&.split(":")&.last,
227
+ storage: storage,
228
+ size: format_bytes_to_size(data[:size]),
229
+ format: data[:format],
230
+ content: data[:content],
231
+ resource_type: resource_type,
232
+ resource_id: resource_id,
233
+ node: node
234
+ )
235
+ end
236
+
237
+ # Extracts resource type and ID from a volume identifier.
238
+ #
239
+ # Volume IDs follow patterns like:
240
+ # - "vm-100-disk-0" => ["vm", 100]
241
+ # - "subvol-200-disk-0" => ["ct", 200]
242
+ # - "base-100-disk-0" => ["vm", 100]
243
+ #
244
+ # @param volume_id [String, nil] full volid (e.g., "local-lvm:vm-100-disk-0")
245
+ # @param content [String, nil] content type from API
246
+ # @return [Array(String?, Integer?)] [resource_type, resource_id]
247
+ def extract_resource_from_volume_id(volume_id, content)
248
+ return [nil, nil] unless volume_id
249
+
250
+ vol_part = volume_id.split(":").last
251
+ return [nil, nil] unless vol_part
252
+
253
+ case vol_part
254
+ when /\Avm-(\d+)-/
255
+ ["vm", ::Regexp.last_match(1).to_i]
256
+ when /\Asubvol-(\d+)-/
257
+ ["ct", ::Regexp.last_match(1).to_i]
258
+ when /\Abase-(\d+)-/
259
+ type = content == "rootdir" ? "ct" : "vm"
260
+ [type, ::Regexp.last_match(1).to_i]
261
+ else
262
+ [nil, nil]
263
+ end
264
+ end
265
+
266
+ # Formats bytes to human-readable size string.
267
+ #
268
+ # @param bytes [Integer, nil] size in bytes
269
+ # @return [String, nil] formatted size (e.g., "32G") or nil
270
+ def format_bytes_to_size(bytes)
271
+ return nil unless bytes.is_a?(Integer) && bytes.positive?
272
+
273
+ gb = bytes / (1024 * 1024 * 1024)
274
+ return "#{gb}G" if gb.positive?
275
+
276
+ mb = bytes / (1024 * 1024)
277
+ return "#{mb}M" if mb.positive?
278
+
279
+ "#{bytes}B"
280
+ end
281
+
282
+ # Fetches list of online node names.
283
+ #
284
+ # @return [Array<String>] online node names
285
+ def online_nodes
286
+ response = connection.client["nodes"].get
287
+ nodes_data = unwrap(response)
288
+ nodes_data
289
+ .select { |n| n[:status] == "online" }
290
+ .map { |n| n[:node] || n[:name] }
291
+ rescue StandardError
292
+ []
293
+ end
294
+
295
+ # Parses a string value to integer.
296
+ #
297
+ # @param value [String, nil] string value
298
+ # @return [Integer, nil] parsed integer or nil
299
+ def parse_int(value)
300
+ return nil unless value
301
+
302
+ value.to_i
303
+ end
304
+ end
305
+ end
306
+ end
@@ -0,0 +1,201 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pvectl
4
+ module Selectors
5
+ # Base class for parsing and applying selectors.
6
+ #
7
+ # Selectors use kubectl-style syntax to filter resources:
8
+ # -l key=value # equality
9
+ # -l key!=value # inequality
10
+ # -l key=~pattern # wildcard pattern
11
+ # -l key in (a,b,c) # one of many
12
+ #
13
+ # @example Parsing selectors
14
+ # selector = Base.parse("status=running,tags=prod")
15
+ # selector.conditions
16
+ # #=> [{field: "status", operator: :eq, value: "running"},
17
+ # # {field: "tags", operator: :eq, value: "prod"}]
18
+ #
19
+ # @example Applying selectors (subclass responsibility)
20
+ # selector = Vm.parse("status=running")
21
+ # filtered = selector.apply(vms)
22
+ #
23
+ class Base
24
+ # Parsed conditions
25
+ # @return [Array<Hash>] Array of {field:, operator:, value:}
26
+ attr_reader :conditions
27
+
28
+ # Parses selector string into Base instance.
29
+ #
30
+ # @param selector_string [String] Selector like "status=running,tags=prod"
31
+ # @return [Base] Selector instance with parsed conditions
32
+ def self.parse(selector_string)
33
+ new(parse_conditions(selector_string))
34
+ end
35
+
36
+ # Parses multiple selector strings (from multiple -l flags).
37
+ #
38
+ # @param selector_strings [Array<String>] Array of selector strings
39
+ # @return [Base] Selector instance with all conditions merged
40
+ def self.parse_all(selector_strings)
41
+ conditions = selector_strings.flat_map { |s| parse_conditions(s) }
42
+ new(conditions)
43
+ end
44
+
45
+ # Creates selector with parsed conditions.
46
+ #
47
+ # @param conditions [Array<Hash>] Pre-parsed conditions
48
+ def initialize(conditions = [])
49
+ @conditions = conditions
50
+ end
51
+
52
+ # Checks if selector is empty (no conditions).
53
+ #
54
+ # @return [Boolean] true if no conditions
55
+ def empty?
56
+ @conditions.empty?
57
+ end
58
+
59
+ # Applies selector to collection (subclass responsibility).
60
+ #
61
+ # @param collection [Array] Items to filter
62
+ # @return [Array] Filtered items
63
+ # @raise [NotImplementedError] if not implemented by subclass
64
+ def apply(collection)
65
+ raise NotImplementedError, "#{self.class}#apply must be implemented"
66
+ end
67
+
68
+ # Checks if a single item matches all conditions.
69
+ #
70
+ # @param item [Object] Item to check
71
+ # @return [Boolean] true if all conditions match
72
+ def matches?(item)
73
+ @conditions.all? { |cond| match_condition?(item, cond) }
74
+ end
75
+
76
+ protected
77
+
78
+ # Checks if item matches a single condition.
79
+ # Subclasses should override to extract field values from items.
80
+ #
81
+ # @param item [Object] Item to check
82
+ # @param condition [Hash] Condition with :field, :operator, :value
83
+ # @return [Boolean] true if condition matches
84
+ def match_condition?(item, condition)
85
+ actual_value = extract_value(item, condition[:field])
86
+ compare_value(actual_value, condition[:operator], condition[:value])
87
+ end
88
+
89
+ # Extracts field value from item (subclass responsibility).
90
+ #
91
+ # @param item [Object] Item
92
+ # @param field [String] Field name
93
+ # @return [Object] Field value
94
+ # @raise [NotImplementedError] if not implemented by subclass
95
+ def extract_value(item, field)
96
+ raise NotImplementedError, "#{self.class}#extract_value must be implemented"
97
+ end
98
+
99
+ # Compares actual value against expected using operator.
100
+ #
101
+ # @param actual [Object] Actual value from item
102
+ # @param operator [Symbol] :eq, :neq, :match, :in
103
+ # @param expected [Object] Expected value (String or Array for :in)
104
+ # @return [Boolean] true if comparison passes
105
+ def compare_value(actual, operator, expected)
106
+ case operator
107
+ when :eq
108
+ actual.to_s == expected.to_s
109
+ when :neq
110
+ actual.to_s != expected.to_s
111
+ when :match
112
+ wildcard_match?(actual.to_s, expected.to_s)
113
+ when :in
114
+ expected.any? { |v| actual.to_s == v.to_s }
115
+ else
116
+ false
117
+ end
118
+ end
119
+
120
+ # Matches string against wildcard pattern.
121
+ # Converts * to regex .* for matching.
122
+ #
123
+ # @param value [String] Value to match
124
+ # @param pattern [String] Wildcard pattern (e.g., "web-*")
125
+ # @return [Boolean] true if matches
126
+ def wildcard_match?(value, pattern)
127
+ regex = Regexp.new("\\A" + Regexp.escape(pattern).gsub("\\*", ".*") + "\\z")
128
+ regex.match?(value)
129
+ end
130
+
131
+ # Class method to parse conditions from string.
132
+ def self.parse_conditions(selector_string)
133
+ return [] if selector_string.nil? || selector_string.empty?
134
+
135
+ # Split by comma (but not inside parentheses)
136
+ parts = split_selectors(selector_string)
137
+ parts.map { |part| parse_single_condition(part.strip) }
138
+ end
139
+
140
+ # Splits selector string by commas, respecting parentheses.
141
+ def self.split_selectors(str)
142
+ parts = []
143
+ current = ""
144
+ depth = 0
145
+
146
+ str.each_char do |char|
147
+ case char
148
+ when "("
149
+ depth += 1
150
+ current += char
151
+ when ")"
152
+ depth -= 1
153
+ current += char
154
+ when ","
155
+ if depth == 0
156
+ parts << current
157
+ current = ""
158
+ else
159
+ current += char
160
+ end
161
+ else
162
+ current += char
163
+ end
164
+ end
165
+
166
+ parts << current unless current.empty?
167
+ parts
168
+ end
169
+
170
+ # Parses a single condition like "status=running" or "status in (a,b)".
171
+ def self.parse_single_condition(condition_str)
172
+ # Try "in" operator first (has spaces)
173
+ if condition_str =~ /\A(\w+)\s+in\s+\(([^)]+)\)\z/i
174
+ field = Regexp.last_match(1)
175
+ values = Regexp.last_match(2).split(",").map(&:strip)
176
+ return { field: field, operator: :in, value: values }
177
+ end
178
+
179
+ # Try other operators
180
+ if condition_str =~ /\A(\w+)(!=|=~|=)(.*)\z/
181
+ field = Regexp.last_match(1)
182
+ op_str = Regexp.last_match(2)
183
+ value = Regexp.last_match(3).strip
184
+
185
+ operator = case op_str
186
+ when "=" then :eq
187
+ when "!=" then :neq
188
+ when "=~" then :match
189
+ end
190
+
191
+ return { field: field, operator: operator, value: value }
192
+ end
193
+
194
+ # Invalid syntax - raise error
195
+ raise ArgumentError, "Invalid selector syntax: #{condition_str}"
196
+ end
197
+
198
+ private_class_method :parse_conditions, :split_selectors, :parse_single_condition
199
+ end
200
+ end
201
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pvectl
4
+ module Selectors
5
+ # Selector for filtering containers.
6
+ #
7
+ # Extends Base with container-specific field extraction.
8
+ # Supports: status, tags, pool, name, template.
9
+ #
10
+ # @example Filter running containers
11
+ # selector = Container.parse("status=running")
12
+ # running_containers = selector.apply(all_containers)
13
+ #
14
+ # @example Filter by multiple criteria
15
+ # selector = Container.parse("status=running,tags=prod")
16
+ # filtered = selector.apply(all_containers)
17
+ #
18
+ # @example Filter by name pattern
19
+ # selector = Container.parse("name=~web-*")
20
+ # web_containers = selector.apply(all_containers)
21
+ #
22
+ class Container < Base
23
+ SUPPORTED_FIELDS = %w[status tags pool name template].freeze
24
+
25
+ # Applies selector to container collection.
26
+ #
27
+ # @param containers [Array<Models::Container>] Containers to filter
28
+ # @return [Array<Models::Container>] Filtered containers
29
+ def apply(containers)
30
+ return containers if empty?
31
+
32
+ containers.select { |ct| matches?(ct) }
33
+ end
34
+
35
+ protected
36
+
37
+ # Extracts field value from Container model.
38
+ #
39
+ # @param container [Models::Container] Container 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(container, field)
44
+ case field
45
+ when "status"
46
+ container.status
47
+ when "tags"
48
+ container.tags
49
+ when "pool"
50
+ container.pool
51
+ when "name"
52
+ container.name
53
+ when "template"
54
+ container.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 container [Models::Container] Container model
65
+ # @param condition [Hash] Condition
66
+ # @return [Boolean] true if matches
67
+ def match_condition?(container, condition)
68
+ return match_tags_condition?(container, condition) if condition[:field] == "tags"
69
+ return super(container, 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 container [Models::Container] Container model
95
+ # @param condition [Hash] Condition
96
+ # @return [Boolean] true if matches
97
+ def match_tags_condition?(container, condition)
98
+ tags_string = container.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 physical disks.
6
+ #
7
+ # Extends Base with disk-specific field extraction.
8
+ # Supports: type, health, used, node, gpt, mounted.
9
+ #
10
+ # @example Filter SSDs only
11
+ # selector = Disk.parse("type=ssd")
12
+ # ssds = selector.apply(all_disks)
13
+ #
14
+ # @example Filter healthy disks on a specific node
15
+ # selector = Disk.parse("health=PASSED,node=pve1")
16
+ # filtered = selector.apply(all_disks)
17
+ #
18
+ class Disk < Base
19
+ SUPPORTED_FIELDS = %w[type health used node gpt mounted].freeze
20
+
21
+ # Applies selector to disk collection.
22
+ #
23
+ # @param disks [Array<Models::PhysicalDisk>] disks to filter
24
+ # @return [Array<Models::PhysicalDisk>] filtered disks
25
+ def apply(disks)
26
+ return disks if empty?
27
+
28
+ disks.select { |disk| matches?(disk) }
29
+ end
30
+
31
+ protected
32
+
33
+ # Extracts field value from PhysicalDisk model.
34
+ #
35
+ # @param disk [Models::PhysicalDisk] disk 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(disk, field)
40
+ case field
41
+ when "type"
42
+ disk.type
43
+ when "health"
44
+ disk.health
45
+ when "used"
46
+ disk.used
47
+ when "node"
48
+ disk.node
49
+ when "gpt"
50
+ disk.gpt? ? "yes" : "no"
51
+ when "mounted"
52
+ disk.mounted? ? "yes" : "no"
53
+ else
54
+ raise ArgumentError, "Unknown field: #{field}. Supported: #{SUPPORTED_FIELDS.join(', ')}"
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end