proxcli 0.16.0__tar.gz → 0.16.2__tar.gz

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 (139) hide show
  1. {proxcli-0.16.0 → proxcli-0.16.2}/CHANGELOG.md +34 -1
  2. {proxcli-0.16.0 → proxcli-0.16.2}/PKG-INFO +16 -1
  3. {proxcli-0.16.0 → proxcli-0.16.2}/README.md +15 -0
  4. {proxcli-0.16.0 → proxcli-0.16.2}/TODO.md +3 -0
  5. {proxcli-0.16.0 → proxcli-0.16.2}/docs/cloud-init.md +30 -1
  6. {proxcli-0.16.0 → proxcli-0.16.2}/docs/production-automation.md +1 -1
  7. {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/main.py +4 -2
  8. proxcli-0.16.2/proxmox/cli/update.py +154 -0
  9. {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/vm.py +119 -0
  10. {proxcli-0.16.0 → proxcli-0.16.2}/pyproject.toml +1 -1
  11. {proxcli-0.16.0 → proxcli-0.16.2}/uv.lock +1 -1
  12. {proxcli-0.16.0 → proxcli-0.16.2}/.env.example +0 -0
  13. {proxcli-0.16.0 → proxcli-0.16.2}/.github/workflows/ci.yml +0 -0
  14. {proxcli-0.16.0 → proxcli-0.16.2}/.gitignore +0 -0
  15. {proxcli-0.16.0 → proxcli-0.16.2}/.python-version +0 -0
  16. {proxcli-0.16.0 → proxcli-0.16.2}/AGENTS.md +0 -0
  17. {proxcli-0.16.0 → proxcli-0.16.2}/PLAN.md +0 -0
  18. {proxcli-0.16.0 → proxcli-0.16.2}/PROJECT.md +0 -0
  19. {proxcli-0.16.0 → proxcli-0.16.2}/PROMPT.md +0 -0
  20. {proxcli-0.16.0 → proxcli-0.16.2}/docs/DESIGN.md +0 -0
  21. {proxcli-0.16.0 → proxcli-0.16.2}/docs/api-coverage.md +0 -0
  22. {proxcli-0.16.0 → proxcli-0.16.2}/docs/api-permissions.md +0 -0
  23. {proxcli-0.16.0 → proxcli-0.16.2}/docs/assets/index-Bzx5-fXr.css +0 -0
  24. {proxcli-0.16.0 → proxcli-0.16.2}/docs/assets/index-CXpOjuxt.css +0 -0
  25. {proxcli-0.16.0 → proxcli-0.16.2}/docs/assets/index-DLDs0H6j.js +0 -0
  26. {proxcli-0.16.0 → proxcli-0.16.2}/docs/assets/index-DYQPMK8x.js +0 -0
  27. {proxcli-0.16.0 → proxcli-0.16.2}/docs/coding-agents.md +0 -0
  28. {proxcli-0.16.0 → proxcli-0.16.2}/docs/coverage.json +0 -0
  29. {proxcli-0.16.0 → proxcli-0.16.2}/docs/demos/cluster.gif +0 -0
  30. {proxcli-0.16.0 → proxcli-0.16.2}/docs/demos/node-list.gif +0 -0
  31. {proxcli-0.16.0 → proxcli-0.16.2}/docs/demos/vm-list.gif +0 -0
  32. {proxcli-0.16.0 → proxcli-0.16.2}/docs/demos/vm-show.gif +0 -0
  33. {proxcli-0.16.0 → proxcli-0.16.2}/docs/demos/yaml-spec.gif +0 -0
  34. {proxcli-0.16.0 → proxcli-0.16.2}/docs/favicon.svg +0 -0
  35. {proxcli-0.16.0 → proxcli-0.16.2}/docs/icons.svg +0 -0
  36. {proxcli-0.16.0 → proxcli-0.16.2}/docs/index.html +0 -0
  37. {proxcli-0.16.0 → proxcli-0.16.2}/docs/quickstart.md +0 -0
  38. {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/.gitignore +0 -0
  39. {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/README.md +0 -0
  40. {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/build-all.sh +0 -0
  41. {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/eslint.config.js +0 -0
  42. {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/index.html +0 -0
  43. {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/package-lock.json +0 -0
  44. {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/package.json +0 -0
  45. {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/public/coverage.json +0 -0
  46. {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/public/demos/cluster.gif +0 -0
  47. {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/public/demos/node-list.gif +0 -0
  48. {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/public/demos/vm-list.gif +0 -0
  49. {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/public/demos/vm-show.gif +0 -0
  50. {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/public/demos/yaml-spec.gif +0 -0
  51. {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/public/favicon.svg +0 -0
  52. {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/public/icons.svg +0 -0
  53. {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/remotion-demos/.gitignore +0 -0
  54. {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/remotion-demos/.prettierrc +0 -0
  55. {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/remotion-demos/README.md +0 -0
  56. {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/remotion-demos/eslint.config.mjs +0 -0
  57. {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/remotion-demos/package-lock.json +0 -0
  58. {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/remotion-demos/package.json +0 -0
  59. {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/remotion-demos/public/cluster-real.txt +0 -0
  60. {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/remotion-demos/public/node-list-real.txt +0 -0
  61. {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/remotion-demos/public/vm-list-real.txt +0 -0
  62. {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/remotion-demos/public/vm-show-real.txt +0 -0
  63. {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/remotion-demos/remotion.config.ts +0 -0
  64. {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/remotion-demos/src/Composition.tsx +0 -0
  65. {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/remotion-demos/src/Root.tsx +0 -0
  66. {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/remotion-demos/src/index.css +0 -0
  67. {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/remotion-demos/src/index.ts +0 -0
  68. {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/remotion-demos/tsconfig.json +0 -0
  69. {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/src/App.jsx +0 -0
  70. {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/src/components/CoverageGrid.jsx +0 -0
  71. {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/src/components/SplitFlapAgent.jsx +0 -0
  72. {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/src/index.css +0 -0
  73. {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/src/main.jsx +0 -0
  74. {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/src/pages/Docs.jsx +0 -0
  75. {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/src/pages/Landing.jsx +0 -0
  76. {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/vite.config.js +0 -0
  77. {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/__init__.py +0 -0
  78. {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/__init__.py +0 -0
  79. {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/acl.py +0 -0
  80. {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/api.py +0 -0
  81. {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/auth.py +0 -0
  82. {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/backup.py +0 -0
  83. {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/ceph.py +0 -0
  84. {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/cluster.py +0 -0
  85. {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/completion.py +0 -0
  86. {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/container.py +0 -0
  87. {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/firewall_helpers.py +0 -0
  88. {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/network.py +0 -0
  89. {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/node.py +0 -0
  90. {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/pool.py +0 -0
  91. {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/role.py +0 -0
  92. {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/storage.py +0 -0
  93. {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/tasks.py +0 -0
  94. {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/user.py +0 -0
  95. {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/vm_spec.py +0 -0
  96. {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/client/__init__.py +0 -0
  97. {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/client/auth.py +0 -0
  98. {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/client/client.py +0 -0
  99. {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/client/exceptions.py +0 -0
  100. {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/config/__init__.py +0 -0
  101. {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/config/config.py +0 -0
  102. {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/config/models.py +0 -0
  103. {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/output/__init__.py +0 -0
  104. {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/output/formatter.py +0 -0
  105. {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/output/json_fmt.py +0 -0
  106. {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/output/log_fmt.py +0 -0
  107. {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/output/table_fmt.py +0 -0
  108. {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/output/yaml_fmt.py +0 -0
  109. {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/utils/__init__.py +0 -0
  110. {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/utils/helpers.py +0 -0
  111. {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/utils/logging.py +0 -0
  112. {proxcli-0.16.0 → proxcli-0.16.2}/tests/__init__.py +0 -0
  113. {proxcli-0.16.0 → proxcli-0.16.2}/tests/conftest.py +0 -0
  114. {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_auth.py +0 -0
  115. {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/__init__.py +0 -0
  116. {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/test_api.py +0 -0
  117. {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/test_backup.py +0 -0
  118. {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/test_backup_restore.py +0 -0
  119. {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/test_ceph.py +0 -0
  120. {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/test_container_ip.py +0 -0
  121. {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/test_main.py +0 -0
  122. {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/test_network.py +0 -0
  123. {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/test_node_system.py +0 -0
  124. {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/test_role_acl.py +0 -0
  125. {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/test_task_wait.py +0 -0
  126. {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/test_user.py +0 -0
  127. {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/test_vm_agent.py +0 -0
  128. {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/test_vm_clone.py +0 -0
  129. {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/test_vm_disk.py +0 -0
  130. {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/test_vm_ip.py +0 -0
  131. {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/test_vm_iso.py +0 -0
  132. {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/test_vm_migrate.py +0 -0
  133. {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/test_vm_set.py +0 -0
  134. {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/test_vm_template.py +0 -0
  135. {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_client.py +0 -0
  136. {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_config.py +0 -0
  137. {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_integration/__init__.py +0 -0
  138. {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_output/__init__.py +0 -0
  139. {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_output/test_formatter.py +0 -0
@@ -5,7 +5,37 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [0.15.0] - 2026-06-30
8
+ ## [0.16.2] - 2026-06-30
9
+
10
+ ### Added
11
+ - **``proxmox update``**: self-upgrade proxcli from PyPI. Queries the PyPI JSON
12
+ API to check for new versions and uses ``uv tool install proxcli --reinstall``
13
+ to upgrade in-place. ``--check`` flag reports available updates without
14
+ installing. ``--pre`` includes pre-release versions.
15
+
16
+ ## [0.16.1] - 2026-06-30
17
+
18
+ ### Added
19
+ - **``proxmox vm disk import``**: import a disk image into an existing VM.
20
+ Supports two sources:
21
+ - ``--image <path>``: a disk image already on the PVE node filesystem
22
+ (e.g. ``/var/lib/vz/import/deb13.qcow2``).
23
+ - ``--url <url>``: downloads the image locally, uploads it to Proxmox
24
+ storage via the API, then imports it into the VM — all in one command.
25
+ The temp file is cleaned up automatically. Wraps
26
+ ``PUT /nodes/{node}/qemu/{vmid}/config`` with `import-from`.
27
+ - **Cloud-init auto-config**: ``vm create`` now automatically adds
28
+ ``serial0=socket`` and ``vga=serial0`` when any cloud-init flags
29
+ (``--citype``, ``--ciuser``, etc.) are present. This matches the
30
+ official Proxmox documentation and is required by Debian generic
31
+ cloud images.
32
+
33
+ ### Changed
34
+ - **Docs**: added ``vm disk import`` to README command reference;
35
+ updated ``docs/cloud-init.md`` with ``--url`` import workflow;
36
+ updated ``docs/production-automation.md`` with the new commands.
37
+
38
+ ## [0.16.0] - 2026-06-30
9
39
 
10
40
  ### Added
11
41
  - **``proxmox vm set``**: update VM configuration keys. Wraps
@@ -333,6 +363,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
333
363
  - CSRF ticket auto-refresh on 401.
334
364
  - AI-agent-friendly: default JSON output, strict exit codes, `--dry-run` mode.
335
365
 
366
+ [0.16.2]: https://github.com/xezpeleta/proxcli/releases/tag/v0.16.2
367
+ [0.16.1]: https://github.com/xezpeleta/proxcli/releases/tag/v0.16.1
368
+ [0.16.0]: https://github.com/xezpeleta/proxcli/releases/tag/v0.16.0
336
369
  [0.15.0]: https://github.com/xezpeleta/proxcli/releases/tag/v0.15.0
337
370
  [0.14.0]: https://github.com/xezpeleta/proxcli/releases/tag/v0.14.0
338
371
  [0.13.1]: https://github.com/xezpeleta/proxcli/releases/tag/v0.13.1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proxcli
3
- Version: 0.16.0
3
+ Version: 0.16.2
4
4
  Summary: A CLI tool to interact with Proxmox VE nodes and clusters via the REST API
5
5
  Author-email: Xabi Ezpeleta <xezpeleta@gmail.com>
6
6
  License: MIT
@@ -192,6 +192,14 @@ proxmox auth setup # Create recommended roles + ACLs (needs Administ
192
192
  proxmox auth check # Live permission test table (39 checks)
193
193
  ```
194
194
 
195
+ ### Self-update
196
+
197
+ ```bash
198
+ proxmox update # Check for updates and install the latest version
199
+ proxmox update --check # Only check (no install)
200
+ proxmox update --pre # Include pre-release versions
201
+ ```
202
+
195
203
  ### Completion
196
204
 
197
205
  ```bash
@@ -251,6 +259,13 @@ proxmox vm firewall rules show <vmid> <pos>
251
259
  proxmox vm firewall rules update <vmid> <pos> --action DROP
252
260
  proxmox vm firewall rules delete <vmid> <pos>
253
261
  proxmox vm firewall refs <vmid> [--type alias|ipset|group]
262
+
263
+ # VM disk management
264
+ proxmox vm disk import <vmid> --image /path/to/image.qcow2 [--disk scsi0] [--storage rbd_ssd]
265
+ proxmox vm disk import <vmid> --url https://.../image.qcow2 [--disk scsi0] [--storage rbd_ssd]
266
+ proxmox vm disk resize <vmid> --disk scsi0 --size +10G
267
+ proxmox vm disk detach <vmid> --disk scsi0
268
+ proxmox vm disk remove <vmid> --disk scsi0 [--force]
254
269
  ```
255
270
 
256
271
  #### Declarative VM specs (`--file`)
@@ -169,6 +169,14 @@ proxmox auth setup # Create recommended roles + ACLs (needs Administ
169
169
  proxmox auth check # Live permission test table (39 checks)
170
170
  ```
171
171
 
172
+ ### Self-update
173
+
174
+ ```bash
175
+ proxmox update # Check for updates and install the latest version
176
+ proxmox update --check # Only check (no install)
177
+ proxmox update --pre # Include pre-release versions
178
+ ```
179
+
172
180
  ### Completion
173
181
 
174
182
  ```bash
@@ -228,6 +236,13 @@ proxmox vm firewall rules show <vmid> <pos>
228
236
  proxmox vm firewall rules update <vmid> <pos> --action DROP
229
237
  proxmox vm firewall rules delete <vmid> <pos>
230
238
  proxmox vm firewall refs <vmid> [--type alias|ipset|group]
239
+
240
+ # VM disk management
241
+ proxmox vm disk import <vmid> --image /path/to/image.qcow2 [--disk scsi0] [--storage rbd_ssd]
242
+ proxmox vm disk import <vmid> --url https://.../image.qcow2 [--disk scsi0] [--storage rbd_ssd]
243
+ proxmox vm disk resize <vmid> --disk scsi0 --size +10G
244
+ proxmox vm disk detach <vmid> --disk scsi0
245
+ proxmox vm disk remove <vmid> --disk scsi0 [--force]
231
246
  ```
232
247
 
233
248
  #### Declarative VM specs (`--file`)
@@ -27,6 +27,9 @@ Completed items are marked with a check. Implementation notes are preserved for
27
27
  - [x] **Cluster log & options** — `proxmox cluster log [--limit N]` and `proxmox cluster options`.
28
28
  - [x] **Storage status** — `proxmox storage status <storage> [--node]` for usage stats.
29
29
  - [x] **API coverage doc** — `docs/api-coverage.md` tracking all implemented and remaining endpoints.
30
+ - [x] **VM disk import from URL** — `vm disk import --url` downloads, uploads, and imports a disk image from a remote URL in one command. `--image` for local PVE node paths.
31
+ - [x] **Cloud-init auto serial console** — `vm create` with cloud-init flags now auto-adds `serial0=socket` and `vga=serial0`, required by Debian generic cloud images.
32
+ - [x] **Self-update** — `proxmox update` checks PyPI for newer versions and upgrades via `uv tool install --reinstall`. `--check` for dry-run, `--pre` for pre-releases.
30
33
 
31
34
  ## v1.1 — Polish & Usability
32
35
 
@@ -386,6 +386,8 @@ Stable release URLs follow the pattern:
386
386
 
387
387
  The `/latest/` symlink always points to the most recent weekly build.
388
388
 
389
+ **Option A — Download & upload manually:**
390
+
389
391
  ```bash
390
392
  # Download (run on a Proxmox node or any machine with proxcli)
391
393
  curl -LO https://cloud.debian.org/images/cloud/trixie/latest/debian-13-generic-amd64.qcow2
@@ -402,6 +404,18 @@ proxmox storage content local --node <node>
402
404
  # Look for: local:import/debian-13-generic-amd64.qcow2
403
405
  ```
404
406
 
407
+ **Option B — Use `vm disk import --url` (download + upload + import in one command):**
408
+
409
+ ```bash
410
+ # After creating the VM skeleton, import directly from URL:
411
+ proxmox vm disk import <template-vmid> \
412
+ --url https://cloud.debian.org/images/cloud/trixie/latest/debian-13-generic-amd64.qcow2 \
413
+ --storage rbd_ssd
414
+ ```
415
+
416
+ This handles downloading, uploading to PVE storage, and importing the disk in a single command.
417
+ The temporary file is cleaned up automatically.
418
+
405
419
  ### Step 2: Create the VM with cloud-init (stopped)
406
420
 
407
421
  ```bash
@@ -481,8 +495,9 @@ SSH keys).
481
495
 
482
496
  ```bash
483
497
  # --- One-time setup ---
484
- curl -LO https://cloud.debian.org/images/cloud/trixie/latest/debian-13-generic-amd64.qcow2
485
498
 
499
+ # Option A: Manual download + upload + create
500
+ curl -LO https://cloud.debian.org/images/cloud/trixie/latest/debian-13-generic-amd64.qcow2
486
501
  proxmox storage upload \
487
502
  --node pve01 --storage local --content-type import \
488
503
  --file debian-13-generic-amd64.qcow2
@@ -497,6 +512,20 @@ proxmox vm create \
497
512
  --sshkeys ~/.ssh/id_rsa.pub \
498
513
  --import-from local:import/debian-13-generic-amd64.qcow2
499
514
 
515
+ # Option B: One-liner with --url (no manual download needed)
516
+ proxmox vm create \
517
+ --node pve01 --vmid 9003 \
518
+ --name debian13-cloud-template \
519
+ --memory 2048 --cores 2 \
520
+ --net 'virtio,bridge=vmbr0' \
521
+ --scsihw virtio-scsi-pci \
522
+ --citype nocloud --ciuser debian \
523
+ --sshkeys ~/.ssh/id_rsa.pub
524
+ proxmox vm disk import 9003 \
525
+ --url https://cloud.debian.org/images/cloud/trixie/latest/debian-13-generic-amd64.qcow2 \
526
+ --storage rbd_ssd
527
+ proxmox vm set 9003 --option boot=order=scsi0
528
+
500
529
  proxmox vm template 9003 --node pve01
501
530
 
502
531
  # --- Clone for each new VM (repeat as needed) ---
@@ -156,7 +156,7 @@ jobs:
156
156
  PROXMOX_TOKEN_ID: ${{ secrets.PROXMOX_TOKEN_ID }}
157
157
  PROXMOX_TOKEN_SECRET: ${{ secrets.PROXMOX_TOKEN_SECRET }}
158
158
  run: |
159
- proxmox auth login
159
+ proxmox auth status
160
160
  for spec in vm-specs/*.yaml; do
161
161
  proxmox vm create --file "$spec"
162
162
  done
@@ -74,6 +74,7 @@ def build_root_parser() -> argparse.ArgumentParser:
74
74
  from proxmox.cli.role import register_role_parser
75
75
  from proxmox.cli.storage import register_storage_parser
76
76
  from proxmox.cli.tasks import register_task_parser
77
+ from proxmox.cli.update import register_update_parser
77
78
  from proxmox.cli.user import register_user_parser
78
79
  from proxmox.cli.vm import register_vm_parser
79
80
 
@@ -93,6 +94,7 @@ def build_root_parser() -> argparse.ArgumentParser:
93
94
  register_task_parser(subparsers)
94
95
  register_role_parser(subparsers)
95
96
  register_user_parser(subparsers)
97
+ register_update_parser(subparsers)
96
98
 
97
99
  return parser
98
100
 
@@ -224,7 +226,7 @@ GLOBAL_FLAGS_WITH_VALUE = {"--url", "--username", "--password", "--api-token", "
224
226
  def _hint_global_flags_order(argv: list[str]) -> None:
225
227
  """If user placed global flags after the resource, show a helpful hint."""
226
228
  resource_pos = -1
227
- resources = {"acl", "api", "auth", "backup", "ceph", "vm", "node", "pool", "container", "storage", "cluster", "completion", "task", "user", "role", "network"}
229
+ resources = {"acl", "api", "auth", "backup", "ceph", "vm", "node", "pool", "container", "storage", "cluster", "completion", "task", "user", "role", "network", "update"}
228
230
  for i, arg in enumerate(argv):
229
231
  if arg in resources:
230
232
  resource_pos = i
@@ -297,7 +299,7 @@ def main(argv: list[str] | None = None) -> None:
297
299
 
298
300
  try:
299
301
  # auth status (without --permissions) and completion don't need a client
300
- if args.resource == "completion" or (
302
+ if args.resource == "completion" or args.resource == "update" or (
301
303
  args.resource == "auth"
302
304
  and args.action == "status"
303
305
  and not getattr(args, "permissions", False)
@@ -0,0 +1,154 @@
1
+ """proxmox update — self-upgrade proxcli from PyPI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import subprocess
8
+ import sys
9
+ import urllib.request
10
+ from importlib.metadata import version
11
+
12
+ from proxmox.utils.logging import log_error
13
+
14
+
15
+ def register_update_parser(subparsers: argparse._SubParsersAction) -> None:
16
+ update_parser = subparsers.add_parser("update", help="Check for and install proxcli updates")
17
+ update_parser.add_argument(
18
+ "--check",
19
+ action="store_true",
20
+ help="Only check if an update is available (do not install)",
21
+ )
22
+ update_parser.add_argument(
23
+ "--pre",
24
+ action="store_true",
25
+ help="Include pre-release versions when checking",
26
+ )
27
+ update_parser.set_defaults(func=_update, output_format="table")
28
+
29
+
30
+ def _get_latest_version(include_pre: bool = False) -> str:
31
+ """Fetch the latest version tag from PyPI."""
32
+ url = "https://pypi.org/pypi/proxcli/json"
33
+ try:
34
+ with urllib.request.urlopen(url, timeout=10) as resp:
35
+ data = json.loads(resp.read().decode())
36
+ except Exception as exc:
37
+ raise RuntimeError(f"Failed to query PyPI: {exc}") from exc
38
+
39
+ versions = list(data.get("releases", {}).keys())
40
+
41
+ if not versions:
42
+ raise RuntimeError("No releases found on PyPI")
43
+
44
+ if not include_pre:
45
+ versions = [v for v in versions if _is_stable(v)]
46
+ if not versions:
47
+ raise RuntimeError("No stable releases found on PyPI")
48
+
49
+ return max(versions, key=_version_key)
50
+
51
+
52
+ def _is_stable(ver: str) -> bool:
53
+ """Return True if version looks like a stable release (no dev/pre markers)."""
54
+ # Strip epoch if present
55
+ v = ver
56
+ # Simple check: no a/b/rc/dev/post markers
57
+ for marker in ("a", "b", "rc", "dev", "post"):
58
+ if marker in v:
59
+ return False
60
+ return True
61
+
62
+
63
+ def _version_key(ver: str) -> tuple:
64
+ """Parse a version string into a comparable tuple."""
65
+ parts = ver.split(".")
66
+ result = []
67
+ for p in parts:
68
+ # Try int first
69
+ try:
70
+ result.append(int(p))
71
+ except ValueError:
72
+ result.append(p)
73
+ return tuple(result)
74
+
75
+
76
+ def _update(args: argparse.Namespace, client: object | None = None) -> dict | str:
77
+ """Check for updates and optionally install them.
78
+
79
+ Returns a dict for the output formatter.
80
+ """
81
+ current = version("proxcli")
82
+
83
+ try:
84
+ latest = _get_latest_version(include_pre=args.pre)
85
+ except RuntimeError as exc:
86
+ return {"error": str(exc)}
87
+
88
+ up_to_date = _version_key(current) >= _version_key(latest)
89
+
90
+ if args.check:
91
+ if up_to_date:
92
+ return {
93
+ "current": current,
94
+ "latest": latest,
95
+ "status": "up-to-date",
96
+ "message": f"proxcli {current} is the latest version.",
97
+ }
98
+ else:
99
+ return {
100
+ "current": current,
101
+ "latest": latest,
102
+ "status": "update-available",
103
+ "message": f"proxcli {latest} is available (you have {current}). Run 'proxmox update' to upgrade.",
104
+ }
105
+
106
+ if up_to_date:
107
+ return {
108
+ "current": current,
109
+ "latest": latest,
110
+ "status": "up-to-date",
111
+ "message": f"proxcli {current} is already the latest version.",
112
+ }
113
+
114
+ # Run uv tool install
115
+ log_error(f"Upgrading proxcli from {current} to {latest}...", file=sys.stderr)
116
+ try:
117
+ result = subprocess.run(
118
+ ["uv", "tool", "install", "proxcli", "--reinstall"],
119
+ capture_output=True,
120
+ text=True,
121
+ timeout=120,
122
+ )
123
+ if result.returncode != 0:
124
+ error_msg = result.stderr.strip() or result.stdout.strip() or f"exit code {result.returncode}"
125
+ log_error(error_msg)
126
+ return {
127
+ "current": current,
128
+ "latest": latest,
129
+ "status": "error",
130
+ "message": f"Upgrade failed: {error_msg}",
131
+ }
132
+ except FileNotFoundError:
133
+ return {
134
+ "current": current,
135
+ "latest": latest,
136
+ "status": "error",
137
+ "message": "uv not found in PATH. Install uv (https://docs.astral.sh/uv/) or upgrade manually with 'uv tool install proxcli --reinstall'.",
138
+ }
139
+ except subprocess.TimeoutExpired:
140
+ return {
141
+ "current": current,
142
+ "latest": latest,
143
+ "status": "error",
144
+ "message": "Upgrade timed out. Try again or upgrade manually.",
145
+ }
146
+
147
+ new_version = version("proxcli")
148
+ return {
149
+ "current": new_version,
150
+ "previous": current,
151
+ "latest": latest,
152
+ "status": "upgraded",
153
+ "message": f"proxcli upgraded from {current} to {new_version}.",
154
+ }
@@ -186,6 +186,17 @@ def register_vm_parser(subparsers: argparse._SubParsersAction) -> None:
186
186
  vm_disk = vm_sub.add_parser("disk", help="Manage VM disks")
187
187
  disk_sub = vm_disk.add_subparsers(dest="disk_action", title="disk actions", required=True)
188
188
 
189
+ disk_import = disk_sub.add_parser("import", help="Import a disk image into a VM")
190
+ disk_import.add_argument("vmid", type=vmid_type, help="VM ID")
191
+ disk_import.add_argument("--node", help="Node name (auto-detected if omitted)")
192
+ disk_import.add_argument("--disk", default="scsi0", help="Disk slot to attach to (default: scsi0)")
193
+ disk_import.add_argument("--storage", default=None, help="Target storage (default: rbd_ssd)")
194
+ disk_import.add_argument("--image", default=None, dest="image",
195
+ help="Path to disk image on the PVE node (e.g. /var/lib/vz/import/deb13.qcow2)")
196
+ disk_import.add_argument("--url", default=None, dest="image_url",
197
+ help="URL to download the disk image from (downloads locally, uploads to PVE, then imports)")
198
+ disk_import.set_defaults(func=_vm_disk_import)
199
+
189
200
  disk_resize = disk_sub.add_parser("resize", help="Resize a VM disk")
190
201
  disk_resize.add_argument("vmid", type=vmid_type, help="VM ID")
191
202
  disk_resize.add_argument("--disk", required=True, help="Disk to resize (e.g. scsi0, virtio0)")
@@ -564,6 +575,13 @@ def _vm_create(args: argparse.Namespace, client: ProxmoxClient) -> dict:
564
575
  if has_cloudinit and not has_cdrom:
565
576
  data["ide2"] = "local:cloudinit,media=cdrom"
566
577
 
578
+ # Auto-configure serial console and VGA for cloud-init VMs
579
+ if has_cloudinit:
580
+ if not data.get("serial0"):
581
+ data["serial0"] = "socket"
582
+ if not data.get("vga"):
583
+ data["vga"] = "serial0"
584
+
567
585
  # Validate required fields
568
586
  if "memory" not in data:
569
587
  return {"error": "--memory is required (or set 'memory:' in the spec file)"}
@@ -885,6 +903,107 @@ def _vm_ip(args: argparse.Namespace, client: ProxmoxClient) -> dict | list:
885
903
  return ips if ips else {"message": f"No non-local IPs found for VM {args.vmid}"}
886
904
 
887
905
 
906
+ def _vm_disk_import(args: argparse.Namespace, client: ProxmoxClient) -> dict:
907
+ """Import a disk image into an existing VM.
908
+
909
+ Supports two sources:
910
+ - ``--image``: a path to a disk image already on the PVE node
911
+ (e.g. /var/lib/vz/import/deb13.qcow2).
912
+ - ``--url``: downloads the image locally, uploads it to PVE storage,
913
+ then imports it — all in one command.
914
+
915
+ Wraps ``PUT /nodes/{node}/qemu/{vmid}/config`` with
916
+ ``{disk}: {storage}:0,import-from={path}``.
917
+ """
918
+ import os
919
+ import tempfile
920
+ import urllib.error
921
+ import urllib.request
922
+
923
+ node = _resolve_node(client, args.node, args.vmid)
924
+ if not node:
925
+ return {"error": f"VM {args.vmid} not found"}
926
+
927
+ if not args.image and not args.image_url:
928
+ return {"error": "Either --image or --url is required"}
929
+ if args.image and args.image_url:
930
+ return {"error": "Only one of --image or --url can be specified"}
931
+
932
+ disk_slot = args.disk
933
+ storage = args.storage or "rbd_ssd"
934
+
935
+ def _do_import(image_path: str) -> dict:
936
+ """Attach the disk via import-from and return result."""
937
+ import_raw = f"{storage}:0,import-from={image_path}"
938
+ encoded = import_raw.replace("=", "%3D").replace(":", "%3A").replace(",", "%2C")
939
+
940
+ from urllib.parse import urlencode
941
+ body = urlencode({disk_slot: encoded}, safe="%")
942
+
943
+ result = client.request("PUT", f"/nodes/{node}/qemu/{args.vmid}/config", content=body)
944
+ if isinstance(result, dict) and "data" not in result and "error" not in result:
945
+ result = {"data": result, "vmid": args.vmid, "disk": disk_slot, "image": image_path, "_node": node}
946
+ return result
947
+
948
+ # --- Option A: local image path on PVE node ---
949
+ if args.image:
950
+ return _do_import(args.image)
951
+
952
+ # --- Option B: URL download + upload + import ---
953
+ url = args.image_url
954
+ tmp_path = None
955
+
956
+ try:
957
+ # 1. Download to a temporary file
958
+ fname = url.rsplit("/", 1)[-1].split("?")[0] or "disk-image.qcow2"
959
+ with tempfile.NamedTemporaryFile(suffix=f"-{fname}", delete=False) as tmp:
960
+ tmp_path = tmp.name
961
+
962
+ print(f"Downloading {url} ...")
963
+ try:
964
+ urllib.request.urlretrieve(url, tmp_path)
965
+ except urllib.error.URLError as exc:
966
+ if tmp_path and os.path.isfile(tmp_path):
967
+ os.unlink(tmp_path)
968
+ return {"error": f"Failed to download image: {exc}"}
969
+
970
+ file_size = os.path.getsize(tmp_path)
971
+ print(f"Downloaded {fname} ({file_size} bytes)")
972
+
973
+ # 2. Upload to PVE storage as 'import' content
974
+ print(f"Uploading to node {node}, storage {storage} ...")
975
+ try:
976
+ upload_result = client.upload(
977
+ node=node,
978
+ storage=storage,
979
+ file_path=tmp_path,
980
+ content_type="import",
981
+ )
982
+ except Exception as exc:
983
+ return {"error": f"Failed to upload image: {exc}"}
984
+
985
+ # Determine the volid from upload response
986
+ volid = None
987
+ if isinstance(upload_result, dict):
988
+ volid = upload_result.get("volid") or upload_result.get("data", {}).get("volid")
989
+ if not volid:
990
+ # Fallback: construct volid from storage + filename
991
+ volid = f"{storage}:import/{fname}"
992
+
993
+ print(f"Uploaded as {volid}")
994
+
995
+ # 3. Import the uploaded image into the VM
996
+ print(f"Importing {volid} as {disk_slot} into VM {args.vmid} ...")
997
+ result = _do_import(volid)
998
+
999
+ return {"vmid": args.vmid, "disk": disk_slot, "image": volid, "_node": node, "data": result}
1000
+
1001
+ finally:
1002
+ # Clean up temp file
1003
+ if tmp_path and os.path.isfile(tmp_path):
1004
+ os.unlink(tmp_path)
1005
+
1006
+
888
1007
  def _vm_disk_resize(args: argparse.Namespace, client: ProxmoxClient) -> dict:
889
1008
  """Resize a VM disk.
890
1009
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "proxcli"
3
- version = "0.16.0"
3
+ version = "0.16.2"
4
4
  description = "A CLI tool to interact with Proxmox VE nodes and clusters via the REST API"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -254,7 +254,7 @@ wheels = [
254
254
 
255
255
  [[package]]
256
256
  name = "proxcli"
257
- version = "0.16.0"
257
+ version = "0.16.2"
258
258
  source = { editable = "." }
259
259
  dependencies = [
260
260
  { name = "httpx" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes