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,524 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pvectl
|
|
4
|
+
module Services
|
|
5
|
+
# Orchestrates pushing YAML manifests to the Proxmox cluster.
|
|
6
|
+
# Implements a two-phase approach: prepare (validate + diff) then apply.
|
|
7
|
+
#
|
|
8
|
+
# @example Push a single manifest
|
|
9
|
+
# service = PushConfig.new(vm_repository: vm_repo, container_repository: ct_repo)
|
|
10
|
+
# result = service.prepare(yaml_string)
|
|
11
|
+
# service.apply(result[:plans]) unless result[:plans].empty?
|
|
12
|
+
class PushConfig
|
|
13
|
+
DEFAULT_TASK_TIMEOUT = 120
|
|
14
|
+
|
|
15
|
+
# @param vm_repository [Repositories::Vm] VM repository
|
|
16
|
+
# @param container_repository [Repositories::Container] container repository
|
|
17
|
+
# @param task_repository [Repositories::Task, nil] task repository for tracking async operations
|
|
18
|
+
def initialize(vm_repository:, container_repository:, task_repository: nil)
|
|
19
|
+
@vm_repository = vm_repository
|
|
20
|
+
@container_repository = container_repository
|
|
21
|
+
@task_repository = task_repository
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Prepares a push plan from a single YAML manifest string.
|
|
25
|
+
# Validates the manifest, determines update vs create, computes diff.
|
|
26
|
+
#
|
|
27
|
+
# @param yaml_string [String] YAML manifest content
|
|
28
|
+
# @return [Hash] { plans: Array<Hash>, errors: Array<String> }
|
|
29
|
+
def prepare(yaml_string)
|
|
30
|
+
errors = ManifestSerializer.validate(yaml_string)
|
|
31
|
+
return { plans: [], errors: errors } unless errors.empty?
|
|
32
|
+
|
|
33
|
+
manifest = ManifestSerializer.from_yaml(yaml_string)
|
|
34
|
+
type = manifest[:type]
|
|
35
|
+
metadata = manifest[:metadata]
|
|
36
|
+
spec = manifest[:spec]
|
|
37
|
+
vmid = metadata[:vmid]
|
|
38
|
+
repo = repository_for(type)
|
|
39
|
+
|
|
40
|
+
# Convert nested spec to flat config
|
|
41
|
+
flat_from_manifest = ConfigSerializer.from_nested(spec, type: type)
|
|
42
|
+
|
|
43
|
+
# No VMID → always create with auto-allocated ID
|
|
44
|
+
unless vmid
|
|
45
|
+
return prepare_create(type, metadata, flat_from_manifest, repo, auto_id: true)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Check if resource exists (update) or not (create)
|
|
49
|
+
resource = repo.get(vmid)
|
|
50
|
+
|
|
51
|
+
if resource
|
|
52
|
+
# UPDATE path: fetch current config, compute diff
|
|
53
|
+
current_config = repo.fetch_config(resource.node, vmid)
|
|
54
|
+
original_flat = ConfigSerializer.from_nested(
|
|
55
|
+
ConfigSerializer.to_nested(current_config, type: type), type: type
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Filter nil/empty values from manifest (treated as "not specified").
|
|
59
|
+
# YAML null or empty strings mean the user didn't set the value.
|
|
60
|
+
flat_from_manifest = flat_from_manifest.reject { |_, v| v.nil? || (v.is_a?(String) && v.empty?) }
|
|
61
|
+
|
|
62
|
+
# Complete manifest's complex values with sub-properties from API.
|
|
63
|
+
# When a manifest omits sub-properties (volume, MAC, size) the API
|
|
64
|
+
# values fill them in, preventing false diffs from partial specs.
|
|
65
|
+
flat_from_manifest = ConfigSerializer.complete_from_api(flat_from_manifest, original_flat, type: type)
|
|
66
|
+
|
|
67
|
+
# Collect readonly keys and strip them from both sides.
|
|
68
|
+
readonly_keys = collect_readonly_keys(flat_from_manifest.merge(original_flat), type)
|
|
69
|
+
comparable_manifest = flat_from_manifest.reject { |k, _| readonly_keys.include?(k) }
|
|
70
|
+
|
|
71
|
+
# Only compare API keys that are also present in manifest.
|
|
72
|
+
# Keys only in API are "not specified" and should not generate diffs.
|
|
73
|
+
comparable_original = original_flat.select { |k, _| comparable_manifest.key?(k) }
|
|
74
|
+
|
|
75
|
+
diff = ConfigSerializer.diff(comparable_original, comparable_manifest)
|
|
76
|
+
|
|
77
|
+
if diff[:changed].empty? && diff[:added].empty? && diff[:removed].empty?
|
|
78
|
+
return { plans: [], errors: [], no_changes: true, vmid: vmid, type: type }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
update_result = build_update_params(diff, current_config, type)
|
|
82
|
+
|
|
83
|
+
plan = {
|
|
84
|
+
action: :update,
|
|
85
|
+
type: type,
|
|
86
|
+
vmid: vmid,
|
|
87
|
+
node: resource.node,
|
|
88
|
+
diff: diff,
|
|
89
|
+
params: update_result[:params],
|
|
90
|
+
resize_ops: update_result[:resize_ops]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
{ plans: [plan], errors: [] }
|
|
94
|
+
else
|
|
95
|
+
prepare_create(type, metadata, flat_from_manifest, repo, vmid: vmid)
|
|
96
|
+
end
|
|
97
|
+
rescue StandardError => e
|
|
98
|
+
{ plans: [], errors: [e.message] }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Prepares push plans from multiple YAML contents.
|
|
102
|
+
#
|
|
103
|
+
# @param yaml_contents [Array<Hash>] array of { filename: String, content: String }
|
|
104
|
+
# @param filter_type [Symbol, nil] optional type filter (:vm or :container)
|
|
105
|
+
# @return [Hash] { plans: Array<Hash>, errors: Array<String>, skipped: Array<String> }
|
|
106
|
+
def prepare_batch(yaml_contents, filter_type: nil)
|
|
107
|
+
plans = []
|
|
108
|
+
errors = []
|
|
109
|
+
skipped = []
|
|
110
|
+
unchanged = []
|
|
111
|
+
|
|
112
|
+
yaml_contents.each do |entry|
|
|
113
|
+
filename = entry[:filename]
|
|
114
|
+
content = entry[:content]
|
|
115
|
+
|
|
116
|
+
# Pre-check kind filter before full prepare
|
|
117
|
+
if filter_type
|
|
118
|
+
begin
|
|
119
|
+
parsed = YAML.safe_load(content)
|
|
120
|
+
kind = ManifestSerializer::KINDS_REVERSE[parsed&.dig("kind")]
|
|
121
|
+
if kind && kind != filter_type
|
|
122
|
+
skipped << "#{filename}: skipped (kind #{parsed['kind']} doesn't match filter)"
|
|
123
|
+
next
|
|
124
|
+
end
|
|
125
|
+
rescue Psych::SyntaxError
|
|
126
|
+
# Will be caught by prepare
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
result = prepare(content)
|
|
131
|
+
|
|
132
|
+
if result[:no_changes]
|
|
133
|
+
skipped << "#{filename}: no changes"
|
|
134
|
+
unchanged << { vmid: result[:vmid], type: result[:type], source_path: entry[:path] }
|
|
135
|
+
next
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
result[:plans].each do |p|
|
|
139
|
+
p[:filename] = filename
|
|
140
|
+
p[:source_path] = entry[:path]
|
|
141
|
+
end
|
|
142
|
+
plans.concat(result[:plans])
|
|
143
|
+
errors.concat(result[:errors].map { |e| "#{filename}: #{e}" })
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
{ plans: plans, errors: errors, skipped: skipped, unchanged: unchanged }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Applies prepared plans (executes API calls).
|
|
150
|
+
# Tracks async task completion for resize and create operations.
|
|
151
|
+
#
|
|
152
|
+
# @param plans [Array<Hash>] plans from prepare/prepare_batch
|
|
153
|
+
# @return [Hash] { results: Array<Hash>, errors: Array<String> }
|
|
154
|
+
def apply(plans)
|
|
155
|
+
results = []
|
|
156
|
+
errors = []
|
|
157
|
+
|
|
158
|
+
plans.each do |plan|
|
|
159
|
+
begin
|
|
160
|
+
repo = repository_for(plan[:type])
|
|
161
|
+
|
|
162
|
+
if plan[:action] == :update
|
|
163
|
+
config_params = plan[:params].reject { |k, _| k == :digest }
|
|
164
|
+
unless config_params.empty?
|
|
165
|
+
repo.update(plan[:vmid], plan[:node], plan[:params])
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
resize_errors = apply_resize_ops(repo, plan)
|
|
169
|
+
if resize_errors.any?
|
|
170
|
+
resize_errors.each { |e| errors << "Error resizing #{type_label(plan[:type])} #{plan[:vmid]}: #{e}" }
|
|
171
|
+
results << { action: :update, vmid: plan[:vmid], type: plan[:type], success: false, error: resize_errors.join("; ") }
|
|
172
|
+
else
|
|
173
|
+
results << { action: :update, vmid: plan[:vmid], type: plan[:type], success: true }
|
|
174
|
+
end
|
|
175
|
+
elsif plan[:action] == :create
|
|
176
|
+
upid = repo.create(plan[:node], plan[:vmid], plan[:params])
|
|
177
|
+
task = wait_for_task(upid)
|
|
178
|
+
|
|
179
|
+
if task&.failed?
|
|
180
|
+
error_msg = task.exitstatus
|
|
181
|
+
errors << "Error creating #{type_label(plan[:type])} #{plan[:vmid]}: #{error_msg}"
|
|
182
|
+
results << { action: :create, vmid: plan[:vmid], type: plan[:type], success: false, error: error_msg }
|
|
183
|
+
else
|
|
184
|
+
results << {
|
|
185
|
+
action: :create, vmid: plan[:vmid], type: plan[:type], success: true,
|
|
186
|
+
auto_id: plan[:auto_id], source_path: plan[:source_path]
|
|
187
|
+
}
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
rescue ProxmoxAPI::ApiException => e
|
|
191
|
+
detail = extract_api_error(e)
|
|
192
|
+
errors << "Error applying #{plan[:action]} for #{type_label(plan[:type])} #{plan[:vmid]}: #{detail}"
|
|
193
|
+
results << { action: plan[:action], vmid: plan[:vmid], type: plan[:type], success: false, error: detail }
|
|
194
|
+
rescue StandardError => e
|
|
195
|
+
errors << "Error applying #{plan[:action]} for #{type_label(plan[:type])} #{plan[:vmid]}: #{e.message}"
|
|
196
|
+
results << { action: plan[:action], vmid: plan[:vmid], type: plan[:type], success: false, error: e.message }
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
{ results: results, errors: errors }
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
private
|
|
204
|
+
|
|
205
|
+
# Prepares a create plan, optionally allocating a VMID.
|
|
206
|
+
#
|
|
207
|
+
# @param type [Symbol] :vm or :container
|
|
208
|
+
# @param metadata [Hash] manifest metadata
|
|
209
|
+
# @param flat_config [Hash] flat config from manifest spec
|
|
210
|
+
# @param repo [Repositories::Vm, Repositories::Container] repository
|
|
211
|
+
# @param vmid [Integer, nil] explicit VMID (nil when auto_id)
|
|
212
|
+
# @param auto_id [Boolean] whether to auto-allocate a VMID
|
|
213
|
+
# @return [Hash] { plans: Array<Hash>, errors: Array<String> }
|
|
214
|
+
def prepare_create(type, metadata, flat_config, repo, vmid: nil, auto_id: false)
|
|
215
|
+
node = metadata[:node]
|
|
216
|
+
unless node
|
|
217
|
+
label = vmid ? "VMID #{vmid}" : "new resource"
|
|
218
|
+
return { plans: [], errors: ["Node is required for creating #{label}"] }
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
if auto_id
|
|
222
|
+
vmid = allocate_vmid(repo, type)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
create_config = transform_disks_for_create(flat_config, type)
|
|
226
|
+
create_config = create_config.reject { |_, v| v.nil? || (v.is_a?(String) && v.empty?) }
|
|
227
|
+
|
|
228
|
+
plan = {
|
|
229
|
+
action: :create,
|
|
230
|
+
type: type,
|
|
231
|
+
vmid: vmid,
|
|
232
|
+
node: node,
|
|
233
|
+
params: create_config,
|
|
234
|
+
auto_id: auto_id
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
{ plans: [plan], errors: [] }
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Allocates the next available VMID from the repository.
|
|
241
|
+
#
|
|
242
|
+
# @param repo [Repositories::Vm, Repositories::Container] repository
|
|
243
|
+
# @param type [Symbol] :vm or :container
|
|
244
|
+
# @return [Integer] next available VMID
|
|
245
|
+
def allocate_vmid(repo, type)
|
|
246
|
+
if type == :container
|
|
247
|
+
repo.next_available_ctid
|
|
248
|
+
else
|
|
249
|
+
repo.next_available_vmid
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Returns the appropriate repository for the given resource type.
|
|
254
|
+
#
|
|
255
|
+
# @param type [Symbol] :vm or :container
|
|
256
|
+
# @return [Repositories::Vm, Repositories::Container]
|
|
257
|
+
def repository_for(type)
|
|
258
|
+
type == :container ? @container_repository : @vm_repository
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Builds flat update params from a diff and original config.
|
|
262
|
+
# Includes changed keys, added keys, and a delete list for removed keys.
|
|
263
|
+
# Preserves the digest from original config for optimistic locking.
|
|
264
|
+
# Extracts disk resize operations into a separate list (Proxmox requires
|
|
265
|
+
# the dedicated /resize endpoint for actual disk size changes).
|
|
266
|
+
#
|
|
267
|
+
# @param diff [Hash] diff from ConfigSerializer.diff
|
|
268
|
+
# @param original_config [Hash] original flat config from API
|
|
269
|
+
# @param type [Symbol] resource type (:vm or :container)
|
|
270
|
+
# @return [Hash] { params: Hash, resize_ops: Array<Hash> }
|
|
271
|
+
def build_update_params(diff, original_config, type)
|
|
272
|
+
params = {}
|
|
273
|
+
resize_ops = []
|
|
274
|
+
|
|
275
|
+
diff[:changed].each do |key, (old_val, new_val)|
|
|
276
|
+
if vm_disk_key?(key) && type == :vm
|
|
277
|
+
old_size = extract_disk_size(old_val.to_s)
|
|
278
|
+
new_size = extract_disk_size(new_val.to_s)
|
|
279
|
+
|
|
280
|
+
if old_size && new_size && old_size != new_size
|
|
281
|
+
resize_ops << { disk: key.to_s, size: new_size }
|
|
282
|
+
# Check if other disk options changed besides size
|
|
283
|
+
if disk_value_without_size(old_val.to_s) != disk_value_without_size(new_val.to_s)
|
|
284
|
+
params[key] = replace_disk_size(new_val.to_s, old_size)
|
|
285
|
+
end
|
|
286
|
+
next
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
params[key] = new_val
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
diff[:added].each { |key, val| params[key] = val }
|
|
293
|
+
unless diff[:removed].empty?
|
|
294
|
+
params[:delete] = diff[:removed].map(&:to_s).join(",")
|
|
295
|
+
end
|
|
296
|
+
params[:digest] = original_config[:digest] if original_config[:digest]
|
|
297
|
+
|
|
298
|
+
{ params: params, resize_ops: resize_ops }
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Applies disk resize operations from a plan.
|
|
302
|
+
# Waits for each resize task to complete and returns errors.
|
|
303
|
+
#
|
|
304
|
+
# @param repo [Repositories::Vm, Repositories::Container] repository
|
|
305
|
+
# @param plan [Hash] update plan with optional :resize_ops
|
|
306
|
+
# @return [Array<String>] list of error messages (empty if all succeeded)
|
|
307
|
+
def apply_resize_ops(repo, plan)
|
|
308
|
+
errors = []
|
|
309
|
+
return errors unless plan[:resize_ops]&.any?
|
|
310
|
+
|
|
311
|
+
plan[:resize_ops].each do |op|
|
|
312
|
+
upid = repo.resize(plan[:vmid], plan[:node], disk: op[:disk], size: op[:size])
|
|
313
|
+
task = wait_for_task(upid)
|
|
314
|
+
if task&.failed?
|
|
315
|
+
errors << "#{op[:disk]}: #{task.exitstatus}"
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
errors
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Waits for an async Proxmox task to complete.
|
|
323
|
+
# Returns nil when task_repository is not configured (fire-and-forget mode).
|
|
324
|
+
#
|
|
325
|
+
# @param upid [String, nil] task UPID
|
|
326
|
+
# @return [Models::Task, nil] completed task or nil
|
|
327
|
+
def wait_for_task(upid)
|
|
328
|
+
return nil unless @task_repository && upid
|
|
329
|
+
|
|
330
|
+
@task_repository.wait(upid, timeout: DEFAULT_TASK_TIMEOUT)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Checks if a key is a VM disk key (scsi, ide, virtio, sata, efidisk, tpmstate).
|
|
334
|
+
#
|
|
335
|
+
# @param key [Symbol, String] config key
|
|
336
|
+
# @return [Boolean]
|
|
337
|
+
def vm_disk_key?(key)
|
|
338
|
+
ConfigSerializer::VM_COMPLEX_KEYS[:disk][:pattern].match?(key.to_s)
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# Checks if a key is a disk key that needs create-format transformation.
|
|
342
|
+
# For VMs: scsi*, ide*, virtio*, sata*, efidisk*, tpmstate*
|
|
343
|
+
# For containers: rootfs, mp*
|
|
344
|
+
#
|
|
345
|
+
# @param key [Symbol, String] config key
|
|
346
|
+
# @param type [Symbol] :vm or :container
|
|
347
|
+
# @return [Boolean]
|
|
348
|
+
def create_disk_key?(key, type)
|
|
349
|
+
key_str = key.to_s
|
|
350
|
+
if type == :container
|
|
351
|
+
ConfigSerializer::CT_COMPLEX_KEYS[:rootfs][:pattern].match?(key_str) ||
|
|
352
|
+
ConfigSerializer::CT_COMPLEX_KEYS[:mp][:pattern].match?(key_str)
|
|
353
|
+
else
|
|
354
|
+
vm_disk_key?(key)
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# Transforms disk values in flat config to Proxmox create API format.
|
|
359
|
+
# Replaces volume names with STORAGE_ID:SIZE_IN_GiB syntax.
|
|
360
|
+
#
|
|
361
|
+
# @param flat_config [Hash] flat config from manifest
|
|
362
|
+
# @param type [Symbol] :vm or :container
|
|
363
|
+
# @return [Hash] config with disk values in create format
|
|
364
|
+
def transform_disks_for_create(flat_config, type)
|
|
365
|
+
flat_config.each_with_object({}) do |(key, value), result|
|
|
366
|
+
if create_disk_key?(key, type) && value.is_a?(String)
|
|
367
|
+
result[key] = disk_value_for_create(value)
|
|
368
|
+
else
|
|
369
|
+
result[key] = value
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Converts a single disk config string to Proxmox create API format.
|
|
375
|
+
# Handles: regular disks, cloud-init, EFI/TPM, and empty CD-ROMs.
|
|
376
|
+
#
|
|
377
|
+
# @param value [String] disk config string (e.g. "local-lvm:vm-100-disk-0,size=8G,iothread=1")
|
|
378
|
+
# @return [String] create format (e.g. "local-lvm:8,iothread=1")
|
|
379
|
+
def disk_value_for_create(value)
|
|
380
|
+
parts = value.split(",")
|
|
381
|
+
first = parts.first.strip
|
|
382
|
+
storage, volume = first.split(":", 2)
|
|
383
|
+
|
|
384
|
+
# "none" storage = keep as-is (empty CD-ROM: "none,media=cdrom")
|
|
385
|
+
return value if storage == "none"
|
|
386
|
+
|
|
387
|
+
# Cloud-init disk (volume contains "cloudinit")
|
|
388
|
+
return "#{storage}:cloudinit" if volume&.include?("cloudinit")
|
|
389
|
+
|
|
390
|
+
# Already in create format (volume is a plain number)
|
|
391
|
+
return value if volume&.match?(/\A\d+(\.\d+)?\z/)
|
|
392
|
+
|
|
393
|
+
# Extract size from size= option
|
|
394
|
+
size_str = extract_disk_size(value)
|
|
395
|
+
|
|
396
|
+
if size_str
|
|
397
|
+
gib = size_to_gib(size_str)
|
|
398
|
+
elsif volume
|
|
399
|
+
# Has volume name but no size (e.g., efidisk, tpmstate) — use default
|
|
400
|
+
gib = "1"
|
|
401
|
+
elsif parts.any? { |p| p.strip.start_with?("media=") }
|
|
402
|
+
# No volume, no size, but has media= (e.g., cloud-init on real storage)
|
|
403
|
+
# The only valid case for storage + media=cdrom without volume is cloud-init
|
|
404
|
+
return "#{storage}:cloudinit"
|
|
405
|
+
else
|
|
406
|
+
# No volume, no size, no media — can't determine create format
|
|
407
|
+
return value
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
# Build create format: storage:size,options (without size= key)
|
|
411
|
+
options = parts[1..].map(&:strip).reject { |p| p.start_with?("size=") }
|
|
412
|
+
create_parts = ["#{storage}:#{gib}"]
|
|
413
|
+
create_parts.concat(options) if options.any?
|
|
414
|
+
create_parts.join(",")
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# Converts a Proxmox size string to GiB number for create API.
|
|
418
|
+
#
|
|
419
|
+
# @param size_str [String] e.g. "8G", "32G", "1T"
|
|
420
|
+
# @return [String] size in GiB as a string number
|
|
421
|
+
def size_to_gib(size_str)
|
|
422
|
+
if size_str.end_with?("G")
|
|
423
|
+
size_str.chomp("G")
|
|
424
|
+
elsif size_str.end_with?("T")
|
|
425
|
+
(size_str.chomp("T").to_i * 1024).to_s
|
|
426
|
+
elsif size_str.end_with?("M")
|
|
427
|
+
(size_str.chomp("M").to_f / 1024).to_s
|
|
428
|
+
elsif size_str.end_with?("K")
|
|
429
|
+
"1"
|
|
430
|
+
else
|
|
431
|
+
size_str
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
# Extracts the size value from a Proxmox disk config string.
|
|
436
|
+
#
|
|
437
|
+
# @param disk_value [String] e.g. "local-lvm:vm-100-disk-0,size=8G,iothread=1"
|
|
438
|
+
# @return [String, nil] size value or nil if not found
|
|
439
|
+
def extract_disk_size(disk_value)
|
|
440
|
+
match = disk_value.match(/(?:^|,)size=([^,]+)/)
|
|
441
|
+
match ? match[1] : nil
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# Returns a disk config string with the size= part removed.
|
|
445
|
+
#
|
|
446
|
+
# @param value [String] disk config string
|
|
447
|
+
# @return [String] string without size= component
|
|
448
|
+
def disk_value_without_size(value)
|
|
449
|
+
value.split(",").reject { |p| p.strip.start_with?("size=") }.join(",")
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
# Replaces the size value in a disk config string.
|
|
453
|
+
#
|
|
454
|
+
# @param value [String] disk config string
|
|
455
|
+
# @param size [String] new size value
|
|
456
|
+
# @return [String] string with replaced size
|
|
457
|
+
def replace_disk_size(value, size)
|
|
458
|
+
value.split(",").map { |p| p.strip.start_with?("size=") ? "size=#{size}" : p }.join(",")
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
# Collects read-only keys present in the given flat config.
|
|
462
|
+
# Uses ConfigSerializer section definitions to identify readonly fields.
|
|
463
|
+
#
|
|
464
|
+
# @param flat_config [Hash] flat config hash
|
|
465
|
+
# @param type [Symbol] :vm or :container
|
|
466
|
+
# @return [Array<Symbol>] read-only keys found in the config
|
|
467
|
+
def collect_readonly_keys(flat_config, type)
|
|
468
|
+
sections = type == :container ? ConfigSerializer::CONTAINER_SECTIONS : ConfigSerializer::VM_SECTIONS
|
|
469
|
+
readonly = []
|
|
470
|
+
|
|
471
|
+
each_leaf_section(sections) do |section_def|
|
|
472
|
+
section_def[:readonly].each do |ro|
|
|
473
|
+
if ro.is_a?(Regexp)
|
|
474
|
+
flat_config.each_key { |k| readonly << k if ro.match?(k.to_s) }
|
|
475
|
+
else
|
|
476
|
+
readonly << ro if flat_config.key?(ro)
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
readonly.uniq
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
# Yields each leaf section definition (non-wrapper) from the sections hash.
|
|
485
|
+
#
|
|
486
|
+
# @param sections [Hash] section mapping
|
|
487
|
+
# @yield [Hash] leaf section definition
|
|
488
|
+
# @return [void]
|
|
489
|
+
def each_leaf_section(sections)
|
|
490
|
+
sections.each_value do |section_def|
|
|
491
|
+
if section_def.key?(:static)
|
|
492
|
+
yield section_def
|
|
493
|
+
else
|
|
494
|
+
section_def.each_value { |sub_def| yield sub_def }
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# Returns a human-readable label for the resource type.
|
|
500
|
+
#
|
|
501
|
+
# @param type [Symbol] :vm or :container
|
|
502
|
+
# @return [String] "VM" or "Container"
|
|
503
|
+
def type_label(type)
|
|
504
|
+
type == :container ? "Container" : "VM"
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
# Extracts detailed error info from a Proxmox API exception.
|
|
508
|
+
# Parses the JSON response body to find field-level error messages.
|
|
509
|
+
#
|
|
510
|
+
# @param exception [ProxmoxAPI::ApiException] API exception with response
|
|
511
|
+
# @return [String] human-readable error detail
|
|
512
|
+
def extract_api_error(exception)
|
|
513
|
+
body = JSON.parse(exception.response.body)
|
|
514
|
+
if body["errors"]
|
|
515
|
+
body["errors"].map { |k, v| "#{k}: #{v}" }.join("; ")
|
|
516
|
+
else
|
|
517
|
+
exception.message
|
|
518
|
+
end
|
|
519
|
+
rescue StandardError
|
|
520
|
+
exception.message
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
end
|