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.
- {proxcli-0.16.0 → proxcli-0.16.2}/CHANGELOG.md +34 -1
- {proxcli-0.16.0 → proxcli-0.16.2}/PKG-INFO +16 -1
- {proxcli-0.16.0 → proxcli-0.16.2}/README.md +15 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/TODO.md +3 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/cloud-init.md +30 -1
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/production-automation.md +1 -1
- {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/main.py +4 -2
- proxcli-0.16.2/proxmox/cli/update.py +154 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/vm.py +119 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/pyproject.toml +1 -1
- {proxcli-0.16.0 → proxcli-0.16.2}/uv.lock +1 -1
- {proxcli-0.16.0 → proxcli-0.16.2}/.env.example +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/.github/workflows/ci.yml +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/.gitignore +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/.python-version +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/AGENTS.md +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/PLAN.md +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/PROJECT.md +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/PROMPT.md +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/DESIGN.md +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/api-coverage.md +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/api-permissions.md +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/assets/index-Bzx5-fXr.css +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/assets/index-CXpOjuxt.css +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/assets/index-DLDs0H6j.js +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/assets/index-DYQPMK8x.js +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/coding-agents.md +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/coverage.json +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/demos/cluster.gif +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/demos/node-list.gif +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/demos/vm-list.gif +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/demos/vm-show.gif +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/demos/yaml-spec.gif +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/favicon.svg +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/icons.svg +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/index.html +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/quickstart.md +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/.gitignore +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/README.md +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/build-all.sh +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/eslint.config.js +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/index.html +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/package-lock.json +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/package.json +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/public/coverage.json +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/public/demos/cluster.gif +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/public/demos/node-list.gif +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/public/demos/vm-list.gif +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/public/demos/vm-show.gif +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/public/demos/yaml-spec.gif +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/public/favicon.svg +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/public/icons.svg +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/remotion-demos/.gitignore +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/remotion-demos/.prettierrc +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/remotion-demos/README.md +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/remotion-demos/eslint.config.mjs +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/remotion-demos/package-lock.json +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/remotion-demos/package.json +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/remotion-demos/public/cluster-real.txt +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/remotion-demos/public/node-list-real.txt +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/remotion-demos/public/vm-list-real.txt +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/remotion-demos/public/vm-show-real.txt +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/remotion-demos/remotion.config.ts +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/remotion-demos/src/Composition.tsx +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/remotion-demos/src/Root.tsx +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/remotion-demos/src/index.css +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/remotion-demos/src/index.ts +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/remotion-demos/tsconfig.json +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/src/App.jsx +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/src/components/CoverageGrid.jsx +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/src/components/SplitFlapAgent.jsx +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/src/index.css +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/src/main.jsx +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/src/pages/Docs.jsx +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/src/pages/Landing.jsx +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/docs/website/vite.config.js +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/__init__.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/__init__.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/acl.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/api.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/auth.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/backup.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/ceph.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/cluster.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/completion.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/container.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/firewall_helpers.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/network.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/node.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/pool.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/role.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/storage.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/tasks.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/user.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/cli/vm_spec.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/client/__init__.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/client/auth.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/client/client.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/client/exceptions.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/config/__init__.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/config/config.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/config/models.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/output/__init__.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/output/formatter.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/output/json_fmt.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/output/log_fmt.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/output/table_fmt.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/output/yaml_fmt.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/utils/__init__.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/utils/helpers.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/proxmox/utils/logging.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/tests/__init__.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/tests/conftest.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_auth.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/__init__.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/test_api.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/test_backup.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/test_backup_restore.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/test_ceph.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/test_container_ip.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/test_main.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/test_network.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/test_node_system.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/test_role_acl.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/test_task_wait.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/test_user.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/test_vm_agent.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/test_vm_clone.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/test_vm_disk.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/test_vm_ip.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/test_vm_iso.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/test_vm_migrate.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/test_vm_set.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_cli/test_vm_template.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_client.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_config.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_integration/__init__.py +0 -0
- {proxcli-0.16.0 → proxcli-0.16.2}/tests/test_output/__init__.py +0 -0
- {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.
|
|
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.
|
|
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
|
|
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
|
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|