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,449 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require "socket"
5
+ require "openssl"
6
+ require "websocket/driver"
7
+ require "io/console"
8
+
9
+ module Pvectl
10
+ module Console
11
+ # Manages an interactive terminal session over a Proxmox VNC WebSocket.
12
+ #
13
+ # TerminalSession handles the full lifecycle of a console connection:
14
+ # 1. Opens a raw TCP/SSL socket to the Proxmox host
15
+ # 2. Performs a WebSocket handshake with authentication
16
+ # 3. Bridges local stdin/stdout with the remote terminal via the xtermjs wire protocol
17
+ # 4. Manages raw terminal mode and signal handling (SIGWINCH for resize)
18
+ #
19
+ # The xtermjs protocol uses numbered message types:
20
+ # - Type 0: input data — +0:<bytesize>:<data>+
21
+ # - Type 1: terminal resize — +1:<cols>:<rows>:+
22
+ # - Type 2: ping — +2+
23
+ #
24
+ # @example Basic usage (called by Console::Command)
25
+ # session = Pvectl::Console::TerminalSession.new(
26
+ # url: "wss://pve1:8006/api2/json/nodes/pve1/qemu/100/vncwebsocket?port=5900&vncticket=TICKET",
27
+ # cookie: "PVEAuthCookie=PVE:root@pam:abc",
28
+ # user: "root@pam",
29
+ # ticket: "PVEVNC:abc123",
30
+ # verify_ssl: true
31
+ # )
32
+ # session.run
33
+ #
34
+ # @see https://pve.proxmox.com/wiki/VNC_Proxy Proxmox VNC Proxy documentation
35
+ #
36
+ class TerminalSession
37
+ # Ctrl+] — standard disconnect key (same as telnet/SSH escape)
38
+ CTRL_CLOSE_BRACKET = "\x1d"
39
+
40
+ # Seconds between keepalive pings sent to the server
41
+ PING_INTERVAL = 120
42
+
43
+ # Bytes to read per socket read call
44
+ READ_CHUNK_SIZE = 4096
45
+
46
+ # Creates a new terminal session.
47
+ #
48
+ # @param url [String] WebSocket URL for the VNC proxy endpoint
49
+ # @param cookie [String] PVEAuthCookie header value for authentication
50
+ # @param user [String] Proxmox user identifier (e.g., "root@pam")
51
+ # @param ticket [String] VNC ticket for the handshake (e.g., "PVEVNC:abc123")
52
+ # @param verify_ssl [Boolean] whether to verify the server's SSL certificate
53
+ #
54
+ def initialize(url:, cookie:, user:, ticket:, verify_ssl:)
55
+ @url = url
56
+ @cookie = cookie
57
+ @user = user
58
+ @ticket = ticket
59
+ @verify_ssl = verify_ssl
60
+ @running = false
61
+ @saved_stty = nil
62
+ end
63
+
64
+ # Runs the interactive terminal session.
65
+ #
66
+ # Opens the WebSocket connection, performs the handshake, and enters
67
+ # the I/O loop bridging stdin to the remote terminal. Restores the
68
+ # local terminal state on exit (even on error).
69
+ #
70
+ # @return [void]
71
+ # @raise [RuntimeError] if the WebSocket handshake fails
72
+ #
73
+ def run
74
+ uri = URI.parse(@url)
75
+ socket = open_socket(uri)
76
+ driver = create_driver(uri, socket)
77
+ perform_websocket_handshake(driver, socket)
78
+ run_io_loop(driver, socket)
79
+ ensure
80
+ restore_terminal
81
+ socket&.close
82
+ end
83
+
84
+ private
85
+
86
+ # --- Protocol encoding (xtermjs wire format) ---
87
+
88
+ # Encodes user input for the xtermjs protocol.
89
+ #
90
+ # @param data [String] raw input bytes from stdin
91
+ # @return [String] encoded message in format "0:<bytesize>:<data>"
92
+ #
93
+ def encode_input(data)
94
+ "0:#{data.bytesize}:#{data}"
95
+ end
96
+
97
+ # Encodes a terminal resize notification.
98
+ #
99
+ # @param cols [Integer] new terminal width in columns
100
+ # @param rows [Integer] new terminal height in rows
101
+ # @return [String] encoded message in format "1:<cols>:<rows>:"
102
+ #
103
+ def encode_resize(cols, rows)
104
+ "1:#{cols}:#{rows}:"
105
+ end
106
+
107
+ # Encodes a keepalive ping message.
108
+ #
109
+ # @return [String] the ping message "2"
110
+ #
111
+ def encode_ping
112
+ "2"
113
+ end
114
+
115
+ # Checks if the given byte is the disconnect key (Ctrl+]).
116
+ #
117
+ # @param byte [String] a single byte of input
118
+ # @return [Boolean] true if the byte is the disconnect sequence
119
+ #
120
+ def disconnect_key?(byte)
121
+ byte == CTRL_CLOSE_BRACKET
122
+ end
123
+
124
+ # Builds the authentication handshake message.
125
+ #
126
+ # @return [String] handshake in format "<user>:<ticket>\n"
127
+ #
128
+ def handshake_message
129
+ "#{@user}:#{@ticket}\n"
130
+ end
131
+
132
+ # --- Networking ---
133
+
134
+ # Opens a TCP socket with optional SSL wrapping.
135
+ #
136
+ # @param uri [URI] parsed WebSocket URL
137
+ # @return [TCPSocket, OpenSSL::SSL::SSLSocket] the connected socket
138
+ #
139
+ def open_socket(uri)
140
+ tcp = TCPSocket.new(uri.host, uri.port)
141
+
142
+ if uri.scheme == "wss"
143
+ wrap_ssl(tcp, uri.host)
144
+ else
145
+ tcp
146
+ end
147
+ end
148
+
149
+ # Wraps a TCP socket in SSL.
150
+ #
151
+ # @param tcp [TCPSocket] raw TCP socket
152
+ # @param hostname [String] server hostname for SNI
153
+ # @return [OpenSSL::SSL::SSLSocket] SSL-wrapped socket
154
+ #
155
+ def wrap_ssl(tcp, hostname)
156
+ ctx = OpenSSL::SSL::SSLContext.new
157
+ ctx.verify_mode = @verify_ssl ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
158
+
159
+ ssl = OpenSSL::SSL::SSLSocket.new(tcp, ctx)
160
+ ssl.hostname = hostname
161
+ ssl.connect
162
+ ssl
163
+ end
164
+
165
+ # Creates a WebSocket protocol driver for the given socket.
166
+ #
167
+ # Uses {SocketAdapter} to satisfy websocket-driver's interface requirements
168
+ # (the adapter must respond to +#url+ and +#write+).
169
+ #
170
+ # Sets required headers for Proxmox xtermjs:
171
+ # - +Cookie+ — PVEAuthCookie for session authentication
172
+ # - +Referer+ — must include +xtermjs=1+ query param so the server
173
+ # uses text-based xtermjs protocol instead of binary VNC (RFB)
174
+ #
175
+ # @param uri [URI] parsed WebSocket URL
176
+ # @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] the underlying socket
177
+ # @return [WebSocket::Driver::Client] configured WebSocket driver
178
+ #
179
+ def create_driver(uri, socket)
180
+ adapter = SocketAdapter.new(uri.to_s, socket)
181
+ driver = WebSocket::Driver.client(adapter, protocols: ["binary"])
182
+ driver.set_header("Cookie", @cookie)
183
+ driver.set_header("Referer", build_referer(uri))
184
+ driver
185
+ end
186
+
187
+ # Builds a Referer header that signals xtermjs mode to Proxmox.
188
+ #
189
+ # Proxmox checks the Referer header's query parameters to decide
190
+ # whether to use xtermjs (text) or noVNC (binary RFB) protocol.
191
+ #
192
+ # @param uri [URI] parsed WebSocket URL
193
+ # @return [String] referer URL with xtermjs=1 query param
194
+ #
195
+ def build_referer(uri)
196
+ "https://#{uri.host}:#{uri.port}/?console=shell&xtermjs=1&vmid=0&vmname=&node=localhost&cmd="
197
+ end
198
+
199
+ # Reads available data from a socket with a timeout.
200
+ #
201
+ # For SSL sockets, checks +pending+ first to handle buffered data that
202
+ # IO.select cannot detect.
203
+ #
204
+ # @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] socket to read from
205
+ # @param timeout [Numeric] maximum seconds to wait for data
206
+ # @return [String, nil] raw data or nil on timeout/EOF
207
+ #
208
+ def read_from_socket(socket, timeout:)
209
+ # SSL sockets may have buffered data not visible to IO.select
210
+ if socket.respond_to?(:pending) && socket.pending > 0
211
+ return socket.readpartial(READ_CHUNK_SIZE)
212
+ end
213
+
214
+ ready = IO.select([socket], nil, nil, timeout)
215
+ return nil unless ready
216
+
217
+ socket.readpartial(READ_CHUNK_SIZE)
218
+ rescue EOFError, Errno::ECONNRESET, IO::WaitReadable
219
+ nil
220
+ end
221
+
222
+ # --- WebSocket handshake ---
223
+
224
+ # Performs the WebSocket handshake and Proxmox authentication.
225
+ #
226
+ # Starts the WebSocket driver, waits for the +:open+ event, sends
227
+ # the authentication message, and waits for an "OK" response.
228
+ #
229
+ # @param driver [WebSocket::Driver::Client] WebSocket protocol driver
230
+ # @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] underlying socket
231
+ # @return [void]
232
+ # @raise [RuntimeError] if the handshake times out or authentication fails
233
+ #
234
+ def perform_websocket_handshake(driver, socket)
235
+ open = false
236
+ authenticated = false
237
+
238
+ driver.on(:open) { open = true }
239
+ driver.on(:message) do |msg|
240
+ authenticated = true if msg.data == "OK"
241
+ end
242
+
243
+ driver.start
244
+
245
+ # Wait for WebSocket open
246
+ until open
247
+ data = read_from_socket(socket, timeout: 10)
248
+ raise "WebSocket handshake timed out" unless data
249
+
250
+ driver.parse(data)
251
+ end
252
+
253
+ # Send auth and wait for OK
254
+ driver.text(handshake_message)
255
+
256
+ until authenticated
257
+ data = read_from_socket(socket, timeout: 10)
258
+ raise "Authentication timed out" unless data
259
+
260
+ driver.parse(data)
261
+ end
262
+ end
263
+
264
+ # --- I/O loop ---
265
+
266
+ # Main I/O loop bridging local terminal and remote WebSocket.
267
+ #
268
+ # Puts the terminal in raw mode, then multiplexes between stdin and the
269
+ # remote socket using IO.select. Sends keepalive pings every {PING_INTERVAL}
270
+ # seconds. Exits on disconnect key (Ctrl+]) or connection close.
271
+ #
272
+ # @param driver [WebSocket::Driver::Client] WebSocket protocol driver
273
+ # @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] underlying socket
274
+ # @return [void]
275
+ #
276
+ def run_io_loop(driver, socket)
277
+ @running = true
278
+
279
+ enable_raw_terminal
280
+ send_initial_resize(driver)
281
+ # Send an initial empty input to wake the remote terminal prompt
282
+ driver.text(encode_input("\n"))
283
+ trap_resize(driver)
284
+
285
+ driver.on(:message) do |msg|
286
+ $stdout.write(msg.data)
287
+ $stdout.flush
288
+ end
289
+
290
+ driver.on(:close) { @running = false }
291
+
292
+ last_ping = Time.now
293
+
294
+ while @running
295
+ # SSL sockets may have buffered data
296
+ if socket.respond_to?(:pending) && socket.pending > 0
297
+ driver.parse(socket.readpartial(READ_CHUNK_SIZE))
298
+ next
299
+ end
300
+
301
+ timeout = [PING_INTERVAL - (Time.now - last_ping), 1].max
302
+ ready = IO.select([$stdin, socket], nil, nil, timeout)
303
+
304
+ # Send ping on timeout
305
+ if ready.nil?
306
+ driver.text(encode_ping)
307
+ last_ping = Time.now
308
+ next
309
+ end
310
+
311
+ ready[0].each do |io|
312
+ if io == $stdin
313
+ handle_stdin(driver)
314
+ else
315
+ handle_socket(driver, socket)
316
+ end
317
+ end
318
+
319
+ # Periodic ping
320
+ if Time.now - last_ping >= PING_INTERVAL
321
+ driver.text(encode_ping)
322
+ last_ping = Time.now
323
+ end
324
+ end
325
+ end
326
+
327
+ # Reads from stdin and sends encoded input to the WebSocket.
328
+ #
329
+ # @param driver [WebSocket::Driver::Client] WebSocket protocol driver
330
+ # @return [void]
331
+ #
332
+ def handle_stdin(driver)
333
+ data = $stdin.readpartial(READ_CHUNK_SIZE)
334
+
335
+ if disconnect_key?(data)
336
+ @running = false
337
+ return
338
+ end
339
+
340
+ driver.text(encode_input(data))
341
+ rescue EOFError, IO::WaitReadable
342
+ @running = false
343
+ end
344
+
345
+ # Reads from the socket and feeds data to the WebSocket driver.
346
+ #
347
+ # @param driver [WebSocket::Driver::Client] WebSocket protocol driver
348
+ # @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] underlying socket
349
+ # @return [void]
350
+ #
351
+ def handle_socket(driver, socket)
352
+ data = socket.readpartial(READ_CHUNK_SIZE)
353
+ driver.parse(data)
354
+ rescue EOFError, Errno::ECONNRESET
355
+ @running = false
356
+ end
357
+
358
+ # --- Terminal management ---
359
+
360
+ # Enables raw terminal mode for direct character input.
361
+ #
362
+ # Saves the current terminal state so it can be restored later.
363
+ # Uses stty for portability.
364
+ #
365
+ # @return [void]
366
+ #
367
+ def enable_raw_terminal
368
+ @saved_stty = `stty -g`.chomp
369
+ system("stty raw -echo -icanon -isig")
370
+ end
371
+
372
+ # Restores the terminal to its saved state.
373
+ #
374
+ # Called in an ensure block to guarantee cleanup even on errors.
375
+ #
376
+ # @return [void]
377
+ #
378
+ def restore_terminal
379
+ system("stty #{@saved_stty}") if @saved_stty
380
+ @saved_stty = nil
381
+ end
382
+
383
+ # Sends the initial terminal size to the remote server.
384
+ #
385
+ # @param driver [WebSocket::Driver::Client] WebSocket protocol driver
386
+ # @return [void]
387
+ #
388
+ def send_initial_resize(driver)
389
+ cols, rows = detect_terminal_size
390
+ driver.text(encode_resize(cols, rows))
391
+ end
392
+
393
+ # Installs a SIGWINCH handler to send resize events on terminal size changes.
394
+ #
395
+ # @param driver [WebSocket::Driver::Client] WebSocket protocol driver
396
+ # @return [void]
397
+ #
398
+ def trap_resize(driver)
399
+ Signal.trap("WINCH") do
400
+ cols, rows = detect_terminal_size
401
+ driver.text(encode_resize(cols, rows))
402
+ end
403
+ end
404
+
405
+ # Detects the current terminal dimensions.
406
+ #
407
+ # @return [Array<Integer>] columns and rows as +[cols, rows]+
408
+ #
409
+ def detect_terminal_size
410
+ io = IO.console
411
+ return [80, 24] unless io
412
+
413
+ rows, cols = io.winsize
414
+ [cols, rows]
415
+ end
416
+
417
+ # Minimal adapter satisfying websocket-driver's socket interface.
418
+ #
419
+ # The driver calls +#url+ to build the HTTP upgrade request and +#write+
420
+ # to send framed data over the wire.
421
+ #
422
+ # @api private
423
+ #
424
+ class SocketAdapter
425
+ # @return [String] the WebSocket URL
426
+ attr_reader :url
427
+
428
+ # Creates a new socket adapter.
429
+ #
430
+ # @param url [String] WebSocket URL
431
+ # @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] underlying socket
432
+ #
433
+ def initialize(url, socket)
434
+ @url = url
435
+ @socket = socket
436
+ end
437
+
438
+ # Writes data to the underlying socket.
439
+ #
440
+ # @param data [String] raw bytes to send
441
+ # @return [Integer] number of bytes written
442
+ #
443
+ def write(data)
444
+ @socket.write(data)
445
+ end
446
+ end
447
+ end
448
+ end
449
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tempfile"
4
+
5
+ module Pvectl
6
+ # Manages the editor lifecycle for interactive config editing.
7
+ #
8
+ # Creates a temporary file with content, opens it in an editor,
9
+ # reads the result, and supports a retry loop with error injection
10
+ # when validation fails.
11
+ #
12
+ # @example Production usage with system editor
13
+ # session = EditorSession.new
14
+ # result = session.edit("cpu:\n cores: 4\n")
15
+ #
16
+ # @example Testing with injected editor
17
+ # editor = ->(path) { File.write(path, "modified") }
18
+ # session = EditorSession.new(editor: editor)
19
+ # result = session.edit("original")
20
+ #
21
+ # @example With validator
22
+ # validator = ->(content) { content.include?("bad") ? ["Error: bad value"] : [] }
23
+ # session = EditorSession.new(editor: editor, validator: validator)
24
+ # result = session.edit("original")
25
+ #
26
+ class EditorSession
27
+ ERROR_SEPARATOR = "# -----------------------------------------------"
28
+
29
+ # Creates a new EditorSession.
30
+ #
31
+ # @param editor [#call, nil] callable that receives a file path and opens it for editing.
32
+ # Defaults to {#system_editor} which uses $EDITOR/$VISUAL/vi.
33
+ # @param validator [#call, nil] callable that receives edited content and returns
34
+ # an array of error strings. Empty array means valid.
35
+ def initialize(editor: nil, validator: nil)
36
+ @editor = editor || method(:system_editor)
37
+ @validator = validator
38
+ end
39
+
40
+ # Opens content in an editor and returns the edited result.
41
+ #
42
+ # Creates a temp file, invokes the editor, and reads back the content.
43
+ # Supports validation with retry loop and error injection.
44
+ #
45
+ # @param original_content [String] the initial content to edit
46
+ # @return [String, nil] edited content, or nil if cancelled
47
+ # @raise [RuntimeError] if no editor is found (system editor mode)
48
+ def edit(original_content)
49
+ tempfile = Tempfile.new(["pvectl-edit-", ".yaml"])
50
+ tempfile.write(original_content)
51
+ tempfile.flush
52
+ path = tempfile.path
53
+
54
+ edit_loop(path, original_content)
55
+ ensure
56
+ tempfile&.close
57
+ tempfile&.unlink
58
+ end
59
+
60
+ private
61
+
62
+ # Runs the edit-validate-retry loop.
63
+ #
64
+ # @param path [String] path to the temporary file
65
+ # @param original_content [String] the original content for cancellation detection
66
+ # @return [String, nil] edited content, or nil if cancelled
67
+ def edit_loop(path, original_content)
68
+ loop do
69
+ @editor.call(path)
70
+ content = File.read(path)
71
+
72
+ return nil if cancelled?(content, original_content)
73
+ return content unless @validator
74
+
75
+ errors = @validator.call(content)
76
+ return content if errors.empty?
77
+
78
+ inject_errors(path, content, errors)
79
+ end
80
+ end
81
+
82
+ # Detects whether the user cancelled editing.
83
+ #
84
+ # @param content [String] current file content
85
+ # @param original_content [String] the original content before editing
86
+ # @return [Boolean] true if editing was cancelled
87
+ def cancelled?(content, original_content)
88
+ content.empty? || content == original_content
89
+ end
90
+
91
+ # Injects error comments at the top of the file for the retry loop.
92
+ # Strips any previous error block before injecting new ones.
93
+ #
94
+ # @param path [String] path to the temporary file
95
+ # @param content [String] current file content (may contain previous error block)
96
+ # @param errors [Array<String>] error messages to inject
97
+ def inject_errors(path, content, errors)
98
+ clean_content = strip_error_block(content)
99
+ error_block = build_error_block(errors)
100
+ File.write(path, "#{error_block}#{clean_content}")
101
+ end
102
+
103
+ # Strips a previously injected error block from content.
104
+ #
105
+ # @param content [String] content potentially containing an error block
106
+ # @return [String] content without the error block
107
+ def strip_error_block(content)
108
+ lines = content.lines
109
+ return content unless lines.first&.strip == ERROR_SEPARATOR
110
+
111
+ # Find the end of the error block (second separator line)
112
+ separator_count = 0
113
+ end_index = lines.index do |line|
114
+ separator_count += 1 if line.strip == ERROR_SEPARATOR
115
+ separator_count >= 2
116
+ end
117
+
118
+ return content unless end_index
119
+
120
+ lines[(end_index + 1)..].join
121
+ end
122
+
123
+ # Builds an error block string from error messages.
124
+ #
125
+ # @param errors [Array<String>] error messages
126
+ # @return [String] formatted error block with separators
127
+ def build_error_block(errors)
128
+ lines = [ERROR_SEPARATOR]
129
+ errors.each { |error| lines << "# ERROR: #{error}" }
130
+ lines << "# Please fix the error above or save empty file to cancel."
131
+ lines << ERROR_SEPARATOR
132
+ "#{lines.join("\n")}\n"
133
+ end
134
+
135
+ # Opens a file in the system editor.
136
+ #
137
+ # @param path [String] path to the file to edit
138
+ # @raise [RuntimeError] if no editor is configured
139
+ def system_editor(path)
140
+ editor = ENV["EDITOR"] || ENV["VISUAL"] || default_editor
141
+ raise "No editor found. Set $EDITOR or $VISUAL environment variable." unless editor
142
+
143
+ system(editor, path)
144
+ end
145
+
146
+ # Returns the default editor fallback.
147
+ # Tries vi first, then nano. Returns nil if neither is found.
148
+ #
149
+ # @return [String, nil] the default editor command
150
+ def default_editor
151
+ %w[vi nano].each do |cmd|
152
+ return cmd if system("which", cmd, out: File::NULL, err: File::NULL)
153
+ end
154
+ nil
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pvectl
4
+ # Application exit codes following UNIX conventions.
5
+ #
6
+ # This module defines standard exit codes used by pvectl to communicate
7
+ # process termination status. Codes are aligned with UNIX conventions and
8
+ # section 6.1 of ARCHITECTURE.md.
9
+ #
10
+ # @example Usage in error handling
11
+ # exit Pvectl::ExitCodes::USAGE_ERROR if invalid_arguments?
12
+ #
13
+ # @example Checking exit code in bash scripts
14
+ # pvectl get nodes || echo "Error: code $?"
15
+ #
16
+ # @see https://man.openbsd.org/sysexits BSD exit code conventions
17
+ #
18
+ module ExitCodes
19
+ # @return [Integer] Operation completed successfully
20
+ SUCCESS = 0
21
+
22
+ # @return [Integer] General application error (unhandled exception)
23
+ GENERAL_ERROR = 1
24
+
25
+ # @return [Integer] CLI usage error (invalid arguments, unknown command)
26
+ USAGE_ERROR = 2
27
+
28
+ # @return [Integer] Configuration error (missing file, invalid format)
29
+ CONFIG_ERROR = 3
30
+
31
+ # @return [Integer] Proxmox API connection error (timeout, no network)
32
+ CONNECTION_ERROR = 4
33
+
34
+ # @return [Integer] Requested resource not found
35
+ NOT_FOUND = 5
36
+
37
+ # @return [Integer] Permission denied for resource or operation
38
+ PERMISSION_DENIED = 6
39
+
40
+ # @return [Integer] Interrupted by user (Ctrl+C / SIGINT)
41
+ INTERRUPTED = 130
42
+ end
43
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pvectl
4
+ module Formatters
5
+ # Abstract base class for output formatters.
6
+ #
7
+ # Formatters implement the Strategy Pattern, converting model data
8
+ # to various output formats (table, json, yaml, wide).
9
+ #
10
+ # @abstract Subclass and implement {#format} to create a formatter.
11
+ #
12
+ # @example Implementing a custom formatter
13
+ # class MyFormatter < Base
14
+ # def format(data, presenter, color_enabled: true, **context)
15
+ # # Return formatted string
16
+ # end
17
+ # end
18
+ #
19
+ # @see Pvectl::Formatters::Registry for looking up formatters by name
20
+ # @see Pvectl::Presenters::Base for presenter interface
21
+ #
22
+ class Base
23
+ # Formats data for output.
24
+ #
25
+ # @param data [Array, Object] collection of models or single model
26
+ # @param presenter [Presenters::Base] presenter for column/row definitions
27
+ # @param color_enabled [Boolean] whether to apply color formatting
28
+ # @param context [Hash] additional context (e.g., current_context for contexts)
29
+ # @return [String] formatted output string
30
+ # @raise [NotImplementedError] if not implemented by subclass
31
+ def format(data, presenter, color_enabled: true, **context)
32
+ raise NotImplementedError, "#{self.class}#format must be implemented"
33
+ end
34
+
35
+ protected
36
+
37
+ # Determines if data is a collection or single resource.
38
+ #
39
+ # @param data [Array, Object] data to check
40
+ # @return [Boolean] true if data is a collection (Array)
41
+ def collection?(data)
42
+ data.is_a?(Array)
43
+ end
44
+
45
+ # Normalizes nil values for display.
46
+ #
47
+ # @param value [Object, nil] value to normalize
48
+ # @param nil_placeholder [String] placeholder for nil values (default: "-")
49
+ # @return [Object, String] original value or placeholder
50
+ def normalize_nil(value, nil_placeholder = "-")
51
+ value.nil? ? nil_placeholder : value
52
+ end
53
+ end
54
+ end
55
+ end