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,297 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Pvectl
6
+ module Config
7
+ # Loads and resolves configuration from multiple sources.
8
+ #
9
+ # Provider handles loading configuration from YAML files and environment
10
+ # variables, then merging them with proper priority to produce a final
11
+ # ResolvedConfig. Priority order (highest to lowest):
12
+ # 1. CLI options
13
+ # 2. Environment variables
14
+ # 3. Configuration file
15
+ #
16
+ # @example Loading configuration
17
+ # provider = Provider.new
18
+ # config = provider.resolve(
19
+ # config_path: "~/.pvectl/config",
20
+ # cli_options: { context: "production" }
21
+ # )
22
+ #
23
+ class Provider
24
+ # Mapping of environment variables to configuration keys
25
+ ENV_VARS = {
26
+ "PROXMOX_HOST" => :server,
27
+ "PROXMOX_TOKEN_ID" => :token_id,
28
+ "PROXMOX_TOKEN_SECRET" => :token_secret,
29
+ "PROXMOX_USER" => :username,
30
+ "PROXMOX_PASSWORD" => :password,
31
+ "PROXMOX_VERIFY_SSL" => :verify_ssl,
32
+ "PVECTL_CONTEXT" => :context,
33
+ "PVECTL_CONFIG" => :config_path,
34
+ # Retry/timeout settings
35
+ "PROXMOX_TIMEOUT" => :timeout,
36
+ "PROXMOX_RETRY_COUNT" => :retry_count,
37
+ "PROXMOX_RETRY_DELAY" => :retry_delay,
38
+ "PROXMOX_MAX_RETRY_DELAY" => :max_retry_delay,
39
+ "PROXMOX_RETRY_WRITES" => :retry_writes
40
+ }.freeze
41
+
42
+ # Keys that should be parsed as integers
43
+ INTEGER_VARS = %i[timeout retry_count retry_delay max_retry_delay].freeze
44
+
45
+ # Keys that should be parsed as booleans
46
+ BOOLEAN_VARS = %i[verify_ssl retry_writes].freeze
47
+
48
+ # Checks if a configuration file exists at the given path.
49
+ #
50
+ # @param path [String] path to configuration file
51
+ # @return [Boolean] true if file exists
52
+ def file_exists?(path)
53
+ File.exist?(path)
54
+ end
55
+
56
+ # Checks if a file has insecure permissions (readable by group/others).
57
+ #
58
+ # @param path [String] path to file
59
+ # @return [Boolean] true if permissions are insecure
60
+ def insecure_permissions?(path)
61
+ return false unless File.exist?(path)
62
+
63
+ mode = File.stat(path).mode & 0o777
64
+ (mode & 0o077) != 0
65
+ end
66
+
67
+ # Loads configuration from a YAML file.
68
+ #
69
+ # @param path [String] path to configuration file
70
+ # @return [Hash] parsed configuration hash
71
+ # @raise [ConfigNotFoundError] if file does not exist
72
+ # @raise [InvalidConfigError] if YAML is invalid
73
+ def load_file(path)
74
+ raise ConfigNotFoundError, "Configuration file not found: #{path}" unless File.exist?(path)
75
+
76
+ content = File.read(path)
77
+ YAML.safe_load(content, permitted_classes: [Symbol])
78
+ rescue Psych::SyntaxError => e
79
+ raise InvalidConfigError, "Invalid YAML in #{path}: #{e.message}"
80
+ end
81
+
82
+ # Loads configuration from environment variables.
83
+ #
84
+ # @return [Hash] configuration values from environment
85
+ # @raise [InvalidConfigError] if integer values are invalid
86
+ def load_env
87
+ result = {}
88
+
89
+ ENV_VARS.each do |env_var, config_key|
90
+ value = ENV[env_var]
91
+ next if value.nil? || value.empty?
92
+
93
+ result[config_key] = parse_env_value(config_key, value)
94
+ end
95
+
96
+ result
97
+ end
98
+
99
+ # Resolves the context name from various sources.
100
+ #
101
+ # Priority: CLI > ENV > file
102
+ #
103
+ # @param cli_options [Hash] CLI options (may contain :context)
104
+ # @param file_config [Hash] configuration from file (may contain "current-context")
105
+ # @return [String, nil] resolved context name
106
+ def resolve_context_name(cli_options:, file_config: {})
107
+ cli_options[:context] || ENV["PVECTL_CONTEXT"] || file_config["current-context"]
108
+ end
109
+
110
+ # Resolves full configuration by merging all sources.
111
+ #
112
+ # @param config_path [String] path to configuration file
113
+ # @param cli_options [Hash] CLI options
114
+ # @param cluster_override [String, nil] for testing invalid cluster references
115
+ # @return [Models::ResolvedConfig] resolved configuration
116
+ # @raise [ConfigNotFoundError] if config file not found
117
+ # @raise [ContextNotFoundError] if context not found
118
+ # @raise [ClusterNotFoundError] if cluster not found
119
+ # @raise [UserNotFoundError] if user not found
120
+ def resolve(config_path:, cli_options:, cluster_override: nil)
121
+ file_config = load_file(config_path)
122
+ env_config = load_env
123
+
124
+ context_name = resolve_context_name(cli_options: cli_options, file_config: file_config)
125
+ context = find_context(file_config, context_name)
126
+
127
+ cluster_name = cluster_override || context.cluster_ref
128
+ cluster = find_cluster(file_config, cluster_name)
129
+ user = find_user(file_config, context.user_ref)
130
+
131
+ build_resolved_config(
132
+ context: context,
133
+ cluster: cluster,
134
+ user: user,
135
+ env_config: env_config,
136
+ cli_options: cli_options
137
+ )
138
+ end
139
+
140
+ private
141
+
142
+ # Parses an environment variable value to the appropriate type.
143
+ #
144
+ # @param key [Symbol] configuration key
145
+ # @param value [String] raw environment variable value
146
+ # @return [Object] parsed value (Integer, Boolean, or String)
147
+ # @raise [InvalidConfigError] if value cannot be parsed
148
+ def parse_env_value(key, value)
149
+ if BOOLEAN_VARS.include?(key)
150
+ parse_boolean_env(value)
151
+ elsif INTEGER_VARS.include?(key)
152
+ parse_integer_env(key, value)
153
+ else
154
+ value
155
+ end
156
+ end
157
+
158
+ # Parses a boolean environment variable.
159
+ #
160
+ # @param value [String] raw value
161
+ # @return [Boolean] parsed boolean
162
+ def parse_boolean_env(value)
163
+ %w[true 1 yes].include?(value.to_s.downcase)
164
+ end
165
+
166
+ # Parses an integer environment variable with validation.
167
+ #
168
+ # @param key [Symbol] configuration key (for error messages)
169
+ # @param value [String] raw environment variable value
170
+ # @return [Integer] parsed integer value
171
+ # @raise [InvalidConfigError] if value is not a valid non-negative integer
172
+ def parse_integer_env(key, value)
173
+ unless value.to_s.match?(/\A\d+\z/)
174
+ raise InvalidConfigError,
175
+ "Invalid integer for #{key.to_s.tr('_', '-')}: '#{value}' " \
176
+ "(must be a non-negative integer)"
177
+ end
178
+
179
+ value.to_i
180
+ end
181
+
182
+ # Finds a context by name in the configuration.
183
+ #
184
+ # @param config [Hash] configuration hash
185
+ # @param name [String] context name
186
+ # @return [Models::Context] context model
187
+ # @raise [ContextNotFoundError] if context not found
188
+ def find_context(config, name)
189
+ contexts = config["contexts"] || []
190
+ context_hash = contexts.find { |c| c["name"] == name }
191
+
192
+ if context_hash.nil?
193
+ available = contexts.map { |c| c["name"] }.join(", ")
194
+ raise ContextNotFoundError, "Context '#{name}' not found. Available: #{available}"
195
+ end
196
+
197
+ Models::Context.from_hash(context_hash)
198
+ end
199
+
200
+ # Finds a cluster by name in the configuration.
201
+ #
202
+ # @param config [Hash] configuration hash
203
+ # @param name [String] cluster name
204
+ # @return [Models::Cluster] cluster model
205
+ # @raise [ClusterNotFoundError] if cluster not found
206
+ def find_cluster(config, name)
207
+ clusters = config["clusters"] || []
208
+ cluster_hash = clusters.find { |c| c["name"] == name }
209
+
210
+ raise ClusterNotFoundError, "Cluster '#{name}' not found in configuration" if cluster_hash.nil?
211
+
212
+ Models::Cluster.from_hash(cluster_hash)
213
+ end
214
+
215
+ # Finds a user by name in the configuration.
216
+ #
217
+ # @param config [Hash] configuration hash
218
+ # @param name [String] user name
219
+ # @return [Models::User] user model
220
+ # @raise [UserNotFoundError] if user not found
221
+ def find_user(config, name)
222
+ users = config["users"] || []
223
+ user_hash = users.find { |u| u["name"] == name }
224
+
225
+ raise UserNotFoundError, "User '#{name}' not found in configuration" if user_hash.nil?
226
+
227
+ Models::User.from_hash(user_hash)
228
+ end
229
+
230
+ # Builds a ResolvedConfig from merged sources.
231
+ #
232
+ # @param context [Models::Context] resolved context
233
+ # @param cluster [Models::Cluster] resolved cluster
234
+ # @param user [Models::User] resolved user
235
+ # @param env_config [Hash] environment configuration
236
+ # @param cli_options [Hash] CLI options
237
+ # @return [Models::ResolvedConfig] resolved configuration
238
+ def build_resolved_config(context:, cluster:, user:, env_config:, cli_options:)
239
+ # Merge with priority: CLI > ENV > file
240
+ server = env_config[:server] || cluster.server
241
+ verify_ssl = env_config.key?(:verify_ssl) ? env_config[:verify_ssl] : cluster.verify_ssl
242
+
243
+ # Determine auth type and credentials
244
+ auth_type, token_id, token_secret, username, password = resolve_auth(user, env_config)
245
+
246
+ # Resolve retry/timeout settings with priority: ENV > file
247
+ timeout = env_config[:timeout] || cluster.timeout
248
+ retry_count = env_config[:retry_count] || cluster.retry_count
249
+ retry_delay = env_config[:retry_delay] || cluster.retry_delay
250
+ max_retry_delay = env_config[:max_retry_delay] || cluster.max_retry_delay
251
+ retry_writes = env_config.key?(:retry_writes) ? env_config[:retry_writes] : cluster.retry_writes
252
+
253
+ Models::ResolvedConfig.new(
254
+ context_name: context.name,
255
+ server: server,
256
+ verify_ssl: verify_ssl,
257
+ certificate_authority: cluster.certificate_authority,
258
+ auth_type: auth_type,
259
+ token_id: token_id,
260
+ token_secret: token_secret,
261
+ username: username,
262
+ password: password,
263
+ default_node: context.default_node,
264
+ timeout: timeout,
265
+ retry_count: retry_count,
266
+ retry_delay: retry_delay,
267
+ max_retry_delay: max_retry_delay,
268
+ retry_writes: retry_writes
269
+ )
270
+ end
271
+
272
+ # Resolves authentication type and credentials.
273
+ #
274
+ # @param user [Models::User] user from config
275
+ # @param env_config [Hash] environment configuration
276
+ # @return [Array] [auth_type, token_id, token_secret, username, password]
277
+ def resolve_auth(user, env_config)
278
+ # ENV token auth takes precedence
279
+ if env_config[:token_id] && env_config[:token_secret]
280
+ return [:token, env_config[:token_id], env_config[:token_secret], nil, nil]
281
+ end
282
+
283
+ # ENV password auth
284
+ if env_config[:username] && env_config[:password]
285
+ return [:password, nil, nil, env_config[:username], env_config[:password]]
286
+ end
287
+
288
+ # Fall back to user from config
289
+ if user.token_auth?
290
+ [:token, user.token_id, user.token_secret, nil, nil]
291
+ else
292
+ [:password, nil, nil, user.username, user.password]
293
+ end
294
+ end
295
+ end
296
+ end
297
+ end
@@ -0,0 +1,300 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pvectl
4
+ module Config
5
+ # Facade for configuration management operations.
6
+ #
7
+ # Service coordinates Provider, Store, and Wizard to provide a unified
8
+ # interface for loading, modifying, and saving configuration. It handles
9
+ # the complete lifecycle of configuration management.
10
+ #
11
+ # @example Loading configuration
12
+ # service = Service.new
13
+ # service.load(config: "~/.pvectl/config", context: "production")
14
+ # puts service.current_config.server
15
+ #
16
+ # @example Switching contexts
17
+ # service.load(config: path)
18
+ # service.use_context("development")
19
+ #
20
+ # @example Listing contexts
21
+ # service.contexts.each do |ctx|
22
+ # puts ctx.name
23
+ # end
24
+ #
25
+ class Service
26
+ # Default path for configuration file
27
+ DEFAULT_CONFIG_PATH = File.expand_path("~/.pvectl/config").freeze
28
+
29
+ # @return [String] path to loaded configuration file
30
+ attr_reader :config_path
31
+
32
+ # @return [Hash] raw configuration hash from file
33
+ attr_reader :raw_config
34
+
35
+ # @return [String] name of the current context
36
+ attr_reader :current_context_name
37
+
38
+ # Creates a new Service instance.
39
+ #
40
+ # @param provider [Provider, nil] configuration provider (default: new Provider)
41
+ # @param store [Store, nil] configuration store (default: new Store)
42
+ # @param wizard [Wizard, nil] configuration wizard (default: nil, created on demand)
43
+ def initialize(provider: nil, store: nil, wizard: nil)
44
+ @provider = provider || Provider.new
45
+ @store = store || Store.new
46
+ @wizard = wizard
47
+ @loaded = false
48
+ end
49
+
50
+ # Loads configuration from file and CLI options.
51
+ #
52
+ # Determines the configuration path from CLI options, environment,
53
+ # or default location. Warns if file permissions are insecure.
54
+ #
55
+ # @param cli_options [Hash] CLI options (:config, :context)
56
+ # @return [Service] self for chaining
57
+ # @raise [ConfigNotFoundError] if config file not found and wizard not available
58
+ # @raise [InvalidConfigError] if config file contains invalid YAML
59
+ def load(cli_options = {})
60
+ @config_path = resolve_config_path(cli_options)
61
+
62
+ unless @provider.file_exists?(@config_path)
63
+ raise ConfigNotFoundError, "Configuration file not found: #{@config_path}"
64
+ end
65
+
66
+ warn_insecure_permissions if @provider.insecure_permissions?(@config_path)
67
+
68
+ @raw_config = @provider.load_file(@config_path)
69
+ @current_context_name = @provider.resolve_context_name(
70
+ cli_options: cli_options,
71
+ file_config: @raw_config
72
+ )
73
+ @resolved_config = nil # Reset cached resolved config
74
+ @loaded = true
75
+
76
+ self
77
+ end
78
+
79
+ # Returns the current resolved configuration.
80
+ #
81
+ # @return [Models::ResolvedConfig] resolved configuration
82
+ # @raise [ConfigError] if configuration not loaded
83
+ def current_config
84
+ raise ConfigError, "Configuration not loaded. Call #load first." unless @loaded
85
+
86
+ @resolved_config ||= @provider.resolve(
87
+ config_path: @config_path,
88
+ cli_options: { context: @current_context_name }
89
+ )
90
+ end
91
+
92
+ # Returns all contexts from the configuration.
93
+ #
94
+ # @return [Array<Models::Context>] list of context models
95
+ def contexts
96
+ (@raw_config["contexts"] || []).map do |ctx_hash|
97
+ Models::Context.from_hash(ctx_hash)
98
+ end
99
+ end
100
+
101
+ # Returns a specific context by name.
102
+ #
103
+ # @param name [String] context name
104
+ # @return [Models::Context, nil] context or nil if not found
105
+ def context(name)
106
+ contexts.find { |ctx| ctx.name == name }
107
+ end
108
+
109
+ # Switches to a different context.
110
+ #
111
+ # Updates both the in-memory state and the configuration file.
112
+ #
113
+ # @param context_name [String] name of context to switch to
114
+ # @return [void]
115
+ # @raise [ContextNotFoundError] if context not found
116
+ def use_context(context_name)
117
+ unless context(context_name)
118
+ available = contexts.map(&:name).join(", ")
119
+ raise ContextNotFoundError, "Context '#{context_name}' not found. Available: #{available}"
120
+ end
121
+
122
+ @store.update_current_context(@config_path, context_name)
123
+ @current_context_name = context_name
124
+ @resolved_config = nil # Reset cached config
125
+ @raw_config["current-context"] = context_name
126
+ end
127
+
128
+ # Creates or updates a context.
129
+ #
130
+ # @param name [String] context name
131
+ # @param cluster [String] cluster reference
132
+ # @param user [String] user reference
133
+ # @param default_node [String, nil] optional default node
134
+ # @return [Models::Context] the new or updated context
135
+ def set_context(name:, cluster:, user:, default_node: nil)
136
+ new_context = Models::Context.new(
137
+ name: name,
138
+ cluster_ref: cluster,
139
+ user_ref: user,
140
+ default_node: default_node
141
+ )
142
+
143
+ @store.upsert_context(@config_path, new_context)
144
+
145
+ # Update in-memory config
146
+ refresh_raw_config
147
+
148
+ new_context
149
+ end
150
+
151
+ # Returns all clusters from the configuration.
152
+ #
153
+ # @return [Array<Models::Cluster>] list of cluster models
154
+ def clusters
155
+ (@raw_config["clusters"] || []).map do |cluster_hash|
156
+ Models::Cluster.from_hash(cluster_hash)
157
+ end
158
+ end
159
+
160
+ # Returns a specific cluster by name.
161
+ #
162
+ # @param name [String] cluster name
163
+ # @return [Models::Cluster, nil] cluster or nil if not found
164
+ def cluster(name)
165
+ clusters.find { |c| c.name == name }
166
+ end
167
+
168
+ # Creates or updates a cluster.
169
+ #
170
+ # @param name [String] cluster name
171
+ # @param server [String] Proxmox server URL
172
+ # @param verify_ssl [Boolean] whether to verify SSL (default: true)
173
+ # @param certificate_authority [String, nil] path to CA certificate
174
+ # @return [Models::Cluster] the new or updated cluster
175
+ def set_cluster(name:, server:, verify_ssl: true, certificate_authority: nil)
176
+ new_cluster = Models::Cluster.new(
177
+ name: name,
178
+ server: server,
179
+ verify_ssl: verify_ssl,
180
+ certificate_authority: certificate_authority
181
+ )
182
+
183
+ @store.upsert_cluster(@config_path, new_cluster)
184
+
185
+ # Update in-memory config
186
+ refresh_raw_config
187
+
188
+ new_cluster
189
+ end
190
+
191
+ # Returns all users from the configuration.
192
+ #
193
+ # @return [Array<Models::User>] list of user models
194
+ def users
195
+ (@raw_config["users"] || []).map do |user_hash|
196
+ Models::User.from_hash(user_hash)
197
+ end
198
+ end
199
+
200
+ # Returns a specific user by name.
201
+ #
202
+ # @param name [String] user name
203
+ # @return [Models::User, nil] user or nil if not found
204
+ def user(name)
205
+ users.find { |u| u.name == name }
206
+ end
207
+
208
+ # Creates or updates user credentials.
209
+ #
210
+ # @param name [String] user name
211
+ # @param token_id [String, nil] API token ID
212
+ # @param token_secret [String, nil] API token secret
213
+ # @param username [String, nil] username for password auth
214
+ # @param password [String, nil] password for password auth
215
+ # @return [Models::User] the new or updated user
216
+ def set_credentials(name:, token_id: nil, token_secret: nil, username: nil, password: nil)
217
+ new_user = Models::User.new(
218
+ name: name,
219
+ token_id: token_id,
220
+ token_secret: token_secret,
221
+ username: username,
222
+ password: password
223
+ )
224
+
225
+ @store.upsert_user(@config_path, new_user)
226
+
227
+ # Update in-memory config
228
+ refresh_raw_config
229
+
230
+ new_user
231
+ end
232
+
233
+ # Returns configuration with secrets masked for display.
234
+ #
235
+ # @return [Hash] configuration with masked secrets
236
+ def masked_config
237
+ config = deep_copy(@raw_config)
238
+
239
+ (config["users"] || []).each do |user|
240
+ user_data = user["user"] || {}
241
+ user_data["token-secret"] = "********" if user_data["token-secret"]
242
+ user_data["password"] = "********" if user_data["password"]
243
+ end
244
+
245
+ config
246
+ end
247
+
248
+ # Saves the current configuration to file.
249
+ #
250
+ # @return [void]
251
+ def save
252
+ @store.save(@config_path, @raw_config)
253
+ end
254
+
255
+ private
256
+
257
+ # Resolves the configuration file path.
258
+ #
259
+ # Priority: CLI option > ENV > default
260
+ #
261
+ # @param cli_options [Hash] CLI options
262
+ # @return [String] resolved path
263
+ def resolve_config_path(cli_options)
264
+ cli_options[:config] ||
265
+ ENV["PVECTL_CONFIG"] ||
266
+ DEFAULT_CONFIG_PATH
267
+ end
268
+
269
+ # Outputs a warning about insecure file permissions.
270
+ #
271
+ # @return [void]
272
+ def warn_insecure_permissions
273
+ $stderr.puts "Warning: Configuration file has insecure permissions. " \
274
+ "Consider running: chmod 600 #{@config_path}"
275
+ end
276
+
277
+ # Refreshes the raw configuration from disk.
278
+ #
279
+ # @return [void]
280
+ def refresh_raw_config
281
+ @raw_config = @provider.load_file(@config_path)
282
+ end
283
+
284
+ # Creates a deep copy of a hash.
285
+ #
286
+ # @param obj [Object] object to copy
287
+ # @return [Object] deep copy
288
+ def deep_copy(obj)
289
+ case obj
290
+ when Hash
291
+ obj.transform_values { |v| deep_copy(v) }
292
+ when Array
293
+ obj.map { |v| deep_copy(v) }
294
+ else
295
+ obj
296
+ end
297
+ end
298
+ end
299
+ end
300
+ end