proxcli 0.8.1__tar.gz → 0.9.0__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.8.1 → proxcli-0.9.0}/.github/workflows/ci.yml +1 -1
- {proxcli-0.8.1 → proxcli-0.9.0}/CHANGELOG.md +33 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/PKG-INFO +14 -2
- {proxcli-0.8.1 → proxcli-0.9.0}/README.md +13 -1
- {proxcli-0.8.1 → proxcli-0.9.0}/TODO.md +4 -3
- proxcli-0.9.0/docs/cloud-init.md +255 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/proxmox/cli/vm.py +118 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/pyproject.toml +1 -1
- {proxcli-0.8.1 → proxcli-0.9.0}/uv.lock +2 -2
- {proxcli-0.8.1 → proxcli-0.9.0}/.env.example +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/.gitignore +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/.python-version +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/AGENTS.md +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/PLAN.md +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/PROJECT.md +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/PROMPT.md +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/proxmox/__init__.py +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/proxmox/cli/__init__.py +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/proxmox/cli/auth.py +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/proxmox/cli/cluster.py +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/proxmox/cli/completion.py +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/proxmox/cli/container.py +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/proxmox/cli/firewall_helpers.py +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/proxmox/cli/main.py +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/proxmox/cli/node.py +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/proxmox/cli/pool.py +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/proxmox/cli/storage.py +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/proxmox/cli/tasks.py +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/proxmox/client/__init__.py +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/proxmox/client/auth.py +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/proxmox/client/client.py +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/proxmox/client/exceptions.py +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/proxmox/config/__init__.py +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/proxmox/config/config.py +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/proxmox/config/models.py +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/proxmox/output/__init__.py +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/proxmox/output/formatter.py +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/proxmox/output/json_fmt.py +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/proxmox/output/table_fmt.py +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/proxmox/output/yaml_fmt.py +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/proxmox/utils/__init__.py +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/proxmox/utils/helpers.py +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/proxmox/utils/logging.py +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/tests/__init__.py +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/tests/conftest.py +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/tests/test_auth.py +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/tests/test_cli/__init__.py +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/tests/test_cli/test_main.py +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/tests/test_client.py +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/tests/test_config.py +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/tests/test_integration/__init__.py +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/tests/test_output/__init__.py +0 -0
- {proxcli-0.8.1 → proxcli-0.9.0}/tests/test_output/test_formatter.py +0 -0
|
@@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.9.0] - 2026-06-20
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **vm create cloud-init support**: ``--citype``, ``--ciuser``,
|
|
14
|
+
``--cipassword``, ``--sshkeys`` (file path or inline),
|
|
15
|
+
``--nameserver``, ``--searchdomain``, ``--cicustom``.
|
|
16
|
+
- **vm create --import-from**: import an existing disk image from storage
|
|
17
|
+
as the VM's boot disk (e.g. ``--import-from local:import/deb12.qcow2``).
|
|
18
|
+
Requires a Proxmox storage with ``images`` or ``import`` content types.
|
|
19
|
+
- **vm cloud-init drive auto-creation**: when cloud-init flags are used
|
|
20
|
+
on ``vm create``, an ``ide2`` cloud-init drive is automatically attached.
|
|
21
|
+
Proxmox VE 9 regenerates the ISO on config change — no separate generate step.
|
|
22
|
+
- **vm cloudinit generate**: re-submits the current ``citype`` to trigger
|
|
23
|
+
regeneration. Adapted for Proxmox VE 9 which removed the
|
|
24
|
+
``POST /cloudinit`` endpoint.
|
|
25
|
+
- **docs/cloud-init.md**: complete guide on creating and managing
|
|
26
|
+
cloud-init VMs with proxcli, including prerequisites, examples,
|
|
27
|
+
custom user-data, and troubleshooting.
|
|
28
|
+
|
|
29
|
+
## [0.8.2] - 2026-06-20
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
- **vm agent interfaces** — query QEMU guest agent for network interface
|
|
33
|
+
and IP information via ``proxmox vm agent interfaces <vmid>``.
|
|
34
|
+
Requires ``qemu-guest-agent`` in the VM.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
- CI publish job now uses ``uv publish --check-url https://pypi.org/simple``
|
|
38
|
+
to skip already-published versions instead of failing with "File already
|
|
39
|
+
exists".
|
|
40
|
+
|
|
10
41
|
## [0.8.1] - 2026-06-20
|
|
11
42
|
|
|
12
43
|
### Added
|
|
@@ -124,6 +155,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
124
155
|
- CSRF ticket auto-refresh on 401.
|
|
125
156
|
- AI-agent-friendly: default JSON output, strict exit codes, `--dry-run` mode.
|
|
126
157
|
|
|
158
|
+
[0.9.0]: https://github.com/xezpeleta/proxcli/releases/tag/v0.9.0
|
|
159
|
+
[0.8.2]: https://github.com/xezpeleta/proxcli/releases/tag/v0.8.2
|
|
127
160
|
[0.8.1]: https://github.com/xezpeleta/proxcli/releases/tag/v0.8.1
|
|
128
161
|
[0.8.0]: https://github.com/xezpeleta/proxcli/releases/tag/v0.8.0
|
|
129
162
|
[0.7.2]: https://github.com/xezpeleta/proxcli/releases/tag/v0.7.2
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: proxcli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
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
|
|
@@ -177,7 +177,9 @@ proxmox completion fish > ~/.config/fish/completions/proxmox.fish
|
|
|
177
177
|
```bash
|
|
178
178
|
proxmox vm list [--node <node>]
|
|
179
179
|
proxmox vm show <vmid> [--node <node>]
|
|
180
|
-
proxmox vm create --node <node> --
|
|
180
|
+
proxmox vm create --node <node> --memory <mb> [--vmid <id>] [--cores <n>] \
|
|
181
|
+
[--name <name>] [--cdrom <iso>] [--net <config>] [--disk <size>] \
|
|
182
|
+
[--scsihw <type>] [--bios seabios|ovmf] [--machine <type>] [--boot <order>]
|
|
181
183
|
proxmox vm start <vmid> [--node <node>]
|
|
182
184
|
proxmox vm stop <vmid> [--node <node>]
|
|
183
185
|
proxmox vm reboot <vmid> [--node <node>]
|
|
@@ -185,6 +187,16 @@ proxmox vm suspend <vmid> [--node <node>]
|
|
|
185
187
|
proxmox vm resume <vmid> [--node <node>]
|
|
186
188
|
proxmox vm delete <vmid> [--node <node>] [--force] [--purge]
|
|
187
189
|
|
|
190
|
+
# VM snapshots
|
|
191
|
+
proxmox vm snapshot list <vmid> [--node <node>]
|
|
192
|
+
proxmox vm snapshot create <vmid> <snapname> [--description <text>] [--vmstate 1]
|
|
193
|
+
proxmox vm snapshot show <vmid> <snapname> [--node <node>]
|
|
194
|
+
proxmox vm snapshot rollback <vmid> <snapname> [--start 1]
|
|
195
|
+
proxmox vm snapshot delete <vmid> <snapname> [--force 1]
|
|
196
|
+
|
|
197
|
+
# VM guest agent
|
|
198
|
+
proxmox vm agent interfaces <vmid> [--node <node>]
|
|
199
|
+
|
|
188
200
|
# VM firewall
|
|
189
201
|
proxmox vm firewall options <vmid> [--node <node>]
|
|
190
202
|
proxmox vm firewall enable <vmid> [--node <node>]
|
|
@@ -154,7 +154,9 @@ proxmox completion fish > ~/.config/fish/completions/proxmox.fish
|
|
|
154
154
|
```bash
|
|
155
155
|
proxmox vm list [--node <node>]
|
|
156
156
|
proxmox vm show <vmid> [--node <node>]
|
|
157
|
-
proxmox vm create --node <node> --
|
|
157
|
+
proxmox vm create --node <node> --memory <mb> [--vmid <id>] [--cores <n>] \
|
|
158
|
+
[--name <name>] [--cdrom <iso>] [--net <config>] [--disk <size>] \
|
|
159
|
+
[--scsihw <type>] [--bios seabios|ovmf] [--machine <type>] [--boot <order>]
|
|
158
160
|
proxmox vm start <vmid> [--node <node>]
|
|
159
161
|
proxmox vm stop <vmid> [--node <node>]
|
|
160
162
|
proxmox vm reboot <vmid> [--node <node>]
|
|
@@ -162,6 +164,16 @@ proxmox vm suspend <vmid> [--node <node>]
|
|
|
162
164
|
proxmox vm resume <vmid> [--node <node>]
|
|
163
165
|
proxmox vm delete <vmid> [--node <node>] [--force] [--purge]
|
|
164
166
|
|
|
167
|
+
# VM snapshots
|
|
168
|
+
proxmox vm snapshot list <vmid> [--node <node>]
|
|
169
|
+
proxmox vm snapshot create <vmid> <snapname> [--description <text>] [--vmstate 1]
|
|
170
|
+
proxmox vm snapshot show <vmid> <snapname> [--node <node>]
|
|
171
|
+
proxmox vm snapshot rollback <vmid> <snapname> [--start 1]
|
|
172
|
+
proxmox vm snapshot delete <vmid> <snapname> [--force 1]
|
|
173
|
+
|
|
174
|
+
# VM guest agent
|
|
175
|
+
proxmox vm agent interfaces <vmid> [--node <node>]
|
|
176
|
+
|
|
165
177
|
# VM firewall
|
|
166
178
|
proxmox vm firewall options <vmid> [--node <node>]
|
|
167
179
|
proxmox vm firewall enable <vmid> [--node <node>]
|
|
@@ -11,12 +11,13 @@ Completed items are marked with a check. Implementation notes are preserved for
|
|
|
11
11
|
- [x] **Firewall management** — cluster, node, VM, and container. Options, enable/disable, policy, rules (CRUD), aliases (cluster), ipsets with CIDR mgmt (cluster), refs.
|
|
12
12
|
- [x] **Pool management** — `proxmox pool`: list, show, create, update, delete. Wraps `/pools`.
|
|
13
13
|
- [x] **Shell completions** — `proxmox completion bash|zsh|fish`. Dynamic, introspects the parser tree.
|
|
14
|
+
- [x] **VM snapshot management** — `proxmox vm snapshot`: list, create, show, rollback, delete. Wraps `/nodes/{node}/qemu/{vmid}/snapshot`.
|
|
15
|
+
- [x] **QEMU guest agent interfaces** — `proxmox vm agent interfaces <vmid>`. Wraps `/nodes/{node}/qemu/{vmid}/agent/network-get-interfaces`.
|
|
16
|
+
- [x] **Streaming task logs** — `proxmox task log <upid> [--follow]`. Polls `/nodes/{node}/tasks/{upid}/log`.
|
|
17
|
+
- [x] **Global flag hint** — If user places `--output` / `--dry-run` / etc. after the resource, a hint suggests the correct order.
|
|
14
18
|
|
|
15
19
|
## v1.1 — Polish & Usability
|
|
16
20
|
|
|
17
|
-
- [ ] **Streaming task logs (`--follow`)**
|
|
18
|
-
- `proxmox task log <upid> --follow` that streams task output in real time (like `tail -f`). Requires httpx streaming.
|
|
19
|
-
|
|
20
21
|
- [ ] **Startup time optimization**
|
|
21
22
|
- Current `proxmox --help` takes ~350ms. Lazy-load subcommand modules so only the requested resource's code is imported. Move `import rich`, `import yaml` inside formatter functions. Target: <200ms.
|
|
22
23
|
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# Cloud-Init VM Management with proxcli
|
|
2
|
+
|
|
3
|
+
Cloud-init allows you to create and fully configure VMs declaratively — users,
|
|
4
|
+
SSH keys, networking, and packages are defined via `proxcli` flags rather than
|
|
5
|
+
manual post-install steps.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
### 1. Storage with `images` and `import` content types
|
|
10
|
+
|
|
11
|
+
You need a Proxmox storage that supports both `images` (for VM disks) and
|
|
12
|
+
`import` (for uploading disk images). On Proxmox VE, edit the storage in
|
|
13
|
+
**Datacenter → Storage → Edit** and add the content types:
|
|
14
|
+
|
|
15
|
+
- `Disk image` — to store VM disks
|
|
16
|
+
- `ISO image` — optional, if you also want to store ISOs there
|
|
17
|
+
|
|
18
|
+
Example: a `dir` type storage with path `/var/lib/vz` that has `images`, `iso`,
|
|
19
|
+
and `import` enabled.
|
|
20
|
+
|
|
21
|
+
Verify with:
|
|
22
|
+
```bash
|
|
23
|
+
proxmox storage list
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Look for a storage where `content` includes `images` and (for uploads) `import`.
|
|
27
|
+
|
|
28
|
+
### 2. Download a cloud-init image
|
|
29
|
+
|
|
30
|
+
Cloud images are pre-built OS images with cloud-init installed. You can find
|
|
31
|
+
them at:
|
|
32
|
+
|
|
33
|
+
- **Debian**: https://cloud.debian.org/images/cloud/bookworm/latest/
|
|
34
|
+
- File: `debian-12-genericcloud-amd64.qcow2` (333 MB)
|
|
35
|
+
- **Ubuntu**: https://cloud-images.ubuntu.com/releases/24.04/release/
|
|
36
|
+
- File: `ubuntu-24.04-server-cloudimg-amd64.img` (593 MB)
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Download examples
|
|
40
|
+
curl -LO https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-amd64.qcow2
|
|
41
|
+
curl -LO https://cloud-images.ubuntu.com/releases/noble/release/ubuntu-24.04-server-cloudimg-amd64.img
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 3. Upload the image to Proxmox storage
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
proxmox storage upload \
|
|
48
|
+
--node <node> \
|
|
49
|
+
--storage <storage> \
|
|
50
|
+
--content-type import \
|
|
51
|
+
--file debian-12-genericcloud-amd64.qcow2
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
After upload, the image will be available as `<storage>:import/<filename>`.
|
|
55
|
+
You can verify with:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
proxmox storage content <storage> --node <node>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Creating a Cloud-Init VM
|
|
64
|
+
|
|
65
|
+
The complete workflow is a **single `vm create` command** — Proxmox handles
|
|
66
|
+
disk import, cloud-init drive creation, and ISO generation automatically:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
proxmox vm create \
|
|
70
|
+
--node sanmarko \
|
|
71
|
+
--memory 4096 \
|
|
72
|
+
--cores 2 \
|
|
73
|
+
--name my-cloud-vm \
|
|
74
|
+
--import-from local:import/debian-12-genericcloud-amd64.qcow2 \
|
|
75
|
+
--citype nocloud \
|
|
76
|
+
--ciuser debian \
|
|
77
|
+
--cipassword 'SecurePassword123!' \
|
|
78
|
+
--sshkeys ~/.ssh/id_rsa.pub \
|
|
79
|
+
--nameserver 1.1.1.1 \
|
|
80
|
+
--searchdomain mydomain.lan \
|
|
81
|
+
--net 'virtio,bridge=vmbr0'
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Flag reference
|
|
85
|
+
|
|
86
|
+
| Flag | Description |
|
|
87
|
+
|------|-------------|
|
|
88
|
+
| `--import-from <storage:path>` | Import the boot disk from an existing volume |
|
|
89
|
+
| `--citype nocloud\|configdrive2` | Cloud-init data source type |
|
|
90
|
+
| `--ciuser <username>` | Default user (e.g. `debian`, `ubuntu`) |
|
|
91
|
+
| `--cipassword <password>` | User password (hashed by cloud-init) |
|
|
92
|
+
| `--sshkeys <file\|inline>` | SSH public keys (reads file if it exists, otherwise uses inline) |
|
|
93
|
+
| `--nameserver <ip>` | DNS server |
|
|
94
|
+
| `--searchdomain <domain>` | DNS search domain |
|
|
95
|
+
| `--cicustom <config>` | Custom cloud-init config (`user=...,vendor=...`) |
|
|
96
|
+
| `--storage <name>` | Target storage for the imported disk (defaults to `rbd_ssd`) |
|
|
97
|
+
|
|
98
|
+
### What happens
|
|
99
|
+
|
|
100
|
+
1. Proxmox assigns a VM ID (or use `--vmid` to specify)
|
|
101
|
+
2. The cloud image is **streamed from the import storage to the target storage**
|
|
102
|
+
and attached as `scsi0`
|
|
103
|
+
3. A cloud-init drive is created (`ide2`) with the configured user, password,
|
|
104
|
+
SSH keys, and network settings
|
|
105
|
+
4. The VM is created in *stopped* state, ready to start
|
|
106
|
+
|
|
107
|
+
### Verify the configuration
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
proxmox vm show <vmid>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The output includes:
|
|
114
|
+
- `scsi0` pointing to the imported disk on the target storage
|
|
115
|
+
- `citype`, `ciuser`, `cipassword`, `nameserver`, etc.
|
|
116
|
+
- `ide2` with the cloud-init ISO
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Managing Cloud-Init VMs
|
|
121
|
+
|
|
122
|
+
### Updating cloud-init configuration
|
|
123
|
+
|
|
124
|
+
After the VM is created, you can update cloud-init parameters using
|
|
125
|
+
`PUT /nodes/{node}/qemu/{vmid}/config` (no dedicated proxcli command yet):
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
# Currently via curl; a proxcli vm update command is planned
|
|
129
|
+
# This triggers automatic cloud-init ISO regeneration in Proxmox VE 9
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
In Proxmox VE 9+, the cloud-init ISO is **regenerated automatically** whenever
|
|
133
|
+
cloud-init config parameters change. There is no separate "generate" step.
|
|
134
|
+
|
|
135
|
+
### Dumping current cloud-init config
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
proxmox vm cloudinit generate <vmid>
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
This reads the current `citype` from the VM config and re-submits it to
|
|
142
|
+
trigger regeneration. (In Proxmox VE 9, config changes auto-regenerate,
|
|
143
|
+
but this command is provided for environments where explicit generation
|
|
144
|
+
is needed.)
|
|
145
|
+
|
|
146
|
+
### Starting the VM
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
proxmox vm start <vmid>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
On first boot, cloud-init will:
|
|
153
|
+
1. Set the hostname
|
|
154
|
+
2. Create the specified user with the given password
|
|
155
|
+
3. Install the SSH keys
|
|
156
|
+
4. Configure networking
|
|
157
|
+
5. Run any `#cloud-config` directives (if using `--cicustom`)
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Complete Example: Debian 12 Cloud VM
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
# 1. Upload the cloud image (one-time)
|
|
165
|
+
proxmox storage upload \
|
|
166
|
+
--node sanmarko --storage local --content-type import \
|
|
167
|
+
--file debian-12-genericcloud-amd64.qcow2
|
|
168
|
+
|
|
169
|
+
# 2. Create the VM
|
|
170
|
+
proxmox vm create \
|
|
171
|
+
--node sanmarko \
|
|
172
|
+
--name webserver \
|
|
173
|
+
--memory 4096 --cores 2 \
|
|
174
|
+
--import-from local:import/debian-12-genericcloud-amd64.qcow2 \
|
|
175
|
+
--citype nocloud \
|
|
176
|
+
--ciuser debian \
|
|
177
|
+
--cipassword 'ChangeMe123!' \
|
|
178
|
+
--sshkeys ~/.ssh/id_rsa.pub \
|
|
179
|
+
--nameserver 1.1.1.1 \
|
|
180
|
+
--searchdomain tknika.net \
|
|
181
|
+
--net 'virtio,bridge=vmbr0'
|
|
182
|
+
|
|
183
|
+
# 3. Start it
|
|
184
|
+
proxmox vm start <vmid>
|
|
185
|
+
|
|
186
|
+
# 4. SSH in (after boot completes)
|
|
187
|
+
ssh debian@<ip>
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Custom Cloud-Init Config
|
|
193
|
+
|
|
194
|
+
For advanced customization (packages, runcmd, write_files), use `--cicustom`
|
|
195
|
+
with a user-data YAML file:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
proxmox vm create \
|
|
199
|
+
... \
|
|
200
|
+
--cicustom 'user=local:snippets/user-data.yaml'
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Upload the snippet first:
|
|
204
|
+
```bash
|
|
205
|
+
proxmox storage upload \
|
|
206
|
+
--node sanmarko --storage local --content-type snippets \
|
|
207
|
+
--file user-data.yaml
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Example `user-data.yaml`:
|
|
211
|
+
```yaml
|
|
212
|
+
#cloud-config
|
|
213
|
+
packages:
|
|
214
|
+
- nginx
|
|
215
|
+
- htop
|
|
216
|
+
- curl
|
|
217
|
+
|
|
218
|
+
package_update: true
|
|
219
|
+
package_upgrade: true
|
|
220
|
+
|
|
221
|
+
runcmd:
|
|
222
|
+
- systemctl enable --now nginx
|
|
223
|
+
- echo "Hello from cloud-init" > /etc/motd
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Troubleshooting
|
|
229
|
+
|
|
230
|
+
### "storage does not support 'import' content"
|
|
231
|
+
|
|
232
|
+
Your storage only has `iso` or `images` enabled. Go to **Datacenter → Storage**
|
|
233
|
+
in the Proxmox UI and add `Disk image` and `Import` content types to the storage.
|
|
234
|
+
|
|
235
|
+
### "has wrong type 'iso' - needs to be 'images' or 'import'"
|
|
236
|
+
|
|
237
|
+
You uploaded the image with `--content-type iso` instead of `--content-type import`.
|
|
238
|
+
Re-upload with the correct content type, or change the storage's content types.
|
|
239
|
+
|
|
240
|
+
### "Only root can pass arbitrary filesystem paths"
|
|
241
|
+
|
|
242
|
+
The `import-from` parameter uses a **volume ID** (`local:import/file.qcow2`),
|
|
243
|
+
not a filesystem path (`/var/lib/vz/...`). Make sure the image is uploaded to
|
|
244
|
+
Proxmox storage first via `storage upload`.
|
|
245
|
+
|
|
246
|
+
### Cloud-init not running on boot
|
|
247
|
+
|
|
248
|
+
Check that the cloud-init drive is attached:
|
|
249
|
+
```bash
|
|
250
|
+
proxmox vm show <vmid>
|
|
251
|
+
# Look for: ide2: local:XXX/vm-XXX-cloudinit.qcow2,media=cdrom
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
If missing, the cloud-init drive wasn't created. This happens when no
|
|
255
|
+
cloud-init flags (`--ciuser`, `--citype`, etc.) are passed to `vm create`.
|
|
@@ -44,6 +44,16 @@ def register_vm_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
|
44
44
|
vm_create.add_argument("--machine", default=None, help="Machine type (e.g. q35)")
|
|
45
45
|
vm_create.add_argument("--boot", default=None, help="Boot order (e.g. order=cd;net)")
|
|
46
46
|
vm_create.add_argument("--disk", default=None, help="Disk size (e.g. 32G). Uses --storage if set, else local-lvm.")
|
|
47
|
+
vm_create.add_argument("--citype", default=None, choices=["nocloud", "configdrive2"],
|
|
48
|
+
help="Cloud-init type")
|
|
49
|
+
vm_create.add_argument("--ciuser", default=None, help="Cloud-init user")
|
|
50
|
+
vm_create.add_argument("--cipassword", default=None, help="Cloud-init password")
|
|
51
|
+
vm_create.add_argument("--sshkeys", default=None, help="Cloud-init SSH public keys (file path or inline)")
|
|
52
|
+
vm_create.add_argument("--nameserver", default=None, help="Cloud-init DNS server")
|
|
53
|
+
vm_create.add_argument("--searchdomain", default=None, help="Cloud-init DNS search domain")
|
|
54
|
+
vm_create.add_argument("--cicustom", default=None, help="Cloud-init custom config (user=...,vendor=...)")
|
|
55
|
+
vm_create.add_argument("--import-from", default=None, dest="import_from",
|
|
56
|
+
help="Import disk from existing volume (e.g. local:import/deb12.qcow2)")
|
|
47
57
|
vm_create.set_defaults(func=_vm_create)
|
|
48
58
|
|
|
49
59
|
# --- vm start ---
|
|
@@ -124,6 +134,24 @@ def register_vm_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
|
124
134
|
help="Force removal (1=yes, 0=no, default: 0)")
|
|
125
135
|
snap_delete.set_defaults(func=_vm_snapshot_delete)
|
|
126
136
|
|
|
137
|
+
# --- agent ---
|
|
138
|
+
agent = vm_sub.add_parser("agent", help="Query QEMU guest agent")
|
|
139
|
+
agent_sub = agent.add_subparsers(dest="agent_action", title="agent actions", required=True)
|
|
140
|
+
|
|
141
|
+
agent_ifaces = agent_sub.add_parser("interfaces", help="List network interfaces via guest agent")
|
|
142
|
+
agent_ifaces.add_argument("vmid", type=vmid_type, help="VM ID")
|
|
143
|
+
agent_ifaces.add_argument("--node", help="Node name (auto-detected if omitted)")
|
|
144
|
+
agent_ifaces.set_defaults(func=_vm_agent_interfaces)
|
|
145
|
+
|
|
146
|
+
# --- cloudinit ---
|
|
147
|
+
cloudinit = vm_sub.add_parser("cloudinit", help="Manage cloud-init")
|
|
148
|
+
cloudinit_sub = cloudinit.add_subparsers(dest="cloudinit_action", title="cloud-init actions", required=True)
|
|
149
|
+
|
|
150
|
+
ci_generate = cloudinit_sub.add_parser("generate", help="Regenerate cloud-init ISO from current config")
|
|
151
|
+
ci_generate.add_argument("vmid", type=vmid_type, help="VM ID")
|
|
152
|
+
ci_generate.add_argument("--node", help="Node name (auto-detected if omitted)")
|
|
153
|
+
ci_generate.set_defaults(func=_vm_cloudinit_generate)
|
|
154
|
+
|
|
127
155
|
# --- firewall ---
|
|
128
156
|
fw = vm_sub.add_parser("firewall", help="Manage VM firewall")
|
|
129
157
|
fw_sub = fw.add_subparsers(dest="fw_resource", title="resources", required=True)
|
|
@@ -303,6 +331,46 @@ def _vm_create(args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
|
303
331
|
else:
|
|
304
332
|
disk_raw = args.disk
|
|
305
333
|
data["scsi0"] = disk_raw.replace("=", "%3D").replace(":", "%3A").replace(",", "%2C")
|
|
334
|
+
elif args.import_from:
|
|
335
|
+
# Import from existing volume: storage_id=0,import-from=<volid>
|
|
336
|
+
parts = args.import_from.split(":", 1)
|
|
337
|
+
if len(parts) == 2:
|
|
338
|
+
src_storage, src_file = parts
|
|
339
|
+
else:
|
|
340
|
+
return {"error": f"Invalid import-from format: {args.import_from}. Use <storage>:<path>"}
|
|
341
|
+
# Build: dst_storage:0,import-from=src_storage:path
|
|
342
|
+
target_storage = args.storage or "rbd_ssd"
|
|
343
|
+
import_raw = f"{target_storage}:0,import-from={args.import_from}"
|
|
344
|
+
data["scsi0"] = import_raw.replace("=", "%3D").replace(":", "%3A").replace(",", "%2C")
|
|
345
|
+
|
|
346
|
+
# Cloud-init
|
|
347
|
+
cloudinit_used = bool(args.citype or args.ciuser or args.cipassword or args.sshkeys
|
|
348
|
+
or args.nameserver or args.searchdomain or args.cicustom)
|
|
349
|
+
if args.citype:
|
|
350
|
+
data["citype"] = args.citype
|
|
351
|
+
if args.ciuser:
|
|
352
|
+
data["ciuser"] = args.ciuser
|
|
353
|
+
if args.cipassword:
|
|
354
|
+
data["cipassword"] = args.cipassword
|
|
355
|
+
if args.nameserver:
|
|
356
|
+
data["nameserver"] = args.nameserver
|
|
357
|
+
if args.searchdomain:
|
|
358
|
+
data["searchdomain"] = args.searchdomain
|
|
359
|
+
if args.cicustom:
|
|
360
|
+
data["cicustom"] = args.cicustom
|
|
361
|
+
if args.sshkeys:
|
|
362
|
+
# sshkeys can be a file path or inline content
|
|
363
|
+
try:
|
|
364
|
+
with open(args.sshkeys) as f:
|
|
365
|
+
sshkeys_value = f.read().strip()
|
|
366
|
+
except (OSError, FileNotFoundError):
|
|
367
|
+
sshkeys_value = args.sshkeys
|
|
368
|
+
# URL-encode the SSH keys for form body
|
|
369
|
+
# Replace newlines with %0A
|
|
370
|
+
data["sshkeys"] = sshkeys_value.replace("\n", "%0A").replace("\r", "%0D").replace("=", "%3D").replace(",", "%2C").replace(":", "%3A")
|
|
371
|
+
# Add cloud-init drive if cloud-init is being used and no cdrom is set
|
|
372
|
+
if cloudinit_used and not args.cdrom:
|
|
373
|
+
data["ide2"] = "local:cloudinit,media=cdrom"
|
|
306
374
|
|
|
307
375
|
# Build form-encoded body manually — httpx's data= would double-encode %
|
|
308
376
|
from urllib.parse import urlencode
|
|
@@ -365,6 +433,56 @@ def _vm_delete(args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
|
365
433
|
return result if isinstance(result, dict) else {"data": result}
|
|
366
434
|
|
|
367
435
|
|
|
436
|
+
# ---------------------------------------------------------------------------
|
|
437
|
+
# VM guest agent handlers
|
|
438
|
+
# ---------------------------------------------------------------------------
|
|
439
|
+
|
|
440
|
+
def _vm_agent_interfaces(args: argparse.Namespace, client: ProxmoxClient) -> dict | list:
|
|
441
|
+
"""Retrieve network interfaces via QEMU guest agent.
|
|
442
|
+
|
|
443
|
+
Requires qemu-guest-agent installed in the VM and agent enabled in VM options.
|
|
444
|
+
"""
|
|
445
|
+
node = _resolve_node(client, args.node, args.vmid)
|
|
446
|
+
if not node:
|
|
447
|
+
return {"error": f"VM {args.vmid} not found"}
|
|
448
|
+
result = client.get(f"/nodes/{node}/qemu/{args.vmid}/agent/network-get-interfaces")
|
|
449
|
+
# The result is a list of interfaces, each with 'name', 'ip-addresses', etc.
|
|
450
|
+
if isinstance(result, list):
|
|
451
|
+
for iface in result:
|
|
452
|
+
if isinstance(iface, dict):
|
|
453
|
+
iface["_node"] = node
|
|
454
|
+
iface["_vmid"] = args.vmid
|
|
455
|
+
return result
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
# ---------------------------------------------------------------------------
|
|
459
|
+
# VM cloud-init handlers
|
|
460
|
+
# ---------------------------------------------------------------------------
|
|
461
|
+
|
|
462
|
+
def _vm_cloudinit_generate(args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
463
|
+
"""Regenerate the cloud-init ISO from the VM's current cloud-init config.
|
|
464
|
+
|
|
465
|
+
In Proxmox VE 9, cloud-init ISOs are regenerated automatically when
|
|
466
|
+
config changes. This triggers regeneration by re-setting the citype.
|
|
467
|
+
"""
|
|
468
|
+
node = _resolve_node(client, args.node, args.vmid)
|
|
469
|
+
if not node:
|
|
470
|
+
return {"error": f"VM {args.vmid} not found"}
|
|
471
|
+
# Proxmox VE 9 regenerates cloud-init on config change.
|
|
472
|
+
# Re-submit the current citype to trigger regeneration.
|
|
473
|
+
config = client.get(f"/nodes/{node}/qemu/{args.vmid}/config")
|
|
474
|
+
if not isinstance(config, dict):
|
|
475
|
+
return {"error": f"Could not read config for VM {args.vmid}"}
|
|
476
|
+
citype = config.get("citype")
|
|
477
|
+
if not citype:
|
|
478
|
+
return {"error": f"VM {args.vmid} has no cloud-init configured (missing citype)"}
|
|
479
|
+
result = client.put(
|
|
480
|
+
f"/nodes/{node}/qemu/{args.vmid}/config",
|
|
481
|
+
data={"citype": citype},
|
|
482
|
+
)
|
|
483
|
+
return {"data": result} if isinstance(result, (str, type(None))) or not isinstance(result, dict) else result
|
|
484
|
+
|
|
485
|
+
|
|
368
486
|
# ---------------------------------------------------------------------------
|
|
369
487
|
# VM snapshot handlers
|
|
370
488
|
# ---------------------------------------------------------------------------
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
version = 1
|
|
2
|
-
revision =
|
|
2
|
+
revision = 2
|
|
3
3
|
requires-python = ">=3.10"
|
|
4
4
|
|
|
5
5
|
[[package]]
|
|
@@ -254,7 +254,7 @@ wheels = [
|
|
|
254
254
|
|
|
255
255
|
[[package]]
|
|
256
256
|
name = "proxcli"
|
|
257
|
-
version = "0.
|
|
257
|
+
version = "0.9.0"
|
|
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
|
|
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
|