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,1034 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Pvectl
|
|
6
|
+
# Converts flat Proxmox config hashes into nested, section-grouped YAML
|
|
7
|
+
# and back. Used by the `edit` command to present VM/container configuration
|
|
8
|
+
# in a human-friendly, structured format.
|
|
9
|
+
#
|
|
10
|
+
# All methods are class-level; no instance state is needed.
|
|
11
|
+
#
|
|
12
|
+
# @example Round-trip conversion
|
|
13
|
+
# yaml = ConfigSerializer.to_yaml(flat_config, type: :vm, resource: { vmid: 100, node: "pve1", status: "running" })
|
|
14
|
+
# flat = ConfigSerializer.from_yaml(yaml, type: :vm)
|
|
15
|
+
#
|
|
16
|
+
module ConfigSerializer
|
|
17
|
+
# Section mappings for QEMU VMs.
|
|
18
|
+
# Each section maps to an array of static keys and an array of dynamic key patterns.
|
|
19
|
+
# Keys marked as read-only are listed separately.
|
|
20
|
+
# Sections without a :static key are "wrapper" sections containing named subsections.
|
|
21
|
+
VM_SECTIONS = {
|
|
22
|
+
general: {
|
|
23
|
+
static: %i[vmid name description tags template lock digest],
|
|
24
|
+
dynamic: [],
|
|
25
|
+
readonly: %i[vmid template lock digest]
|
|
26
|
+
},
|
|
27
|
+
hardware: {
|
|
28
|
+
cpu: {
|
|
29
|
+
static: %i[cores sockets cpu cpulimit cpuunits numa affinity],
|
|
30
|
+
dynamic: [/\Anuma\d+\z/],
|
|
31
|
+
readonly: []
|
|
32
|
+
},
|
|
33
|
+
memory: {
|
|
34
|
+
static: %i[memory balloon shares hugepages keephugepages],
|
|
35
|
+
dynamic: [],
|
|
36
|
+
readonly: []
|
|
37
|
+
},
|
|
38
|
+
disks: {
|
|
39
|
+
static: %i[efidisk0 tpmstate0],
|
|
40
|
+
dynamic: [/\Ascsi\d+\z/, /\Aide\d+\z/, /\Avirtio\d+\z/, /\Asata\d+\z/, /\Aunused\d+\z/],
|
|
41
|
+
readonly: [/\Aunused\d+\z/]
|
|
42
|
+
},
|
|
43
|
+
network: {
|
|
44
|
+
static: [],
|
|
45
|
+
dynamic: [/\Anet\d+\z/],
|
|
46
|
+
readonly: []
|
|
47
|
+
},
|
|
48
|
+
display: {
|
|
49
|
+
static: %i[vga spice_enhancements keyboard],
|
|
50
|
+
dynamic: [],
|
|
51
|
+
readonly: []
|
|
52
|
+
},
|
|
53
|
+
devices: {
|
|
54
|
+
static: %i[audio0 rng0 ivshmem],
|
|
55
|
+
dynamic: [/\Aserial\d+\z/, /\Aparallel\d+\z/, /\Ausb\d+\z/, /\Ahostpci\d+\z/],
|
|
56
|
+
readonly: []
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
cloud_init: {
|
|
60
|
+
static: %i[citype cicustom ciuser cipassword ciupgrade nameserver searchdomain sshkeys],
|
|
61
|
+
dynamic: [/\Aipconfig\d+\z/],
|
|
62
|
+
readonly: []
|
|
63
|
+
},
|
|
64
|
+
options: {
|
|
65
|
+
static: %i[onboot startup boot bootdisk bios machine arch ostype scsihw kvm agent hotplug
|
|
66
|
+
tablet args hookscript smbios1 localtime reboot freeze protection],
|
|
67
|
+
dynamic: [],
|
|
68
|
+
readonly: []
|
|
69
|
+
},
|
|
70
|
+
migration: {
|
|
71
|
+
static: %i[migrate_downtime migrate_speed],
|
|
72
|
+
dynamic: [],
|
|
73
|
+
readonly: []
|
|
74
|
+
},
|
|
75
|
+
security: {
|
|
76
|
+
static: %i[amd_sev intel_tdx],
|
|
77
|
+
dynamic: [],
|
|
78
|
+
readonly: []
|
|
79
|
+
}
|
|
80
|
+
}.freeze
|
|
81
|
+
|
|
82
|
+
# Section mappings for LXC containers.
|
|
83
|
+
# Sections without a :static key are "wrapper" sections containing named subsections.
|
|
84
|
+
CONTAINER_SECTIONS = {
|
|
85
|
+
general: {
|
|
86
|
+
static: %i[vmid hostname description tags template lock digest],
|
|
87
|
+
dynamic: [],
|
|
88
|
+
readonly: %i[vmid template lock digest]
|
|
89
|
+
},
|
|
90
|
+
resources: {
|
|
91
|
+
cpu: {
|
|
92
|
+
static: %i[cores cpulimit cpuunits],
|
|
93
|
+
dynamic: [],
|
|
94
|
+
readonly: []
|
|
95
|
+
},
|
|
96
|
+
memory: {
|
|
97
|
+
static: %i[memory swap],
|
|
98
|
+
dynamic: [],
|
|
99
|
+
readonly: []
|
|
100
|
+
},
|
|
101
|
+
disks: {
|
|
102
|
+
static: %i[rootfs],
|
|
103
|
+
dynamic: [/\Amp\d+\z/, /\Adev\d+\z/, /\Aunused\d+\z/],
|
|
104
|
+
readonly: [/\Aunused\d+\z/]
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
network: {
|
|
108
|
+
static: [],
|
|
109
|
+
dynamic: [/\Anet\d+\z/],
|
|
110
|
+
readonly: []
|
|
111
|
+
},
|
|
112
|
+
dns: {
|
|
113
|
+
static: %i[nameserver searchdomain],
|
|
114
|
+
dynamic: [],
|
|
115
|
+
readonly: []
|
|
116
|
+
},
|
|
117
|
+
options: {
|
|
118
|
+
static: %i[onboot startup ostype arch unprivileged features hookscript protection
|
|
119
|
+
debug timezone entrypoint env],
|
|
120
|
+
dynamic: [],
|
|
121
|
+
readonly: %i[arch]
|
|
122
|
+
},
|
|
123
|
+
console: {
|
|
124
|
+
static: %i[console cmode tty],
|
|
125
|
+
dynamic: [],
|
|
126
|
+
readonly: []
|
|
127
|
+
}
|
|
128
|
+
}.freeze
|
|
129
|
+
|
|
130
|
+
# Characters that require quoting in YAML output.
|
|
131
|
+
YAML_SPECIAL_CHARS = %w[: # [ ] { } > | * & ! % @ ` , ? -].freeze
|
|
132
|
+
|
|
133
|
+
# Default values for QEMU Guest Agent properties (from Proxmox API docs).
|
|
134
|
+
# Used to fill in missing sub-properties when parsing agent config strings.
|
|
135
|
+
AGENT_DEFAULTS = {
|
|
136
|
+
enabled: "0",
|
|
137
|
+
fstrim_cloned_disks: "0",
|
|
138
|
+
:"freeze-fs-on-backup" => "1",
|
|
139
|
+
type: "virtio"
|
|
140
|
+
}.freeze
|
|
141
|
+
|
|
142
|
+
# Default values for VM config keys that Proxmox API omits when using defaults.
|
|
143
|
+
# These are injected by to_nested to produce complete manifests.
|
|
144
|
+
# Values sourced from Proxmox API docs (nodes-qemu-config.json).
|
|
145
|
+
VM_DEFAULTS = {
|
|
146
|
+
hotplug: "network,disk,usb"
|
|
147
|
+
}.freeze
|
|
148
|
+
|
|
149
|
+
# Top-level VM config keys that are boolean (0/1 in Proxmox API).
|
|
150
|
+
VM_BOOLEAN_KEYS = %i[onboot kvm tablet reboot freeze localtime protection numa keephugepages].freeze
|
|
151
|
+
|
|
152
|
+
# Top-level container config keys that are boolean (0/1 in Proxmox API).
|
|
153
|
+
CT_BOOLEAN_KEYS = %i[onboot unprivileged protection debug console].freeze
|
|
154
|
+
|
|
155
|
+
# All possible hotplug capabilities for QEMU VMs.
|
|
156
|
+
HOTPLUG_CAPABILITIES = %i[network disk usb cpu memory cloudinit].freeze
|
|
157
|
+
|
|
158
|
+
# Sub-keys within agent config that are boolean (0/1 strings).
|
|
159
|
+
AGENT_BOOLEAN_SUBKEYS = [:enabled, :fstrim_cloned_disks, :"freeze-fs-on-backup"].freeze
|
|
160
|
+
|
|
161
|
+
# Sub-keys within VM network config that are boolean (0/1 strings).
|
|
162
|
+
NET_BOOLEAN_SUBKEYS = %i[firewall link_down].freeze
|
|
163
|
+
|
|
164
|
+
# Sub-keys within disk config that are boolean (0/1 strings).
|
|
165
|
+
DISK_BOOLEAN_SUBKEYS = %i[iothread backup replicate ssd ro].freeze
|
|
166
|
+
|
|
167
|
+
# Complex key mappings for QEMU VMs.
|
|
168
|
+
# Each entry maps a category to a regex pattern and parser/serializer method names.
|
|
169
|
+
# Used by to_nested/from_nested for bidirectional conversion of Proxmox config strings.
|
|
170
|
+
VM_COMPLEX_KEYS = {
|
|
171
|
+
net: { pattern: /\Anet\d+\z/, parser: :parse_vm_net_value, serializer: :serialize_vm_net_value },
|
|
172
|
+
disk: { pattern: /\A(?:scsi|ide|virtio|sata|efidisk|tpmstate)\d*\z/, parser: :parse_disk_value,
|
|
173
|
+
serializer: :serialize_disk_value },
|
|
174
|
+
unused: { pattern: /\Aunused\d+\z/, parser: :parse_disk_value, serializer: :serialize_disk_value },
|
|
175
|
+
boot: { pattern: /\Aboot\z/, parser: :parse_boot_value, serializer: :serialize_boot_value },
|
|
176
|
+
agent: { pattern: /\Aagent\z/, parser: :parse_agent_value, serializer: :serialize_agent_value },
|
|
177
|
+
hotplug: { pattern: /\Ahotplug\z/, parser: :parse_hotplug_value, serializer: :serialize_hotplug_value },
|
|
178
|
+
startup: { pattern: /\Astartup\z/, parser: :parse_kv_value, serializer: :serialize_kv_value },
|
|
179
|
+
ipconfig: { pattern: /\Aipconfig\d+\z/, parser: :parse_kv_value, serializer: :serialize_kv_value },
|
|
180
|
+
smbios1: { pattern: /\Asmbios1\z/, parser: :parse_kv_value, serializer: :serialize_kv_value },
|
|
181
|
+
numa_dev: { pattern: /\Anuma\d+\z/, parser: :parse_kv_value, serializer: :serialize_kv_value }
|
|
182
|
+
}.freeze
|
|
183
|
+
|
|
184
|
+
# Complex key mappings for LXC containers.
|
|
185
|
+
CT_COMPLEX_KEYS = {
|
|
186
|
+
net: { pattern: /\Anet\d+\z/, parser: :parse_kv_value, serializer: :serialize_kv_value },
|
|
187
|
+
rootfs: { pattern: /\Arootfs\z/, parser: :parse_disk_value, serializer: :serialize_disk_value },
|
|
188
|
+
mp: { pattern: /\Amp\d+\z/, parser: :parse_disk_value, serializer: :serialize_disk_value },
|
|
189
|
+
dev: { pattern: /\Adev\d+\z/, parser: :parse_disk_value, serializer: :serialize_disk_value },
|
|
190
|
+
unused: { pattern: /\Aunused\d+\z/, parser: :parse_disk_value, serializer: :serialize_disk_value },
|
|
191
|
+
startup: { pattern: /\Astartup\z/, parser: :parse_kv_value, serializer: :serialize_kv_value },
|
|
192
|
+
features: { pattern: /\Afeatures\z/, parser: :parse_kv_value, serializer: :serialize_kv_value }
|
|
193
|
+
}.freeze
|
|
194
|
+
|
|
195
|
+
class << self
|
|
196
|
+
# Converts a flat Proxmox config hash into a nested, section-grouped YAML string
|
|
197
|
+
# with header comments and read-only markers.
|
|
198
|
+
#
|
|
199
|
+
# @param flat_config [Hash] flat config hash with symbol keys
|
|
200
|
+
# @param type [Symbol] resource type (:vm or :container)
|
|
201
|
+
# @param resource [Hash] resource metadata (vmid, node, status) for header
|
|
202
|
+
# @return [String] formatted YAML string with comments
|
|
203
|
+
#
|
|
204
|
+
# @example
|
|
205
|
+
# ConfigSerializer.to_yaml({ vmid: 100, cores: 4 }, type: :vm,
|
|
206
|
+
# resource: { vmid: 100, node: "pve1", status: "running" })
|
|
207
|
+
def to_yaml(flat_config, type:, resource: {})
|
|
208
|
+
sections = sections_for(type)
|
|
209
|
+
lines = []
|
|
210
|
+
|
|
211
|
+
lines << header_comment(type, resource)
|
|
212
|
+
lines << ""
|
|
213
|
+
|
|
214
|
+
sections.each do |section_name, section_def|
|
|
215
|
+
if wrapper_section?(section_def)
|
|
216
|
+
render_wrapper_section(lines, section_name, section_def, flat_config)
|
|
217
|
+
else
|
|
218
|
+
render_leaf_section(lines, section_name, section_def, flat_config)
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
lines.join("\n")
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Parses a YAML string back into a flat config hash with symbol keys.
|
|
226
|
+
# Strips comment lines before parsing, then flattens nested sections.
|
|
227
|
+
#
|
|
228
|
+
# @param yaml_string [String] YAML string (potentially with comments)
|
|
229
|
+
# @param type [Symbol] resource type (:vm or :container) - reserved for future use
|
|
230
|
+
# @return [Hash{Symbol => Object}] flat config hash
|
|
231
|
+
#
|
|
232
|
+
# @example
|
|
233
|
+
# ConfigSerializer.from_yaml("general:\n name: web\ncpu:\n cores: 4", type: :vm)
|
|
234
|
+
# #=> { name: "web", cores: 4 }
|
|
235
|
+
def from_yaml(yaml_string, type:)
|
|
236
|
+
cleaned = strip_comments(yaml_string)
|
|
237
|
+
return {} if cleaned.strip.empty?
|
|
238
|
+
|
|
239
|
+
begin
|
|
240
|
+
parsed = YAML.safe_load(cleaned)
|
|
241
|
+
rescue Psych::SyntaxError
|
|
242
|
+
return {}
|
|
243
|
+
end
|
|
244
|
+
return {} unless parsed.is_a?(Hash)
|
|
245
|
+
|
|
246
|
+
flatten_sections(parsed)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Validates a YAML string against known section/key mappings.
|
|
250
|
+
#
|
|
251
|
+
# @param yaml_string [String] YAML string to validate
|
|
252
|
+
# @param type [Symbol] resource type (:vm or :container)
|
|
253
|
+
# @return [Array<String>] list of error messages (empty if valid)
|
|
254
|
+
#
|
|
255
|
+
# @example
|
|
256
|
+
# ConfigSerializer.validate("foo:\n bar: 1", type: :vm)
|
|
257
|
+
# #=> ["Unknown section 'foo'"]
|
|
258
|
+
def validate(yaml_string, type:)
|
|
259
|
+
errors = []
|
|
260
|
+
cleaned = strip_comments(yaml_string)
|
|
261
|
+
|
|
262
|
+
begin
|
|
263
|
+
parsed = YAML.safe_load(cleaned)
|
|
264
|
+
rescue Psych::SyntaxError => e
|
|
265
|
+
return ["YAML syntax error: #{e.message}"]
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
return errors unless parsed.is_a?(Hash)
|
|
269
|
+
|
|
270
|
+
sections = sections_for(type)
|
|
271
|
+
|
|
272
|
+
parsed.each do |section_name, section_values|
|
|
273
|
+
unless sections.key?(section_name.to_sym)
|
|
274
|
+
errors << "Unknown section '#{section_name}'"
|
|
275
|
+
next
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
next unless section_values.is_a?(Hash)
|
|
279
|
+
|
|
280
|
+
section_def = sections[section_name.to_sym]
|
|
281
|
+
|
|
282
|
+
if wrapper_section?(section_def)
|
|
283
|
+
validate_wrapper_section(errors, section_name, section_def, section_values)
|
|
284
|
+
else
|
|
285
|
+
section_values.each_key do |key|
|
|
286
|
+
unless key_in_section?(key.to_sym, section_def)
|
|
287
|
+
errors << "Unknown key '#{key}' in section '#{section_name}'"
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
errors
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Checks if any read-only fields were modified between original and edited configs.
|
|
297
|
+
#
|
|
298
|
+
# @param original_flat [Hash{Symbol => Object}] original flat config
|
|
299
|
+
# @param edited_flat [Hash{Symbol => Object}] edited flat config
|
|
300
|
+
# @param type [Symbol] resource type (:vm or :container)
|
|
301
|
+
# @return [Array<String>] list of read-only field names that were changed
|
|
302
|
+
#
|
|
303
|
+
# @example
|
|
304
|
+
# ConfigSerializer.readonly_violations({ vmid: 100 }, { vmid: 999 }, type: :vm)
|
|
305
|
+
# #=> ["vmid"]
|
|
306
|
+
def readonly_violations(original_flat, edited_flat, type:)
|
|
307
|
+
sections = sections_for(type)
|
|
308
|
+
readonly_keys = collect_readonly_keys(original_flat.keys | edited_flat.keys, sections)
|
|
309
|
+
|
|
310
|
+
readonly_keys.select { |key| original_flat[key] != edited_flat[key] }
|
|
311
|
+
.map(&:to_s)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Computes the diff between two flat config hashes.
|
|
315
|
+
#
|
|
316
|
+
# @param original [Hash{Symbol => Object}] original config
|
|
317
|
+
# @param edited [Hash{Symbol => Object}] edited config
|
|
318
|
+
# @return [Hash{Symbol => Hash, Array}] diff with :changed, :added, :removed
|
|
319
|
+
#
|
|
320
|
+
# @example
|
|
321
|
+
# ConfigSerializer.diff({ cores: 4 }, { cores: 8, balloon: 2048 })
|
|
322
|
+
# #=> { changed: { cores: [4, 8] }, added: { balloon: 2048 }, removed: [] }
|
|
323
|
+
def diff(original, edited)
|
|
324
|
+
changed = {}
|
|
325
|
+
added = {}
|
|
326
|
+
removed = []
|
|
327
|
+
|
|
328
|
+
all_keys = original.keys | edited.keys
|
|
329
|
+
|
|
330
|
+
all_keys.each do |key|
|
|
331
|
+
if original.key?(key) && edited.key?(key)
|
|
332
|
+
changed[key] = [original[key], edited[key]] if original[key] != edited[key]
|
|
333
|
+
elsif edited.key?(key)
|
|
334
|
+
added[key] = edited[key]
|
|
335
|
+
else
|
|
336
|
+
removed << key
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
{ changed: changed, added: added, removed: removed }
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# Formats a diff hash for colored terminal display.
|
|
344
|
+
#
|
|
345
|
+
# @param diff_hash [Hash] diff hash from {.diff}
|
|
346
|
+
# @return [String] ANSI-colored diff output
|
|
347
|
+
#
|
|
348
|
+
# @example
|
|
349
|
+
# ConfigSerializer.format_diff(changed: { cores: [4, 8] }, added: {}, removed: [])
|
|
350
|
+
# #=> " ~ cores: 4 -> 8" (yellow)
|
|
351
|
+
def format_diff(diff_hash)
|
|
352
|
+
lines = []
|
|
353
|
+
|
|
354
|
+
diff_hash[:changed].each do |key, (old_val, new_val)|
|
|
355
|
+
lines << "\e[33m ~ #{key}: #{old_val} -> #{new_val}\e[0m"
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
diff_hash[:added].each do |key, value|
|
|
359
|
+
lines << "\e[32m + #{key}: #{value}\e[0m"
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
diff_hash[:removed].each do |key|
|
|
363
|
+
lines << "\e[31m - #{key}\e[0m"
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
lines.join("\n")
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# Converts a flat Proxmox config hash into a nested Hash with parsed complex values.
|
|
370
|
+
# Used by ManifestSerializer to build the spec section of YAML manifests.
|
|
371
|
+
#
|
|
372
|
+
# @param flat_config [Hash{Symbol => Object}] flat config hash with symbol keys
|
|
373
|
+
# @param type [Symbol] resource type (:vm or :container)
|
|
374
|
+
# @return [Hash{Symbol => Hash}] nested hash matching section structure
|
|
375
|
+
#
|
|
376
|
+
# @example
|
|
377
|
+
# ConfigSerializer.to_nested({ cores: 4, net0: "virtio=AA:BB,bridge=vmbr0" }, type: :vm)
|
|
378
|
+
# #=> { hardware: { cpu: { cores: 4 }, network: { net0: { model: "virtio", mac: "AA:BB", bridge: "vmbr0" } } } }
|
|
379
|
+
def to_nested(flat_config, type:)
|
|
380
|
+
sections = sections_for(type)
|
|
381
|
+
config_with_defaults = inject_defaults(flat_config, type)
|
|
382
|
+
result = {}
|
|
383
|
+
|
|
384
|
+
sections.each do |section_name, section_def|
|
|
385
|
+
if wrapper_section?(section_def)
|
|
386
|
+
wrapper = {}
|
|
387
|
+
section_def.each do |sub_name, sub_def|
|
|
388
|
+
sub_hash = build_nested_section(config_with_defaults, sub_def, type)
|
|
389
|
+
wrapper[sub_name] = sub_hash unless sub_hash.empty?
|
|
390
|
+
end
|
|
391
|
+
result[section_name] = wrapper unless wrapper.empty?
|
|
392
|
+
else
|
|
393
|
+
section_hash = build_nested_section(config_with_defaults, section_def, type)
|
|
394
|
+
result[section_name] = section_hash unless section_hash.empty?
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
result
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# Converts a nested Hash (from manifest spec) back into a flat Proxmox config hash.
|
|
402
|
+
# Serializes parsed complex values back to Proxmox string format.
|
|
403
|
+
#
|
|
404
|
+
# @param nested [Hash{Symbol => Hash}] nested hash from to_nested
|
|
405
|
+
# @param type [Symbol] resource type (:vm or :container)
|
|
406
|
+
# @return [Hash{Symbol => Object}] flat config hash
|
|
407
|
+
#
|
|
408
|
+
# @example
|
|
409
|
+
# nested = { hardware: { cpu: { cores: 4 } } }
|
|
410
|
+
# ConfigSerializer.from_nested(nested, type: :vm)
|
|
411
|
+
# #=> { cores: 4 }
|
|
412
|
+
def from_nested(nested, type:)
|
|
413
|
+
sections = sections_for(type)
|
|
414
|
+
result = {}
|
|
415
|
+
|
|
416
|
+
nested.each do |section_name, section_value|
|
|
417
|
+
next unless section_value.is_a?(Hash)
|
|
418
|
+
|
|
419
|
+
section_def = sections[section_name]
|
|
420
|
+
next unless section_def
|
|
421
|
+
|
|
422
|
+
if wrapper_section?(section_def)
|
|
423
|
+
section_value.each do |_sub_name, sub_values|
|
|
424
|
+
next unless sub_values.is_a?(Hash)
|
|
425
|
+
|
|
426
|
+
flatten_nested_section(sub_values, type, result)
|
|
427
|
+
end
|
|
428
|
+
else
|
|
429
|
+
flatten_nested_section(section_value, type, result)
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
inject_defaults(result, type)
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# Completes a manifest's flat config with sub-properties from the API config.
|
|
437
|
+
# When a manifest omits sub-properties of complex keys (e.g., disk without volume,
|
|
438
|
+
# net without MAC address), fills them from the current API config to prevent
|
|
439
|
+
# false diffs during update comparison.
|
|
440
|
+
#
|
|
441
|
+
# @param manifest_flat [Hash{Symbol => Object}] flat config from manifest
|
|
442
|
+
# @param api_flat [Hash{Symbol => Object}] flat config from API (round-tripped)
|
|
443
|
+
# @param type [Symbol] resource type (:vm or :container)
|
|
444
|
+
# @return [Hash{Symbol => Object}] manifest config with completed complex values
|
|
445
|
+
#
|
|
446
|
+
# @example
|
|
447
|
+
# manifest = { scsi0: "local-lvm,iothread=1,size=9G" }
|
|
448
|
+
# api = { scsi0: "local-lvm:vm-100-disk-0,iothread=1,size=9G" }
|
|
449
|
+
# ConfigSerializer.complete_from_api(manifest, api, type: :vm)
|
|
450
|
+
# #=> { scsi0: "local-lvm:vm-100-disk-0,iothread=1,size=9G" }
|
|
451
|
+
def complete_from_api(manifest_flat, api_flat, type:)
|
|
452
|
+
manifest_flat.each_with_object({}) do |(key, value), result|
|
|
453
|
+
api_value = api_flat[key]
|
|
454
|
+
complex = find_complex_key(key, type)
|
|
455
|
+
|
|
456
|
+
if complex && value.is_a?(String) && api_value.is_a?(String)
|
|
457
|
+
parsed_api = send(complex[:parser], api_value)
|
|
458
|
+
parsed_manifest = send(complex[:parser], value)
|
|
459
|
+
merged = parsed_api.merge(parsed_manifest.compact)
|
|
460
|
+
result[key] = send(complex[:serializer], merged)
|
|
461
|
+
elsif api_value.is_a?(Integer) && value.is_a?(String) && value.match?(/\A\d+\z/)
|
|
462
|
+
result[key] = value.to_i
|
|
463
|
+
else
|
|
464
|
+
result[key] = value
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
private
|
|
470
|
+
|
|
471
|
+
# Returns the section mappings for the given resource type.
|
|
472
|
+
#
|
|
473
|
+
# @param type [Symbol] :vm or :container
|
|
474
|
+
# @return [Hash] section mapping hash
|
|
475
|
+
def sections_for(type)
|
|
476
|
+
type == :container ? CONTAINER_SECTIONS : VM_SECTIONS
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
# Generates the YAML header comment block.
|
|
480
|
+
#
|
|
481
|
+
# @param type [Symbol] :vm or :container
|
|
482
|
+
# @param resource [Hash] resource metadata
|
|
483
|
+
# @return [String] multi-line comment string
|
|
484
|
+
def header_comment(type, resource)
|
|
485
|
+
label = type == :container ? "Container" : "VM"
|
|
486
|
+
vmid = resource[:vmid] || resource[:ctid]
|
|
487
|
+
node = resource[:node]
|
|
488
|
+
status = resource[:status]
|
|
489
|
+
|
|
490
|
+
<<~COMMENT.chomp
|
|
491
|
+
# Editing #{label} #{vmid} on node #{node} (status: #{status})
|
|
492
|
+
# Fields marked "# read-only" cannot be changed.
|
|
493
|
+
# Save and close to apply changes. Empty file to cancel.
|
|
494
|
+
COMMENT
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
# Collects config keys that belong to a given section definition.
|
|
498
|
+
#
|
|
499
|
+
# @param config [Hash] flat config hash
|
|
500
|
+
# @param section_def [Hash] section definition with :static and :dynamic
|
|
501
|
+
# @return [Array<Symbol>] matching keys in stable order
|
|
502
|
+
def keys_for_section(config, section_def)
|
|
503
|
+
config.keys.select { |key| key_in_section?(key, section_def) }
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
# Checks if a key matches a section definition (static or dynamic).
|
|
507
|
+
#
|
|
508
|
+
# @param key [Symbol] config key
|
|
509
|
+
# @param section_def [Hash] section definition
|
|
510
|
+
# @return [Boolean]
|
|
511
|
+
def key_in_section?(key, section_def)
|
|
512
|
+
return true if section_def[:static].include?(key.to_sym)
|
|
513
|
+
|
|
514
|
+
key_str = key.to_s
|
|
515
|
+
section_def[:dynamic].any? { |pattern| pattern.match?(key_str) }
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
# Checks if a key is read-only within its section definition.
|
|
519
|
+
#
|
|
520
|
+
# @param key [Symbol] config key
|
|
521
|
+
# @param section_def [Hash] section definition
|
|
522
|
+
# @return [Boolean]
|
|
523
|
+
def readonly_key?(key, section_def)
|
|
524
|
+
readonly = section_def[:readonly]
|
|
525
|
+
return true if readonly.include?(key.to_sym)
|
|
526
|
+
|
|
527
|
+
key_str = key.to_s
|
|
528
|
+
readonly.select { |r| r.is_a?(Regexp) }.any? { |pattern| pattern.match?(key_str) }
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
# Collects all read-only keys from the given key set.
|
|
532
|
+
# Recurses into wrapper sections to check subsection definitions.
|
|
533
|
+
#
|
|
534
|
+
# @param keys [Array<Symbol>] all keys to check
|
|
535
|
+
# @param sections [Hash] section mapping
|
|
536
|
+
# @return [Array<Symbol>] read-only keys
|
|
537
|
+
def collect_readonly_keys(keys, sections)
|
|
538
|
+
leaf_defs = each_leaf_section(sections)
|
|
539
|
+
keys.select do |key|
|
|
540
|
+
leaf_defs.any? do |section_def|
|
|
541
|
+
key_in_section?(key, section_def) && readonly_key?(key, section_def)
|
|
542
|
+
end
|
|
543
|
+
end
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
# Formats a value for YAML output, quoting strings with special characters.
|
|
547
|
+
#
|
|
548
|
+
# @param value [Object] value to format
|
|
549
|
+
# @return [String] formatted value
|
|
550
|
+
def format_yaml_value(value)
|
|
551
|
+
return value.inspect if value.nil?
|
|
552
|
+
|
|
553
|
+
case value
|
|
554
|
+
when String
|
|
555
|
+
needs_quoting?(value) ? value.inspect : value
|
|
556
|
+
when TrueClass, FalseClass, Numeric
|
|
557
|
+
value.to_s
|
|
558
|
+
else
|
|
559
|
+
value.to_s
|
|
560
|
+
end
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
# Checks if a string value requires YAML quoting.
|
|
564
|
+
#
|
|
565
|
+
# @param value [String] value to check
|
|
566
|
+
# @return [Boolean]
|
|
567
|
+
def needs_quoting?(value)
|
|
568
|
+
return true if value.empty?
|
|
569
|
+
|
|
570
|
+
YAML_SPECIAL_CHARS.any? { |char| value.include?(char) }
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
# Strips comment lines (lines starting with #) from YAML.
|
|
574
|
+
# Preserves inline content but removes full-line comments.
|
|
575
|
+
#
|
|
576
|
+
# @param yaml_string [String] YAML with comments
|
|
577
|
+
# @return [String] YAML without comment lines
|
|
578
|
+
def strip_comments(yaml_string)
|
|
579
|
+
yaml_string.lines.reject { |line| line.strip.start_with?("#") }.join
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
# Checks if a section definition is a wrapper (contains named subsections)
|
|
583
|
+
# rather than a leaf section (contains :static/:dynamic/:readonly arrays).
|
|
584
|
+
#
|
|
585
|
+
# @param section_def [Hash] section definition
|
|
586
|
+
# @return [Boolean] true if wrapper section
|
|
587
|
+
def wrapper_section?(section_def)
|
|
588
|
+
!section_def.key?(:static)
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
# Merges default values for keys that Proxmox API omits when using defaults.
|
|
592
|
+
# Explicit values from the API response take precedence.
|
|
593
|
+
#
|
|
594
|
+
# @param flat_config [Hash] flat config from API
|
|
595
|
+
# @param type [Symbol] :vm or :container
|
|
596
|
+
# @return [Hash] config with defaults injected
|
|
597
|
+
def inject_defaults(flat_config, type)
|
|
598
|
+
defaults = type == :vm ? VM_DEFAULTS : {}
|
|
599
|
+
return flat_config if defaults.empty?
|
|
600
|
+
|
|
601
|
+
defaults.merge(flat_config)
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
# Converts a Proxmox 0/1 value to a Ruby boolean.
|
|
605
|
+
#
|
|
606
|
+
# @param value [Object] value to convert
|
|
607
|
+
# @return [Boolean, Object] true/false for 0/1 values, original otherwise
|
|
608
|
+
def to_boolean(value)
|
|
609
|
+
case value
|
|
610
|
+
when true, 1, "1" then true
|
|
611
|
+
when false, 0, "0" then false
|
|
612
|
+
else value
|
|
613
|
+
end
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
# Converts a Ruby boolean back to a Proxmox integer (0/1).
|
|
617
|
+
#
|
|
618
|
+
# @param value [Object] value to convert
|
|
619
|
+
# @return [Integer, Object] 0/1 for booleans, original otherwise
|
|
620
|
+
def from_boolean(value)
|
|
621
|
+
case value
|
|
622
|
+
when true then 1
|
|
623
|
+
when false then 0
|
|
624
|
+
else value
|
|
625
|
+
end
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
# Returns the set of boolean keys for the given resource type.
|
|
629
|
+
#
|
|
630
|
+
# @param type [Symbol] :vm or :container
|
|
631
|
+
# @return [Array<Symbol>] boolean key names
|
|
632
|
+
def boolean_keys_for(type)
|
|
633
|
+
type == :container ? CT_BOOLEAN_KEYS : VM_BOOLEAN_KEYS
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
# Renders a leaf section (non-wrapper) into YAML output lines.
|
|
637
|
+
#
|
|
638
|
+
# @param lines [Array<String>] accumulator for output lines
|
|
639
|
+
# @param section_name [Symbol] section name
|
|
640
|
+
# @param section_def [Hash] leaf section definition
|
|
641
|
+
# @param flat_config [Hash] flat config hash
|
|
642
|
+
# @return [void]
|
|
643
|
+
def render_leaf_section(lines, section_name, section_def, flat_config)
|
|
644
|
+
section_keys = keys_for_section(flat_config, section_def)
|
|
645
|
+
return if section_keys.empty?
|
|
646
|
+
|
|
647
|
+
lines << "#{section_name}:"
|
|
648
|
+
section_keys.each do |key|
|
|
649
|
+
value = flat_config[key]
|
|
650
|
+
formatted_value = format_yaml_value(value)
|
|
651
|
+
readonly = readonly_key?(key, section_def) ? " # read-only" : ""
|
|
652
|
+
lines << " #{key}: #{formatted_value}#{readonly}"
|
|
653
|
+
end
|
|
654
|
+
lines << ""
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
# Renders a wrapper section with subsections into YAML output lines.
|
|
658
|
+
# Produces 3-level indentation: wrapper -> subsection -> key: value.
|
|
659
|
+
#
|
|
660
|
+
# @param lines [Array<String>] accumulator for output lines
|
|
661
|
+
# @param wrapper_name [Symbol] wrapper section name
|
|
662
|
+
# @param wrapper_def [Hash] wrapper definition containing subsection definitions
|
|
663
|
+
# @param flat_config [Hash] flat config hash
|
|
664
|
+
# @return [void]
|
|
665
|
+
def render_wrapper_section(lines, wrapper_name, wrapper_def, flat_config)
|
|
666
|
+
has_any_keys = wrapper_def.any? do |_sub_name, sub_def|
|
|
667
|
+
keys_for_section(flat_config, sub_def).any?
|
|
668
|
+
end
|
|
669
|
+
return unless has_any_keys
|
|
670
|
+
|
|
671
|
+
lines << "#{wrapper_name}:"
|
|
672
|
+
wrapper_def.each do |sub_name, sub_def|
|
|
673
|
+
sub_keys = keys_for_section(flat_config, sub_def)
|
|
674
|
+
next if sub_keys.empty?
|
|
675
|
+
|
|
676
|
+
lines << " #{sub_name}:"
|
|
677
|
+
sub_keys.each do |key|
|
|
678
|
+
value = flat_config[key]
|
|
679
|
+
formatted_value = format_yaml_value(value)
|
|
680
|
+
readonly = readonly_key?(key, sub_def) ? " # read-only" : ""
|
|
681
|
+
lines << " #{key}: #{formatted_value}#{readonly}"
|
|
682
|
+
end
|
|
683
|
+
end
|
|
684
|
+
lines << ""
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
# Validates keys within a wrapper section's subsections.
|
|
688
|
+
#
|
|
689
|
+
# @param errors [Array<String>] accumulator for error messages
|
|
690
|
+
# @param wrapper_name [String] wrapper section name
|
|
691
|
+
# @param wrapper_def [Hash] wrapper definition with subsection definitions
|
|
692
|
+
# @param wrapper_values [Hash] parsed YAML values for this wrapper
|
|
693
|
+
# @return [void]
|
|
694
|
+
def validate_wrapper_section(errors, wrapper_name, wrapper_def, wrapper_values)
|
|
695
|
+
wrapper_values.each do |sub_name, sub_values|
|
|
696
|
+
unless wrapper_def.key?(sub_name.to_sym)
|
|
697
|
+
errors << "Unknown subsection '#{sub_name}' in section '#{wrapper_name}'"
|
|
698
|
+
next
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
next unless sub_values.is_a?(Hash)
|
|
702
|
+
|
|
703
|
+
sub_def = wrapper_def[sub_name.to_sym]
|
|
704
|
+
sub_values.each_key do |key|
|
|
705
|
+
unless key_in_section?(key.to_sym, sub_def)
|
|
706
|
+
errors << "Unknown key '#{key}' in section '#{wrapper_name}/#{sub_name}'"
|
|
707
|
+
end
|
|
708
|
+
end
|
|
709
|
+
end
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
# Yields all leaf section definitions from the sections hash,
|
|
713
|
+
# recursing into wrapper sections.
|
|
714
|
+
#
|
|
715
|
+
# @param sections [Hash] section mapping (may contain wrappers)
|
|
716
|
+
# @return [Array<Hash>] array of leaf section definitions
|
|
717
|
+
def each_leaf_section(sections)
|
|
718
|
+
result = []
|
|
719
|
+
sections.each_value do |section_def|
|
|
720
|
+
if wrapper_section?(section_def)
|
|
721
|
+
section_def.each_value { |sub_def| result << sub_def }
|
|
722
|
+
else
|
|
723
|
+
result << section_def
|
|
724
|
+
end
|
|
725
|
+
end
|
|
726
|
+
result
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
# Flattens a nested section hash to a flat symbol-keyed hash.
|
|
730
|
+
# Handles both 2-level (section -> keys) and 3-level (wrapper -> subsection -> keys) nesting.
|
|
731
|
+
#
|
|
732
|
+
# @param parsed [Hash] nested hash from YAML.safe_load
|
|
733
|
+
# @return [Hash{Symbol => Object}] flat hash
|
|
734
|
+
def flatten_sections(parsed)
|
|
735
|
+
result = {}
|
|
736
|
+
parsed.each_value do |section_values|
|
|
737
|
+
next unless section_values.is_a?(Hash)
|
|
738
|
+
|
|
739
|
+
section_values.each do |key, value|
|
|
740
|
+
if value.is_a?(Hash)
|
|
741
|
+
# 3-level nesting: wrapper -> subsection -> key/value pairs
|
|
742
|
+
value.each do |inner_key, inner_value|
|
|
743
|
+
result[inner_key.to_sym] = inner_value
|
|
744
|
+
end
|
|
745
|
+
else
|
|
746
|
+
result[key.to_sym] = value
|
|
747
|
+
end
|
|
748
|
+
end
|
|
749
|
+
end
|
|
750
|
+
result
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
# Returns the complex key mappings for the given resource type.
|
|
754
|
+
#
|
|
755
|
+
# @param type [Symbol] :vm or :container
|
|
756
|
+
# @return [Hash] complex key mapping hash
|
|
757
|
+
def complex_keys_for(type)
|
|
758
|
+
type == :container ? CT_COMPLEX_KEYS : VM_COMPLEX_KEYS
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
# Finds the complex key spec (parser/serializer) for a given config key.
|
|
762
|
+
# Returns nil if the key is a simple value (not a complex Proxmox string).
|
|
763
|
+
#
|
|
764
|
+
# @param key [Symbol] config key to look up
|
|
765
|
+
# @param type [Symbol] :vm or :container
|
|
766
|
+
# @return [Hash, nil] spec hash with :pattern, :parser, :serializer, or nil
|
|
767
|
+
def find_complex_key(key, type)
|
|
768
|
+
complex_keys_for(type).each_value do |spec|
|
|
769
|
+
return spec if spec[:pattern].match?(key.to_s)
|
|
770
|
+
end
|
|
771
|
+
nil
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
# Builds a nested section hash from flat config, parsing complex values
|
|
775
|
+
# and converting known boolean keys to Ruby booleans.
|
|
776
|
+
#
|
|
777
|
+
# @param flat_config [Hash] flat config hash
|
|
778
|
+
# @param section_def [Hash] section definition with :static and :dynamic
|
|
779
|
+
# @param type [Symbol] :vm or :container
|
|
780
|
+
# @return [Hash{Symbol => Object}] section hash with parsed complex values
|
|
781
|
+
def build_nested_section(flat_config, section_def, type)
|
|
782
|
+
bool_keys = boolean_keys_for(type)
|
|
783
|
+
result = {}
|
|
784
|
+
keys_for_section(flat_config, section_def).each do |key|
|
|
785
|
+
value = flat_config[key]
|
|
786
|
+
complex = find_complex_key(key, type)
|
|
787
|
+
result[key] = if complex && value.is_a?(String)
|
|
788
|
+
parsed = if complex[:default_key]
|
|
789
|
+
send(complex[:parser], value, default_key: complex[:default_key])
|
|
790
|
+
else
|
|
791
|
+
send(complex[:parser], value)
|
|
792
|
+
end
|
|
793
|
+
normalize_cloudinit_volume(parsed)
|
|
794
|
+
elsif bool_keys.include?(key)
|
|
795
|
+
to_boolean(value)
|
|
796
|
+
else
|
|
797
|
+
value
|
|
798
|
+
end
|
|
799
|
+
end
|
|
800
|
+
result
|
|
801
|
+
end
|
|
802
|
+
|
|
803
|
+
# Flattens a nested section hash back to flat config, serializing complex values
|
|
804
|
+
# and converting Ruby booleans back to Proxmox integers (0/1).
|
|
805
|
+
#
|
|
806
|
+
# @param section_hash [Hash] nested section with potentially parsed complex values
|
|
807
|
+
# @param type [Symbol] :vm or :container
|
|
808
|
+
# @param result [Hash] accumulator for flat config
|
|
809
|
+
# @return [void]
|
|
810
|
+
def flatten_nested_section(section_hash, type, result)
|
|
811
|
+
bool_keys = boolean_keys_for(type)
|
|
812
|
+
section_hash.each do |key, value|
|
|
813
|
+
complex = find_complex_key(key, type)
|
|
814
|
+
result[key] = if complex && value.is_a?(Hash)
|
|
815
|
+
send(complex[:serializer], normalize_cloudinit_volume(value))
|
|
816
|
+
elsif bool_keys.include?(key) && (value.is_a?(TrueClass) || value.is_a?(FalseClass))
|
|
817
|
+
from_boolean(value)
|
|
818
|
+
else
|
|
819
|
+
value
|
|
820
|
+
end
|
|
821
|
+
end
|
|
822
|
+
end
|
|
823
|
+
|
|
824
|
+
# Parses a VM network config string into a structured hash.
|
|
825
|
+
# Format: "model=MAC,key=value,..." (e.g., "virtio=AA:BB:CC:DD:EE:FF,bridge=vmbr0,firewall=1")
|
|
826
|
+
# Boolean sub-keys (firewall, link_down) are converted to Ruby booleans.
|
|
827
|
+
#
|
|
828
|
+
# @param string [String] Proxmox VM network config value
|
|
829
|
+
# @return [Hash{Symbol => Object}] parsed network config
|
|
830
|
+
def parse_vm_net_value(string)
|
|
831
|
+
parts = string.split(",")
|
|
832
|
+
first = parts.shift.strip
|
|
833
|
+
model, mac = first.split("=", 2)
|
|
834
|
+
result = { model: model }
|
|
835
|
+
result[:mac] = mac if mac
|
|
836
|
+
parts.each do |part|
|
|
837
|
+
k, v = part.strip.split("=", 2)
|
|
838
|
+
sym = k.to_sym
|
|
839
|
+
result[sym] = NET_BOOLEAN_SUBKEYS.include?(sym) ? to_boolean(v) : v
|
|
840
|
+
end
|
|
841
|
+
result
|
|
842
|
+
end
|
|
843
|
+
|
|
844
|
+
# Serializes a VM network hash back to Proxmox config string format.
|
|
845
|
+
# Converts Ruby booleans back to 0/1 strings.
|
|
846
|
+
#
|
|
847
|
+
# @param hash [Hash{Symbol => Object}] parsed network config
|
|
848
|
+
# @return [String] Proxmox VM network config string
|
|
849
|
+
def serialize_vm_net_value(hash)
|
|
850
|
+
parts = []
|
|
851
|
+
model = hash[:model] || "virtio"
|
|
852
|
+
mac = hash[:mac]
|
|
853
|
+
parts << (mac ? "#{model}=#{mac}" : model)
|
|
854
|
+
hash.except(:model, :mac).each do |k, v|
|
|
855
|
+
v = from_boolean(v) if NET_BOOLEAN_SUBKEYS.include?(k)
|
|
856
|
+
parts << "#{k}=#{v}"
|
|
857
|
+
end
|
|
858
|
+
parts.join(",")
|
|
859
|
+
end
|
|
860
|
+
|
|
861
|
+
# Normalizes cloud-init volume names for manifest portability.
|
|
862
|
+
# Strips the VM-specific prefix (e.g., "vm-100-cloudinit" → "cloudinit")
|
|
863
|
+
# so that pulled manifests can be reused for creating new VMs.
|
|
864
|
+
#
|
|
865
|
+
# @param parsed [Hash, Object] parsed value from a complex key parser
|
|
866
|
+
# @return [Hash, Object] value with normalized cloud-init volume (if applicable)
|
|
867
|
+
def normalize_cloudinit_volume(parsed)
|
|
868
|
+
if parsed.is_a?(Hash) && parsed[:volume]&.include?("cloudinit")
|
|
869
|
+
parsed[:volume] = "cloudinit"
|
|
870
|
+
end
|
|
871
|
+
parsed
|
|
872
|
+
end
|
|
873
|
+
|
|
874
|
+
# Parses a disk config string into a structured hash.
|
|
875
|
+
# Format: "storage:volume,key=value,..." (e.g., "local-lvm:vm-100-disk-0,size=32G,iothread=1")
|
|
876
|
+
# Boolean sub-keys (iothread, backup, replicate, ssd, ro) are converted to Ruby booleans.
|
|
877
|
+
#
|
|
878
|
+
# @param string [String] Proxmox disk config value
|
|
879
|
+
# @return [Hash{Symbol => Object}] parsed disk config
|
|
880
|
+
def parse_disk_value(string)
|
|
881
|
+
parts = string.split(",")
|
|
882
|
+
first = parts.shift.strip
|
|
883
|
+
storage, volume = first.split(":", 2)
|
|
884
|
+
result = { storage: storage }
|
|
885
|
+
result[:volume] = volume if volume
|
|
886
|
+
parts.each do |part|
|
|
887
|
+
k, v = part.strip.split("=", 2)
|
|
888
|
+
sym = k.to_sym
|
|
889
|
+
result[sym] = DISK_BOOLEAN_SUBKEYS.include?(sym) ? to_boolean(v) : v
|
|
890
|
+
end
|
|
891
|
+
result
|
|
892
|
+
end
|
|
893
|
+
|
|
894
|
+
# Serializes a disk hash back to Proxmox config string format.
|
|
895
|
+
# Converts Ruby booleans back to 0/1 strings.
|
|
896
|
+
#
|
|
897
|
+
# @param hash [Hash{Symbol => Object}] parsed disk config
|
|
898
|
+
# @return [String] Proxmox disk config string
|
|
899
|
+
def serialize_disk_value(hash)
|
|
900
|
+
parts = []
|
|
901
|
+
storage = hash[:storage]
|
|
902
|
+
volume = hash[:volume]
|
|
903
|
+
parts << [storage, volume].compact.join(":")
|
|
904
|
+
hash.except(:storage, :volume).each do |k, v|
|
|
905
|
+
v = from_boolean(v) if DISK_BOOLEAN_SUBKEYS.include?(k)
|
|
906
|
+
parts << "#{k}=#{v}"
|
|
907
|
+
end
|
|
908
|
+
parts.join(",")
|
|
909
|
+
end
|
|
910
|
+
|
|
911
|
+
# Parses a generic key=value config string into a hash.
|
|
912
|
+
# Format: "key=value,key=value,..." (e.g., "enabled=1,fstrim_cloned_disks=1")
|
|
913
|
+
#
|
|
914
|
+
# @param string [String] comma-separated key=value string
|
|
915
|
+
# @return [Hash{Symbol => String}] parsed key-value pairs
|
|
916
|
+
def parse_kv_value(string, default_key: nil)
|
|
917
|
+
string.split(",").to_h do |pair|
|
|
918
|
+
pair = pair.strip
|
|
919
|
+
if pair.include?("=")
|
|
920
|
+
k, v = pair.split("=", 2)
|
|
921
|
+
[k.to_sym, v]
|
|
922
|
+
elsif default_key
|
|
923
|
+
[default_key, pair]
|
|
924
|
+
else
|
|
925
|
+
[pair.to_sym, nil]
|
|
926
|
+
end
|
|
927
|
+
end
|
|
928
|
+
end
|
|
929
|
+
|
|
930
|
+
# Serializes a hash back to comma-separated key=value string.
|
|
931
|
+
#
|
|
932
|
+
# @param hash [Hash{Symbol => String}] key-value pairs
|
|
933
|
+
# @return [String] comma-separated key=value string
|
|
934
|
+
def serialize_kv_value(hash)
|
|
935
|
+
hash.map { |k, v| "#{k}=#{v}" }.join(",")
|
|
936
|
+
end
|
|
937
|
+
|
|
938
|
+
# Parses a QEMU Guest Agent config string and fills in default values.
|
|
939
|
+
# Proxmox returns bare "1" for enabled-only, but the full format includes
|
|
940
|
+
# fstrim_cloned_disks, freeze-fs-on-backup, and type.
|
|
941
|
+
# Boolean sub-keys are converted to Ruby booleans.
|
|
942
|
+
#
|
|
943
|
+
# @param string [String] agent config value (e.g., "1" or "enabled=1,fstrim_cloned_disks=1")
|
|
944
|
+
# @return [Hash{Symbol => Object}] parsed agent config with all properties
|
|
945
|
+
def parse_agent_value(string)
|
|
946
|
+
parsed = parse_kv_value(string, default_key: :enabled)
|
|
947
|
+
merged = AGENT_DEFAULTS.merge(parsed)
|
|
948
|
+
merged.each_with_object({}) do |(k, v), h|
|
|
949
|
+
h[k] = AGENT_BOOLEAN_SUBKEYS.include?(k) ? to_boolean(v) : v
|
|
950
|
+
end
|
|
951
|
+
end
|
|
952
|
+
|
|
953
|
+
# Serializes an agent config hash back to Proxmox string, omitting default values.
|
|
954
|
+
# Only includes properties that differ from AGENT_DEFAULTS for a clean config string.
|
|
955
|
+
# Handles both boolean and string input values.
|
|
956
|
+
#
|
|
957
|
+
# @param hash [Hash{Symbol => Object}] agent config hash
|
|
958
|
+
# @return [String] agent config string
|
|
959
|
+
def serialize_agent_value(hash)
|
|
960
|
+
# Normalize booleans back to "0"/"1" strings for comparison with AGENT_DEFAULTS
|
|
961
|
+
string_hash = hash.each_with_object({}) do |(k, v), h|
|
|
962
|
+
h[k] = case v
|
|
963
|
+
when true then "1"
|
|
964
|
+
when false then "0"
|
|
965
|
+
else v.to_s
|
|
966
|
+
end
|
|
967
|
+
end
|
|
968
|
+
non_defaults = string_hash.reject { |k, v| AGENT_DEFAULTS[k] == v }
|
|
969
|
+
return "0" if non_defaults.empty?
|
|
970
|
+
|
|
971
|
+
# Ensure enabled is always first
|
|
972
|
+
parts = []
|
|
973
|
+
parts << "enabled=#{string_hash[:enabled]}" if non_defaults.key?(:enabled)
|
|
974
|
+
non_defaults.each do |k, v|
|
|
975
|
+
next if k == :enabled
|
|
976
|
+
parts << "#{k}=#{v}"
|
|
977
|
+
end
|
|
978
|
+
parts.join(",")
|
|
979
|
+
end
|
|
980
|
+
|
|
981
|
+
# Parses a boot order config string. The order value uses semicolons as separators.
|
|
982
|
+
# Format: "order=scsi0;net0"
|
|
983
|
+
#
|
|
984
|
+
# @param string [String] boot config value
|
|
985
|
+
# @return [Hash{Symbol => Object}] parsed boot config with :order as Array
|
|
986
|
+
def parse_boot_value(string)
|
|
987
|
+
kv = parse_kv_value(string)
|
|
988
|
+
kv[:order] = kv[:order].split(";") if kv[:order].is_a?(String)
|
|
989
|
+
kv
|
|
990
|
+
end
|
|
991
|
+
|
|
992
|
+
# Serializes a boot order hash back to Proxmox config string format.
|
|
993
|
+
# Joins the :order array with semicolons.
|
|
994
|
+
#
|
|
995
|
+
# @param hash [Hash{Symbol => Object}] parsed boot config
|
|
996
|
+
# @return [String] boot config string
|
|
997
|
+
def serialize_boot_value(hash)
|
|
998
|
+
result = hash.transform_values do |v|
|
|
999
|
+
v.is_a?(Array) ? v.join(";") : v
|
|
1000
|
+
end
|
|
1001
|
+
serialize_kv_value(result)
|
|
1002
|
+
end
|
|
1003
|
+
|
|
1004
|
+
# Parses a hotplug config string into a capability map with boolean values.
|
|
1005
|
+
# Proxmox hotplug is a CSV of enabled capabilities (e.g., "network,disk,usb").
|
|
1006
|
+
# Special values: "0" = all disabled, "1" = default (network,disk,usb).
|
|
1007
|
+
#
|
|
1008
|
+
# @param string [String] hotplug config value
|
|
1009
|
+
# @return [Hash{Symbol => Boolean}] map of all capabilities with true/false
|
|
1010
|
+
def parse_hotplug_value(string)
|
|
1011
|
+
if string == "0"
|
|
1012
|
+
HOTPLUG_CAPABILITIES.to_h { |cap| [cap, false] }
|
|
1013
|
+
elsif string == "1"
|
|
1014
|
+
parse_hotplug_value("network,disk,usb")
|
|
1015
|
+
else
|
|
1016
|
+
enabled = string.split(",").map { |s| s.strip.to_sym }
|
|
1017
|
+
HOTPLUG_CAPABILITIES.to_h { |cap| [cap, enabled.include?(cap)] }
|
|
1018
|
+
end
|
|
1019
|
+
end
|
|
1020
|
+
|
|
1021
|
+
# Serializes a hotplug capability map back to Proxmox CSV format.
|
|
1022
|
+
# Only enabled capabilities are included. Returns "0" if all disabled.
|
|
1023
|
+
#
|
|
1024
|
+
# @param hash [Hash{Symbol => Boolean}] capability map
|
|
1025
|
+
# @return [String] hotplug config string
|
|
1026
|
+
def serialize_hotplug_value(hash)
|
|
1027
|
+
enabled = HOTPLUG_CAPABILITIES.select { |cap| hash[cap] == true }
|
|
1028
|
+
return "0" if enabled.empty?
|
|
1029
|
+
|
|
1030
|
+
enabled.map(&:to_s).join(",")
|
|
1031
|
+
end
|
|
1032
|
+
end
|
|
1033
|
+
end
|
|
1034
|
+
end
|