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,331 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pvectl
|
|
4
|
+
module Commands
|
|
5
|
+
# Handler for the `pvectl clone vm` command.
|
|
6
|
+
#
|
|
7
|
+
# Clones a VM by VMID, supporting full and linked clones,
|
|
8
|
+
# custom name, target node, storage, pool, and description.
|
|
9
|
+
# No batch operations - clones exactly one VM at a time.
|
|
10
|
+
#
|
|
11
|
+
# @example Full clone with auto-generated VMID
|
|
12
|
+
# pvectl clone vm 100
|
|
13
|
+
#
|
|
14
|
+
# @example Clone with custom name and target VMID
|
|
15
|
+
# pvectl clone vm 100 --vmid 200 --name web-clone
|
|
16
|
+
#
|
|
17
|
+
# @example Linked clone to different node
|
|
18
|
+
# pvectl clone vm 100 --linked --target pve2
|
|
19
|
+
#
|
|
20
|
+
class CloneVm
|
|
21
|
+
include SharedConfigParsers
|
|
22
|
+
|
|
23
|
+
# Registers the clone command with the CLI.
|
|
24
|
+
#
|
|
25
|
+
# @param cli [GLI::App] the CLI application object
|
|
26
|
+
# @return [void]
|
|
27
|
+
def self.register(cli)
|
|
28
|
+
cli.desc "Clone a resource"
|
|
29
|
+
cli.long_desc <<~HELP
|
|
30
|
+
Clone a virtual machine or container, optionally modifying the
|
|
31
|
+
configuration of the clone (CPU, memory, disks, network).
|
|
32
|
+
|
|
33
|
+
Supports full clones (independent copy) and linked clones (shares
|
|
34
|
+
base image with source — requires source to be a template).
|
|
35
|
+
|
|
36
|
+
EXAMPLES
|
|
37
|
+
Clone a VM to the same node:
|
|
38
|
+
$ pvectl clone vm 100 --name web-clone
|
|
39
|
+
|
|
40
|
+
Clone to a different node:
|
|
41
|
+
$ pvectl clone vm 100 --name web-prod --target pve2
|
|
42
|
+
|
|
43
|
+
Clone with modified configuration:
|
|
44
|
+
$ pvectl clone vm 100 --name web-prod --cores 4 --memory 8192
|
|
45
|
+
|
|
46
|
+
Linked clone (thin provisioning, requires template):
|
|
47
|
+
$ pvectl clone vm 100 --linked --name thin-clone
|
|
48
|
+
|
|
49
|
+
Clone a container with new network config:
|
|
50
|
+
$ pvectl clone ct 200 --name db-clone --memory 4096 --net bridge=vmbr1
|
|
51
|
+
|
|
52
|
+
Clone with explicit new ID:
|
|
53
|
+
$ pvectl clone vm 100 --newid 150 --name web-test
|
|
54
|
+
|
|
55
|
+
NOTES
|
|
56
|
+
Config modification is a two-step process: clone first, then update
|
|
57
|
+
configuration via the Proxmox API. If the config update fails, the
|
|
58
|
+
clone still exists but with the original configuration.
|
|
59
|
+
|
|
60
|
+
Linked clones share the base disk with the source. They are faster
|
|
61
|
+
to create and use less storage, but the source cannot be deleted.
|
|
62
|
+
|
|
63
|
+
If --name is not specified, Proxmox auto-generates a name.
|
|
64
|
+
|
|
65
|
+
SEE ALSO
|
|
66
|
+
pvectl help create Create new VMs/containers from scratch
|
|
67
|
+
pvectl help migrate Move resources between nodes
|
|
68
|
+
pvectl help template Convert to template for linked clones
|
|
69
|
+
HELP
|
|
70
|
+
cli.arg_name "RESOURCE_TYPE ID"
|
|
71
|
+
cli.command :clone do |c|
|
|
72
|
+
c.desc "Name/hostname for the new resource"
|
|
73
|
+
c.flag [:name, :n], arg_name: "NAME"
|
|
74
|
+
|
|
75
|
+
c.desc "ID for the new resource (auto-selected if not specified)"
|
|
76
|
+
c.flag [:newid], type: Integer, arg_name: "ID"
|
|
77
|
+
|
|
78
|
+
c.desc "Target node for the clone"
|
|
79
|
+
c.flag [:target, :t], arg_name: "NODE"
|
|
80
|
+
|
|
81
|
+
c.desc "Target storage for the clone"
|
|
82
|
+
c.flag [:storage, :s], arg_name: "STORAGE"
|
|
83
|
+
|
|
84
|
+
c.desc "Create a linked clone (requires source to be a template)"
|
|
85
|
+
c.switch [:linked], negatable: false
|
|
86
|
+
|
|
87
|
+
c.desc "Resource pool for the new resource"
|
|
88
|
+
c.flag [:pool, :p], arg_name: "POOL"
|
|
89
|
+
|
|
90
|
+
c.desc "Description for the new resource"
|
|
91
|
+
c.flag [:description, :d], arg_name: "DESCRIPTION"
|
|
92
|
+
|
|
93
|
+
c.desc "Timeout in seconds for sync operations (default: 300)"
|
|
94
|
+
c.flag [:timeout], type: Integer, arg_name: "SECONDS"
|
|
95
|
+
|
|
96
|
+
c.desc "Async mode (return task ID immediately)"
|
|
97
|
+
c.switch [:async], negatable: false
|
|
98
|
+
|
|
99
|
+
c.desc "Skip confirmation prompt"
|
|
100
|
+
c.switch [:yes, :y], negatable: false
|
|
101
|
+
|
|
102
|
+
# Shared config flags for VM/container modification after clone
|
|
103
|
+
SharedFlags.common_config(c)
|
|
104
|
+
SharedFlags.vm_config(c)
|
|
105
|
+
SharedFlags.container_config(c)
|
|
106
|
+
|
|
107
|
+
c.action do |global_options, options, args|
|
|
108
|
+
resource_type = args.shift
|
|
109
|
+
|
|
110
|
+
exit_code = case resource_type
|
|
111
|
+
when "vm"
|
|
112
|
+
Commands::CloneVm.execute(args, options, global_options)
|
|
113
|
+
when "container", "ct"
|
|
114
|
+
Commands::CloneContainer.execute(args, options, global_options)
|
|
115
|
+
else
|
|
116
|
+
$stderr.puts "Error: Unknown resource type: #{resource_type}"
|
|
117
|
+
$stderr.puts "Valid types: vm, container, ct"
|
|
118
|
+
ExitCodes::USAGE_ERROR
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
exit exit_code if exit_code != 0
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Executes the clone VM command.
|
|
127
|
+
#
|
|
128
|
+
# @param args [Array<String>] command arguments (VMID)
|
|
129
|
+
# @param options [Hash] command options
|
|
130
|
+
# @param global_options [Hash] global CLI options
|
|
131
|
+
# @return [Integer] exit code
|
|
132
|
+
def self.execute(args, options, global_options)
|
|
133
|
+
new(args, options, global_options).execute
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Initializes a clone VM command.
|
|
137
|
+
#
|
|
138
|
+
# @param args [Array<String>] command arguments
|
|
139
|
+
# @param options [Hash] command options
|
|
140
|
+
# @param global_options [Hash] global CLI options
|
|
141
|
+
def initialize(args, options, global_options)
|
|
142
|
+
@args = args
|
|
143
|
+
@options = options
|
|
144
|
+
@global_options = global_options
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Executes the clone VM command.
|
|
148
|
+
#
|
|
149
|
+
# Builds config params from shared flags, validates async+config
|
|
150
|
+
# compatibility, and delegates to the clone operation.
|
|
151
|
+
#
|
|
152
|
+
# @return [Integer] exit code
|
|
153
|
+
def execute
|
|
154
|
+
vmid = @args.first
|
|
155
|
+
return usage_error("Source VMID required") unless vmid
|
|
156
|
+
|
|
157
|
+
config_params = build_vm_config_params
|
|
158
|
+
|
|
159
|
+
if @options[:async] && !config_params.empty?
|
|
160
|
+
return usage_error("Config flags require sync mode (remove --async)")
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
perform_clone(vmid.to_i, config_params)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
private
|
|
167
|
+
|
|
168
|
+
# Performs the clone operation.
|
|
169
|
+
#
|
|
170
|
+
# When config params are present, displays a summary and prompts
|
|
171
|
+
# for confirmation before proceeding. Passes config_params to the
|
|
172
|
+
# service for the two-step clone+configure flow.
|
|
173
|
+
#
|
|
174
|
+
# @param vmid [Integer] source VM identifier
|
|
175
|
+
# @param config_params [Hash] VM config parameters to apply after clone
|
|
176
|
+
# @return [Integer] exit code
|
|
177
|
+
def perform_clone(vmid, config_params)
|
|
178
|
+
unless config_params.empty?
|
|
179
|
+
return ExitCodes::SUCCESS if display_clone_summary(vmid, config_params) == :cancelled
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
load_config
|
|
183
|
+
connection = Pvectl::Connection.new(@config)
|
|
184
|
+
|
|
185
|
+
vm_repo = Pvectl::Repositories::Vm.new(connection)
|
|
186
|
+
task_repo = Pvectl::Repositories::Task.new(connection)
|
|
187
|
+
|
|
188
|
+
service = Pvectl::Services::CloneVm.new(
|
|
189
|
+
vm_repository: vm_repo,
|
|
190
|
+
task_repository: task_repo,
|
|
191
|
+
options: service_options
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
result = service.execute(
|
|
195
|
+
vmid: vmid,
|
|
196
|
+
new_vmid: @options[:newid]&.to_i,
|
|
197
|
+
name: @options[:name],
|
|
198
|
+
target_node: @options[:target],
|
|
199
|
+
storage: @options[:storage],
|
|
200
|
+
linked: @options[:linked],
|
|
201
|
+
pool: @options[:pool],
|
|
202
|
+
description: @options[:description],
|
|
203
|
+
config_params: config_params
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
print_progress(result) if !@options[:async] && result.vm
|
|
207
|
+
|
|
208
|
+
output_result(result)
|
|
209
|
+
result.failed? ? ExitCodes::GENERAL_ERROR : ExitCodes::SUCCESS
|
|
210
|
+
rescue Pvectl::Config::ConfigNotFoundError,
|
|
211
|
+
Pvectl::Config::InvalidConfigError,
|
|
212
|
+
Pvectl::Config::ContextNotFoundError,
|
|
213
|
+
Pvectl::Config::ClusterNotFoundError,
|
|
214
|
+
Pvectl::Config::UserNotFoundError
|
|
215
|
+
raise
|
|
216
|
+
rescue StandardError => e
|
|
217
|
+
$stderr.puts "Error: #{e.message}"
|
|
218
|
+
ExitCodes::GENERAL_ERROR
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Prints progress message for sync mode.
|
|
222
|
+
#
|
|
223
|
+
# @param result [Models::OperationResult] clone result
|
|
224
|
+
# @return [void]
|
|
225
|
+
def print_progress(result)
|
|
226
|
+
source = result.vm
|
|
227
|
+
new_name = result.resource&.dig(:name) || "clone"
|
|
228
|
+
new_id = result.resource&.dig(:new_vmid)
|
|
229
|
+
$stderr.puts "Cloning VM #{source.vmid} (#{source.name || 'unnamed'}) to #{new_id} (#{new_name})..."
|
|
230
|
+
$stderr.puts ""
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Loads configuration from file or environment.
|
|
234
|
+
#
|
|
235
|
+
# @return [void]
|
|
236
|
+
def load_config
|
|
237
|
+
service = Pvectl::Config::Service.new
|
|
238
|
+
service.load(config: @global_options[:config])
|
|
239
|
+
@config = service.current_config
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Builds service options from command options.
|
|
243
|
+
#
|
|
244
|
+
# @return [Hash] service options
|
|
245
|
+
def service_options
|
|
246
|
+
opts = {}
|
|
247
|
+
opts[:timeout] = @options[:timeout] if @options[:timeout]
|
|
248
|
+
opts[:async] = true if @options[:async]
|
|
249
|
+
opts[:start] = true if @options[:start]
|
|
250
|
+
opts
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Outputs operation result using the configured formatter.
|
|
254
|
+
#
|
|
255
|
+
# @param result [Models::OperationResult] operation result
|
|
256
|
+
# @return [void]
|
|
257
|
+
def output_result(result)
|
|
258
|
+
presenter = Pvectl::Presenters::VmOperationResult.new
|
|
259
|
+
format = @global_options[:output] || "table"
|
|
260
|
+
color_flag = @global_options[:color]
|
|
261
|
+
|
|
262
|
+
formatter = Pvectl::Formatters::Registry.for(format)
|
|
263
|
+
output = formatter.format([result], presenter, color: color_flag)
|
|
264
|
+
puts output
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Displays clone summary with config changes and prompts for confirmation.
|
|
268
|
+
#
|
|
269
|
+
# Only called when config params are present. Shows source/target info
|
|
270
|
+
# and the config changes that will be applied after cloning.
|
|
271
|
+
#
|
|
272
|
+
# @param vmid [Integer] source VM identifier
|
|
273
|
+
# @param config_params [Hash] config parameters to display
|
|
274
|
+
# @return [Symbol, nil] +:cancelled+ if user declines, +nil+ otherwise
|
|
275
|
+
def display_clone_summary(vmid, config_params)
|
|
276
|
+
$stdout.puts ""
|
|
277
|
+
$stdout.puts " Clone VM - Summary"
|
|
278
|
+
$stdout.puts " #{'─' * 40}"
|
|
279
|
+
$stdout.puts " Source: #{vmid}"
|
|
280
|
+
$stdout.puts " New ID: #{@options[:newid] || '(auto)'}"
|
|
281
|
+
$stdout.puts " Name: #{@options[:name] || '(auto)'}"
|
|
282
|
+
target_display = @options[:target] ? "→ #{@options[:target]}" : "(same)"
|
|
283
|
+
$stdout.puts " Node: #{target_display}"
|
|
284
|
+
$stdout.puts " Storage: #{@options[:storage]}" if @options[:storage]
|
|
285
|
+
display_config_changes(config_params)
|
|
286
|
+
$stdout.puts " #{'─' * 40}"
|
|
287
|
+
$stdout.puts ""
|
|
288
|
+
|
|
289
|
+
return nil if @options[:yes]
|
|
290
|
+
|
|
291
|
+
$stdout.print "Clone and configure this VM? [y/N] "
|
|
292
|
+
$stdout.flush
|
|
293
|
+
answer = $stdin.gets&.strip&.downcase
|
|
294
|
+
answer == "y" ? nil : :cancelled
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Displays the config changes section of the clone summary.
|
|
298
|
+
#
|
|
299
|
+
# @param params [Hash] config parameters
|
|
300
|
+
# @return [void]
|
|
301
|
+
def display_config_changes(params)
|
|
302
|
+
$stdout.puts " ── Config changes #{'─' * 23}"
|
|
303
|
+
$stdout.puts " CPU: #{params[:cores]} cores" if params[:cores]
|
|
304
|
+
$stdout.puts " Sockets: #{params[:sockets]}" if params[:sockets]
|
|
305
|
+
$stdout.puts " Memory: #{params[:memory]} MB" if params[:memory]
|
|
306
|
+
if params[:disks]
|
|
307
|
+
params[:disks].each_with_index do |d, i|
|
|
308
|
+
$stdout.puts " Disk#{i}: #{d[:storage]}, #{d[:size]}"
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
if params[:nets]
|
|
312
|
+
params[:nets].each_with_index do |n, i|
|
|
313
|
+
$stdout.puts " Net#{i}: #{n[:bridge]}"
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
$stdout.puts " OS Type: #{params[:ostype]}" if params[:ostype]
|
|
317
|
+
$stdout.puts " Agent: enabled" if params[:agent]
|
|
318
|
+
$stdout.puts " Tags: #{params[:tags]}" if params[:tags]
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Outputs usage error and returns exit code.
|
|
322
|
+
#
|
|
323
|
+
# @param message [String] error message
|
|
324
|
+
# @return [Integer] exit code
|
|
325
|
+
def usage_error(message)
|
|
326
|
+
$stderr.puts "Error: #{message}"
|
|
327
|
+
ExitCodes::USAGE_ERROR
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pvectl
|
|
4
|
+
module Commands
|
|
5
|
+
# Namespace for the +pvectl cloudinit+ command group.
|
|
6
|
+
#
|
|
7
|
+
# Provides three subcommands that operate on cloud-init configuration
|
|
8
|
+
# of QEMU VMs:
|
|
9
|
+
#
|
|
10
|
+
# - +regenerate vm <id>+ — rebuild the cloud-init ISO
|
|
11
|
+
# - +pending vm <id>+ — list pending configuration changes
|
|
12
|
+
# - +dump vm <id> <type>+ — print generated cloud-init YAML
|
|
13
|
+
#
|
|
14
|
+
# All subcommands share a common service-construction helper and a
|
|
15
|
+
# unified error-mapping path that distinguishes usage errors from
|
|
16
|
+
# not-found and connection errors.
|
|
17
|
+
module Cloudinit
|
|
18
|
+
# Registers the cloudinit command group with the CLI.
|
|
19
|
+
#
|
|
20
|
+
# @param cli [GLI::App] the CLI application object
|
|
21
|
+
# @return [void]
|
|
22
|
+
def self.register(cli)
|
|
23
|
+
cli.desc "Manage cloud-init configuration for VMs"
|
|
24
|
+
cli.long_desc <<~HELP
|
|
25
|
+
DESCRIPTION
|
|
26
|
+
Manage cloud-init configuration for QEMU virtual machines.
|
|
27
|
+
Cloud-init lets you configure users, SSH keys, network, and
|
|
28
|
+
other guest-side settings without baking them into the
|
|
29
|
+
template image.
|
|
30
|
+
|
|
31
|
+
Cloud-init is a VM-only feature — LXC containers do not
|
|
32
|
+
expose cloud-init endpoints.
|
|
33
|
+
|
|
34
|
+
SUBCOMMANDS
|
|
35
|
+
cloudinit regenerate vm ID Rebuild the cloud-init ISO
|
|
36
|
+
cloudinit pending vm ID List pending changes
|
|
37
|
+
cloudinit dump vm ID TYPE Print generated YAML
|
|
38
|
+
|
|
39
|
+
EXAMPLES
|
|
40
|
+
Apply pending cloud-init changes:
|
|
41
|
+
$ pvectl cloudinit regenerate vm 100
|
|
42
|
+
|
|
43
|
+
Preview what would change on next regenerate:
|
|
44
|
+
$ pvectl cloudinit pending vm 100
|
|
45
|
+
|
|
46
|
+
Inspect the user-data that the guest will see:
|
|
47
|
+
$ pvectl cloudinit dump vm 100 user
|
|
48
|
+
|
|
49
|
+
NOTES
|
|
50
|
+
+TYPE+ for +dump+ must be one of: +user+, +network+, +meta+.
|
|
51
|
+
|
|
52
|
+
SEE ALSO
|
|
53
|
+
pvectl help edit vm Edit cloud-init keys (cipassword, sshkeys, etc.)
|
|
54
|
+
HELP
|
|
55
|
+
cli.command :cloudinit do |c|
|
|
56
|
+
c.desc "Source node (skips VMID lookup)"
|
|
57
|
+
c.flag [:node, :n], arg_name: "NODE"
|
|
58
|
+
|
|
59
|
+
Regenerate.register_subcommand(c)
|
|
60
|
+
Pending.register_subcommand(c)
|
|
61
|
+
Dump.register_subcommand(c)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Builds a fresh +Services::Cloudinit+ from the active configuration
|
|
66
|
+
# and yields it to the caller. Maps known exceptions to pvectl exit
|
|
67
|
+
# codes — anything else bubbles up to +CLI.on_error+.
|
|
68
|
+
#
|
|
69
|
+
# @param global_options [Hash] global CLI options (includes :config)
|
|
70
|
+
# @yieldparam service [Pvectl::Services::Cloudinit] cloudinit service
|
|
71
|
+
# @return [Integer] exit code
|
|
72
|
+
def self.with_service(global_options)
|
|
73
|
+
config_service = Pvectl::Config::Service.new
|
|
74
|
+
config_service.load(config: global_options[:config])
|
|
75
|
+
config = config_service.current_config
|
|
76
|
+
|
|
77
|
+
connection = Pvectl::Connection.new(config)
|
|
78
|
+
service = Pvectl::Services::Cloudinit.new(
|
|
79
|
+
vm_repository: Pvectl::Repositories::Vm.new(connection),
|
|
80
|
+
resource_resolver: Pvectl::Utils::ResourceResolver.new(connection)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
yield service
|
|
84
|
+
Pvectl::ExitCodes::SUCCESS
|
|
85
|
+
rescue Pvectl::ResourceNotFoundError => e
|
|
86
|
+
$stderr.puts "Error: #{e.message}"
|
|
87
|
+
Pvectl::ExitCodes::NOT_FOUND
|
|
88
|
+
rescue ArgumentError => e
|
|
89
|
+
$stderr.puts "Error: #{e.message}"
|
|
90
|
+
Pvectl::ExitCodes::USAGE_ERROR
|
|
91
|
+
rescue Pvectl::Config::ConfigNotFoundError,
|
|
92
|
+
Pvectl::Config::InvalidConfigError,
|
|
93
|
+
Pvectl::Config::ContextNotFoundError,
|
|
94
|
+
Pvectl::Config::ClusterNotFoundError,
|
|
95
|
+
Pvectl::Config::UserNotFoundError
|
|
96
|
+
raise
|
|
97
|
+
rescue StandardError => e
|
|
98
|
+
$stderr.puts "Error: #{e.message}"
|
|
99
|
+
Pvectl::ExitCodes::GENERAL_ERROR
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Prints a usage error and returns the standard usage exit code.
|
|
103
|
+
#
|
|
104
|
+
# @param message [String] message printed to stderr
|
|
105
|
+
# @return [Integer] usage error exit code
|
|
106
|
+
def self.usage_error(message)
|
|
107
|
+
$stderr.puts "Error: #{message}"
|
|
108
|
+
Pvectl::ExitCodes::USAGE_ERROR
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Prints the standard "unknown resource type" error.
|
|
112
|
+
#
|
|
113
|
+
# @param resource_type [String] the unrecognised resource type
|
|
114
|
+
# @return [Integer] usage error exit code
|
|
115
|
+
def self.unknown_resource_type(resource_type)
|
|
116
|
+
$stderr.puts "Error: Unknown or invalid resource type for cloudinit: #{resource_type}"
|
|
117
|
+
$stderr.puts "Valid types: vm"
|
|
118
|
+
Pvectl::ExitCodes::USAGE_ERROR
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pvectl
|
|
4
|
+
module Commands
|
|
5
|
+
module Cloudinit
|
|
6
|
+
# Handler for the `pvectl cloudinit dump vm <id> <type>` subcommand.
|
|
7
|
+
#
|
|
8
|
+
# Prints the generated cloud-init configuration to stdout as raw
|
|
9
|
+
# YAML/text. The +type+ argument selects which document is dumped:
|
|
10
|
+
# +user+ (user-data), +network+ (network-config), or +meta+
|
|
11
|
+
# (meta-data).
|
|
12
|
+
#
|
|
13
|
+
# @example Usage
|
|
14
|
+
# pvectl cloudinit dump vm 100 user
|
|
15
|
+
# pvectl cloudinit dump vm 100 network
|
|
16
|
+
#
|
|
17
|
+
class Dump
|
|
18
|
+
# Registers the dump subcommand under the cloudinit parent.
|
|
19
|
+
#
|
|
20
|
+
# @param parent [GLI::Command] parent cloudinit command
|
|
21
|
+
# @return [void]
|
|
22
|
+
def self.register_subcommand(parent)
|
|
23
|
+
parent.desc "Dump generated cloud-init configuration for a VM"
|
|
24
|
+
parent.long_desc <<~HELP
|
|
25
|
+
DESCRIPTION
|
|
26
|
+
Print the auto-generated cloud-init configuration that would
|
|
27
|
+
be served to the guest. The +TYPE+ argument selects which
|
|
28
|
+
cloud-init document to display:
|
|
29
|
+
|
|
30
|
+
user User-data (#cloud-config YAML)
|
|
31
|
+
network Network-config (cloud-init network spec)
|
|
32
|
+
meta Meta-data (instance-id, hostname)
|
|
33
|
+
|
|
34
|
+
EXAMPLES
|
|
35
|
+
Dump user-data:
|
|
36
|
+
$ pvectl cloudinit dump vm 100 user
|
|
37
|
+
|
|
38
|
+
Dump network-config:
|
|
39
|
+
$ pvectl cloudinit dump vm 100 network
|
|
40
|
+
|
|
41
|
+
Pipe to a file:
|
|
42
|
+
$ pvectl cloudinit dump vm 100 user > user-data.yml
|
|
43
|
+
|
|
44
|
+
NOTES
|
|
45
|
+
The output is the raw payload returned by Proxmox — no
|
|
46
|
+
additional formatting is applied. This means +-o json+ is
|
|
47
|
+
ignored for this subcommand.
|
|
48
|
+
|
|
49
|
+
SEE ALSO
|
|
50
|
+
pvectl help cloudinit regenerate Rebuild the ISO
|
|
51
|
+
pvectl help cloudinit pending Show pending changes
|
|
52
|
+
HELP
|
|
53
|
+
parent.arg_name "RESOURCE_TYPE ID TYPE"
|
|
54
|
+
parent.command :dump do |c|
|
|
55
|
+
c.action do |global_options, options, args|
|
|
56
|
+
exit_code = execute(args, options, global_options)
|
|
57
|
+
exit exit_code if exit_code != 0
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Valid cloud-init dump types.
|
|
63
|
+
VALID_TYPES = %w[user network meta].freeze
|
|
64
|
+
|
|
65
|
+
# Executes the dump subcommand.
|
|
66
|
+
#
|
|
67
|
+
# @param args [Array<String>] command arguments (RESOURCE_TYPE, ID, TYPE)
|
|
68
|
+
# @param options [Hash] command-local options
|
|
69
|
+
# @param global_options [Hash] global CLI options
|
|
70
|
+
# @return [Integer] exit code
|
|
71
|
+
def self.execute(args, options, global_options)
|
|
72
|
+
resource_type = args[0]
|
|
73
|
+
vmid_arg = args[1]
|
|
74
|
+
type = args[2]
|
|
75
|
+
|
|
76
|
+
return Cloudinit.usage_error("Resource type required (vm)") unless resource_type
|
|
77
|
+
return Cloudinit.usage_error("VMID is required") unless vmid_arg
|
|
78
|
+
return Cloudinit.usage_error("Config TYPE is required (user, network, meta)") unless type
|
|
79
|
+
return Cloudinit.unknown_resource_type(resource_type) unless resource_type == "vm"
|
|
80
|
+
unless VALID_TYPES.include?(type)
|
|
81
|
+
return Cloudinit.usage_error(
|
|
82
|
+
"Invalid config type: #{type} (valid: #{VALID_TYPES.join(', ')})"
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
Cloudinit.with_service(global_options) do |service|
|
|
87
|
+
yaml = service.dump(vmid_arg.to_i, type, node: options[:node])
|
|
88
|
+
$stdout.puts yaml
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pvectl
|
|
4
|
+
module Commands
|
|
5
|
+
module Cloudinit
|
|
6
|
+
# Handler for the `pvectl cloudinit pending vm <id>` subcommand.
|
|
7
|
+
#
|
|
8
|
+
# Lists cloud-init configuration entries currently differing from
|
|
9
|
+
# the values used to build the active ISO. Each entry contains a
|
|
10
|
+
# key, the current value, the pending value, and a delete flag.
|
|
11
|
+
#
|
|
12
|
+
# @example Usage
|
|
13
|
+
# pvectl cloudinit pending vm 100
|
|
14
|
+
# pvectl cloudinit pending vm 100 -o json
|
|
15
|
+
#
|
|
16
|
+
class Pending
|
|
17
|
+
# Registers the pending subcommand under the cloudinit parent.
|
|
18
|
+
#
|
|
19
|
+
# @param parent [GLI::Command] parent cloudinit command
|
|
20
|
+
# @return [void]
|
|
21
|
+
def self.register_subcommand(parent)
|
|
22
|
+
parent.desc "List pending cloud-init configuration changes"
|
|
23
|
+
parent.long_desc <<~HELP
|
|
24
|
+
DESCRIPTION
|
|
25
|
+
Show cloud-init configuration entries that differ from the
|
|
26
|
+
values currently embedded in the active cloud-init ISO. Use
|
|
27
|
+
this to preview what will change on the next regeneration.
|
|
28
|
+
|
|
29
|
+
EXAMPLES
|
|
30
|
+
Show pending changes for VM 100:
|
|
31
|
+
$ pvectl cloudinit pending vm 100
|
|
32
|
+
|
|
33
|
+
JSON output for scripting:
|
|
34
|
+
$ pvectl cloudinit pending vm 100 -o json
|
|
35
|
+
|
|
36
|
+
NOTES
|
|
37
|
+
An empty list means the active ISO is in sync with the
|
|
38
|
+
current Proxmox configuration.
|
|
39
|
+
|
|
40
|
+
Entries with the +action+ column set to +delete+ are about
|
|
41
|
+
to be removed from the ISO.
|
|
42
|
+
|
|
43
|
+
SEE ALSO
|
|
44
|
+
pvectl help cloudinit regenerate Apply pending changes
|
|
45
|
+
pvectl help cloudinit dump Inspect generated YAML
|
|
46
|
+
HELP
|
|
47
|
+
parent.arg_name "RESOURCE_TYPE ID"
|
|
48
|
+
parent.command :pending do |c|
|
|
49
|
+
c.action do |global_options, options, args|
|
|
50
|
+
exit_code = execute(args, options, global_options)
|
|
51
|
+
exit exit_code if exit_code != 0
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Executes the pending subcommand.
|
|
57
|
+
#
|
|
58
|
+
# @param args [Array<String>] command arguments
|
|
59
|
+
# @param options [Hash] command-local options
|
|
60
|
+
# @param global_options [Hash] global CLI options
|
|
61
|
+
# @return [Integer] exit code
|
|
62
|
+
def self.execute(args, options, global_options)
|
|
63
|
+
resource_type = args[0]
|
|
64
|
+
vmid_arg = args[1]
|
|
65
|
+
|
|
66
|
+
return Cloudinit.usage_error("Resource type required (vm)") unless resource_type
|
|
67
|
+
return Cloudinit.usage_error("VMID is required") unless vmid_arg
|
|
68
|
+
return Cloudinit.unknown_resource_type(resource_type) unless resource_type == "vm"
|
|
69
|
+
|
|
70
|
+
Cloudinit.with_service(global_options) do |service|
|
|
71
|
+
entries = service.pending(vmid_arg.to_i, node: options[:node])
|
|
72
|
+
render(entries, global_options)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Renders pending entries via the configured output formatter.
|
|
77
|
+
#
|
|
78
|
+
# For +table+ output, prints a flat 4-column table (key, current,
|
|
79
|
+
# pending, action) or a friendly "no pending changes" notice when
|
|
80
|
+
# the list is empty. For +json+/+yaml+ output, emits the raw
|
|
81
|
+
# collection so it can be parsed downstream.
|
|
82
|
+
#
|
|
83
|
+
# @param entries [Array<Hash>] pending entries from the service
|
|
84
|
+
# @param global_options [Hash] global CLI options
|
|
85
|
+
# @return [void]
|
|
86
|
+
def self.render(entries, global_options)
|
|
87
|
+
format = global_options[:output] || "table"
|
|
88
|
+
|
|
89
|
+
case format
|
|
90
|
+
when "json"
|
|
91
|
+
require "json"
|
|
92
|
+
$stdout.puts JSON.pretty_generate(entries)
|
|
93
|
+
when "yaml"
|
|
94
|
+
require "yaml"
|
|
95
|
+
$stdout.puts entries.map { |e| stringify_keys(e) }.to_yaml
|
|
96
|
+
else
|
|
97
|
+
render_table(entries)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Renders pending entries as a plain text table.
|
|
102
|
+
#
|
|
103
|
+
# @param entries [Array<Hash>] pending entries
|
|
104
|
+
# @return [void]
|
|
105
|
+
def self.render_table(entries)
|
|
106
|
+
if entries.empty?
|
|
107
|
+
$stdout.puts "No pending cloud-init changes."
|
|
108
|
+
return
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
rows = entries.map do |e|
|
|
112
|
+
action = e[:delete].to_i.positive? ? "delete" : (e[:pending] ? "update" : "-")
|
|
113
|
+
[e[:key].to_s, (e[:value] || "-").to_s, (e[:pending] || "-").to_s, action]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
headers = %w[KEY CURRENT PENDING ACTION]
|
|
117
|
+
widths = headers.each_with_index.map do |h, i|
|
|
118
|
+
[h.length, *rows.map { |r| r[i].length }].max
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
$stdout.puts headers.each_with_index.map { |h, i| h.ljust(widths[i]) }.join(" ")
|
|
122
|
+
rows.each do |row|
|
|
123
|
+
$stdout.puts row.each_with_index.map { |v, i| v.ljust(widths[i]) }.join(" ")
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Stringifies hash keys for YAML output consistency.
|
|
128
|
+
#
|
|
129
|
+
# @param hash [Hash] hash with symbol or string keys
|
|
130
|
+
# @return [Hash] hash with string keys
|
|
131
|
+
def self.stringify_keys(hash)
|
|
132
|
+
hash.each_with_object({}) { |(k, v), out| out[k.to_s] = v }
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|