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