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,853 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module Pvectl
6
+ module Presenters
7
+ # Presenter for QEMU virtual machines.
8
+ #
9
+ # Defines column layout and formatting for table output.
10
+ # Used by formatters to render VM data in various formats.
11
+ #
12
+ # Standard columns: NAME, VMID, STATUS, NODE, CPU, MEMORY
13
+ # Wide columns add: UPTIME, TEMPLATE, TAGS, DISK, IP, AGENT, HA, BACKUP
14
+ #
15
+ # @example Using with formatter
16
+ # presenter = Vm.new
17
+ # formatter = Formatters::Table.new
18
+ # output = formatter.format(vms, presenter)
19
+ #
20
+ # @see Pvectl::Models::Vm VM model
21
+ # @see Pvectl::Formatters::Table Table formatter
22
+ #
23
+ class Vm < Base
24
+ # Returns column headers for standard table output.
25
+ #
26
+ # @return [Array<String>] column headers
27
+ def columns
28
+ %w[NAME VMID STATUS NODE CPU MEMORY]
29
+ end
30
+
31
+ # Returns additional column headers for wide output.
32
+ #
33
+ # @return [Array<String>] extra column headers
34
+ def extra_columns
35
+ %w[UPTIME TEMPLATE TAGS DISK IP AGENT HA BACKUP]
36
+ end
37
+
38
+ # Converts VM model to table row values.
39
+ #
40
+ # @param model [Models::Vm] VM model
41
+ # @param context [Hash] optional context
42
+ # @return [Array<String>] row values matching columns order
43
+ def to_row(model, **_context)
44
+ @vm = model
45
+ [
46
+ display_name,
47
+ vm.vmid.to_s,
48
+ vm.status,
49
+ vm.node,
50
+ cpu_percent,
51
+ memory_display
52
+ ]
53
+ end
54
+
55
+ # Returns additional values for wide output.
56
+ #
57
+ # Note: IP, AGENT, and BACKUP are placeholders for future implementation
58
+ # that would require additional API calls to the QEMU guest agent.
59
+ #
60
+ # @param model [Models::Vm] VM model
61
+ # @param context [Hash] optional context
62
+ # @return [Array<String>] extra values matching extra_columns order
63
+ def extra_values(model, **_context)
64
+ @vm = model
65
+ [
66
+ uptime_human,
67
+ template_display,
68
+ tags_display,
69
+ disk_display,
70
+ "-", # IP - requires QEMU agent (future enhancement)
71
+ "-", # AGENT status (future enhancement)
72
+ vm.hastate || "-",
73
+ "-" # BACKUP schedule (future enhancement)
74
+ ]
75
+ end
76
+
77
+ # Converts VM model to hash for JSON/YAML output.
78
+ #
79
+ # Returns a structured hash with nested objects for complex data
80
+ # like CPU, memory, disk, uptime, and network.
81
+ #
82
+ # @param model [Models::Vm] VM model
83
+ # @return [Hash] hash representation with string keys
84
+ def to_hash(model)
85
+ @vm = model
86
+ {
87
+ "vmid" => vm.vmid,
88
+ "name" => vm.name,
89
+ "status" => vm.status,
90
+ "node" => vm.node,
91
+ "template" => vm.template?,
92
+ "cpu" => {
93
+ "usage_percent" => vm.cpu.nil? ? nil : (vm.cpu * 100).round,
94
+ "cores" => vm.maxcpu
95
+ },
96
+ "memory" => {
97
+ "used_gb" => memory_used_gb,
98
+ "total_gb" => memory_total_gb,
99
+ "used_bytes" => vm.mem,
100
+ "total_bytes" => vm.maxmem
101
+ },
102
+ "disk" => {
103
+ "used_gb" => disk_used_gb,
104
+ "total_gb" => disk_total_gb,
105
+ "used_bytes" => vm.disk,
106
+ "total_bytes" => vm.maxdisk
107
+ },
108
+ "uptime" => {
109
+ "seconds" => vm.uptime,
110
+ "human" => uptime_human
111
+ },
112
+ "network" => {
113
+ "in_bytes" => vm.netin,
114
+ "out_bytes" => vm.netout
115
+ },
116
+ "ha" => {
117
+ "state" => vm.hastate
118
+ },
119
+ "tags" => tags_array
120
+ }
121
+ end
122
+
123
+ # Converts VM model to description format for describe command.
124
+ #
125
+ # Returns a structured Hash organized by Proxmox VE web UI tabs:
126
+ # Summary, Hardware, Cloud-Init, Options, Task History, Snapshots,
127
+ # Pending Changes. Nested Hashes create indented subsections.
128
+ # Arrays of Hashes render as inline tables.
129
+ #
130
+ # @param model [Models::Vm] VM model with describe details
131
+ # @return [Hash] structured hash for describe formatter
132
+ def to_description(model)
133
+ @vm = model
134
+ @consumed_keys = Set.new
135
+ data = vm.describe_data || {}
136
+ config = data[:config] || {}
137
+ status = data[:status] || {}
138
+
139
+ consume(:name, :description, :tags, :pool, :template)
140
+
141
+ {
142
+ "Name" => display_name,
143
+ "VMID" => vm.vmid,
144
+ "Status" => vm.status,
145
+ "Node" => vm.node,
146
+ "Tags" => tags_display,
147
+ "Description" => config[:description] || "-",
148
+ "Summary" => format_summary(config, status),
149
+ "Block Device Statistics" => format_blockstat(status),
150
+ "Hardware" => format_hardware(config, data),
151
+ "Cloud-Init" => format_cloud_init(config),
152
+ "Options" => format_options(config),
153
+ "Firewall" => format_firewall(data[:firewall]),
154
+ "Firewall Rules" => format_firewall_rules(data[:firewall]),
155
+ "Task History" => format_task_history(data[:tasks]),
156
+ "Snapshots" => format_snapshots(data[:snapshots]),
157
+ "Pending Changes" => format_pending_changes(data[:pending]),
158
+ "Additional Configuration" => format_remaining(config)
159
+ }
160
+ end
161
+
162
+ # ---------------------------
163
+ # Display Methods (from Model)
164
+ # ---------------------------
165
+
166
+ # Returns display name, falling back to "VM-{vmid}" if name is nil.
167
+ #
168
+ # @return [String] display name
169
+ def display_name
170
+ vm.name || "VM-#{vm.vmid}"
171
+ end
172
+
173
+ # Returns CPU usage as percentage string.
174
+ #
175
+ # For running VMs, shows actual usage percentage.
176
+ # For stopped VMs, shows "-" for usage but includes core count if available.
177
+ #
178
+ # @return [String] CPU percentage (e.g., "12%") or "-/4" for stopped VMs
179
+ def cpu_percent
180
+ return "-" if vm.maxcpu.nil?
181
+ return "-/#{vm.maxcpu}" unless vm.running?
182
+ return "-/#{vm.maxcpu}" if vm.cpu.nil?
183
+
184
+ "#{(vm.cpu * 100).round}%/#{vm.maxcpu}"
185
+ end
186
+
187
+ # Returns memory used in GB.
188
+ #
189
+ # @return [Float, nil] memory used in GB, or nil if unavailable
190
+ def memory_used_gb
191
+ return nil if vm.mem.nil?
192
+
193
+ (vm.mem.to_f / 1024 / 1024 / 1024).round(1)
194
+ end
195
+
196
+ # Returns total memory in GB.
197
+ #
198
+ # @return [Float, nil] total memory in GB, or nil if unavailable
199
+ def memory_total_gb
200
+ return nil if vm.maxmem.nil?
201
+
202
+ (vm.maxmem.to_f / 1024 / 1024 / 1024).round(1)
203
+ end
204
+
205
+ # Returns memory formatted as "used/total GB".
206
+ #
207
+ # For running VMs, shows actual usage and total.
208
+ # For stopped VMs, shows "-" for usage but includes total if available.
209
+ #
210
+ # @return [String] formatted memory (e.g., "2.1/4.0 GB") or "-/4.0 GB" for stopped VMs
211
+ def memory_display
212
+ return "-" if memory_total_gb.nil?
213
+ return "-/#{memory_total_gb} GB" unless vm.running?
214
+ return "-/#{memory_total_gb} GB" if memory_used_gb.nil?
215
+
216
+ "#{memory_used_gb}/#{memory_total_gb} GB"
217
+ end
218
+
219
+ # Returns disk used in GB.
220
+ #
221
+ # @return [Integer, nil] disk used in GB, or nil if unavailable
222
+ def disk_used_gb
223
+ return nil if vm.disk.nil?
224
+
225
+ (vm.disk.to_f / 1024 / 1024 / 1024).round
226
+ end
227
+
228
+ # Returns total disk in GB.
229
+ #
230
+ # @return [Integer, nil] total disk in GB, or nil if unavailable
231
+ def disk_total_gb
232
+ return nil if vm.maxdisk.nil?
233
+
234
+ (vm.maxdisk.to_f / 1024 / 1024 / 1024).round
235
+ end
236
+
237
+ # Returns disk formatted as "used/total GB".
238
+ #
239
+ # @return [String] formatted disk (e.g., "15/50 GB") or "-" if unavailable
240
+ def disk_display
241
+ return "-" if disk_used_gb.nil?
242
+
243
+ "#{disk_used_gb}/#{disk_total_gb} GB"
244
+ end
245
+
246
+ private
247
+
248
+ # @return [Models::Vm] current VM model
249
+ attr_reader :vm
250
+
251
+ alias resource vm
252
+
253
+ # Formats Summary section (PVE Summary tab).
254
+ #
255
+ # Shows HA state, resource usage, and (for running VMs) runtime info
256
+ # including uptime, PID, QEMU version, machine type, and I/O statistics.
257
+ #
258
+ # @param config [Hash] VM config
259
+ # @param status [Hash] VM status
260
+ # @return [Hash] summary info
261
+ def format_summary(config, status)
262
+ sockets = config[:sockets] || 1
263
+ cores = config[:cores] || 1
264
+ total_cpus = sockets * cores
265
+ cpu_usage = vm.running? && vm.cpu ? "#{(vm.cpu * 100).round(2)}% of #{total_cpus} CPU(s)" : "-"
266
+
267
+ mem_usage = if vm.running? && vm.mem && vm.maxmem && vm.maxmem > 0
268
+ pct = ((vm.mem.to_f / vm.maxmem) * 100).round(2)
269
+ "#{pct}% (#{format_bytes(vm.mem)} of #{format_bytes(vm.maxmem)})"
270
+ else
271
+ "-"
272
+ end
273
+
274
+ bootdisk_size = find_bootdisk_size(config)
275
+
276
+ result = { "HA State" => vm.hastate || "-", "CPU Usage" => cpu_usage,
277
+ "Memory Usage" => mem_usage, "Bootdisk Size" => bootdisk_size }
278
+ if vm.running?
279
+ result["Uptime"] = uptime_human
280
+ result["PID"] = (status[:pid] || "-").to_s
281
+ result["QEMU Version"] = status[:"running-qemu"] || "-"
282
+ result["Machine Type"] = status[:"running-machine"] || "-"
283
+ result["Network In"] = format_bytes(vm.netin)
284
+ result["Network Out"] = format_bytes(vm.netout)
285
+ result["Disk Read"] = format_bytes(status[:diskread])
286
+ result["Disk Written"] = format_bytes(status[:diskwrite])
287
+ end
288
+ result
289
+ end
290
+
291
+ # Finds the bootdisk size from boot order config.
292
+ #
293
+ # Parses the boot order to find the first device, then extracts its
294
+ # size from the disk config string. Falls back to maxdisk from model.
295
+ #
296
+ # @param config [Hash] VM config
297
+ # @return [String] bootdisk size or "-"
298
+ def find_bootdisk_size(config)
299
+ boot = config[:boot]
300
+ if boot
301
+ first_dev = boot.to_s.sub(/^order=/, "").split(";").first
302
+ if first_dev && config[first_dev.to_sym]
303
+ disk_str = config[first_dev.to_sym].to_s
304
+ size_part = disk_str.split(",").find { |p| p.start_with?("size=") }
305
+ return size_part.sub("size=", "") if size_part
306
+ end
307
+ end
308
+ vm.maxdisk ? format_bytes(vm.maxdisk) : "-"
309
+ end
310
+
311
+ # Formats Block Device Statistics section.
312
+ #
313
+ # Renders per-disk I/O statistics from the running VM's status payload.
314
+ # Available only for running VMs — Proxmox populates +blockstat+ as
315
+ # part of +/status/current+ when the QEMU process is alive.
316
+ #
317
+ # @param status [Hash] VM status payload from /status/current
318
+ # @return [Array<Hash>, String] per-device I/O table or "-" when absent
319
+ def format_blockstat(status)
320
+ blockstat = status[:blockstat]
321
+ return "-" if blockstat.nil? || blockstat.empty?
322
+
323
+ blockstat.sort_by { |name, _| name.to_s }.map do |name, stats|
324
+ stats ||= {}
325
+ {
326
+ "DEVICE" => name.to_s,
327
+ "READ" => format_bytes(stats[:rd_bytes]),
328
+ "WRITTEN" => format_bytes(stats[:wr_bytes]),
329
+ "READ_IOPS" => (stats[:rd_operations] || 0).to_s,
330
+ "WRITE_IOPS" => (stats[:wr_operations] || 0).to_s
331
+ }
332
+ end
333
+ end
334
+
335
+ # Formats Hardware section (PVE Hardware tab).
336
+ #
337
+ # Shows memory, balloon, processors, BIOS, machine type, display,
338
+ # SCSI controller, disks, network, and peripheral devices.
339
+ #
340
+ # @param config [Hash] VM config
341
+ # @param data [Hash] full describe data (for agent_ips)
342
+ # @return [Hash] hardware info with mixed String values and Array sub-tables
343
+ def format_hardware(config, data)
344
+ consume(:bios, :machine, :scsihw, :memory, :balloon, :shares,
345
+ :sockets, :cores, :cpu, :vcpus, :cpulimit, :cpuunits, :vga)
346
+
347
+ # Memory line — flat string by default, expanded to a Hash with
348
+ # ballooning details when status payload contains ballooninfo.
349
+ total_mb = config[:memory] || (vm.maxmem ? vm.maxmem / 1024 / 1024 : nil)
350
+ memory_str = total_mb ? "#{(total_mb.to_f / 1024).round(2)} GiB" : "-"
351
+ memory_section = format_memory_section(memory_str, data[:status])
352
+
353
+ # Balloon line
354
+ balloon = config[:balloon]
355
+ balloon_str = if balloon && balloon > 0
356
+ "enabled (min: #{(balloon.to_f / 1024).round(1)} GiB)"
357
+ else
358
+ "disabled"
359
+ end
360
+
361
+ # Processors line: "4 (2 sockets, 2 cores) [host]"
362
+ sockets = config[:sockets] || 1
363
+ cores = config[:cores] || 1
364
+ cpu_type = config[:cpu] || "kvm64"
365
+ total = sockets * cores
366
+ processors_str = "#{total} (#{sockets} sockets, #{cores} cores) [#{cpu_type}]"
367
+
368
+ # BIOS
369
+ bios = config[:bios] || "seabios"
370
+ bios_display = bios == "ovmf" ? "UEFI (OVMF)" : "SeaBIOS"
371
+
372
+ # Machine
373
+ machine_str = config[:machine] || "i440fx"
374
+
375
+ {
376
+ "Memory" => memory_section,
377
+ "Balloon" => balloon_str,
378
+ "Processors" => processors_str,
379
+ "BIOS" => bios_display,
380
+ "Machine" => machine_str,
381
+ "Display" => config[:vga] || "Default",
382
+ "SCSI Controller" => config[:scsihw] || "lsi",
383
+ "EFI Disk" => format_efi_disk(config),
384
+ "TPM" => format_tpm(config),
385
+ "Disks" => parse_disks(config),
386
+ "Network" => format_network(config, data[:agent_ips]),
387
+ "USB Devices" => format_usb_devices(config),
388
+ "PCI Passthrough" => format_pci_passthrough(config),
389
+ "Serial Ports" => format_serial_ports(config),
390
+ "Audio" => format_audio(config)
391
+ }
392
+ end
393
+
394
+ # Formats Memory entry in the Hardware section.
395
+ #
396
+ # Returns a flat string with configured memory when ballooning info
397
+ # is not available (balloon driver inactive or VM stopped). When the
398
+ # status payload contains +ballooninfo+, returns a Hash with the
399
+ # configured value plus runtime balloon metrics from the guest:
400
+ # actual ballooned size, maximum, free memory inside the guest,
401
+ # and total guest-visible memory.
402
+ #
403
+ # @param memory_str [String] formatted configured memory
404
+ # @param status [Hash, nil] VM status payload
405
+ # @return [String, Hash] flat string or nested sub-section
406
+ def format_memory_section(memory_str, status)
407
+ info = status.is_a?(Hash) ? status[:ballooninfo] : nil
408
+ return memory_str unless info.is_a?(Hash)
409
+
410
+ {
411
+ "Configured" => memory_str,
412
+ "Actual" => format_bytes(info[:actual]),
413
+ "Max" => format_bytes(info[:max_mem]),
414
+ "Free (inside guest)" => format_bytes(info[:free_mem]),
415
+ "Total (guest visible)" => format_bytes(info[:total_mem])
416
+ }
417
+ end
418
+
419
+ # Formats Options section (PVE Options tab).
420
+ #
421
+ # Shows boot, startup, OS type, agent, security, and other VM options.
422
+ #
423
+ # @param config [Hash] VM config
424
+ # @return [Hash] options info
425
+ def format_options(config)
426
+ consume(:onboot, :startup, :ostype, :boot, :tablet, :hotplug,
427
+ :acpi, :kvm, :freeze, :localtime, :numa, :agent,
428
+ :protection, :firewall, :lock, :hookscript,
429
+ :args, :vmgenid, :meta, :ha)
430
+ consume_matching(config, /^numa\d+$/)
431
+ consume_matching(config, /^unused\d+$/)
432
+
433
+ on_boot = config[:onboot] == 1 ? "Yes" : "No"
434
+
435
+ startup = config[:startup]
436
+ startup_display = startup ? startup.to_s : "-"
437
+
438
+ ostype_display = format_ostype(config[:ostype])
439
+
440
+ boot = config[:boot]
441
+ boot_display = if boot
442
+ order = boot.to_s.sub(/^order=/, "").split(";").join(", ")
443
+ order.empty? ? "-" : order
444
+ else
445
+ "-"
446
+ end
447
+
448
+ tablet = config[:tablet] == 0 ? "No" : "Yes"
449
+ hotplug_raw = config[:hotplug]
450
+ hotplug = if hotplug_raw
451
+ hotplug_raw.to_s == "0" ? "Disabled" : hotplug_raw.to_s.split(",").join(", ")
452
+ else
453
+ "disk, network, usb"
454
+ end
455
+ acpi = config[:acpi] == 0 ? "No" : "Yes"
456
+ kvm = config[:kvm] == 0 ? "No" : "Yes"
457
+ freeze_cpu = config[:freeze] == 1 ? "Yes" : "No"
458
+ localtime = config[:localtime] == 1 ? "Yes" : "Default"
459
+ numa = config[:numa] == 1 ? "Yes" : "No"
460
+
461
+ agent_display = format_agent_options(config)
462
+ protection = config[:protection] == 1 ? "Yes" : "No"
463
+ firewall = config[:firewall] == 1 ? "Yes" : "No"
464
+ hookscript = config[:hookscript] || "-"
465
+
466
+ {
467
+ "Start at Boot" => on_boot,
468
+ "Start/Shutdown Order" => startup_display,
469
+ "OS Type" => ostype_display,
470
+ "Boot Order" => boot_display,
471
+ "Use Tablet for Pointer" => tablet,
472
+ "Hotplug" => hotplug,
473
+ "ACPI Support" => acpi,
474
+ "KVM Hardware Virtualization" => kvm,
475
+ "Freeze CPU at Startup" => freeze_cpu,
476
+ "Use Local Time for RTC" => localtime,
477
+ "NUMA" => numa,
478
+ "QEMU Guest Agent" => agent_display,
479
+ "Protection" => protection,
480
+ "Firewall" => firewall,
481
+ "Hookscript" => hookscript
482
+ }
483
+ end
484
+
485
+ # Formats QEMU guest agent as sub-section Hash for Options.
486
+ #
487
+ # Matches PVE Options tab layout with separate fields for
488
+ # enable/disable, guest-trim, and freeze-fs-on-backup.
489
+ #
490
+ # @param config [Hash] VM config
491
+ # @return [Hash] agent options sub-section
492
+ def format_agent_options(config)
493
+ agent = config[:agent]
494
+ unless agent
495
+ return {
496
+ "Use QEMU Guest Agent" => "No",
497
+ "Run guest-trim after a disk move or VM migration" => "No",
498
+ "Freeze/thaw guest filesystems on backup for consistency" => "No"
499
+ }
500
+ end
501
+
502
+ parts = agent.to_s.split(",")
503
+ enabled = parts.first == "1"
504
+
505
+ opts = {}
506
+ parts[1..].each { |p| k, v = p.split("=", 2); opts[k] = v }
507
+
508
+ result = {
509
+ "Use QEMU Guest Agent" => enabled ? "Yes" : "No",
510
+ "Run guest-trim after a disk move or VM migration" => opts["fstrim_cloned_disks"] == "1" ? "Yes" : "No",
511
+ "Freeze/thaw guest filesystems on backup for consistency" => opts["freeze-fs-on-backup"] == "1" ? "Yes" : "No"
512
+ }
513
+ result["Type"] = opts["type"] if opts["type"]
514
+ result
515
+ end
516
+
517
+ # Formats OS type for display.
518
+ #
519
+ # @param ostype [String, nil] OS type from config
520
+ # @return [String] formatted OS type
521
+ def format_ostype(ostype)
522
+ case ostype
523
+ when "l26" then "l26 (Linux 2.6+)"
524
+ when "l24" then "l24 (Linux 2.4)"
525
+ when "win11" then "win11 (Windows 11)"
526
+ when "win10" then "win10 (Windows 10)"
527
+ when "win8" then "win8 (Windows 8)"
528
+ when "win7" then "win7 (Windows 7)"
529
+ when "wxp" then "wxp (Windows XP)"
530
+ when "other" then "other"
531
+ else ostype || "-"
532
+ end
533
+ end
534
+
535
+ # Parses disk configuration strings from VM config.
536
+ #
537
+ # Disk keys: scsi0-30, ide0-3, virtio0-15, sata0-5
538
+ # Format: "storage:volume,size=X,format=Y,..."
539
+ # Example: "local-lvm:vm-100-disk-0,size=50G,format=raw"
540
+ #
541
+ # @param config [Hash] VM config
542
+ # @return [Array<Hash>, String] parsed disks or "-"
543
+ def parse_disks(config)
544
+ consume_matching(config, /^(scsi|ide|virtio|sata)\d+$/)
545
+ disk_keys = config.keys.select { |k| k.to_s.match?(/^(scsi|ide|virtio|sata)\d+$/) }
546
+ return "-" if disk_keys.empty?
547
+
548
+ disks = disk_keys.sort.map do |key|
549
+ parse_disk_string(key.to_s, config[key])
550
+ end.compact
551
+
552
+ disks.empty? ? "-" : disks
553
+ end
554
+
555
+ # Parses a single disk config string.
556
+ #
557
+ # @param name [String] disk name (e.g., "scsi0")
558
+ # @param value [String] disk config string
559
+ # @return [Hash, nil] parsed disk info
560
+ def parse_disk_string(name, value)
561
+ return nil if value.nil? || value.to_s.empty?
562
+ return nil if value.to_s == "none"
563
+
564
+ # Format: "storage:volume,key=value,..."
565
+ parts = value.to_s.split(",")
566
+ storage_part = parts.first
567
+
568
+ # Extract storage name (before colon)
569
+ storage = storage_part.include?(":") ? storage_part.split(":").first : storage_part
570
+
571
+ # Extract size and format from key=value pairs
572
+ size = nil
573
+ format = nil
574
+ parts[1..].each do |part|
575
+ key, val = part.split("=", 2)
576
+ case key
577
+ when "size" then size = val
578
+ when "format" then format = val
579
+ end
580
+ end
581
+
582
+ {
583
+ "NAME" => name,
584
+ "SIZE" => size || "-",
585
+ "STORAGE" => storage,
586
+ "FORMAT" => format || "-"
587
+ }
588
+ end
589
+
590
+ # Formats network section with IP addresses from agent.
591
+ #
592
+ # Network keys: net0-31
593
+ # Format: "model=X,bridge=Y,macaddr=Z,..."
594
+ # Example: "virtio=BC:24:11:AA:BB:CC,bridge=vmbr0"
595
+ #
596
+ # @param config [Hash] VM config
597
+ # @param agent_ips [Array<Hash>, nil] agent network interfaces
598
+ # @return [Array<Hash>, String] parsed networks or "-"
599
+ def format_network(config, agent_ips)
600
+ consume_matching(config, /^net\d+$/)
601
+ net_keys = config.keys.select { |k| k.to_s.match?(/^net\d+$/) }
602
+ return "-" if net_keys.empty?
603
+
604
+ # Build MAC -> IP mapping from agent data
605
+ mac_to_ip = build_mac_ip_map(agent_ips)
606
+
607
+ networks = net_keys.sort.map do |key|
608
+ parse_network_string(key.to_s, config[key], mac_to_ip)
609
+ end.compact
610
+
611
+ networks.empty? ? "-" : networks
612
+ end
613
+
614
+ # Builds MAC address to IP mapping from agent interfaces.
615
+ #
616
+ # @param agent_ips [Array<Hash>, nil] agent interfaces
617
+ # @return [Hash] MAC -> IP mapping
618
+ def build_mac_ip_map(agent_ips)
619
+ return {} if agent_ips.nil?
620
+
621
+ map = {}
622
+ agent_ips.each do |iface|
623
+ mac = iface[:"hardware-address"]&.downcase
624
+ next if mac.nil?
625
+
626
+ # Find first non-loopback IPv4 address
627
+ ip_addrs = iface[:"ip-addresses"] || []
628
+ ipv4 = ip_addrs.find { |a| a[:"ip-address-type"] == "ipv4" && a[:"ip-address"] != "127.0.0.1" }
629
+ map[mac] = ipv4[:"ip-address"] if ipv4
630
+ end
631
+ map
632
+ end
633
+
634
+ # Parses a single network config string.
635
+ #
636
+ # @param name [String] net name (e.g., "net0")
637
+ # @param value [String] network config string
638
+ # @param mac_to_ip [Hash] MAC -> IP mapping
639
+ # @return [Hash, nil] parsed network info
640
+ def parse_network_string(name, value, mac_to_ip)
641
+ return nil if value.nil? || value.to_s.empty?
642
+
643
+ parts = value.to_s.split(",")
644
+ model = nil
645
+ mac = nil
646
+ bridge = nil
647
+ firewall = "no"
648
+
649
+ parts.each do |part|
650
+ key, val = part.split("=", 2)
651
+ case key
652
+ when "bridge" then bridge = val
653
+ when "firewall" then firewall = val == "1" ? "yes" : "no"
654
+ when "virtio", "e1000", "rtl8139", "vmxnet3"
655
+ model = key
656
+ mac = val&.upcase
657
+ when "model" then model = val
658
+ when "macaddr" then mac = val&.upcase
659
+ end
660
+ end
661
+
662
+ ip = mac ? mac_to_ip[mac.downcase] : nil
663
+
664
+ {
665
+ "NAME" => name,
666
+ "MODEL" => model || "-",
667
+ "BRIDGE" => bridge || "-",
668
+ "FIREWALL" => firewall,
669
+ "MAC" => mac || "-",
670
+ "IP" => ip || "-"
671
+ }
672
+ end
673
+
674
+ # Formats Cloud-Init section.
675
+ #
676
+ # Detects cloud-init presence by checking for CI config keys (ciuser,
677
+ # sshkeys, etc.) OR a cloud-init drive (any disk with "cloudinit" in
678
+ # the volume name). This matches PVE behavior where the Cloud-Init
679
+ # tab appears when the drive exists, even without configuration.
680
+ #
681
+ # @param config [Hash] VM config
682
+ # @return [Hash, String] cloud-init info or "-"
683
+ def format_cloud_init(config)
684
+ ci_keys = %i[citype ciuser cipassword cicustom ciupgrade searchdomain nameserver sshkeys]
685
+ ipconfig_keys = config.keys.select { |k| k.to_s.match?(/^ipconfig\d+$/) }
686
+ consume(*ci_keys)
687
+ consume_matching(config, /^ipconfig\d+$/)
688
+
689
+ ci_drive = config.keys.any? do |k|
690
+ k.to_s.match?(/^(scsi|ide|virtio|sata)\d+$/) && config[k].to_s.include?("cloudinit")
691
+ end
692
+ has_ci = ci_keys.any? { |k| config[k] } || ipconfig_keys.any? || ci_drive
693
+ return "-" unless has_ci
694
+
695
+ result = {
696
+ "User" => config[:ciuser] || "-",
697
+ "Password" => config[:cipassword] ? "set" : "-",
698
+ "DNS Server" => config[:nameserver] || "-",
699
+ "Search Domain" => config[:searchdomain] || "-",
700
+ "SSH Keys" => config[:sshkeys] ? "configured" : "-",
701
+ "Upgrade Packages" => config[:ciupgrade] == 0 ? "No" : "Yes",
702
+ "CI Type" => config[:citype] || "nocloud",
703
+ "CI Custom" => config[:cicustom] || "-"
704
+ }
705
+
706
+ if ipconfig_keys.any?
707
+ result["IP Config"] = ipconfig_keys.sort.map do |key|
708
+ { "INTERFACE" => key.to_s.sub("ipconfig", "net"), "CONFIG" => config[key].to_s }
709
+ end
710
+ end
711
+
712
+ result
713
+ end
714
+
715
+ # Formats USB devices section.
716
+ #
717
+ # @param config [Hash] VM config
718
+ # @return [Array<Hash>, String] USB devices table or "-"
719
+ def format_usb_devices(config)
720
+ usb_keys = config.keys.select { |k| k.to_s.match?(/^usb\d+$/) }
721
+ consume_matching(config, /^usb\d+$/)
722
+ return "-" if usb_keys.empty?
723
+
724
+ usb_keys.sort.map do |key|
725
+ { "NAME" => key.to_s, "CONFIG" => config[key].to_s }
726
+ end
727
+ end
728
+
729
+ # Formats PCI passthrough section.
730
+ #
731
+ # @param config [Hash] VM config
732
+ # @return [Array<Hash>, String] PCI devices table or "-"
733
+ def format_pci_passthrough(config)
734
+ pci_keys = config.keys.select { |k| k.to_s.match?(/^hostpci\d+$/) }
735
+ consume_matching(config, /^hostpci\d+$/)
736
+ return "-" if pci_keys.empty?
737
+
738
+ pci_keys.sort.map do |key|
739
+ { "NAME" => key.to_s, "CONFIG" => config[key].to_s }
740
+ end
741
+ end
742
+
743
+ # Formats serial ports section.
744
+ #
745
+ # @param config [Hash] VM config
746
+ # @return [Array<Hash>, String] serial ports table or "-"
747
+ def format_serial_ports(config)
748
+ serial_keys = config.keys.select { |k| k.to_s.match?(/^serial\d+$/) }
749
+ consume_matching(config, /^serial\d+$/)
750
+ return "-" if serial_keys.empty?
751
+
752
+ serial_keys.sort.map do |key|
753
+ { "NAME" => key.to_s, "TYPE" => config[key].to_s }
754
+ end
755
+ end
756
+
757
+ # Formats audio device section.
758
+ #
759
+ # @param config [Hash] VM config
760
+ # @return [String] audio config or "-"
761
+ def format_audio(config)
762
+ consume(:audio0)
763
+ config[:audio0]&.to_s || "-"
764
+ end
765
+
766
+ # Formats EFI disk section.
767
+ #
768
+ # @param config [Hash] VM config
769
+ # @return [String] EFI disk info or "-"
770
+ def format_efi_disk(config)
771
+ consume(:efidisk0)
772
+ config[:efidisk0]&.to_s || "-"
773
+ end
774
+
775
+ # Formats TPM section.
776
+ #
777
+ # @param config [Hash] VM config
778
+ # @return [String] TPM info or "-"
779
+ def format_tpm(config)
780
+ consume(:tpmstate0)
781
+ config[:tpmstate0]&.to_s || "-"
782
+ end
783
+
784
+ # Formats snapshots for table display.
785
+ #
786
+ # @param snapshots [Array<Hash>, nil] snapshots
787
+ # @return [Array<Hash>, String] formatted snapshots or message
788
+ def format_snapshots(snapshots)
789
+ return "No snapshots" if snapshots.nil? || snapshots.empty?
790
+
791
+ snapshots.map do |snap|
792
+ snaptime = snap[:snaptime]
793
+ date = snaptime ? Time.at(snaptime).strftime("%Y-%m-%d %H:%M:%S") : "-"
794
+
795
+ {
796
+ "NAME" => snap[:name],
797
+ "DATE" => date,
798
+ "VMSTATE" => snap[:vmstate] ? "yes" : "no",
799
+ "DESCRIPTION" => snap[:description] || "-"
800
+ }
801
+ end
802
+ end
803
+
804
+ # Formats pending configuration changes.
805
+ #
806
+ # @param pending [Array<Hash>, nil] pending changes from API
807
+ # @return [Array<Hash>, String] pending changes table or "-"
808
+ def format_pending_changes(pending)
809
+ return "No pending changes" if pending.nil? || pending.empty?
810
+
811
+ # Only show entries with actual pending changes (new value or deletion)
812
+ changes = pending.select { |c| c.key?(:pending) || c[:delete] }
813
+ return "No pending changes" if changes.empty?
814
+
815
+ changes.map do |change|
816
+ row = { "KEY" => change[:key].to_s, "CURRENT" => change[:value].to_s }
817
+ row["PENDING"] = change[:pending].to_s if change.key?(:pending)
818
+ row["DELETE"] = "yes" if change[:delete]
819
+ row
820
+ end
821
+ end
822
+
823
+ # Registers config keys as consumed by a format method.
824
+ #
825
+ # @param keys [Array<Symbol>] config keys to mark as consumed
826
+ # @return [void]
827
+ def consume(*keys)
828
+ @consumed_keys.merge(keys.map(&:to_sym))
829
+ end
830
+
831
+ # Consumes all config keys matching a pattern.
832
+ #
833
+ # @param config [Hash] config hash
834
+ # @param pattern [Regexp] pattern to match key names
835
+ # @return [void]
836
+ def consume_matching(config, pattern)
837
+ config.keys.select { |k| k.to_s.match?(pattern) }.each { |k| consume(k) }
838
+ end
839
+
840
+ # Formats remaining unconsumed config keys as catch-all table.
841
+ #
842
+ # @param config [Hash] full config hash
843
+ # @return [Array<Hash>, String] remaining keys table or "-"
844
+ def format_remaining(config)
845
+ excluded = %i[digest]
846
+ remaining = config.keys.map(&:to_sym) - @consumed_keys.to_a - excluded
847
+ return "-" if remaining.empty?
848
+
849
+ remaining.sort.map { |k| { "KEY" => k.to_s, "VALUE" => config[k].to_s } }
850
+ end
851
+ end
852
+ end
853
+ end