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.
Files changed (558) hide show
  1. checksums.yaml +7 -0
  2. data/.claude/rules/branch-before-changes.md +52 -0
  3. data/.claude/rules/documentation-updates.md +104 -0
  4. data/.claude/rules/git-workflow.md +84 -0
  5. data/.claude/rules/proxmox-api-docs.md +58 -0
  6. data/.claude/rules/rbs-signatures.md +80 -0
  7. data/.claude/rules/refactoring-as-design-option.md +35 -0
  8. data/.claude/scheduled_tasks.lock +1 -0
  9. data/.claude/settings.json +51 -0
  10. data/.mcp.json +8 -0
  11. data/.ruby-gemset +1 -0
  12. data/.ruby-version +1 -0
  13. data/CHANGELOG.md +138 -0
  14. data/CLAUDE.md +211 -0
  15. data/CODE_OF_CONDUCT.md +132 -0
  16. data/LICENSE.txt +21 -0
  17. data/README.md +143 -0
  18. data/Rakefile +8 -0
  19. data/docs/proxmox-api-update.sh +96 -0
  20. data/exe/pvectl +5 -0
  21. data/lib/pvectl/argv_preprocessor.rb +334 -0
  22. data/lib/pvectl/cli.rb +102 -0
  23. data/lib/pvectl/commands/apt.rb +389 -0
  24. data/lib/pvectl/commands/clone_container.rb +230 -0
  25. data/lib/pvectl/commands/clone_vm.rb +331 -0
  26. data/lib/pvectl/commands/cloudinit/command.rb +122 -0
  27. data/lib/pvectl/commands/cloudinit/dump.rb +94 -0
  28. data/lib/pvectl/commands/cloudinit/pending.rb +137 -0
  29. data/lib/pvectl/commands/cloudinit/regenerate.rb +79 -0
  30. data/lib/pvectl/commands/config/command.rb +65 -0
  31. data/lib/pvectl/commands/config/get_contexts.rb +68 -0
  32. data/lib/pvectl/commands/config/set_cluster.rb +103 -0
  33. data/lib/pvectl/commands/config/set_context.rb +136 -0
  34. data/lib/pvectl/commands/config/set_credentials.rb +181 -0
  35. data/lib/pvectl/commands/config/use_context.rb +69 -0
  36. data/lib/pvectl/commands/config/view.rb +67 -0
  37. data/lib/pvectl/commands/console.rb +93 -0
  38. data/lib/pvectl/commands/console_ct.rb +187 -0
  39. data/lib/pvectl/commands/console_vm.rb +187 -0
  40. data/lib/pvectl/commands/container_lifecycle_command.rb +77 -0
  41. data/lib/pvectl/commands/create_backup.rb +173 -0
  42. data/lib/pvectl/commands/create_container.rb +141 -0
  43. data/lib/pvectl/commands/create_resource_command.rb +244 -0
  44. data/lib/pvectl/commands/create_snapshot.rb +242 -0
  45. data/lib/pvectl/commands/create_vm.rb +267 -0
  46. data/lib/pvectl/commands/delete_backup.rb +139 -0
  47. data/lib/pvectl/commands/delete_command.rb +119 -0
  48. data/lib/pvectl/commands/delete_container.rb +30 -0
  49. data/lib/pvectl/commands/delete_snapshot.rb +248 -0
  50. data/lib/pvectl/commands/delete_vm.rb +127 -0
  51. data/lib/pvectl/commands/describe/command.rb +251 -0
  52. data/lib/pvectl/commands/edit_container.rb +56 -0
  53. data/lib/pvectl/commands/edit_dns.rb +149 -0
  54. data/lib/pvectl/commands/edit_hosts.rb +135 -0
  55. data/lib/pvectl/commands/edit_node.rb +54 -0
  56. data/lib/pvectl/commands/edit_resource_command.rb +180 -0
  57. data/lib/pvectl/commands/edit_vm.rb +154 -0
  58. data/lib/pvectl/commands/edit_volume.rb +189 -0
  59. data/lib/pvectl/commands/feature_command.rb +230 -0
  60. data/lib/pvectl/commands/feature_container.rb +21 -0
  61. data/lib/pvectl/commands/feature_vm.rb +94 -0
  62. data/lib/pvectl/commands/get/command.rb +360 -0
  63. data/lib/pvectl/commands/get/handlers/backups.rb +76 -0
  64. data/lib/pvectl/commands/get/handlers/capabilities.rb +107 -0
  65. data/lib/pvectl/commands/get/handlers/containers.rb +148 -0
  66. data/lib/pvectl/commands/get/handlers/disks.rb +107 -0
  67. data/lib/pvectl/commands/get/handlers/dns.rb +94 -0
  68. data/lib/pvectl/commands/get/handlers/hosts.rb +94 -0
  69. data/lib/pvectl/commands/get/handlers/nodes.rb +162 -0
  70. data/lib/pvectl/commands/get/handlers/services.rb +81 -0
  71. data/lib/pvectl/commands/get/handlers/snapshots.rb +97 -0
  72. data/lib/pvectl/commands/get/handlers/storage.rb +118 -0
  73. data/lib/pvectl/commands/get/handlers/subscription.rb +69 -0
  74. data/lib/pvectl/commands/get/handlers/tasks.rb +89 -0
  75. data/lib/pvectl/commands/get/handlers/templates.rb +175 -0
  76. data/lib/pvectl/commands/get/handlers/time.rb +118 -0
  77. data/lib/pvectl/commands/get/handlers/vms.rb +145 -0
  78. data/lib/pvectl/commands/get/handlers/volume.rb +134 -0
  79. data/lib/pvectl/commands/get/resource_handler.rb +63 -0
  80. data/lib/pvectl/commands/get/resource_registry.rb +18 -0
  81. data/lib/pvectl/commands/get/watch_loop.rb +129 -0
  82. data/lib/pvectl/commands/irreversible_command.rb +265 -0
  83. data/lib/pvectl/commands/logs/command.rb +275 -0
  84. data/lib/pvectl/commands/logs/handlers/journal.rb +46 -0
  85. data/lib/pvectl/commands/logs/handlers/syslog.rb +53 -0
  86. data/lib/pvectl/commands/logs/handlers/task_detail.rb +52 -0
  87. data/lib/pvectl/commands/logs/handlers/task_logs.rb +115 -0
  88. data/lib/pvectl/commands/logs/resource_handler.rb +46 -0
  89. data/lib/pvectl/commands/logs/resource_registry.rb +22 -0
  90. data/lib/pvectl/commands/migrate_command.rb +282 -0
  91. data/lib/pvectl/commands/migrate_container.rb +23 -0
  92. data/lib/pvectl/commands/migrate_vm.rb +122 -0
  93. data/lib/pvectl/commands/move_disk_command.rb +239 -0
  94. data/lib/pvectl/commands/move_disk_container.rb +21 -0
  95. data/lib/pvectl/commands/move_disk_vm.rb +127 -0
  96. data/lib/pvectl/commands/ping.rb +249 -0
  97. data/lib/pvectl/commands/pull.rb +342 -0
  98. data/lib/pvectl/commands/push.rb +352 -0
  99. data/lib/pvectl/commands/reset.rb +64 -0
  100. data/lib/pvectl/commands/resource_lifecycle_command.rb +277 -0
  101. data/lib/pvectl/commands/resource_registry.rb +73 -0
  102. data/lib/pvectl/commands/restart.rb +70 -0
  103. data/lib/pvectl/commands/restart_container.rb +18 -0
  104. data/lib/pvectl/commands/restore_backup.rb +236 -0
  105. data/lib/pvectl/commands/resume.rb +57 -0
  106. data/lib/pvectl/commands/rollback_snapshot.rb +228 -0
  107. data/lib/pvectl/commands/sendkey_vm.rb +205 -0
  108. data/lib/pvectl/commands/service.rb +293 -0
  109. data/lib/pvectl/commands/set_container.rb +50 -0
  110. data/lib/pvectl/commands/set_node.rb +52 -0
  111. data/lib/pvectl/commands/set_resource_command.rb +185 -0
  112. data/lib/pvectl/commands/set_vm.rb +136 -0
  113. data/lib/pvectl/commands/set_volume.rb +212 -0
  114. data/lib/pvectl/commands/shared_config_parsers.rb +126 -0
  115. data/lib/pvectl/commands/shared_flags.rb +155 -0
  116. data/lib/pvectl/commands/shutdown.rb +73 -0
  117. data/lib/pvectl/commands/shutdown_container.rb +18 -0
  118. data/lib/pvectl/commands/start.rb +79 -0
  119. data/lib/pvectl/commands/start_container.rb +18 -0
  120. data/lib/pvectl/commands/stop.rb +75 -0
  121. data/lib/pvectl/commands/stop_container.rb +18 -0
  122. data/lib/pvectl/commands/suspend.rb +64 -0
  123. data/lib/pvectl/commands/template_command.rb +205 -0
  124. data/lib/pvectl/commands/template_container.rb +27 -0
  125. data/lib/pvectl/commands/template_vm.rb +106 -0
  126. data/lib/pvectl/commands/top/command.rb +206 -0
  127. data/lib/pvectl/commands/top/handlers/containers.rb +61 -0
  128. data/lib/pvectl/commands/top/handlers/nodes.rb +61 -0
  129. data/lib/pvectl/commands/top/handlers/vms.rb +61 -0
  130. data/lib/pvectl/commands/top/resource_handler.rb +46 -0
  131. data/lib/pvectl/commands/top/resource_registry.rb +22 -0
  132. data/lib/pvectl/commands/unlink_disk_vm.rb +232 -0
  133. data/lib/pvectl/commands/vm_lifecycle_command.rb +77 -0
  134. data/lib/pvectl/commands/wakeonlan_node.rb +153 -0
  135. data/lib/pvectl/config/errors.rb +62 -0
  136. data/lib/pvectl/config/models/cluster.rb +180 -0
  137. data/lib/pvectl/config/models/context.rb +100 -0
  138. data/lib/pvectl/config/models/resolved_config.rb +171 -0
  139. data/lib/pvectl/config/models/user.rb +133 -0
  140. data/lib/pvectl/config/provider.rb +297 -0
  141. data/lib/pvectl/config/service.rb +300 -0
  142. data/lib/pvectl/config/store.rb +161 -0
  143. data/lib/pvectl/config/wizard.rb +309 -0
  144. data/lib/pvectl/config_serializer.rb +1034 -0
  145. data/lib/pvectl/connection/retry_handler.rb +161 -0
  146. data/lib/pvectl/connection.rb +157 -0
  147. data/lib/pvectl/console/terminal_session.rb +449 -0
  148. data/lib/pvectl/editor_session.rb +157 -0
  149. data/lib/pvectl/exit_codes.rb +43 -0
  150. data/lib/pvectl/formatters/base.rb +55 -0
  151. data/lib/pvectl/formatters/color_support.rb +90 -0
  152. data/lib/pvectl/formatters/json.rb +45 -0
  153. data/lib/pvectl/formatters/output_helper.rb +77 -0
  154. data/lib/pvectl/formatters/registry.rb +72 -0
  155. data/lib/pvectl/formatters/table.rb +235 -0
  156. data/lib/pvectl/formatters/wide.rb +93 -0
  157. data/lib/pvectl/formatters/yaml.rb +49 -0
  158. data/lib/pvectl/manifest_serializer.rb +142 -0
  159. data/lib/pvectl/models/apt_package.rb +107 -0
  160. data/lib/pvectl/models/backup.rb +173 -0
  161. data/lib/pvectl/models/base.rb +49 -0
  162. data/lib/pvectl/models/capability.rb +62 -0
  163. data/lib/pvectl/models/container.rb +205 -0
  164. data/lib/pvectl/models/container_operation_result.rb +27 -0
  165. data/lib/pvectl/models/dns_config.rb +54 -0
  166. data/lib/pvectl/models/hosts_file.rb +47 -0
  167. data/lib/pvectl/models/journal_entry.rb +16 -0
  168. data/lib/pvectl/models/network_interface.rb +85 -0
  169. data/lib/pvectl/models/node.rb +195 -0
  170. data/lib/pvectl/models/node_operation_result.rb +45 -0
  171. data/lib/pvectl/models/operation_result.rb +110 -0
  172. data/lib/pvectl/models/physical_disk.rb +193 -0
  173. data/lib/pvectl/models/service.rb +80 -0
  174. data/lib/pvectl/models/snapshot.rb +101 -0
  175. data/lib/pvectl/models/snapshot_description.rb +39 -0
  176. data/lib/pvectl/models/storage.rb +180 -0
  177. data/lib/pvectl/models/subscription.rb +87 -0
  178. data/lib/pvectl/models/syslog_entry.rb +17 -0
  179. data/lib/pvectl/models/task.rb +95 -0
  180. data/lib/pvectl/models/task_entry.rb +52 -0
  181. data/lib/pvectl/models/task_log_line.rb +17 -0
  182. data/lib/pvectl/models/time_config.rb +47 -0
  183. data/lib/pvectl/models/vm.rb +137 -0
  184. data/lib/pvectl/models/vm_operation_result.rb +27 -0
  185. data/lib/pvectl/models/volume.rb +133 -0
  186. data/lib/pvectl/models/volume_operation_result.rb +26 -0
  187. data/lib/pvectl/parsers/cloud_init_config.rb +92 -0
  188. data/lib/pvectl/parsers/disk_config.rb +97 -0
  189. data/lib/pvectl/parsers/lxc_mount_config.rb +98 -0
  190. data/lib/pvectl/parsers/lxc_net_config.rb +97 -0
  191. data/lib/pvectl/parsers/net_config.rb +95 -0
  192. data/lib/pvectl/parsers/smart_text.rb +42 -0
  193. data/lib/pvectl/plugin_loader.rb +157 -0
  194. data/lib/pvectl/presenters/apt_package.rb +99 -0
  195. data/lib/pvectl/presenters/backup.rb +128 -0
  196. data/lib/pvectl/presenters/base.rb +283 -0
  197. data/lib/pvectl/presenters/capability.rb +104 -0
  198. data/lib/pvectl/presenters/config/context.rb +80 -0
  199. data/lib/pvectl/presenters/container.rb +574 -0
  200. data/lib/pvectl/presenters/container_operation_result.rb +109 -0
  201. data/lib/pvectl/presenters/disk.rb +184 -0
  202. data/lib/pvectl/presenters/dns_config.rb +68 -0
  203. data/lib/pvectl/presenters/hosts_file.rb +61 -0
  204. data/lib/pvectl/presenters/journal_entry.rb +20 -0
  205. data/lib/pvectl/presenters/node.rb +762 -0
  206. data/lib/pvectl/presenters/node_operation_result.rb +50 -0
  207. data/lib/pvectl/presenters/operation_result.rb +61 -0
  208. data/lib/pvectl/presenters/service.rb +76 -0
  209. data/lib/pvectl/presenters/snapshot.rb +239 -0
  210. data/lib/pvectl/presenters/snapshot_operation_result.rb +125 -0
  211. data/lib/pvectl/presenters/storage.rb +329 -0
  212. data/lib/pvectl/presenters/subscription.rb +189 -0
  213. data/lib/pvectl/presenters/syslog_entry.rb +20 -0
  214. data/lib/pvectl/presenters/task_entry.rb +69 -0
  215. data/lib/pvectl/presenters/task_log_line.rb +20 -0
  216. data/lib/pvectl/presenters/template.rb +76 -0
  217. data/lib/pvectl/presenters/time_config.rb +86 -0
  218. data/lib/pvectl/presenters/top_container.rb +112 -0
  219. data/lib/pvectl/presenters/top_node.rb +115 -0
  220. data/lib/pvectl/presenters/top_presenter.rb +59 -0
  221. data/lib/pvectl/presenters/top_vm.rb +105 -0
  222. data/lib/pvectl/presenters/vm.rb +853 -0
  223. data/lib/pvectl/presenters/vm_operation_result.rb +109 -0
  224. data/lib/pvectl/presenters/volume.rb +136 -0
  225. data/lib/pvectl/presenters/volume_operation_result.rb +58 -0
  226. data/lib/pvectl/repositories/apt.rb +93 -0
  227. data/lib/pvectl/repositories/backup.rb +186 -0
  228. data/lib/pvectl/repositories/base.rb +110 -0
  229. data/lib/pvectl/repositories/capabilities.rb +96 -0
  230. data/lib/pvectl/repositories/container.rb +503 -0
  231. data/lib/pvectl/repositories/disk.rb +87 -0
  232. data/lib/pvectl/repositories/dns.rb +54 -0
  233. data/lib/pvectl/repositories/hosts.rb +63 -0
  234. data/lib/pvectl/repositories/journal.rb +23 -0
  235. data/lib/pvectl/repositories/node.rb +537 -0
  236. data/lib/pvectl/repositories/service.rb +139 -0
  237. data/lib/pvectl/repositories/snapshot.rb +133 -0
  238. data/lib/pvectl/repositories/storage.rb +302 -0
  239. data/lib/pvectl/repositories/subscription.rb +77 -0
  240. data/lib/pvectl/repositories/syslog.rb +25 -0
  241. data/lib/pvectl/repositories/task.rb +82 -0
  242. data/lib/pvectl/repositories/task_list.rb +30 -0
  243. data/lib/pvectl/repositories/task_log.rb +31 -0
  244. data/lib/pvectl/repositories/time_config.rb +53 -0
  245. data/lib/pvectl/repositories/vm.rb +616 -0
  246. data/lib/pvectl/repositories/volume.rb +306 -0
  247. data/lib/pvectl/selectors/base.rb +201 -0
  248. data/lib/pvectl/selectors/container.rb +116 -0
  249. data/lib/pvectl/selectors/disk.rb +59 -0
  250. data/lib/pvectl/selectors/vm.rb +116 -0
  251. data/lib/pvectl/selectors/volume.rb +59 -0
  252. data/lib/pvectl/services/backup.rb +209 -0
  253. data/lib/pvectl/services/clone_container.rb +260 -0
  254. data/lib/pvectl/services/clone_vm.rb +265 -0
  255. data/lib/pvectl/services/cloudinit.rb +96 -0
  256. data/lib/pvectl/services/console.rb +152 -0
  257. data/lib/pvectl/services/container_lifecycle.rb +124 -0
  258. data/lib/pvectl/services/create_container.rb +179 -0
  259. data/lib/pvectl/services/create_vm.rb +191 -0
  260. data/lib/pvectl/services/edit_container.rb +125 -0
  261. data/lib/pvectl/services/edit_dns.rb +159 -0
  262. data/lib/pvectl/services/edit_hosts.rb +78 -0
  263. data/lib/pvectl/services/edit_node.rb +147 -0
  264. data/lib/pvectl/services/edit_vm.rb +125 -0
  265. data/lib/pvectl/services/edit_volume.rb +224 -0
  266. data/lib/pvectl/services/get/resource_service.rb +98 -0
  267. data/lib/pvectl/services/move_disk.rb +132 -0
  268. data/lib/pvectl/services/pull_config.rb +94 -0
  269. data/lib/pvectl/services/push_config.rb +524 -0
  270. data/lib/pvectl/services/resize_volume.rb +253 -0
  271. data/lib/pvectl/services/resource_delete.rb +169 -0
  272. data/lib/pvectl/services/resource_migration.rb +170 -0
  273. data/lib/pvectl/services/sendkey.rb +108 -0
  274. data/lib/pvectl/services/service_lifecycle.rb +89 -0
  275. data/lib/pvectl/services/set_container.rb +128 -0
  276. data/lib/pvectl/services/set_node.rb +236 -0
  277. data/lib/pvectl/services/set_vm.rb +128 -0
  278. data/lib/pvectl/services/set_volume.rb +126 -0
  279. data/lib/pvectl/services/snapshot.rb +261 -0
  280. data/lib/pvectl/services/task_listing.rb +75 -0
  281. data/lib/pvectl/services/unlink_disk.rb +86 -0
  282. data/lib/pvectl/services/vm_lifecycle.rb +124 -0
  283. data/lib/pvectl/services/wakeonlan.rb +79 -0
  284. data/lib/pvectl/utils/resource_resolver.rb +80 -0
  285. data/lib/pvectl/version.rb +13 -0
  286. data/lib/pvectl/wizards/create_container.rb +105 -0
  287. data/lib/pvectl/wizards/create_vm.rb +98 -0
  288. data/lib/pvectl.rb +439 -0
  289. data/sig/external/gli.rbs +16 -0
  290. data/sig/external/proxmox_api.rbs +10 -0
  291. data/sig/pvectl/argv_preprocessor.rbs +53 -0
  292. data/sig/pvectl/cli.rbs +26 -0
  293. data/sig/pvectl/commands/apt.rbs +47 -0
  294. data/sig/pvectl/commands/clone_container.rbs +31 -0
  295. data/sig/pvectl/commands/clone_vm.rbs +33 -0
  296. data/sig/pvectl/commands/cloudinit/command.rbs +13 -0
  297. data/sig/pvectl/commands/cloudinit/dump.rbs +13 -0
  298. data/sig/pvectl/commands/cloudinit/pending.rbs +17 -0
  299. data/sig/pvectl/commands/cloudinit/regenerate.rbs +11 -0
  300. data/sig/pvectl/commands/config/command.rbs +9 -0
  301. data/sig/pvectl/commands/config/get_contexts.rbs +11 -0
  302. data/sig/pvectl/commands/config/set_cluster.rbs +11 -0
  303. data/sig/pvectl/commands/config/set_context.rbs +15 -0
  304. data/sig/pvectl/commands/config/set_credentials.rbs +15 -0
  305. data/sig/pvectl/commands/config/use_context.rbs +11 -0
  306. data/sig/pvectl/commands/config/view.rbs +11 -0
  307. data/sig/pvectl/commands/console.rbs +9 -0
  308. data/sig/pvectl/commands/console_ct.rbs +27 -0
  309. data/sig/pvectl/commands/console_vm.rbs +27 -0
  310. data/sig/pvectl/commands/container_lifecycle_command.rbs +25 -0
  311. data/sig/pvectl/commands/create_backup.rbs +29 -0
  312. data/sig/pvectl/commands/create_container.rbs +30 -0
  313. data/sig/pvectl/commands/create_resource_command.rbs +53 -0
  314. data/sig/pvectl/commands/create_snapshot.rbs +35 -0
  315. data/sig/pvectl/commands/create_vm.rbs +30 -0
  316. data/sig/pvectl/commands/delete_backup.rbs +25 -0
  317. data/sig/pvectl/commands/delete_command.rbs +45 -0
  318. data/sig/pvectl/commands/delete_container.rbs +11 -0
  319. data/sig/pvectl/commands/delete_snapshot.rbs +35 -0
  320. data/sig/pvectl/commands/delete_vm.rbs +13 -0
  321. data/sig/pvectl/commands/describe/command.rbs +27 -0
  322. data/sig/pvectl/commands/edit_container.rbs +17 -0
  323. data/sig/pvectl/commands/edit_dns.rbs +25 -0
  324. data/sig/pvectl/commands/edit_hosts.rbs +23 -0
  325. data/sig/pvectl/commands/edit_node.rbs +17 -0
  326. data/sig/pvectl/commands/edit_resource_command.rbs +35 -0
  327. data/sig/pvectl/commands/edit_vm.rbs +19 -0
  328. data/sig/pvectl/commands/edit_volume.rbs +24 -0
  329. data/sig/pvectl/commands/feature_command.rbs +43 -0
  330. data/sig/pvectl/commands/feature_container.rbs +10 -0
  331. data/sig/pvectl/commands/feature_vm.rbs +12 -0
  332. data/sig/pvectl/commands/get/command.rbs +42 -0
  333. data/sig/pvectl/commands/get/handlers/backups.rbs +23 -0
  334. data/sig/pvectl/commands/get/handlers/capabilities.rbs +29 -0
  335. data/sig/pvectl/commands/get/handlers/containers.rbs +35 -0
  336. data/sig/pvectl/commands/get/handlers/disks.rbs +27 -0
  337. data/sig/pvectl/commands/get/handlers/dns.rbs +25 -0
  338. data/sig/pvectl/commands/get/handlers/hosts.rbs +25 -0
  339. data/sig/pvectl/commands/get/handlers/nodes.rbs +33 -0
  340. data/sig/pvectl/commands/get/handlers/services.rbs +23 -0
  341. data/sig/pvectl/commands/get/handlers/snapshots.rbs +27 -0
  342. data/sig/pvectl/commands/get/handlers/storage.rbs +25 -0
  343. data/sig/pvectl/commands/get/handlers/subscription.rbs +25 -0
  344. data/sig/pvectl/commands/get/handlers/tasks.rbs +28 -0
  345. data/sig/pvectl/commands/get/handlers/templates.rbs +35 -0
  346. data/sig/pvectl/commands/get/handlers/time.rbs +29 -0
  347. data/sig/pvectl/commands/get/handlers/vms.rbs +35 -0
  348. data/sig/pvectl/commands/get/handlers/volume.rbs +27 -0
  349. data/sig/pvectl/commands/get/resource_handler.rbs +13 -0
  350. data/sig/pvectl/commands/get/resource_registry.rbs +8 -0
  351. data/sig/pvectl/commands/get/watch_loop.rbs +33 -0
  352. data/sig/pvectl/commands/irreversible_command.rbs +32 -0
  353. data/sig/pvectl/commands/logs/command.rbs +35 -0
  354. data/sig/pvectl/commands/logs/handlers/journal.rbs +21 -0
  355. data/sig/pvectl/commands/logs/handlers/syslog.rbs +21 -0
  356. data/sig/pvectl/commands/logs/handlers/task_detail.rbs +21 -0
  357. data/sig/pvectl/commands/logs/handlers/task_logs.rbs +35 -0
  358. data/sig/pvectl/commands/logs/resource_handler.rbs +11 -0
  359. data/sig/pvectl/commands/logs/resource_registry.rbs +8 -0
  360. data/sig/pvectl/commands/migrate_command.rbs +45 -0
  361. data/sig/pvectl/commands/migrate_container.rbs +11 -0
  362. data/sig/pvectl/commands/migrate_vm.rbs +13 -0
  363. data/sig/pvectl/commands/move_disk_command.rbs +43 -0
  364. data/sig/pvectl/commands/move_disk_container.rbs +11 -0
  365. data/sig/pvectl/commands/move_disk_vm.rbs +13 -0
  366. data/sig/pvectl/commands/ping.rbs +39 -0
  367. data/sig/pvectl/commands/pull.rbs +33 -0
  368. data/sig/pvectl/commands/push.rbs +32 -0
  369. data/sig/pvectl/commands/reset.rbs +11 -0
  370. data/sig/pvectl/commands/resource_lifecycle_command.rbs +55 -0
  371. data/sig/pvectl/commands/resource_registry.rbs +19 -0
  372. data/sig/pvectl/commands/restart.rbs +11 -0
  373. data/sig/pvectl/commands/restart_container.rbs +9 -0
  374. data/sig/pvectl/commands/restore_backup.rbs +27 -0
  375. data/sig/pvectl/commands/resume.rbs +11 -0
  376. data/sig/pvectl/commands/rollback_snapshot.rbs +31 -0
  377. data/sig/pvectl/commands/sendkey_vm.rbs +25 -0
  378. data/sig/pvectl/commands/service.rbs +38 -0
  379. data/sig/pvectl/commands/set_container.rbs +13 -0
  380. data/sig/pvectl/commands/set_node.rbs +13 -0
  381. data/sig/pvectl/commands/set_resource_command.rbs +25 -0
  382. data/sig/pvectl/commands/set_vm.rbs +15 -0
  383. data/sig/pvectl/commands/set_volume.rbs +24 -0
  384. data/sig/pvectl/commands/shared_config_parsers.rbs +19 -0
  385. data/sig/pvectl/commands/shared_flags.rbs +10 -0
  386. data/sig/pvectl/commands/shutdown.rbs +11 -0
  387. data/sig/pvectl/commands/shutdown_container.rbs +9 -0
  388. data/sig/pvectl/commands/start.rbs +11 -0
  389. data/sig/pvectl/commands/start_container.rbs +9 -0
  390. data/sig/pvectl/commands/stop.rbs +11 -0
  391. data/sig/pvectl/commands/stop_container.rbs +9 -0
  392. data/sig/pvectl/commands/suspend.rbs +11 -0
  393. data/sig/pvectl/commands/template_command.rbs +21 -0
  394. data/sig/pvectl/commands/template_container.rbs +10 -0
  395. data/sig/pvectl/commands/template_vm.rbs +12 -0
  396. data/sig/pvectl/commands/top/command.rbs +31 -0
  397. data/sig/pvectl/commands/top/handlers/containers.rbs +21 -0
  398. data/sig/pvectl/commands/top/handlers/nodes.rbs +21 -0
  399. data/sig/pvectl/commands/top/handlers/vms.rbs +21 -0
  400. data/sig/pvectl/commands/top/resource_handler.rbs +11 -0
  401. data/sig/pvectl/commands/top/resource_registry.rbs +8 -0
  402. data/sig/pvectl/commands/unlink_disk_vm.rbs +27 -0
  403. data/sig/pvectl/commands/vm_lifecycle_command.rbs +25 -0
  404. data/sig/pvectl/commands/wakeonlan_node.rbs +21 -0
  405. data/sig/pvectl/config/errors.rbs +24 -0
  406. data/sig/pvectl/config/models/cluster.rbs +39 -0
  407. data/sig/pvectl/config/models/context.rbs +23 -0
  408. data/sig/pvectl/config/models/resolved_config.rbs +51 -0
  409. data/sig/pvectl/config/models/user.rbs +31 -0
  410. data/sig/pvectl/config/provider.rbs +40 -0
  411. data/sig/pvectl/config/service.rbs +65 -0
  412. data/sig/pvectl/config/store.rbs +14 -0
  413. data/sig/pvectl/config/wizard.rbs +48 -0
  414. data/sig/pvectl/config_serializer.rbs +121 -0
  415. data/sig/pvectl/connection/retry_handler.rbs +31 -0
  416. data/sig/pvectl/connection.rbs +35 -0
  417. data/sig/pvectl/console/terminal_session.rbs +63 -0
  418. data/sig/pvectl/editor_session.rbs +33 -0
  419. data/sig/pvectl/exit_codes.rbs +19 -0
  420. data/sig/pvectl/formatters/base.rbs +13 -0
  421. data/sig/pvectl/formatters/color_support.rbs +13 -0
  422. data/sig/pvectl/formatters/json.rbs +7 -0
  423. data/sig/pvectl/formatters/output_helper.rbs +9 -0
  424. data/sig/pvectl/formatters/registry.rbs +13 -0
  425. data/sig/pvectl/formatters/table.rbs +25 -0
  426. data/sig/pvectl/formatters/wide.rbs +15 -0
  427. data/sig/pvectl/formatters/yaml.rbs +7 -0
  428. data/sig/pvectl/manifest_serializer.rbs +18 -0
  429. data/sig/pvectl/models/apt_package.rbs +26 -0
  430. data/sig/pvectl/models/backup.rbs +31 -0
  431. data/sig/pvectl/models/base.rbs +11 -0
  432. data/sig/pvectl/models/capability.rbs +16 -0
  433. data/sig/pvectl/models/container.rbs +44 -0
  434. data/sig/pvectl/models/container_operation_result.rbs +9 -0
  435. data/sig/pvectl/models/dns_config.rbs +15 -0
  436. data/sig/pvectl/models/hosts_file.rbs +13 -0
  437. data/sig/pvectl/models/journal_entry.rbs +10 -0
  438. data/sig/pvectl/models/network_interface.rbs +20 -0
  439. data/sig/pvectl/models/node.rbs +47 -0
  440. data/sig/pvectl/models/node_operation_result.rbs +12 -0
  441. data/sig/pvectl/models/operation_result.rbs +21 -0
  442. data/sig/pvectl/models/physical_disk.rbs +35 -0
  443. data/sig/pvectl/models/service.rbs +18 -0
  444. data/sig/pvectl/models/snapshot.rbs +21 -0
  445. data/sig/pvectl/models/snapshot_description.rbs +18 -0
  446. data/sig/pvectl/models/storage.rbs +39 -0
  447. data/sig/pvectl/models/subscription.rbs +24 -0
  448. data/sig/pvectl/models/syslog_entry.rbs +10 -0
  449. data/sig/pvectl/models/task.rbs +22 -0
  450. data/sig/pvectl/models/task_entry.rbs +24 -0
  451. data/sig/pvectl/models/task_log_line.rbs +10 -0
  452. data/sig/pvectl/models/time_config.rbs +12 -0
  453. data/sig/pvectl/models/vm.rbs +32 -0
  454. data/sig/pvectl/models/vm_operation_result.rbs +9 -0
  455. data/sig/pvectl/models/volume.rbs +29 -0
  456. data/sig/pvectl/models/volume_operation_result.rbs +9 -0
  457. data/sig/pvectl/parsers/cloud_init_config.rbs +15 -0
  458. data/sig/pvectl/parsers/disk_config.rbs +19 -0
  459. data/sig/pvectl/parsers/lxc_mount_config.rbs +19 -0
  460. data/sig/pvectl/parsers/lxc_net_config.rbs +19 -0
  461. data/sig/pvectl/parsers/net_config.rbs +19 -0
  462. data/sig/pvectl/parsers/smart_text.rbs +7 -0
  463. data/sig/pvectl/plugin_loader.rbs +25 -0
  464. data/sig/pvectl/presenters/apt_package.rbs +19 -0
  465. data/sig/pvectl/presenters/backup.rbs +25 -0
  466. data/sig/pvectl/presenters/base.rbs +41 -0
  467. data/sig/pvectl/presenters/capability.rbs +19 -0
  468. data/sig/pvectl/presenters/config/context.rbs +17 -0
  469. data/sig/pvectl/presenters/container.rbs +78 -0
  470. data/sig/pvectl/presenters/container_operation_result.rbs +19 -0
  471. data/sig/pvectl/presenters/disk.rbs +31 -0
  472. data/sig/pvectl/presenters/dns_config.rbs +13 -0
  473. data/sig/pvectl/presenters/hosts_file.rbs +13 -0
  474. data/sig/pvectl/presenters/journal_entry.rbs +11 -0
  475. data/sig/pvectl/presenters/node.rbs +118 -0
  476. data/sig/pvectl/presenters/node_operation_result.rbs +11 -0
  477. data/sig/pvectl/presenters/operation_result.rbs +17 -0
  478. data/sig/pvectl/presenters/service.rbs +15 -0
  479. data/sig/pvectl/presenters/snapshot.rbs +35 -0
  480. data/sig/pvectl/presenters/snapshot_operation_result.rbs +27 -0
  481. data/sig/pvectl/presenters/storage.rbs +59 -0
  482. data/sig/pvectl/presenters/subscription.rbs +36 -0
  483. data/sig/pvectl/presenters/syslog_entry.rbs +11 -0
  484. data/sig/pvectl/presenters/task_entry.rbs +21 -0
  485. data/sig/pvectl/presenters/task_log_line.rbs +11 -0
  486. data/sig/pvectl/presenters/template.rbs +15 -0
  487. data/sig/pvectl/presenters/time_config.rbs +19 -0
  488. data/sig/pvectl/presenters/top_container.rbs +17 -0
  489. data/sig/pvectl/presenters/top_node.rbs +17 -0
  490. data/sig/pvectl/presenters/top_presenter.rbs +13 -0
  491. data/sig/pvectl/presenters/top_vm.rbs +17 -0
  492. data/sig/pvectl/presenters/vm.rbs +91 -0
  493. data/sig/pvectl/presenters/vm_operation_result.rbs +19 -0
  494. data/sig/pvectl/presenters/volume.rbs +23 -0
  495. data/sig/pvectl/presenters/volume_operation_result.rbs +11 -0
  496. data/sig/pvectl/repositories/apt.rbs +17 -0
  497. data/sig/pvectl/repositories/backup.rbs +27 -0
  498. data/sig/pvectl/repositories/base.rbs +23 -0
  499. data/sig/pvectl/repositories/capabilities.rbs +20 -0
  500. data/sig/pvectl/repositories/container.rbs +63 -0
  501. data/sig/pvectl/repositories/disk.rbs +17 -0
  502. data/sig/pvectl/repositories/dns.rbs +13 -0
  503. data/sig/pvectl/repositories/hosts.rbs +13 -0
  504. data/sig/pvectl/repositories/journal.rbs +7 -0
  505. data/sig/pvectl/repositories/node.rbs +68 -0
  506. data/sig/pvectl/repositories/service.rbs +27 -0
  507. data/sig/pvectl/repositories/snapshot.rbs +19 -0
  508. data/sig/pvectl/repositories/storage.rbs +37 -0
  509. data/sig/pvectl/repositories/subscription.rbs +17 -0
  510. data/sig/pvectl/repositories/syslog.rbs +7 -0
  511. data/sig/pvectl/repositories/task.rbs +19 -0
  512. data/sig/pvectl/repositories/task_list.rbs +7 -0
  513. data/sig/pvectl/repositories/task_log.rbs +11 -0
  514. data/sig/pvectl/repositories/time_config.rbs +13 -0
  515. data/sig/pvectl/repositories/vm.rbs +85 -0
  516. data/sig/pvectl/repositories/volume.rbs +43 -0
  517. data/sig/pvectl/selectors/base.rbs +37 -0
  518. data/sig/pvectl/selectors/container.rbs +19 -0
  519. data/sig/pvectl/selectors/disk.rbs +13 -0
  520. data/sig/pvectl/selectors/vm.rbs +19 -0
  521. data/sig/pvectl/selectors/volume.rbs +13 -0
  522. data/sig/pvectl/services/backup.rbs +27 -0
  523. data/sig/pvectl/services/clone_container.rbs +35 -0
  524. data/sig/pvectl/services/clone_vm.rbs +35 -0
  525. data/sig/pvectl/services/cloudinit.rbs +19 -0
  526. data/sig/pvectl/services/console.rbs +23 -0
  527. data/sig/pvectl/services/container_lifecycle.rbs +26 -0
  528. data/sig/pvectl/services/create_container.rbs +64 -0
  529. data/sig/pvectl/services/create_vm.rbs +72 -0
  530. data/sig/pvectl/services/edit_container.rbs +17 -0
  531. data/sig/pvectl/services/edit_dns.rbs +23 -0
  532. data/sig/pvectl/services/edit_hosts.rbs +13 -0
  533. data/sig/pvectl/services/edit_node.rbs +21 -0
  534. data/sig/pvectl/services/edit_vm.rbs +17 -0
  535. data/sig/pvectl/services/edit_volume.rbs +18 -0
  536. data/sig/pvectl/services/get/resource_service.rbs +23 -0
  537. data/sig/pvectl/services/move_disk.rbs +21 -0
  538. data/sig/pvectl/services/pull_config.rbs +18 -0
  539. data/sig/pvectl/services/push_config.rbs +37 -0
  540. data/sig/pvectl/services/resize_volume.rbs +47 -0
  541. data/sig/pvectl/services/resource_delete.rbs +27 -0
  542. data/sig/pvectl/services/resource_migration.rbs +29 -0
  543. data/sig/pvectl/services/sendkey.rbs +19 -0
  544. data/sig/pvectl/services/service_lifecycle.rbs +17 -0
  545. data/sig/pvectl/services/set_container.rbs +14 -0
  546. data/sig/pvectl/services/set_node.rbs +23 -0
  547. data/sig/pvectl/services/set_vm.rbs +14 -0
  548. data/sig/pvectl/services/set_volume.rbs +12 -0
  549. data/sig/pvectl/services/snapshot.rbs +35 -0
  550. data/sig/pvectl/services/task_listing.rbs +13 -0
  551. data/sig/pvectl/services/unlink_disk.rbs +17 -0
  552. data/sig/pvectl/services/vm_lifecycle.rbs +26 -0
  553. data/sig/pvectl/services/wakeonlan.rbs +17 -0
  554. data/sig/pvectl/utils/resource_resolver.rbs +17 -0
  555. data/sig/pvectl/wizards/create_container.rbs +21 -0
  556. data/sig/pvectl/wizards/create_vm.rbs +21 -0
  557. data/sig/pvectl.rbs +9 -0
  558. 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