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,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pvectl
|
|
4
|
+
module Services
|
|
5
|
+
# Orchestrates the interactive editing flow for /etc/hosts on a node.
|
|
6
|
+
#
|
|
7
|
+
# Fetches current /etc/hosts content + digest, opens it in an editor,
|
|
8
|
+
# and POSTs the new content back with the original digest for optimistic
|
|
9
|
+
# locking. If Proxmox returns a digest collision (concurrent modification)
|
|
10
|
+
# or any other error, the message is surfaced verbatim to the user.
|
|
11
|
+
#
|
|
12
|
+
# Unlike DNS (structured YAML), /etc/hosts is edited as raw text —
|
|
13
|
+
# there is no parsing or validation pvectl-side.
|
|
14
|
+
#
|
|
15
|
+
# @example Basic usage
|
|
16
|
+
# service = EditHosts.new(hosts_repository: repo)
|
|
17
|
+
# result = service.execute(node_name: "pve1")
|
|
18
|
+
#
|
|
19
|
+
# @example Dry run
|
|
20
|
+
# service = EditHosts.new(hosts_repository: repo, options: { dry_run: true })
|
|
21
|
+
# result = service.execute(node_name: "pve1")
|
|
22
|
+
#
|
|
23
|
+
class EditHosts
|
|
24
|
+
# Creates a new EditHosts service.
|
|
25
|
+
#
|
|
26
|
+
# @param hosts_repository [Repositories::Hosts] Hosts repository
|
|
27
|
+
# @param editor_session [EditorSession, nil] optional injected editor session
|
|
28
|
+
# @param options [Hash] options (dry_run)
|
|
29
|
+
def initialize(hosts_repository:, editor_session: nil, options: {})
|
|
30
|
+
@hosts_repository = hosts_repository
|
|
31
|
+
@editor_session = editor_session
|
|
32
|
+
@options = options
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Executes the interactive /etc/hosts edit flow.
|
|
36
|
+
#
|
|
37
|
+
# @param node_name [String] node name
|
|
38
|
+
# @return [Models::NodeOperationResult, nil] result, or nil if cancelled/no changes
|
|
39
|
+
def execute(node_name:)
|
|
40
|
+
hosts = @hosts_repository.fetch(node_name)
|
|
41
|
+
original = hosts.data || ""
|
|
42
|
+
digest = hosts.digest
|
|
43
|
+
|
|
44
|
+
session = @editor_session || EditorSession.new
|
|
45
|
+
edited = session.edit(original)
|
|
46
|
+
|
|
47
|
+
return nil if edited.nil?
|
|
48
|
+
return nil if edited == original
|
|
49
|
+
|
|
50
|
+
resource_info = {
|
|
51
|
+
node_name: node_name,
|
|
52
|
+
diff: { original: original, edited: edited }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return build_result(resource_info, success: true) if @options[:dry_run]
|
|
56
|
+
|
|
57
|
+
@hosts_repository.update(node_name, edited, digest)
|
|
58
|
+
build_result(resource_info, success: true)
|
|
59
|
+
rescue StandardError => e
|
|
60
|
+
build_result({ node_name: node_name }, success: false, error: e.message)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
# Builds a NodeOperationResult with the :edit operation.
|
|
66
|
+
#
|
|
67
|
+
# @param resource_info [Hash] resource info (node_name, optional diff)
|
|
68
|
+
# @param attrs [Hash] additional result attributes
|
|
69
|
+
# @return [Models::NodeOperationResult]
|
|
70
|
+
def build_result(resource_info, **attrs)
|
|
71
|
+
node_model = Models::Node.new(name: resource_info[:node_name])
|
|
72
|
+
Models::NodeOperationResult.new(
|
|
73
|
+
operation: :edit, node_model: node_model, resource: resource_info, **attrs
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pvectl
|
|
4
|
+
module Services
|
|
5
|
+
# Orchestrates the interactive node configuration editing flow.
|
|
6
|
+
#
|
|
7
|
+
# Fetches current config, presents it as YAML in an editor,
|
|
8
|
+
# computes diff, and applies changes. Uses plain YAML (not
|
|
9
|
+
# ConfigSerializer) since node config is flat key-value.
|
|
10
|
+
#
|
|
11
|
+
# @example Basic usage
|
|
12
|
+
# service = EditNode.new(node_repository: repo)
|
|
13
|
+
# result = service.execute(node_name: "pve1")
|
|
14
|
+
#
|
|
15
|
+
# @example Dry run with injected editor session
|
|
16
|
+
# service = EditNode.new(node_repository: repo, editor_session: session,
|
|
17
|
+
# options: { dry_run: true })
|
|
18
|
+
# result = service.execute(node_name: "pve1")
|
|
19
|
+
#
|
|
20
|
+
class EditNode
|
|
21
|
+
# Read-only keys that should not be sent back to the API.
|
|
22
|
+
READONLY_KEYS = %i[digest].freeze
|
|
23
|
+
|
|
24
|
+
# Creates a new EditNode service.
|
|
25
|
+
#
|
|
26
|
+
# @param node_repository [Repositories::Node] Node repository
|
|
27
|
+
# @param editor_session [EditorSession, nil] optional injected editor session
|
|
28
|
+
# @param options [Hash] options (dry_run)
|
|
29
|
+
def initialize(node_repository:, editor_session: nil, options: {})
|
|
30
|
+
@node_repository = node_repository
|
|
31
|
+
@editor_session = editor_session
|
|
32
|
+
@options = options
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Executes the interactive node edit flow.
|
|
36
|
+
#
|
|
37
|
+
# @param node_name [String] node name
|
|
38
|
+
# @return [Models::NodeOperationResult, nil] result, or nil if cancelled/no changes
|
|
39
|
+
def execute(node_name:)
|
|
40
|
+
node = @node_repository.get(node_name)
|
|
41
|
+
return not_found_result(node_name) unless node
|
|
42
|
+
|
|
43
|
+
config = @node_repository.fetch_config(node_name)
|
|
44
|
+
resource_info = { node_name: node_name, status: node.status }
|
|
45
|
+
|
|
46
|
+
# Build editable YAML (exclude digest — it's for optimistic locking only)
|
|
47
|
+
editable = config.reject { |k, _| READONLY_KEYS.include?(k) }
|
|
48
|
+
# Convert symbol keys to strings for clean YAML output (avoids :key: format)
|
|
49
|
+
string_keyed = editable.transform_keys(&:to_s)
|
|
50
|
+
yaml_content = "# Node: #{node_name}\n# Edit configuration below. Save and close to apply.\n" +
|
|
51
|
+
string_keyed.to_yaml
|
|
52
|
+
|
|
53
|
+
session = @editor_session || EditorSession.new
|
|
54
|
+
edited = session.edit(yaml_content)
|
|
55
|
+
|
|
56
|
+
return nil unless edited
|
|
57
|
+
|
|
58
|
+
# Parse edited YAML (strip comment lines)
|
|
59
|
+
cleaned = edited.lines.reject { |l| l.strip.start_with?("#") }.join
|
|
60
|
+
edited_config = YAML.safe_load(cleaned, symbolize_names: true) || {}
|
|
61
|
+
|
|
62
|
+
# Compute diff against original editable config (with symbol keys)
|
|
63
|
+
changes = compute_diff(editable, edited_config)
|
|
64
|
+
|
|
65
|
+
if changes[:changed].empty? && changes[:added].empty? && changes[:removed].empty?
|
|
66
|
+
return nil
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
resource_info[:diff] = changes
|
|
70
|
+
|
|
71
|
+
if @options[:dry_run]
|
|
72
|
+
return build_result(resource_info, success: true)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
update_params = build_update_params(changes, config)
|
|
76
|
+
@node_repository.update(node_name, update_params)
|
|
77
|
+
build_result(resource_info, success: true)
|
|
78
|
+
rescue StandardError => e
|
|
79
|
+
build_result({ node_name: node_name }, success: false, error: e.message)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
# Computes diff between original and edited config.
|
|
85
|
+
#
|
|
86
|
+
# @param original [Hash] original config (without readonly keys)
|
|
87
|
+
# @param edited [Hash] edited config
|
|
88
|
+
# @return [Hash] diff with :changed, :added, :removed
|
|
89
|
+
def compute_diff(original, edited)
|
|
90
|
+
changed = {}
|
|
91
|
+
added = {}
|
|
92
|
+
removed = []
|
|
93
|
+
|
|
94
|
+
edited.each do |key, value|
|
|
95
|
+
orig_value = original[key]
|
|
96
|
+
if orig_value.nil?
|
|
97
|
+
added[key] = value
|
|
98
|
+
elsif orig_value.to_s != value.to_s
|
|
99
|
+
changed[key] = [orig_value.to_s, value.to_s]
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
original.each_key do |key|
|
|
104
|
+
removed << key unless edited.key?(key)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
{ changed: changed, added: added, removed: removed }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Builds API update parameters from diff.
|
|
111
|
+
#
|
|
112
|
+
# @param changes [Hash] diff hash with :changed, :added, :removed
|
|
113
|
+
# @param original_config [Hash] original config (for digest)
|
|
114
|
+
# @return [Hash] API parameters
|
|
115
|
+
def build_update_params(changes, original_config)
|
|
116
|
+
params = {}
|
|
117
|
+
changes[:changed].each { |key, (_old, new_val)| params[key] = new_val }
|
|
118
|
+
changes[:added].each { |key, val| params[key] = val }
|
|
119
|
+
unless changes[:removed].empty?
|
|
120
|
+
params[:delete] = changes[:removed].map(&:to_s).join(",")
|
|
121
|
+
end
|
|
122
|
+
params[:digest] = original_config[:digest] if original_config[:digest]
|
|
123
|
+
params
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Builds a NodeOperationResult with the :edit operation.
|
|
127
|
+
#
|
|
128
|
+
# @param resource_info [Hash] resource info (node_name, status)
|
|
129
|
+
# @param attrs [Hash] additional result attributes
|
|
130
|
+
# @return [Models::NodeOperationResult]
|
|
131
|
+
def build_result(resource_info, **attrs)
|
|
132
|
+
node_model = Models::Node.new(name: resource_info[:node_name])
|
|
133
|
+
Models::NodeOperationResult.new(
|
|
134
|
+
operation: :edit, node_model: node_model, resource: resource_info, **attrs
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Builds a not-found error result.
|
|
139
|
+
#
|
|
140
|
+
# @param node_name [String] node name
|
|
141
|
+
# @return [Models::NodeOperationResult]
|
|
142
|
+
def not_found_result(node_name)
|
|
143
|
+
build_result({ node_name: node_name }, success: false, error: "Node #{node_name} not found")
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pvectl
|
|
4
|
+
module Services
|
|
5
|
+
# Orchestrates the interactive VM configuration editing flow.
|
|
6
|
+
#
|
|
7
|
+
# Fetches current config, opens it in an editor as structured YAML,
|
|
8
|
+
# validates changes, computes a diff, and applies updates via the API.
|
|
9
|
+
# Supports dry-run mode and optimistic locking via digest.
|
|
10
|
+
#
|
|
11
|
+
# @example Basic usage
|
|
12
|
+
# service = EditVm.new(vm_repository: repo)
|
|
13
|
+
# result = service.execute(vmid: 100)
|
|
14
|
+
#
|
|
15
|
+
# @example Dry run with injected editor session
|
|
16
|
+
# service = EditVm.new(vm_repository: repo, editor_session: session,
|
|
17
|
+
# options: { dry_run: true })
|
|
18
|
+
# result = service.execute(vmid: 100)
|
|
19
|
+
#
|
|
20
|
+
class EditVm
|
|
21
|
+
# Creates a new EditVm service.
|
|
22
|
+
#
|
|
23
|
+
# @param vm_repository [Repositories::Vm] VM repository
|
|
24
|
+
# @param editor_session [EditorSession, nil] optional injected editor session
|
|
25
|
+
# @param options [Hash] options (dry_run)
|
|
26
|
+
def initialize(vm_repository:, editor_session: nil, options: {})
|
|
27
|
+
@vm_repository = vm_repository
|
|
28
|
+
@editor_session = editor_session
|
|
29
|
+
@options = options
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Executes the interactive VM edit flow.
|
|
33
|
+
#
|
|
34
|
+
# @param vmid [Integer] VM identifier
|
|
35
|
+
# @return [Models::VmOperationResult, nil] operation result, or nil if cancelled/no changes
|
|
36
|
+
def execute(vmid:)
|
|
37
|
+
vm = @vm_repository.get(vmid)
|
|
38
|
+
return not_found_result(vmid) unless vm
|
|
39
|
+
|
|
40
|
+
config = @vm_repository.fetch_config(vm.node, vmid)
|
|
41
|
+
resource_info = { vmid: vmid, node: vm.node, status: vm.status }
|
|
42
|
+
|
|
43
|
+
yaml_content = ConfigSerializer.to_yaml(config, type: :vm, resource: resource_info)
|
|
44
|
+
|
|
45
|
+
validator = ->(content) { ConfigSerializer.validate(content, type: :vm) }
|
|
46
|
+
session = @editor_session || EditorSession.new(validator: validator)
|
|
47
|
+
edited = session.edit(yaml_content)
|
|
48
|
+
|
|
49
|
+
return nil unless edited
|
|
50
|
+
|
|
51
|
+
original_roundtrip = ConfigSerializer.from_yaml(yaml_content, type: :vm)
|
|
52
|
+
edited_flat = ConfigSerializer.from_yaml(edited, type: :vm)
|
|
53
|
+
|
|
54
|
+
violations = ConfigSerializer.readonly_violations(original_roundtrip, edited_flat, type: :vm)
|
|
55
|
+
unless violations.empty?
|
|
56
|
+
return build_result(resource_info, success: false,
|
|
57
|
+
error: "Read-only fields cannot be changed: #{violations.join(', ')}")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
changes = ConfigSerializer.diff(original_roundtrip, edited_flat)
|
|
61
|
+
|
|
62
|
+
if changes[:changed].empty? && changes[:added].empty? && changes[:removed].empty?
|
|
63
|
+
return nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
params = build_update_params(changes, config)
|
|
67
|
+
|
|
68
|
+
resource_info[:diff] = changes
|
|
69
|
+
|
|
70
|
+
if @options[:dry_run]
|
|
71
|
+
return build_result(resource_info, success: true)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
@vm_repository.update(vmid, vm.node, params)
|
|
75
|
+
build_result(resource_info, success: true)
|
|
76
|
+
rescue StandardError => e
|
|
77
|
+
build_result({ vmid: vmid }, success: false, error: e.message)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
# Builds API update parameters from a diff hash.
|
|
83
|
+
#
|
|
84
|
+
# Maps changed/added keys to their new values, removed keys to the
|
|
85
|
+
# Proxmox `delete` parameter, and includes digest for optimistic locking.
|
|
86
|
+
#
|
|
87
|
+
# @param changes [Hash] diff hash with :changed, :added, :removed
|
|
88
|
+
# @param original_config [Hash] original flat config (for digest)
|
|
89
|
+
# @return [Hash] Proxmox API parameters
|
|
90
|
+
def build_update_params(changes, original_config)
|
|
91
|
+
params = {}
|
|
92
|
+
changes[:changed].each { |key, (_old, new_val)| params[key] = new_val }
|
|
93
|
+
changes[:added].each { |key, val| params[key] = val }
|
|
94
|
+
unless changes[:removed].empty?
|
|
95
|
+
params[:delete] = changes[:removed].map(&:to_s).join(",")
|
|
96
|
+
end
|
|
97
|
+
params[:digest] = original_config[:digest] if original_config[:digest]
|
|
98
|
+
params
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Builds a VmOperationResult with the :edit operation.
|
|
102
|
+
#
|
|
103
|
+
# @param resource_info [Hash] resource info (vmid, node, status)
|
|
104
|
+
# @param attrs [Hash] additional result attributes
|
|
105
|
+
# @return [Models::VmOperationResult]
|
|
106
|
+
def build_result(resource_info, **attrs)
|
|
107
|
+
vm = Models::Vm.new(
|
|
108
|
+
vmid: resource_info[:vmid],
|
|
109
|
+
node: resource_info[:node]
|
|
110
|
+
)
|
|
111
|
+
Models::VmOperationResult.new(
|
|
112
|
+
operation: :edit, vm: vm, resource: resource_info, **attrs
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Builds a not-found error result.
|
|
117
|
+
#
|
|
118
|
+
# @param vmid [Integer] VM identifier
|
|
119
|
+
# @return [Models::VmOperationResult]
|
|
120
|
+
def not_found_result(vmid)
|
|
121
|
+
build_result({ vmid: vmid }, success: false, error: "VM #{vmid} not found")
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pvectl
|
|
4
|
+
module Services
|
|
5
|
+
# Orchestrates the interactive volume property editing flow.
|
|
6
|
+
#
|
|
7
|
+
# Fetches the disk config string from VM/CT config, parses it into
|
|
8
|
+
# editable YAML (key-value pairs), opens editor, and applies changes.
|
|
9
|
+
# Size changes delegate to ResizeVolume; config changes rebuild the
|
|
10
|
+
# disk config string.
|
|
11
|
+
#
|
|
12
|
+
# @example Basic usage
|
|
13
|
+
# service = EditVolume.new(repository: vm_repo, resource_type: :vm)
|
|
14
|
+
# result = service.execute(id: 100, disk: "scsi0", node: "pve1")
|
|
15
|
+
#
|
|
16
|
+
# @example Dry run with injected editor session
|
|
17
|
+
# service = EditVolume.new(repository: vm_repo, resource_type: :vm,
|
|
18
|
+
# editor_session: session, options: { dry_run: true })
|
|
19
|
+
# result = service.execute(id: 100, disk: "scsi0", node: "pve1")
|
|
20
|
+
#
|
|
21
|
+
class EditVolume
|
|
22
|
+
# @param repository [Repositories::Vm, Repositories::Container] resource repository
|
|
23
|
+
# @param resource_type [Symbol] :vm or :container
|
|
24
|
+
# @param editor_session [EditorSession, nil] optional injected editor session
|
|
25
|
+
# @param options [Hash] options (dry_run)
|
|
26
|
+
def initialize(repository:, resource_type:, editor_session: nil, options: {})
|
|
27
|
+
@repository = repository
|
|
28
|
+
@resource_type = resource_type
|
|
29
|
+
@editor_session = editor_session
|
|
30
|
+
@options = options
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Executes the interactive volume edit flow.
|
|
34
|
+
#
|
|
35
|
+
# @param id [Integer] resource ID (VMID or CTID)
|
|
36
|
+
# @param disk [String] disk name (e.g., "scsi0", "rootfs")
|
|
37
|
+
# @param node [String] node name
|
|
38
|
+
# @return [Models::VolumeOperationResult, nil] result, or nil if cancelled/no changes
|
|
39
|
+
def execute(id:, disk:, node:)
|
|
40
|
+
config = @repository.fetch_config(node, id)
|
|
41
|
+
disk_value = config[disk.to_sym]
|
|
42
|
+
|
|
43
|
+
unless disk_value
|
|
44
|
+
return build_result(id, disk, node, success: false,
|
|
45
|
+
error: "Volume '#{disk}' not found in config for resource #{id}")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
editable = parse_disk_config(disk_value)
|
|
49
|
+
yaml_content = build_yaml_content(editable, disk, id, node)
|
|
50
|
+
|
|
51
|
+
session = @editor_session || EditorSession.new
|
|
52
|
+
edited = session.edit(yaml_content)
|
|
53
|
+
|
|
54
|
+
return nil unless edited
|
|
55
|
+
|
|
56
|
+
edited_config = parse_edited_yaml(edited)
|
|
57
|
+
changes = compute_diff(editable, edited_config)
|
|
58
|
+
|
|
59
|
+
return nil if no_changes?(changes)
|
|
60
|
+
|
|
61
|
+
if @options[:dry_run]
|
|
62
|
+
return build_result(id, disk, node, success: true,
|
|
63
|
+
resource: { diff: changes })
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
apply_changes(id, disk, node, disk_value, changes)
|
|
67
|
+
build_result(id, disk, node, success: true)
|
|
68
|
+
rescue ResizeVolume::VolumeNotFoundError, ResizeVolume::SizeTooSmallError, ArgumentError => e
|
|
69
|
+
build_result(id, disk, node, success: false, error: e.message)
|
|
70
|
+
rescue StandardError => e
|
|
71
|
+
build_result(id, disk, node, success: false, error: e.message)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
# Parses a disk config string into a hash of editable properties.
|
|
77
|
+
#
|
|
78
|
+
# Input: "local-lvm:vm-100-disk-0,size=32G,cache=none"
|
|
79
|
+
# Output: { "size" => "32G", "cache" => "none" }
|
|
80
|
+
#
|
|
81
|
+
# The base (storage:vol-id) is NOT included — it's read-only.
|
|
82
|
+
#
|
|
83
|
+
# @param disk_value [String] disk config string
|
|
84
|
+
# @return [Hash] editable properties with string keys
|
|
85
|
+
def parse_disk_config(disk_value)
|
|
86
|
+
parts = disk_value.to_s.split(",")
|
|
87
|
+
parts.shift # Remove base "storage:vol-id"
|
|
88
|
+
|
|
89
|
+
props = {}
|
|
90
|
+
parts.each do |part|
|
|
91
|
+
key, value = part.split("=", 2)
|
|
92
|
+
props[key] = value
|
|
93
|
+
end
|
|
94
|
+
props
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Builds YAML content for the editor with comment header.
|
|
98
|
+
#
|
|
99
|
+
# @param editable [Hash] editable properties
|
|
100
|
+
# @param disk [String] disk name
|
|
101
|
+
# @param id [Integer] resource ID
|
|
102
|
+
# @param node [String] node name
|
|
103
|
+
# @return [String] YAML content with comments
|
|
104
|
+
def build_yaml_content(editable, disk, id, node)
|
|
105
|
+
"# Volume: #{disk} (resource #{id} on #{node})\n" \
|
|
106
|
+
"# Edit properties below. Save and close to apply.\n" +
|
|
107
|
+
editable.to_yaml
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Parses edited YAML content, stripping comment lines.
|
|
111
|
+
#
|
|
112
|
+
# @param edited [String] raw editor content
|
|
113
|
+
# @return [Hash] parsed config with string keys
|
|
114
|
+
def parse_edited_yaml(edited)
|
|
115
|
+
cleaned = edited.lines.reject { |l| l.strip.start_with?("#") }.join
|
|
116
|
+
YAML.safe_load(cleaned) || {}
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Checks whether the diff contains any actual changes.
|
|
120
|
+
#
|
|
121
|
+
# @param changes [Hash] diff hash with :changed, :added, :removed
|
|
122
|
+
# @return [Boolean] true if no changes detected
|
|
123
|
+
def no_changes?(changes)
|
|
124
|
+
changes[:changed].empty? && changes[:added].empty? && changes[:removed].empty?
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Computes diff between original and edited config.
|
|
128
|
+
#
|
|
129
|
+
# @param original [Hash] original config (string keys)
|
|
130
|
+
# @param edited [Hash] edited config (string keys)
|
|
131
|
+
# @return [Hash] diff with :changed, :added, :removed
|
|
132
|
+
def compute_diff(original, edited)
|
|
133
|
+
changed = {}
|
|
134
|
+
added = {}
|
|
135
|
+
removed = []
|
|
136
|
+
|
|
137
|
+
edited.each do |key, value|
|
|
138
|
+
orig_value = original[key.to_s]
|
|
139
|
+
if orig_value.nil?
|
|
140
|
+
added[key.to_s] = value
|
|
141
|
+
elsif orig_value.to_s != value.to_s
|
|
142
|
+
changed[key.to_s] = [orig_value.to_s, value.to_s]
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
original.each_key do |key|
|
|
147
|
+
removed << key.to_s unless edited.key?(key.to_s)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
{ changed: changed, added: added, removed: removed }
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Applies changes — delegates size to ResizeVolume, config to rebuild.
|
|
154
|
+
#
|
|
155
|
+
# @param id [Integer] resource ID
|
|
156
|
+
# @param disk [String] disk name
|
|
157
|
+
# @param node [String] node name
|
|
158
|
+
# @param original_disk_value [String] original disk config string
|
|
159
|
+
# @param changes [Hash] diff hash
|
|
160
|
+
def apply_changes(id, disk, node, original_disk_value, changes)
|
|
161
|
+
size_change = changes[:changed].delete("size") || changes[:added].delete("size")
|
|
162
|
+
|
|
163
|
+
if size_change
|
|
164
|
+
new_size = size_change.is_a?(Array) ? size_change[1] : size_change
|
|
165
|
+
parsed_size = ResizeVolume.parse_size(new_size.to_s)
|
|
166
|
+
resize_service = ResizeVolume.new(repository: @repository)
|
|
167
|
+
resize_service.preflight(id, disk, parsed_size, node: node)
|
|
168
|
+
resize_service.perform(id, disk, parsed_size.raw, node: node)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
config_updates = {}
|
|
172
|
+
changes[:changed].each { |key, (_old, new_val)| config_updates[key] = new_val }
|
|
173
|
+
changes[:added].each { |key, val| config_updates[key] = val }
|
|
174
|
+
|
|
175
|
+
if !config_updates.empty? || !changes[:removed].empty?
|
|
176
|
+
new_disk_value = rebuild_disk_config(original_disk_value, config_updates, changes[:removed])
|
|
177
|
+
@repository.update(id, node, { disk.to_sym => new_disk_value })
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Rebuilds a disk config string with updated/removed properties.
|
|
182
|
+
#
|
|
183
|
+
# @param current_value [String] current disk config string
|
|
184
|
+
# @param updates [Hash] key-value pairs to update
|
|
185
|
+
# @param removed_keys [Array<String>] keys to remove
|
|
186
|
+
# @return [String] updated config string
|
|
187
|
+
def rebuild_disk_config(current_value, updates, removed_keys = [])
|
|
188
|
+
parts = current_value.to_s.split(",")
|
|
189
|
+
base = parts.shift
|
|
190
|
+
|
|
191
|
+
existing = {}
|
|
192
|
+
parts.each do |part|
|
|
193
|
+
key, value = part.split("=", 2)
|
|
194
|
+
existing[key] = value
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
updates.each { |key, value| existing[key.to_s] = value.to_s }
|
|
198
|
+
removed_keys.each { |key| existing.delete(key.to_s) }
|
|
199
|
+
|
|
200
|
+
config_parts = existing.map { |k, v| "#{k}=#{v}" }
|
|
201
|
+
([base] + config_parts).join(",")
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Builds a VolumeOperationResult.
|
|
205
|
+
#
|
|
206
|
+
# @param id [Integer] resource ID
|
|
207
|
+
# @param disk [String] disk name
|
|
208
|
+
# @param node [String] node name
|
|
209
|
+
# @param attrs [Hash] additional result attributes
|
|
210
|
+
# @return [Models::VolumeOperationResult]
|
|
211
|
+
def build_result(id, disk, node, **attrs)
|
|
212
|
+
volume = Models::Volume.new(
|
|
213
|
+
name: disk,
|
|
214
|
+
resource_type: @resource_type.to_s,
|
|
215
|
+
resource_id: id,
|
|
216
|
+
node: node
|
|
217
|
+
)
|
|
218
|
+
Models::VolumeOperationResult.new(
|
|
219
|
+
operation: :edit, volume: volume, **attrs
|
|
220
|
+
)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pvectl
|
|
4
|
+
module Services
|
|
5
|
+
module Get
|
|
6
|
+
# Service for fetching and formatting resource data.
|
|
7
|
+
#
|
|
8
|
+
# Orchestrates the data flow between:
|
|
9
|
+
# - ResourceHandler (provides models and presenter)
|
|
10
|
+
# - Formatters::Registry (formats output)
|
|
11
|
+
#
|
|
12
|
+
# This follows ARCHITECTURE.md section 3.4:
|
|
13
|
+
# "Services orchestrate data flow between Repository, Models, and Formatters"
|
|
14
|
+
#
|
|
15
|
+
# @example Basic usage
|
|
16
|
+
# handler = ResourceRegistry.for("nodes")
|
|
17
|
+
# service = ResourceService.new(handler: handler, format: "table")
|
|
18
|
+
# output = service.list(node: "pve1")
|
|
19
|
+
# puts output
|
|
20
|
+
#
|
|
21
|
+
class ResourceService
|
|
22
|
+
# Creates a new ResourceService.
|
|
23
|
+
#
|
|
24
|
+
# @param handler [ResourceHandler] the resource handler for data fetching
|
|
25
|
+
# @param format [String] output format (table, json, yaml, wide)
|
|
26
|
+
# @param color_enabled [Boolean] whether to enable colored output
|
|
27
|
+
def initialize(handler:, format: "table", color_enabled: true)
|
|
28
|
+
@handler = handler
|
|
29
|
+
@format = format
|
|
30
|
+
@color_enabled = color_enabled
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Fetches and formats resources.
|
|
34
|
+
#
|
|
35
|
+
# @param node [String, nil] filter by node name
|
|
36
|
+
# @param name [String, nil] filter by resource name
|
|
37
|
+
# @param args [Array<String>] additional positional arguments (e.g., VMIDs for snapshots)
|
|
38
|
+
# @param storage [String, nil] filter by storage (for backups)
|
|
39
|
+
# @param vmid [Array<String>, nil] filter by VM/CT IDs
|
|
40
|
+
# @param selector [Selectors::Base, nil] client-side selector for filtering results
|
|
41
|
+
# @param options [Hash] additional options passed through to handler (e.g., limit, since, type_filter)
|
|
42
|
+
# @return [String] formatted output string
|
|
43
|
+
def list(node: nil, name: nil, args: [], storage: nil, vmid: nil, selector: nil, **options)
|
|
44
|
+
models = @handler.list(node: node, name: name, args: args, storage: storage, vmid: vmid, **options)
|
|
45
|
+
models = selector.apply(models) if selector && !selector.empty?
|
|
46
|
+
presenter = @handler.presenter
|
|
47
|
+
format_output(models, presenter)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Describes and formats a single resource.
|
|
51
|
+
#
|
|
52
|
+
# For local storage with multiple instances, returns list of nodes
|
|
53
|
+
# when no node specified, or full describe when node is specified.
|
|
54
|
+
#
|
|
55
|
+
# @param name [String] resource name
|
|
56
|
+
# @param node [String, nil] filter by node name (for local storage)
|
|
57
|
+
# @param vmid [Array<String>, nil] filter by VM/CT IDs
|
|
58
|
+
# @return [String] formatted output string
|
|
59
|
+
def describe(name:, node: nil, args: [], vmid: nil)
|
|
60
|
+
result = @handler.describe(name: name, node: node, args: args, vmid: vmid)
|
|
61
|
+
presenter = @handler.presenter
|
|
62
|
+
|
|
63
|
+
if result.is_a?(Array)
|
|
64
|
+
# Multiple instances - format as list
|
|
65
|
+
format_output(result, presenter)
|
|
66
|
+
else
|
|
67
|
+
# Single model - format as describe
|
|
68
|
+
format_output_describe(result, presenter)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
attr_reader :handler, :format, :color_enabled
|
|
75
|
+
|
|
76
|
+
# Formats models for output using the appropriate formatter.
|
|
77
|
+
#
|
|
78
|
+
# @param models [Array<Object>] collection of models
|
|
79
|
+
# @param presenter [Presenters::Base] presenter for the resource type
|
|
80
|
+
# @return [String] formatted output
|
|
81
|
+
def format_output(models, presenter)
|
|
82
|
+
formatter = Formatters::Registry.for(format)
|
|
83
|
+
formatter.format(models, presenter, color_enabled: color_enabled)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Formats single model for describe output.
|
|
87
|
+
#
|
|
88
|
+
# @param model [Object] single model
|
|
89
|
+
# @param presenter [Presenters::Base] presenter for the resource type
|
|
90
|
+
# @return [String] formatted output
|
|
91
|
+
def format_output_describe(model, presenter)
|
|
92
|
+
formatter = Formatters::Registry.for(format)
|
|
93
|
+
formatter.format(model, presenter, color_enabled: color_enabled, describe: true)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|