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.
- checksums.yaml +7 -0
- data/.claude/rules/branch-before-changes.md +52 -0
- data/.claude/rules/documentation-updates.md +104 -0
- data/.claude/rules/git-workflow.md +84 -0
- data/.claude/rules/proxmox-api-docs.md +58 -0
- data/.claude/rules/rbs-signatures.md +80 -0
- data/.claude/rules/refactoring-as-design-option.md +35 -0
- data/.claude/scheduled_tasks.lock +1 -0
- data/.claude/settings.json +51 -0
- data/.mcp.json +8 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +138 -0
- data/CLAUDE.md +211 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +143 -0
- data/Rakefile +8 -0
- data/docs/proxmox-api-update.sh +96 -0
- data/exe/pvectl +5 -0
- data/lib/pvectl/argv_preprocessor.rb +334 -0
- data/lib/pvectl/cli.rb +102 -0
- data/lib/pvectl/commands/apt.rb +389 -0
- data/lib/pvectl/commands/clone_container.rb +230 -0
- data/lib/pvectl/commands/clone_vm.rb +331 -0
- data/lib/pvectl/commands/cloudinit/command.rb +122 -0
- data/lib/pvectl/commands/cloudinit/dump.rb +94 -0
- data/lib/pvectl/commands/cloudinit/pending.rb +137 -0
- data/lib/pvectl/commands/cloudinit/regenerate.rb +79 -0
- data/lib/pvectl/commands/config/command.rb +65 -0
- data/lib/pvectl/commands/config/get_contexts.rb +68 -0
- data/lib/pvectl/commands/config/set_cluster.rb +103 -0
- data/lib/pvectl/commands/config/set_context.rb +136 -0
- data/lib/pvectl/commands/config/set_credentials.rb +181 -0
- data/lib/pvectl/commands/config/use_context.rb +69 -0
- data/lib/pvectl/commands/config/view.rb +67 -0
- data/lib/pvectl/commands/console.rb +93 -0
- data/lib/pvectl/commands/console_ct.rb +187 -0
- data/lib/pvectl/commands/console_vm.rb +187 -0
- data/lib/pvectl/commands/container_lifecycle_command.rb +77 -0
- data/lib/pvectl/commands/create_backup.rb +173 -0
- data/lib/pvectl/commands/create_container.rb +141 -0
- data/lib/pvectl/commands/create_resource_command.rb +244 -0
- data/lib/pvectl/commands/create_snapshot.rb +242 -0
- data/lib/pvectl/commands/create_vm.rb +267 -0
- data/lib/pvectl/commands/delete_backup.rb +139 -0
- data/lib/pvectl/commands/delete_command.rb +119 -0
- data/lib/pvectl/commands/delete_container.rb +30 -0
- data/lib/pvectl/commands/delete_snapshot.rb +248 -0
- data/lib/pvectl/commands/delete_vm.rb +127 -0
- data/lib/pvectl/commands/describe/command.rb +251 -0
- data/lib/pvectl/commands/edit_container.rb +56 -0
- data/lib/pvectl/commands/edit_dns.rb +149 -0
- data/lib/pvectl/commands/edit_hosts.rb +135 -0
- data/lib/pvectl/commands/edit_node.rb +54 -0
- data/lib/pvectl/commands/edit_resource_command.rb +180 -0
- data/lib/pvectl/commands/edit_vm.rb +154 -0
- data/lib/pvectl/commands/edit_volume.rb +189 -0
- data/lib/pvectl/commands/feature_command.rb +230 -0
- data/lib/pvectl/commands/feature_container.rb +21 -0
- data/lib/pvectl/commands/feature_vm.rb +94 -0
- data/lib/pvectl/commands/get/command.rb +360 -0
- data/lib/pvectl/commands/get/handlers/backups.rb +76 -0
- data/lib/pvectl/commands/get/handlers/capabilities.rb +107 -0
- data/lib/pvectl/commands/get/handlers/containers.rb +148 -0
- data/lib/pvectl/commands/get/handlers/disks.rb +107 -0
- data/lib/pvectl/commands/get/handlers/dns.rb +94 -0
- data/lib/pvectl/commands/get/handlers/hosts.rb +94 -0
- data/lib/pvectl/commands/get/handlers/nodes.rb +162 -0
- data/lib/pvectl/commands/get/handlers/services.rb +81 -0
- data/lib/pvectl/commands/get/handlers/snapshots.rb +97 -0
- data/lib/pvectl/commands/get/handlers/storage.rb +118 -0
- data/lib/pvectl/commands/get/handlers/subscription.rb +69 -0
- data/lib/pvectl/commands/get/handlers/tasks.rb +89 -0
- data/lib/pvectl/commands/get/handlers/templates.rb +175 -0
- data/lib/pvectl/commands/get/handlers/time.rb +118 -0
- data/lib/pvectl/commands/get/handlers/vms.rb +145 -0
- data/lib/pvectl/commands/get/handlers/volume.rb +134 -0
- data/lib/pvectl/commands/get/resource_handler.rb +63 -0
- data/lib/pvectl/commands/get/resource_registry.rb +18 -0
- data/lib/pvectl/commands/get/watch_loop.rb +129 -0
- data/lib/pvectl/commands/irreversible_command.rb +265 -0
- data/lib/pvectl/commands/logs/command.rb +275 -0
- data/lib/pvectl/commands/logs/handlers/journal.rb +46 -0
- data/lib/pvectl/commands/logs/handlers/syslog.rb +53 -0
- data/lib/pvectl/commands/logs/handlers/task_detail.rb +52 -0
- data/lib/pvectl/commands/logs/handlers/task_logs.rb +115 -0
- data/lib/pvectl/commands/logs/resource_handler.rb +46 -0
- data/lib/pvectl/commands/logs/resource_registry.rb +22 -0
- data/lib/pvectl/commands/migrate_command.rb +282 -0
- data/lib/pvectl/commands/migrate_container.rb +23 -0
- data/lib/pvectl/commands/migrate_vm.rb +122 -0
- data/lib/pvectl/commands/move_disk_command.rb +239 -0
- data/lib/pvectl/commands/move_disk_container.rb +21 -0
- data/lib/pvectl/commands/move_disk_vm.rb +127 -0
- data/lib/pvectl/commands/ping.rb +249 -0
- data/lib/pvectl/commands/pull.rb +342 -0
- data/lib/pvectl/commands/push.rb +352 -0
- data/lib/pvectl/commands/reset.rb +64 -0
- data/lib/pvectl/commands/resource_lifecycle_command.rb +277 -0
- data/lib/pvectl/commands/resource_registry.rb +73 -0
- data/lib/pvectl/commands/restart.rb +70 -0
- data/lib/pvectl/commands/restart_container.rb +18 -0
- data/lib/pvectl/commands/restore_backup.rb +236 -0
- data/lib/pvectl/commands/resume.rb +57 -0
- data/lib/pvectl/commands/rollback_snapshot.rb +228 -0
- data/lib/pvectl/commands/sendkey_vm.rb +205 -0
- data/lib/pvectl/commands/service.rb +293 -0
- data/lib/pvectl/commands/set_container.rb +50 -0
- data/lib/pvectl/commands/set_node.rb +52 -0
- data/lib/pvectl/commands/set_resource_command.rb +185 -0
- data/lib/pvectl/commands/set_vm.rb +136 -0
- data/lib/pvectl/commands/set_volume.rb +212 -0
- data/lib/pvectl/commands/shared_config_parsers.rb +126 -0
- data/lib/pvectl/commands/shared_flags.rb +155 -0
- data/lib/pvectl/commands/shutdown.rb +73 -0
- data/lib/pvectl/commands/shutdown_container.rb +18 -0
- data/lib/pvectl/commands/start.rb +79 -0
- data/lib/pvectl/commands/start_container.rb +18 -0
- data/lib/pvectl/commands/stop.rb +75 -0
- data/lib/pvectl/commands/stop_container.rb +18 -0
- data/lib/pvectl/commands/suspend.rb +64 -0
- data/lib/pvectl/commands/template_command.rb +205 -0
- data/lib/pvectl/commands/template_container.rb +27 -0
- data/lib/pvectl/commands/template_vm.rb +106 -0
- data/lib/pvectl/commands/top/command.rb +206 -0
- data/lib/pvectl/commands/top/handlers/containers.rb +61 -0
- data/lib/pvectl/commands/top/handlers/nodes.rb +61 -0
- data/lib/pvectl/commands/top/handlers/vms.rb +61 -0
- data/lib/pvectl/commands/top/resource_handler.rb +46 -0
- data/lib/pvectl/commands/top/resource_registry.rb +22 -0
- data/lib/pvectl/commands/unlink_disk_vm.rb +232 -0
- data/lib/pvectl/commands/vm_lifecycle_command.rb +77 -0
- data/lib/pvectl/commands/wakeonlan_node.rb +153 -0
- data/lib/pvectl/config/errors.rb +62 -0
- data/lib/pvectl/config/models/cluster.rb +180 -0
- data/lib/pvectl/config/models/context.rb +100 -0
- data/lib/pvectl/config/models/resolved_config.rb +171 -0
- data/lib/pvectl/config/models/user.rb +133 -0
- data/lib/pvectl/config/provider.rb +297 -0
- data/lib/pvectl/config/service.rb +300 -0
- data/lib/pvectl/config/store.rb +161 -0
- data/lib/pvectl/config/wizard.rb +309 -0
- data/lib/pvectl/config_serializer.rb +1034 -0
- data/lib/pvectl/connection/retry_handler.rb +161 -0
- data/lib/pvectl/connection.rb +157 -0
- data/lib/pvectl/console/terminal_session.rb +449 -0
- data/lib/pvectl/editor_session.rb +157 -0
- data/lib/pvectl/exit_codes.rb +43 -0
- data/lib/pvectl/formatters/base.rb +55 -0
- data/lib/pvectl/formatters/color_support.rb +90 -0
- data/lib/pvectl/formatters/json.rb +45 -0
- data/lib/pvectl/formatters/output_helper.rb +77 -0
- data/lib/pvectl/formatters/registry.rb +72 -0
- data/lib/pvectl/formatters/table.rb +235 -0
- data/lib/pvectl/formatters/wide.rb +93 -0
- data/lib/pvectl/formatters/yaml.rb +49 -0
- data/lib/pvectl/manifest_serializer.rb +142 -0
- data/lib/pvectl/models/apt_package.rb +107 -0
- data/lib/pvectl/models/backup.rb +173 -0
- data/lib/pvectl/models/base.rb +49 -0
- data/lib/pvectl/models/capability.rb +62 -0
- data/lib/pvectl/models/container.rb +205 -0
- data/lib/pvectl/models/container_operation_result.rb +27 -0
- data/lib/pvectl/models/dns_config.rb +54 -0
- data/lib/pvectl/models/hosts_file.rb +47 -0
- data/lib/pvectl/models/journal_entry.rb +16 -0
- data/lib/pvectl/models/network_interface.rb +85 -0
- data/lib/pvectl/models/node.rb +195 -0
- data/lib/pvectl/models/node_operation_result.rb +45 -0
- data/lib/pvectl/models/operation_result.rb +110 -0
- data/lib/pvectl/models/physical_disk.rb +193 -0
- data/lib/pvectl/models/service.rb +80 -0
- data/lib/pvectl/models/snapshot.rb +101 -0
- data/lib/pvectl/models/snapshot_description.rb +39 -0
- data/lib/pvectl/models/storage.rb +180 -0
- data/lib/pvectl/models/subscription.rb +87 -0
- data/lib/pvectl/models/syslog_entry.rb +17 -0
- data/lib/pvectl/models/task.rb +95 -0
- data/lib/pvectl/models/task_entry.rb +52 -0
- data/lib/pvectl/models/task_log_line.rb +17 -0
- data/lib/pvectl/models/time_config.rb +47 -0
- data/lib/pvectl/models/vm.rb +137 -0
- data/lib/pvectl/models/vm_operation_result.rb +27 -0
- data/lib/pvectl/models/volume.rb +133 -0
- data/lib/pvectl/models/volume_operation_result.rb +26 -0
- data/lib/pvectl/parsers/cloud_init_config.rb +92 -0
- data/lib/pvectl/parsers/disk_config.rb +97 -0
- data/lib/pvectl/parsers/lxc_mount_config.rb +98 -0
- data/lib/pvectl/parsers/lxc_net_config.rb +97 -0
- data/lib/pvectl/parsers/net_config.rb +95 -0
- data/lib/pvectl/parsers/smart_text.rb +42 -0
- data/lib/pvectl/plugin_loader.rb +157 -0
- data/lib/pvectl/presenters/apt_package.rb +99 -0
- data/lib/pvectl/presenters/backup.rb +128 -0
- data/lib/pvectl/presenters/base.rb +283 -0
- data/lib/pvectl/presenters/capability.rb +104 -0
- data/lib/pvectl/presenters/config/context.rb +80 -0
- data/lib/pvectl/presenters/container.rb +574 -0
- data/lib/pvectl/presenters/container_operation_result.rb +109 -0
- data/lib/pvectl/presenters/disk.rb +184 -0
- data/lib/pvectl/presenters/dns_config.rb +68 -0
- data/lib/pvectl/presenters/hosts_file.rb +61 -0
- data/lib/pvectl/presenters/journal_entry.rb +20 -0
- data/lib/pvectl/presenters/node.rb +762 -0
- data/lib/pvectl/presenters/node_operation_result.rb +50 -0
- data/lib/pvectl/presenters/operation_result.rb +61 -0
- data/lib/pvectl/presenters/service.rb +76 -0
- data/lib/pvectl/presenters/snapshot.rb +239 -0
- data/lib/pvectl/presenters/snapshot_operation_result.rb +125 -0
- data/lib/pvectl/presenters/storage.rb +329 -0
- data/lib/pvectl/presenters/subscription.rb +189 -0
- data/lib/pvectl/presenters/syslog_entry.rb +20 -0
- data/lib/pvectl/presenters/task_entry.rb +69 -0
- data/lib/pvectl/presenters/task_log_line.rb +20 -0
- data/lib/pvectl/presenters/template.rb +76 -0
- data/lib/pvectl/presenters/time_config.rb +86 -0
- data/lib/pvectl/presenters/top_container.rb +112 -0
- data/lib/pvectl/presenters/top_node.rb +115 -0
- data/lib/pvectl/presenters/top_presenter.rb +59 -0
- data/lib/pvectl/presenters/top_vm.rb +105 -0
- data/lib/pvectl/presenters/vm.rb +853 -0
- data/lib/pvectl/presenters/vm_operation_result.rb +109 -0
- data/lib/pvectl/presenters/volume.rb +136 -0
- data/lib/pvectl/presenters/volume_operation_result.rb +58 -0
- data/lib/pvectl/repositories/apt.rb +93 -0
- data/lib/pvectl/repositories/backup.rb +186 -0
- data/lib/pvectl/repositories/base.rb +110 -0
- data/lib/pvectl/repositories/capabilities.rb +96 -0
- data/lib/pvectl/repositories/container.rb +503 -0
- data/lib/pvectl/repositories/disk.rb +87 -0
- data/lib/pvectl/repositories/dns.rb +54 -0
- data/lib/pvectl/repositories/hosts.rb +63 -0
- data/lib/pvectl/repositories/journal.rb +23 -0
- data/lib/pvectl/repositories/node.rb +537 -0
- data/lib/pvectl/repositories/service.rb +139 -0
- data/lib/pvectl/repositories/snapshot.rb +133 -0
- data/lib/pvectl/repositories/storage.rb +302 -0
- data/lib/pvectl/repositories/subscription.rb +77 -0
- data/lib/pvectl/repositories/syslog.rb +25 -0
- data/lib/pvectl/repositories/task.rb +82 -0
- data/lib/pvectl/repositories/task_list.rb +30 -0
- data/lib/pvectl/repositories/task_log.rb +31 -0
- data/lib/pvectl/repositories/time_config.rb +53 -0
- data/lib/pvectl/repositories/vm.rb +616 -0
- data/lib/pvectl/repositories/volume.rb +306 -0
- data/lib/pvectl/selectors/base.rb +201 -0
- data/lib/pvectl/selectors/container.rb +116 -0
- data/lib/pvectl/selectors/disk.rb +59 -0
- data/lib/pvectl/selectors/vm.rb +116 -0
- data/lib/pvectl/selectors/volume.rb +59 -0
- data/lib/pvectl/services/backup.rb +209 -0
- data/lib/pvectl/services/clone_container.rb +260 -0
- data/lib/pvectl/services/clone_vm.rb +265 -0
- data/lib/pvectl/services/cloudinit.rb +96 -0
- data/lib/pvectl/services/console.rb +152 -0
- data/lib/pvectl/services/container_lifecycle.rb +124 -0
- data/lib/pvectl/services/create_container.rb +179 -0
- data/lib/pvectl/services/create_vm.rb +191 -0
- data/lib/pvectl/services/edit_container.rb +125 -0
- data/lib/pvectl/services/edit_dns.rb +159 -0
- data/lib/pvectl/services/edit_hosts.rb +78 -0
- data/lib/pvectl/services/edit_node.rb +147 -0
- data/lib/pvectl/services/edit_vm.rb +125 -0
- data/lib/pvectl/services/edit_volume.rb +224 -0
- data/lib/pvectl/services/get/resource_service.rb +98 -0
- data/lib/pvectl/services/move_disk.rb +132 -0
- data/lib/pvectl/services/pull_config.rb +94 -0
- data/lib/pvectl/services/push_config.rb +524 -0
- data/lib/pvectl/services/resize_volume.rb +253 -0
- data/lib/pvectl/services/resource_delete.rb +169 -0
- data/lib/pvectl/services/resource_migration.rb +170 -0
- data/lib/pvectl/services/sendkey.rb +108 -0
- data/lib/pvectl/services/service_lifecycle.rb +89 -0
- data/lib/pvectl/services/set_container.rb +128 -0
- data/lib/pvectl/services/set_node.rb +236 -0
- data/lib/pvectl/services/set_vm.rb +128 -0
- data/lib/pvectl/services/set_volume.rb +126 -0
- data/lib/pvectl/services/snapshot.rb +261 -0
- data/lib/pvectl/services/task_listing.rb +75 -0
- data/lib/pvectl/services/unlink_disk.rb +86 -0
- data/lib/pvectl/services/vm_lifecycle.rb +124 -0
- data/lib/pvectl/services/wakeonlan.rb +79 -0
- data/lib/pvectl/utils/resource_resolver.rb +80 -0
- data/lib/pvectl/version.rb +13 -0
- data/lib/pvectl/wizards/create_container.rb +105 -0
- data/lib/pvectl/wizards/create_vm.rb +98 -0
- data/lib/pvectl.rb +439 -0
- data/sig/external/gli.rbs +16 -0
- data/sig/external/proxmox_api.rbs +10 -0
- data/sig/pvectl/argv_preprocessor.rbs +53 -0
- data/sig/pvectl/cli.rbs +26 -0
- data/sig/pvectl/commands/apt.rbs +47 -0
- data/sig/pvectl/commands/clone_container.rbs +31 -0
- data/sig/pvectl/commands/clone_vm.rbs +33 -0
- data/sig/pvectl/commands/cloudinit/command.rbs +13 -0
- data/sig/pvectl/commands/cloudinit/dump.rbs +13 -0
- data/sig/pvectl/commands/cloudinit/pending.rbs +17 -0
- data/sig/pvectl/commands/cloudinit/regenerate.rbs +11 -0
- data/sig/pvectl/commands/config/command.rbs +9 -0
- data/sig/pvectl/commands/config/get_contexts.rbs +11 -0
- data/sig/pvectl/commands/config/set_cluster.rbs +11 -0
- data/sig/pvectl/commands/config/set_context.rbs +15 -0
- data/sig/pvectl/commands/config/set_credentials.rbs +15 -0
- data/sig/pvectl/commands/config/use_context.rbs +11 -0
- data/sig/pvectl/commands/config/view.rbs +11 -0
- data/sig/pvectl/commands/console.rbs +9 -0
- data/sig/pvectl/commands/console_ct.rbs +27 -0
- data/sig/pvectl/commands/console_vm.rbs +27 -0
- data/sig/pvectl/commands/container_lifecycle_command.rbs +25 -0
- data/sig/pvectl/commands/create_backup.rbs +29 -0
- data/sig/pvectl/commands/create_container.rbs +30 -0
- data/sig/pvectl/commands/create_resource_command.rbs +53 -0
- data/sig/pvectl/commands/create_snapshot.rbs +35 -0
- data/sig/pvectl/commands/create_vm.rbs +30 -0
- data/sig/pvectl/commands/delete_backup.rbs +25 -0
- data/sig/pvectl/commands/delete_command.rbs +45 -0
- data/sig/pvectl/commands/delete_container.rbs +11 -0
- data/sig/pvectl/commands/delete_snapshot.rbs +35 -0
- data/sig/pvectl/commands/delete_vm.rbs +13 -0
- data/sig/pvectl/commands/describe/command.rbs +27 -0
- data/sig/pvectl/commands/edit_container.rbs +17 -0
- data/sig/pvectl/commands/edit_dns.rbs +25 -0
- data/sig/pvectl/commands/edit_hosts.rbs +23 -0
- data/sig/pvectl/commands/edit_node.rbs +17 -0
- data/sig/pvectl/commands/edit_resource_command.rbs +35 -0
- data/sig/pvectl/commands/edit_vm.rbs +19 -0
- data/sig/pvectl/commands/edit_volume.rbs +24 -0
- data/sig/pvectl/commands/feature_command.rbs +43 -0
- data/sig/pvectl/commands/feature_container.rbs +10 -0
- data/sig/pvectl/commands/feature_vm.rbs +12 -0
- data/sig/pvectl/commands/get/command.rbs +42 -0
- data/sig/pvectl/commands/get/handlers/backups.rbs +23 -0
- data/sig/pvectl/commands/get/handlers/capabilities.rbs +29 -0
- data/sig/pvectl/commands/get/handlers/containers.rbs +35 -0
- data/sig/pvectl/commands/get/handlers/disks.rbs +27 -0
- data/sig/pvectl/commands/get/handlers/dns.rbs +25 -0
- data/sig/pvectl/commands/get/handlers/hosts.rbs +25 -0
- data/sig/pvectl/commands/get/handlers/nodes.rbs +33 -0
- data/sig/pvectl/commands/get/handlers/services.rbs +23 -0
- data/sig/pvectl/commands/get/handlers/snapshots.rbs +27 -0
- data/sig/pvectl/commands/get/handlers/storage.rbs +25 -0
- data/sig/pvectl/commands/get/handlers/subscription.rbs +25 -0
- data/sig/pvectl/commands/get/handlers/tasks.rbs +28 -0
- data/sig/pvectl/commands/get/handlers/templates.rbs +35 -0
- data/sig/pvectl/commands/get/handlers/time.rbs +29 -0
- data/sig/pvectl/commands/get/handlers/vms.rbs +35 -0
- data/sig/pvectl/commands/get/handlers/volume.rbs +27 -0
- data/sig/pvectl/commands/get/resource_handler.rbs +13 -0
- data/sig/pvectl/commands/get/resource_registry.rbs +8 -0
- data/sig/pvectl/commands/get/watch_loop.rbs +33 -0
- data/sig/pvectl/commands/irreversible_command.rbs +32 -0
- data/sig/pvectl/commands/logs/command.rbs +35 -0
- data/sig/pvectl/commands/logs/handlers/journal.rbs +21 -0
- data/sig/pvectl/commands/logs/handlers/syslog.rbs +21 -0
- data/sig/pvectl/commands/logs/handlers/task_detail.rbs +21 -0
- data/sig/pvectl/commands/logs/handlers/task_logs.rbs +35 -0
- data/sig/pvectl/commands/logs/resource_handler.rbs +11 -0
- data/sig/pvectl/commands/logs/resource_registry.rbs +8 -0
- data/sig/pvectl/commands/migrate_command.rbs +45 -0
- data/sig/pvectl/commands/migrate_container.rbs +11 -0
- data/sig/pvectl/commands/migrate_vm.rbs +13 -0
- data/sig/pvectl/commands/move_disk_command.rbs +43 -0
- data/sig/pvectl/commands/move_disk_container.rbs +11 -0
- data/sig/pvectl/commands/move_disk_vm.rbs +13 -0
- data/sig/pvectl/commands/ping.rbs +39 -0
- data/sig/pvectl/commands/pull.rbs +33 -0
- data/sig/pvectl/commands/push.rbs +32 -0
- data/sig/pvectl/commands/reset.rbs +11 -0
- data/sig/pvectl/commands/resource_lifecycle_command.rbs +55 -0
- data/sig/pvectl/commands/resource_registry.rbs +19 -0
- data/sig/pvectl/commands/restart.rbs +11 -0
- data/sig/pvectl/commands/restart_container.rbs +9 -0
- data/sig/pvectl/commands/restore_backup.rbs +27 -0
- data/sig/pvectl/commands/resume.rbs +11 -0
- data/sig/pvectl/commands/rollback_snapshot.rbs +31 -0
- data/sig/pvectl/commands/sendkey_vm.rbs +25 -0
- data/sig/pvectl/commands/service.rbs +38 -0
- data/sig/pvectl/commands/set_container.rbs +13 -0
- data/sig/pvectl/commands/set_node.rbs +13 -0
- data/sig/pvectl/commands/set_resource_command.rbs +25 -0
- data/sig/pvectl/commands/set_vm.rbs +15 -0
- data/sig/pvectl/commands/set_volume.rbs +24 -0
- data/sig/pvectl/commands/shared_config_parsers.rbs +19 -0
- data/sig/pvectl/commands/shared_flags.rbs +10 -0
- data/sig/pvectl/commands/shutdown.rbs +11 -0
- data/sig/pvectl/commands/shutdown_container.rbs +9 -0
- data/sig/pvectl/commands/start.rbs +11 -0
- data/sig/pvectl/commands/start_container.rbs +9 -0
- data/sig/pvectl/commands/stop.rbs +11 -0
- data/sig/pvectl/commands/stop_container.rbs +9 -0
- data/sig/pvectl/commands/suspend.rbs +11 -0
- data/sig/pvectl/commands/template_command.rbs +21 -0
- data/sig/pvectl/commands/template_container.rbs +10 -0
- data/sig/pvectl/commands/template_vm.rbs +12 -0
- data/sig/pvectl/commands/top/command.rbs +31 -0
- data/sig/pvectl/commands/top/handlers/containers.rbs +21 -0
- data/sig/pvectl/commands/top/handlers/nodes.rbs +21 -0
- data/sig/pvectl/commands/top/handlers/vms.rbs +21 -0
- data/sig/pvectl/commands/top/resource_handler.rbs +11 -0
- data/sig/pvectl/commands/top/resource_registry.rbs +8 -0
- data/sig/pvectl/commands/unlink_disk_vm.rbs +27 -0
- data/sig/pvectl/commands/vm_lifecycle_command.rbs +25 -0
- data/sig/pvectl/commands/wakeonlan_node.rbs +21 -0
- data/sig/pvectl/config/errors.rbs +24 -0
- data/sig/pvectl/config/models/cluster.rbs +39 -0
- data/sig/pvectl/config/models/context.rbs +23 -0
- data/sig/pvectl/config/models/resolved_config.rbs +51 -0
- data/sig/pvectl/config/models/user.rbs +31 -0
- data/sig/pvectl/config/provider.rbs +40 -0
- data/sig/pvectl/config/service.rbs +65 -0
- data/sig/pvectl/config/store.rbs +14 -0
- data/sig/pvectl/config/wizard.rbs +48 -0
- data/sig/pvectl/config_serializer.rbs +121 -0
- data/sig/pvectl/connection/retry_handler.rbs +31 -0
- data/sig/pvectl/connection.rbs +35 -0
- data/sig/pvectl/console/terminal_session.rbs +63 -0
- data/sig/pvectl/editor_session.rbs +33 -0
- data/sig/pvectl/exit_codes.rbs +19 -0
- data/sig/pvectl/formatters/base.rbs +13 -0
- data/sig/pvectl/formatters/color_support.rbs +13 -0
- data/sig/pvectl/formatters/json.rbs +7 -0
- data/sig/pvectl/formatters/output_helper.rbs +9 -0
- data/sig/pvectl/formatters/registry.rbs +13 -0
- data/sig/pvectl/formatters/table.rbs +25 -0
- data/sig/pvectl/formatters/wide.rbs +15 -0
- data/sig/pvectl/formatters/yaml.rbs +7 -0
- data/sig/pvectl/manifest_serializer.rbs +18 -0
- data/sig/pvectl/models/apt_package.rbs +26 -0
- data/sig/pvectl/models/backup.rbs +31 -0
- data/sig/pvectl/models/base.rbs +11 -0
- data/sig/pvectl/models/capability.rbs +16 -0
- data/sig/pvectl/models/container.rbs +44 -0
- data/sig/pvectl/models/container_operation_result.rbs +9 -0
- data/sig/pvectl/models/dns_config.rbs +15 -0
- data/sig/pvectl/models/hosts_file.rbs +13 -0
- data/sig/pvectl/models/journal_entry.rbs +10 -0
- data/sig/pvectl/models/network_interface.rbs +20 -0
- data/sig/pvectl/models/node.rbs +47 -0
- data/sig/pvectl/models/node_operation_result.rbs +12 -0
- data/sig/pvectl/models/operation_result.rbs +21 -0
- data/sig/pvectl/models/physical_disk.rbs +35 -0
- data/sig/pvectl/models/service.rbs +18 -0
- data/sig/pvectl/models/snapshot.rbs +21 -0
- data/sig/pvectl/models/snapshot_description.rbs +18 -0
- data/sig/pvectl/models/storage.rbs +39 -0
- data/sig/pvectl/models/subscription.rbs +24 -0
- data/sig/pvectl/models/syslog_entry.rbs +10 -0
- data/sig/pvectl/models/task.rbs +22 -0
- data/sig/pvectl/models/task_entry.rbs +24 -0
- data/sig/pvectl/models/task_log_line.rbs +10 -0
- data/sig/pvectl/models/time_config.rbs +12 -0
- data/sig/pvectl/models/vm.rbs +32 -0
- data/sig/pvectl/models/vm_operation_result.rbs +9 -0
- data/sig/pvectl/models/volume.rbs +29 -0
- data/sig/pvectl/models/volume_operation_result.rbs +9 -0
- data/sig/pvectl/parsers/cloud_init_config.rbs +15 -0
- data/sig/pvectl/parsers/disk_config.rbs +19 -0
- data/sig/pvectl/parsers/lxc_mount_config.rbs +19 -0
- data/sig/pvectl/parsers/lxc_net_config.rbs +19 -0
- data/sig/pvectl/parsers/net_config.rbs +19 -0
- data/sig/pvectl/parsers/smart_text.rbs +7 -0
- data/sig/pvectl/plugin_loader.rbs +25 -0
- data/sig/pvectl/presenters/apt_package.rbs +19 -0
- data/sig/pvectl/presenters/backup.rbs +25 -0
- data/sig/pvectl/presenters/base.rbs +41 -0
- data/sig/pvectl/presenters/capability.rbs +19 -0
- data/sig/pvectl/presenters/config/context.rbs +17 -0
- data/sig/pvectl/presenters/container.rbs +78 -0
- data/sig/pvectl/presenters/container_operation_result.rbs +19 -0
- data/sig/pvectl/presenters/disk.rbs +31 -0
- data/sig/pvectl/presenters/dns_config.rbs +13 -0
- data/sig/pvectl/presenters/hosts_file.rbs +13 -0
- data/sig/pvectl/presenters/journal_entry.rbs +11 -0
- data/sig/pvectl/presenters/node.rbs +118 -0
- data/sig/pvectl/presenters/node_operation_result.rbs +11 -0
- data/sig/pvectl/presenters/operation_result.rbs +17 -0
- data/sig/pvectl/presenters/service.rbs +15 -0
- data/sig/pvectl/presenters/snapshot.rbs +35 -0
- data/sig/pvectl/presenters/snapshot_operation_result.rbs +27 -0
- data/sig/pvectl/presenters/storage.rbs +59 -0
- data/sig/pvectl/presenters/subscription.rbs +36 -0
- data/sig/pvectl/presenters/syslog_entry.rbs +11 -0
- data/sig/pvectl/presenters/task_entry.rbs +21 -0
- data/sig/pvectl/presenters/task_log_line.rbs +11 -0
- data/sig/pvectl/presenters/template.rbs +15 -0
- data/sig/pvectl/presenters/time_config.rbs +19 -0
- data/sig/pvectl/presenters/top_container.rbs +17 -0
- data/sig/pvectl/presenters/top_node.rbs +17 -0
- data/sig/pvectl/presenters/top_presenter.rbs +13 -0
- data/sig/pvectl/presenters/top_vm.rbs +17 -0
- data/sig/pvectl/presenters/vm.rbs +91 -0
- data/sig/pvectl/presenters/vm_operation_result.rbs +19 -0
- data/sig/pvectl/presenters/volume.rbs +23 -0
- data/sig/pvectl/presenters/volume_operation_result.rbs +11 -0
- data/sig/pvectl/repositories/apt.rbs +17 -0
- data/sig/pvectl/repositories/backup.rbs +27 -0
- data/sig/pvectl/repositories/base.rbs +23 -0
- data/sig/pvectl/repositories/capabilities.rbs +20 -0
- data/sig/pvectl/repositories/container.rbs +63 -0
- data/sig/pvectl/repositories/disk.rbs +17 -0
- data/sig/pvectl/repositories/dns.rbs +13 -0
- data/sig/pvectl/repositories/hosts.rbs +13 -0
- data/sig/pvectl/repositories/journal.rbs +7 -0
- data/sig/pvectl/repositories/node.rbs +68 -0
- data/sig/pvectl/repositories/service.rbs +27 -0
- data/sig/pvectl/repositories/snapshot.rbs +19 -0
- data/sig/pvectl/repositories/storage.rbs +37 -0
- data/sig/pvectl/repositories/subscription.rbs +17 -0
- data/sig/pvectl/repositories/syslog.rbs +7 -0
- data/sig/pvectl/repositories/task.rbs +19 -0
- data/sig/pvectl/repositories/task_list.rbs +7 -0
- data/sig/pvectl/repositories/task_log.rbs +11 -0
- data/sig/pvectl/repositories/time_config.rbs +13 -0
- data/sig/pvectl/repositories/vm.rbs +85 -0
- data/sig/pvectl/repositories/volume.rbs +43 -0
- data/sig/pvectl/selectors/base.rbs +37 -0
- data/sig/pvectl/selectors/container.rbs +19 -0
- data/sig/pvectl/selectors/disk.rbs +13 -0
- data/sig/pvectl/selectors/vm.rbs +19 -0
- data/sig/pvectl/selectors/volume.rbs +13 -0
- data/sig/pvectl/services/backup.rbs +27 -0
- data/sig/pvectl/services/clone_container.rbs +35 -0
- data/sig/pvectl/services/clone_vm.rbs +35 -0
- data/sig/pvectl/services/cloudinit.rbs +19 -0
- data/sig/pvectl/services/console.rbs +23 -0
- data/sig/pvectl/services/container_lifecycle.rbs +26 -0
- data/sig/pvectl/services/create_container.rbs +64 -0
- data/sig/pvectl/services/create_vm.rbs +72 -0
- data/sig/pvectl/services/edit_container.rbs +17 -0
- data/sig/pvectl/services/edit_dns.rbs +23 -0
- data/sig/pvectl/services/edit_hosts.rbs +13 -0
- data/sig/pvectl/services/edit_node.rbs +21 -0
- data/sig/pvectl/services/edit_vm.rbs +17 -0
- data/sig/pvectl/services/edit_volume.rbs +18 -0
- data/sig/pvectl/services/get/resource_service.rbs +23 -0
- data/sig/pvectl/services/move_disk.rbs +21 -0
- data/sig/pvectl/services/pull_config.rbs +18 -0
- data/sig/pvectl/services/push_config.rbs +37 -0
- data/sig/pvectl/services/resize_volume.rbs +47 -0
- data/sig/pvectl/services/resource_delete.rbs +27 -0
- data/sig/pvectl/services/resource_migration.rbs +29 -0
- data/sig/pvectl/services/sendkey.rbs +19 -0
- data/sig/pvectl/services/service_lifecycle.rbs +17 -0
- data/sig/pvectl/services/set_container.rbs +14 -0
- data/sig/pvectl/services/set_node.rbs +23 -0
- data/sig/pvectl/services/set_vm.rbs +14 -0
- data/sig/pvectl/services/set_volume.rbs +12 -0
- data/sig/pvectl/services/snapshot.rbs +35 -0
- data/sig/pvectl/services/task_listing.rbs +13 -0
- data/sig/pvectl/services/unlink_disk.rbs +17 -0
- data/sig/pvectl/services/vm_lifecycle.rbs +26 -0
- data/sig/pvectl/services/wakeonlan.rbs +17 -0
- data/sig/pvectl/utils/resource_resolver.rbs +17 -0
- data/sig/pvectl/wizards/create_container.rbs +21 -0
- data/sig/pvectl/wizards/create_vm.rbs +21 -0
- data/sig/pvectl.rbs +9 -0
- 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
|