Module: Pvectl::ConfigSerializer
- Defined in:
- lib/pvectl/config_serializer.rb
Overview
Converts flat Proxmox config hashes into nested, section-grouped YAML and back. Used by the ‘edit` command to present VM/container configuration in a human-friendly, structured format.
All methods are class-level; no instance state is needed.
Constant Summary collapse
- VM_SECTIONS =
Section mappings for QEMU VMs. Each section maps to an array of static keys and an array of dynamic key patterns. Keys marked as read-only are listed separately. Sections without a :static key are “wrapper” sections containing named subsections.
{ general: { static: %i[vmid name description tags template lock digest], dynamic: [], readonly: %i[vmid template lock digest] }, hardware: { cpu: { static: %i[cores sockets cpu cpulimit cpuunits numa affinity], dynamic: [/\Anuma\d+\z/], readonly: [] }, memory: { static: %i[memory balloon shares hugepages keephugepages], dynamic: [], readonly: [] }, disks: { static: %i[efidisk0 tpmstate0], dynamic: [/\Ascsi\d+\z/, /\Aide\d+\z/, /\Avirtio\d+\z/, /\Asata\d+\z/, /\Aunused\d+\z/], readonly: [/\Aunused\d+\z/] }, network: { static: [], dynamic: [/\Anet\d+\z/], readonly: [] }, display: { static: %i[vga spice_enhancements keyboard], dynamic: [], readonly: [] }, devices: { static: %i[audio0 rng0 ivshmem], dynamic: [/\Aserial\d+\z/, /\Aparallel\d+\z/, /\Ausb\d+\z/, /\Ahostpci\d+\z/], readonly: [] } }, cloud_init: { static: %i[citype cicustom ciuser cipassword ciupgrade nameserver searchdomain sshkeys], dynamic: [/\Aipconfig\d+\z/], readonly: [] }, options: { static: %i[onboot startup boot bootdisk bios machine arch ostype scsihw kvm agent hotplug tablet args hookscript smbios1 localtime reboot freeze protection], dynamic: [], readonly: [] }, migration: { static: %i[migrate_downtime migrate_speed], dynamic: [], readonly: [] }, security: { static: %i[amd_sev intel_tdx], dynamic: [], readonly: [] } }.freeze
- CONTAINER_SECTIONS =
Section mappings for LXC containers. Sections without a :static key are “wrapper” sections containing named subsections.
{ general: { static: %i[vmid hostname description tags template lock digest], dynamic: [], readonly: %i[vmid template lock digest] }, resources: { cpu: { static: %i[cores cpulimit cpuunits], dynamic: [], readonly: [] }, memory: { static: %i[memory swap], dynamic: [], readonly: [] }, disks: { static: %i[rootfs], dynamic: [/\Amp\d+\z/, /\Adev\d+\z/, /\Aunused\d+\z/], readonly: [/\Aunused\d+\z/] } }, network: { static: [], dynamic: [/\Anet\d+\z/], readonly: [] }, dns: { static: %i[nameserver searchdomain], dynamic: [], readonly: [] }, options: { static: %i[onboot startup ostype arch unprivileged features hookscript protection debug timezone entrypoint env], dynamic: [], readonly: %i[arch] }, console: { static: %i[console cmode tty], dynamic: [], readonly: [] } }.freeze
- YAML_SPECIAL_CHARS =
Characters that require quoting in YAML output.
%w[: # [ ] { } > | * & ! % @ ` , ? -].freeze
- AGENT_DEFAULTS =
Default values for QEMU Guest Agent properties (from Proxmox API docs). Used to fill in missing sub-properties when parsing agent config strings.
{ enabled: "0", fstrim_cloned_disks: "0", :"freeze-fs-on-backup" => "1", type: "virtio" }.freeze
- VM_DEFAULTS =
Default values for VM config keys that Proxmox API omits when using defaults. These are injected by to_nested to produce complete manifests. Values sourced from Proxmox API docs (nodes-qemu-config.json).
{ hotplug: "network,disk,usb" }.freeze
- VM_BOOLEAN_KEYS =
Top-level VM config keys that are boolean (0/1 in Proxmox API).
%i[onboot kvm tablet reboot freeze localtime protection numa keephugepages].freeze
- CT_BOOLEAN_KEYS =
Top-level container config keys that are boolean (0/1 in Proxmox API).
%i[onboot unprivileged protection debug console].freeze
- HOTPLUG_CAPABILITIES =
All possible hotplug capabilities for QEMU VMs.
%i[network disk usb cpu memory cloudinit].freeze
- AGENT_BOOLEAN_SUBKEYS =
Sub-keys within agent config that are boolean (0/1 strings).
[:enabled, :fstrim_cloned_disks, :"freeze-fs-on-backup"].freeze
- NET_BOOLEAN_SUBKEYS =
Sub-keys within VM network config that are boolean (0/1 strings).
%i[firewall link_down].freeze
- DISK_BOOLEAN_SUBKEYS =
Sub-keys within disk config that are boolean (0/1 strings).
%i[iothread backup replicate ssd ro].freeze
- VM_COMPLEX_KEYS =
Complex key mappings for QEMU VMs. Each entry maps a category to a regex pattern and parser/serializer method names. Used by to_nested/from_nested for bidirectional conversion of Proxmox config strings.
{ net: { pattern: /\Anet\d+\z/, parser: :parse_vm_net_value, serializer: :serialize_vm_net_value }, disk: { pattern: /\A(?:scsi|ide|virtio|sata|efidisk|tpmstate)\d*\z/, parser: :parse_disk_value, serializer: :serialize_disk_value }, unused: { pattern: /\Aunused\d+\z/, parser: :parse_disk_value, serializer: :serialize_disk_value }, boot: { pattern: /\Aboot\z/, parser: :parse_boot_value, serializer: :serialize_boot_value }, agent: { pattern: /\Aagent\z/, parser: :parse_agent_value, serializer: :serialize_agent_value }, hotplug: { pattern: /\Ahotplug\z/, parser: :parse_hotplug_value, serializer: :serialize_hotplug_value }, startup: { pattern: /\Astartup\z/, parser: :parse_kv_value, serializer: :serialize_kv_value }, ipconfig: { pattern: /\Aipconfig\d+\z/, parser: :parse_kv_value, serializer: :serialize_kv_value }, smbios1: { pattern: /\Asmbios1\z/, parser: :parse_kv_value, serializer: :serialize_kv_value }, numa_dev: { pattern: /\Anuma\d+\z/, parser: :parse_kv_value, serializer: :serialize_kv_value } }.freeze
- CT_COMPLEX_KEYS =
Complex key mappings for LXC containers.
{ net: { pattern: /\Anet\d+\z/, parser: :parse_kv_value, serializer: :serialize_kv_value }, rootfs: { pattern: /\Arootfs\z/, parser: :parse_disk_value, serializer: :serialize_disk_value }, mp: { pattern: /\Amp\d+\z/, parser: :parse_disk_value, serializer: :serialize_disk_value }, dev: { pattern: /\Adev\d+\z/, parser: :parse_disk_value, serializer: :serialize_disk_value }, unused: { pattern: /\Aunused\d+\z/, parser: :parse_disk_value, serializer: :serialize_disk_value }, startup: { pattern: /\Astartup\z/, parser: :parse_kv_value, serializer: :serialize_kv_value }, features: { pattern: /\Afeatures\z/, parser: :parse_kv_value, serializer: :serialize_kv_value } }.freeze
Class Method Summary collapse
-
.complete_from_api(manifest_flat, api_flat, type:) ⇒ Hash{Symbol => Object}
Completes a manifest’s flat config with sub-properties from the API config.
-
.diff(original, edited) ⇒ Hash{Symbol => Hash, Array}
Computes the diff between two flat config hashes.
-
.format_diff(diff_hash) ⇒ String
Formats a diff hash for colored terminal display.
-
.from_nested(nested, type:) ⇒ Hash{Symbol => Object}
Converts a nested Hash (from manifest spec) back into a flat Proxmox config hash.
-
.from_yaml(yaml_string, type:) ⇒ Hash{Symbol => Object}
Parses a YAML string back into a flat config hash with symbol keys.
-
.readonly_violations(original_flat, edited_flat, type:) ⇒ Array<String>
Checks if any read-only fields were modified between original and edited configs.
-
.to_nested(flat_config, type:) ⇒ Hash{Symbol => Hash}
Converts a flat Proxmox config hash into a nested Hash with parsed complex values.
-
.to_yaml(flat_config, type:, resource: {}) ⇒ String
Converts a flat Proxmox config hash into a nested, section-grouped YAML string with header comments and read-only markers.
-
.validate(yaml_string, type:) ⇒ Array<String>
Validates a YAML string against known section/key mappings.
Class Method Details
.complete_from_api(manifest_flat, api_flat, type:) ⇒ Hash{Symbol => Object}
Completes a manifest’s flat config with sub-properties from the API config. When a manifest omits sub-properties of complex keys (e.g., disk without volume, net without MAC address), fills them from the current API config to prevent false diffs during update comparison.
451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 |
# File 'lib/pvectl/config_serializer.rb', line 451 def complete_from_api(manifest_flat, api_flat, type:) manifest_flat.each_with_object({}) do |(key, value), result| api_value = api_flat[key] complex = find_complex_key(key, type) if complex && value.is_a?(String) && api_value.is_a?(String) parsed_api = send(complex[:parser], api_value) parsed_manifest = send(complex[:parser], value) merged = parsed_api.merge(parsed_manifest.compact) result[key] = send(complex[:serializer], merged) elsif api_value.is_a?(Integer) && value.is_a?(String) && value.match?(/\A\d+\z/) result[key] = value.to_i else result[key] = value end end end |
.diff(original, edited) ⇒ Hash{Symbol => Hash, Array}
Computes the diff between two flat config hashes.
323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 |
# File 'lib/pvectl/config_serializer.rb', line 323 def diff(original, edited) changed = {} added = {} removed = [] all_keys = original.keys | edited.keys all_keys.each do |key| if original.key?(key) && edited.key?(key) changed[key] = [original[key], edited[key]] if original[key] != edited[key] elsif edited.key?(key) added[key] = edited[key] else removed << key end end { changed: changed, added: added, removed: removed } end |
.format_diff(diff_hash) ⇒ String
Formats a diff hash for colored terminal display.
351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 |
# File 'lib/pvectl/config_serializer.rb', line 351 def format_diff(diff_hash) lines = [] diff_hash[:changed].each do |key, (old_val, new_val)| lines << "\e[33m ~ #{key}: #{old_val} -> #{new_val}\e[0m" end diff_hash[:added].each do |key, value| lines << "\e[32m + #{key}: #{value}\e[0m" end diff_hash[:removed].each do |key| lines << "\e[31m - #{key}\e[0m" end lines.join("\n") end |
.from_nested(nested, type:) ⇒ Hash{Symbol => Object}
Converts a nested Hash (from manifest spec) back into a flat Proxmox config hash. Serializes parsed complex values back to Proxmox string format.
412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 |
# File 'lib/pvectl/config_serializer.rb', line 412 def from_nested(nested, type:) sections = sections_for(type) result = {} nested.each do |section_name, section_value| next unless section_value.is_a?(Hash) section_def = sections[section_name] next unless section_def if wrapper_section?(section_def) section_value.each do |_sub_name, sub_values| next unless sub_values.is_a?(Hash) flatten_nested_section(sub_values, type, result) end else flatten_nested_section(section_value, type, result) end end inject_defaults(result, type) end |
.from_yaml(yaml_string, type:) ⇒ Hash{Symbol => Object}
Parses a YAML string back into a flat config hash with symbol keys. Strips comment lines before parsing, then flattens nested sections.
235 236 237 238 239 240 241 242 243 244 245 246 247 |
# File 'lib/pvectl/config_serializer.rb', line 235 def from_yaml(yaml_string, type:) cleaned = strip_comments(yaml_string) return {} if cleaned.strip.empty? begin parsed = YAML.safe_load(cleaned) rescue Psych::SyntaxError return {} end return {} unless parsed.is_a?(Hash) flatten_sections(parsed) end |
.readonly_violations(original_flat, edited_flat, type:) ⇒ Array<String>
Checks if any read-only fields were modified between original and edited configs.
306 307 308 309 310 311 312 |
# File 'lib/pvectl/config_serializer.rb', line 306 def readonly_violations(original_flat, edited_flat, type:) sections = sections_for(type) readonly_keys = collect_readonly_keys(original_flat.keys | edited_flat.keys, sections) readonly_keys.select { |key| original_flat[key] != edited_flat[key] } .map(&:to_s) end |
.to_nested(flat_config, type:) ⇒ Hash{Symbol => Hash}
Converts a flat Proxmox config hash into a nested Hash with parsed complex values. Used by ManifestSerializer to build the spec section of YAML manifests.
379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 |
# File 'lib/pvectl/config_serializer.rb', line 379 def to_nested(flat_config, type:) sections = sections_for(type) config_with_defaults = inject_defaults(flat_config, type) result = {} sections.each do |section_name, section_def| if wrapper_section?(section_def) wrapper = {} section_def.each do |sub_name, sub_def| sub_hash = build_nested_section(config_with_defaults, sub_def, type) wrapper[sub_name] = sub_hash unless sub_hash.empty? end result[section_name] = wrapper unless wrapper.empty? else section_hash = build_nested_section(config_with_defaults, section_def, type) result[section_name] = section_hash unless section_hash.empty? end end result end |
.to_yaml(flat_config, type:, resource: {}) ⇒ String
Converts a flat Proxmox config hash into a nested, section-grouped YAML string with header comments and read-only markers.
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 |
# File 'lib/pvectl/config_serializer.rb', line 207 def to_yaml(flat_config, type:, resource: {}) sections = sections_for(type) lines = [] lines << header_comment(type, resource) lines << "" sections.each do |section_name, section_def| if wrapper_section?(section_def) render_wrapper_section(lines, section_name, section_def, flat_config) else render_leaf_section(lines, section_name, section_def, flat_config) end end lines.join("\n") end |
.validate(yaml_string, type:) ⇒ Array<String>
Validates a YAML string against known section/key mappings.
258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 |
# File 'lib/pvectl/config_serializer.rb', line 258 def validate(yaml_string, type:) errors = [] cleaned = strip_comments(yaml_string) begin parsed = YAML.safe_load(cleaned) rescue Psych::SyntaxError => e return ["YAML syntax error: #{e.}"] end return errors unless parsed.is_a?(Hash) sections = sections_for(type) parsed.each do |section_name, section_values| unless sections.key?(section_name.to_sym) errors << "Unknown section '#{section_name}'" next end next unless section_values.is_a?(Hash) section_def = sections[section_name.to_sym] if wrapper_section?(section_def) validate_wrapper_section(errors, section_name, section_def, section_values) else section_values.each_key do |key| unless key_in_section?(key.to_sym, section_def) errors << "Unknown key '#{key}' in section '#{section_name}'" end end end end errors end |