proxcli 0.12.0__tar.gz → 0.13.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.
Files changed (71) hide show
  1. {proxcli-0.12.0 → proxcli-0.13.0}/CHANGELOG.md +31 -0
  2. {proxcli-0.12.0 → proxcli-0.13.0}/PKG-INFO +8 -4
  3. {proxcli-0.12.0 → proxcli-0.13.0}/README.md +7 -3
  4. proxcli-0.13.0/docs/api-permissions.md +216 -0
  5. proxcli-0.13.0/proxmox/cli/auth.py +259 -0
  6. {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/cli/ceph.py +15 -4
  7. {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/cli/cluster.py +8 -3
  8. {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/cli/main.py +31 -7
  9. {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/client/client.py +84 -0
  10. {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/output/formatter.py +4 -1
  11. proxcli-0.13.0/proxmox/output/log_fmt.py +69 -0
  12. {proxcli-0.12.0 → proxcli-0.13.0}/pyproject.toml +1 -1
  13. {proxcli-0.12.0 → proxcli-0.13.0}/uv.lock +1 -1
  14. proxcli-0.12.0/docs/api-permissions.md +0 -153
  15. proxcli-0.12.0/proxmox/cli/auth.py +0 -36
  16. {proxcli-0.12.0 → proxcli-0.13.0}/.env.example +0 -0
  17. {proxcli-0.12.0 → proxcli-0.13.0}/.github/workflows/ci.yml +0 -0
  18. {proxcli-0.12.0 → proxcli-0.13.0}/.gitignore +0 -0
  19. {proxcli-0.12.0 → proxcli-0.13.0}/.python-version +0 -0
  20. {proxcli-0.12.0 → proxcli-0.13.0}/AGENTS.md +0 -0
  21. {proxcli-0.12.0 → proxcli-0.13.0}/PLAN.md +0 -0
  22. {proxcli-0.12.0 → proxcli-0.13.0}/PROJECT.md +0 -0
  23. {proxcli-0.12.0 → proxcli-0.13.0}/PROMPT.md +0 -0
  24. {proxcli-0.12.0 → proxcli-0.13.0}/TODO.md +0 -0
  25. {proxcli-0.12.0 → proxcli-0.13.0}/docs/api-coverage.md +0 -0
  26. {proxcli-0.12.0 → proxcli-0.13.0}/docs/cloud-init.md +0 -0
  27. {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/__init__.py +0 -0
  28. {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/cli/__init__.py +0 -0
  29. {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/cli/acl.py +0 -0
  30. {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/cli/backup.py +0 -0
  31. {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/cli/completion.py +0 -0
  32. {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/cli/container.py +0 -0
  33. {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/cli/firewall_helpers.py +0 -0
  34. {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/cli/network.py +0 -0
  35. {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/cli/node.py +0 -0
  36. {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/cli/pool.py +0 -0
  37. {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/cli/role.py +0 -0
  38. {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/cli/storage.py +0 -0
  39. {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/cli/tasks.py +0 -0
  40. {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/cli/user.py +0 -0
  41. {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/cli/vm.py +0 -0
  42. {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/cli/vm_spec.py +0 -0
  43. {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/client/__init__.py +0 -0
  44. {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/client/auth.py +0 -0
  45. {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/client/exceptions.py +0 -0
  46. {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/config/__init__.py +0 -0
  47. {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/config/config.py +0 -0
  48. {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/config/models.py +0 -0
  49. {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/output/__init__.py +0 -0
  50. {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/output/json_fmt.py +0 -0
  51. {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/output/table_fmt.py +0 -0
  52. {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/output/yaml_fmt.py +0 -0
  53. {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/utils/__init__.py +0 -0
  54. {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/utils/helpers.py +0 -0
  55. {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/utils/logging.py +0 -0
  56. {proxcli-0.12.0 → proxcli-0.13.0}/tests/__init__.py +0 -0
  57. {proxcli-0.12.0 → proxcli-0.13.0}/tests/conftest.py +0 -0
  58. {proxcli-0.12.0 → proxcli-0.13.0}/tests/test_auth.py +0 -0
  59. {proxcli-0.12.0 → proxcli-0.13.0}/tests/test_cli/__init__.py +0 -0
  60. {proxcli-0.12.0 → proxcli-0.13.0}/tests/test_cli/test_backup.py +0 -0
  61. {proxcli-0.12.0 → proxcli-0.13.0}/tests/test_cli/test_ceph.py +0 -0
  62. {proxcli-0.12.0 → proxcli-0.13.0}/tests/test_cli/test_main.py +0 -0
  63. {proxcli-0.12.0 → proxcli-0.13.0}/tests/test_cli/test_network.py +0 -0
  64. {proxcli-0.12.0 → proxcli-0.13.0}/tests/test_cli/test_node_system.py +0 -0
  65. {proxcli-0.12.0 → proxcli-0.13.0}/tests/test_cli/test_role_acl.py +0 -0
  66. {proxcli-0.12.0 → proxcli-0.13.0}/tests/test_cli/test_user.py +0 -0
  67. {proxcli-0.12.0 → proxcli-0.13.0}/tests/test_client.py +0 -0
  68. {proxcli-0.12.0 → proxcli-0.13.0}/tests/test_config.py +0 -0
  69. {proxcli-0.12.0 → proxcli-0.13.0}/tests/test_integration/__init__.py +0 -0
  70. {proxcli-0.12.0 → proxcli-0.13.0}/tests/test_output/__init__.py +0 -0
  71. {proxcli-0.12.0 → proxcli-0.13.0}/tests/test_output/test_formatter.py +0 -0
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.13.0] - 2026-06-21
11
+
12
+ ### Added
13
+ - **``--output log`` format**: plain-text log lines with timestamps.
14
+ ``cluster log`` and ``ceph log`` default to this format.
15
+ - **``--follow`` / ``-f``** for ``cluster log`` and ``ceph log``:
16
+ polls every second and prints new entries until Ctrl+C.
17
+ - **``proxmox auth setup``**: creates the four recommended
18
+ ``proxcli-*`` roles and ACLs in one command (requires Administrator).
19
+ - **``proxmox auth check``**: live permission test — hits each proxcli
20
+ endpoint and reports PASS/FAIL in a table. 39 checks across cluster,
21
+ storage, VMs, snapshots, backups, containers, firewall, pools, and
22
+ admin operations.
23
+ - **``proxmox auth status --permissions`` / ``-p``**: fetches effective
24
+ permissions from the API.
25
+
26
+ ### Changed
27
+ - **Cluster/ceph log output** is now oldest-first (reversed from API order).
28
+ - ``--help`` no longer triggers misplaced-global-flag hint.
29
+ - ``docs/api-permissions.md`` restructured around four path-scoped roles
30
+ and a 3-step quickstart.
31
+
32
+ ### Fixed
33
+ - Ctrl+C / broken pipe in ``--follow`` mode exits cleanly (no error).
34
+ - ``auth check`` defaults to table output.
35
+
10
36
  ## [0.12.0] - 2026-06-20
11
37
 
12
38
  ### Added
@@ -25,6 +51,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
25
51
  ``proxmox ceph log [--node] [--limit N]`` (Ceph log entries),
26
52
  ``proxmox ceph disks [--node]`` (all physical disks with health,
27
53
  wearout, SMART status, OSD mapping).
54
+ - **``--output log`` format**: plain text log lines with timestamps.
55
+ ``cluster log`` and ``ceph log`` default to this format. Override with
56
+ ``--output json``, ``--output table``, or ``--output yaml`.
57
+ - **API coverage doc**: ``docs/api-coverage.md`` tracks all implemented
58
+ and remaining Proxmox VE REST API endpoints.
28
59
 
29
60
  ### Changed
30
61
  - **ConfigLoader is now read-only**. ``proxmox auth login`` and
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proxcli
3
- Version: 0.12.0
3
+ Version: 0.13.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
@@ -175,7 +175,7 @@ For password auth, use `"auth_method": "password"` with a `"password"` field ins
175
175
  | `--password` | — | Password |
176
176
  | `--password-stdin` | — | Read password from stdin |
177
177
  | `--api-token` | — | API token (`user!tokenid=secret`) |
178
- | `--output` | `json` | Output format: `json`, `table`, `yaml` |
178
+ | `--output` | `json` | Output format: `json`, `table`, `yaml`, `log` |
179
179
  | `--columns` | all | Columns to display in table output (e.g. `--columns vmid,name,status`) |
180
180
  | `--dry-run` | off | Print the API request without executing |
181
181
  | `--insecure` | off | Skip TLS verification |
@@ -186,7 +186,10 @@ For password auth, use `"auth_method": "password"` with a `"password"` field ins
186
186
  ### Auth
187
187
 
188
188
  ```bash
189
- proxmox auth status # Show current auth context
189
+ proxmox auth status # Show current auth context
190
+ proxmox auth status --permissions # + effective permissions from API
191
+ proxmox auth setup # Create recommended roles + ACLs (needs Administrator)
192
+ proxmox auth check # Live permission test table (39 checks)
190
193
  ```
191
194
 
192
195
  ### Completion
@@ -414,8 +417,9 @@ proxmox task show <upid>
414
417
  proxmox task log <upid> [--follow]
415
418
  ```
416
419
 
417
- `proxmox task log --follow` polls `/nodes/{node}/tasks/{upid}/log` every second
420
+ `proxmox task log --follow` polls the log endpoint every second
418
421
  and streams new lines until the task completes (like `tail -f`).
422
+ `cluster log --follow` and `ceph log --follow <node>` work the same way.
419
423
 
420
424
  ### Backup (vzdump)
421
425
 
@@ -152,7 +152,7 @@ For password auth, use `"auth_method": "password"` with a `"password"` field ins
152
152
  | `--password` | — | Password |
153
153
  | `--password-stdin` | — | Read password from stdin |
154
154
  | `--api-token` | — | API token (`user!tokenid=secret`) |
155
- | `--output` | `json` | Output format: `json`, `table`, `yaml` |
155
+ | `--output` | `json` | Output format: `json`, `table`, `yaml`, `log` |
156
156
  | `--columns` | all | Columns to display in table output (e.g. `--columns vmid,name,status`) |
157
157
  | `--dry-run` | off | Print the API request without executing |
158
158
  | `--insecure` | off | Skip TLS verification |
@@ -163,7 +163,10 @@ For password auth, use `"auth_method": "password"` with a `"password"` field ins
163
163
  ### Auth
164
164
 
165
165
  ```bash
166
- proxmox auth status # Show current auth context
166
+ proxmox auth status # Show current auth context
167
+ proxmox auth status --permissions # + effective permissions from API
168
+ proxmox auth setup # Create recommended roles + ACLs (needs Administrator)
169
+ proxmox auth check # Live permission test table (39 checks)
167
170
  ```
168
171
 
169
172
  ### Completion
@@ -391,8 +394,9 @@ proxmox task show <upid>
391
394
  proxmox task log <upid> [--follow]
392
395
  ```
393
396
 
394
- `proxmox task log --follow` polls `/nodes/{node}/tasks/{upid}/log` every second
397
+ `proxmox task log --follow` polls the log endpoint every second
395
398
  and streams new lines until the task completes (like `tail -f`).
399
+ `cluster log --follow` and `ceph log --follow <node>` work the same way.
396
400
 
397
401
  ### Backup (vzdump)
398
402
 
@@ -0,0 +1,216 @@
1
+ # API Token Permissions
2
+
3
+ proxcli uses the Proxmox VE REST API. This document describes how to create
4
+ an API token with the right permissions.
5
+
6
+ ## Creating an API Token
7
+
8
+ In the Proxmox VE UI: **Datacenter → Permissions → API Tokens → Add**
9
+
10
+ 1. Select **User**
11
+ 2. Enter **Token ID** (e.g. `proxcli`)
12
+ 3. Uncheck **Privilege Separation** (recommended — see note below)
13
+
14
+ The **Secret** is shown only once — save it immediately.
15
+
16
+ > **Privilege Separation**: when unchecked, the token inherits all of the
17
+ > user's roles. When checked, you must assign roles directly to the token.
18
+ > Unchecking is simpler but broader. Check it if you want to lock down
19
+ > the token independently of the user.
20
+
21
+ ## Quickstart (3 steps)
22
+
23
+ ```bash
24
+ # 1. Bootstrap roles + ACLs (once, needs Administrator)
25
+ proxmox auth setup
26
+
27
+ # 2. Create API token (UI: Datacenter → Permissions → API Tokens → Add)
28
+ # User: xezpeleta@pve
29
+ # Token ID: proxcli
30
+ # ☐ Privilege Separation (unchecked — inherits user's roles)
31
+ # Save the secret!
32
+
33
+ # 3. Write credentials.json with the new token secret
34
+ cat > ~/.config/proxmox-cli/credentials.json <<'EOF'
35
+ {
36
+ "url": "https://your-pve.example.com:8006",
37
+ "username": "xezpeleta@pve",
38
+ "auth_method": "api_token",
39
+ "api_token_id": "proxcli",
40
+ "api_token_secret": "your-token-secret-here",
41
+ "verify_tls": false
42
+ }
43
+ EOF
44
+ chmod 400 ~/.config/proxmox-cli/credentials.json
45
+
46
+ # Done — everything works
47
+ proxmox auth status
48
+ proxmox cluster status
49
+ proxmox vm list
50
+ ```
51
+
52
+ ## Recommended Roles
53
+
54
+ Split privileges by path so each ACL only carries what it needs:
55
+
56
+ ```
57
+ Role name: proxcli-sys
58
+
59
+ Sys.Audit ← cluster/nodes/status/tasks/logs (read-only)
60
+ Sys.Modify ← cluster firewall
61
+
62
+
63
+ Role name: proxcli-storage
64
+
65
+ Datastore.Allocate
66
+ Datastore.AllocateSpace
67
+ Datastore.AllocateTemplate
68
+ Datastore.Audit
69
+
70
+
71
+ Role name: proxcli-vm
72
+
73
+ VM.Allocate
74
+ VM.Audit
75
+ VM.Backup
76
+ VM.Clone
77
+ VM.Config.CDROM
78
+ VM.Config.Cloudinit
79
+ VM.Config.CPU
80
+ VM.Config.Disk
81
+ VM.Config.HWType
82
+ VM.Config.Memory
83
+ VM.Config.Network
84
+ VM.Config.Options
85
+ VM.Console
86
+ VM.Migrate
87
+ VM.PowerMgmt
88
+ VM.Snapshot
89
+ VM.Snapshot.Rollback
90
+
91
+ Pool.Allocate
92
+ Pool.Audit
93
+
94
+
95
+ Role name: proxcli-node
96
+
97
+ VM.GuestAgent.Audit
98
+ VM.GuestAgent.FileRead
99
+ ```
100
+
101
+ ```bash
102
+ # Assign each role to its path:
103
+ pvesh set /access/acl --path / --roles proxcli-sys --users xezpeleta@pve
104
+ pvesh set /access/acl --path /storage --roles proxcli-storage --users xezpeleta@pve
105
+ pvesh set /access/acl --path /vms --roles proxcli-vm --users xezpeleta@pve
106
+ pvesh set /access/acl --path /nodes --roles proxcli-node --users xezpeleta@pve
107
+ ```
108
+
109
+ That's it. Four roles, four ACLs, zero privilege creep — each path
110
+ only gets what proxcli actually uses there.
111
+
112
+ > **One-liner**: `proxmox auth setup` does all of this automatically
113
+ > if your current token has Administrator privileges.
114
+
115
+ | Path | Role | Why |
116
+ |------|------|-----|
117
+ | `/` | `proxcli-sys` | `cluster status`, `node show`, `task list`, `ceph status`, `cluster log`, `cluster firewall` |
118
+ | `/storage` | `proxcli-storage` | `storage list/upload`, `vm create` (disk + import) |
119
+ | `/vms` | `proxcli-vm` | `vm list/create/start/stop`, snapshots, backups, `pool` |
120
+ | `/nodes` | `proxcli-node` | QEMU guest agent interfaces, per-node Ceph logs |
121
+
122
+ > **ACL management** (`proxmox acl`) and **user management**
123
+ > (`proxmox user`) require `Permissions.Modify`, which is only in the
124
+ > built-in **Administrator** role. These are admin-only operations —
125
+ > add `Permissions.Modify` to `proxcli` if you need them, but it's a
126
+ > powerful privilege.
127
+
128
+ ## Permission Model (Reference)
129
+
130
+ Proxmox permissions follow the pattern:
131
+
132
+ ```
133
+ /path/to/resource PrivilegeName[,PrivilegeName...]
134
+ ```
135
+
136
+ - Paths can be broad (`/`) or specific (`/vms/100`)
137
+ - Privileges are inherited — permissions on `/` propagate to all sub-paths
138
+ - An API token's effective permissions are the **intersection** of:
139
+ 1. The token's own ACLs
140
+ 2. The user's ACLs (if privilege separation is enabled)
141
+
142
+ ## Narrower Scoping (Optional)
143
+
144
+ If you want to limit what a token can touch, use the bare minimum
145
+ privileges per workflow:
146
+
147
+ ### Cloud-Init VM Workflow
148
+
149
+ The complete workflow of uploading a cloud image, creating a VM with
150
+ cloud-init, and starting it:
151
+
152
+ | Step | Method | Endpoint | Privilege |
153
+ |------|--------|----------|-----------|
154
+ | 1 | GET | `/cluster/nextid` | `Sys.Audit` |
155
+ | 2 | POST | `/nodes/{node}/storage/{storage}/upload` | `Datastore.AllocateTemplate` |
156
+ | 3 | POST | `/nodes/{node}/qemu` | `VM.Allocate` |
157
+ | 3 | — | (reads imported image) | `Datastore.Allocate` |
158
+ | 3 | — | (allocates disk on target storage) | `Datastore.AllocateSpace` |
159
+ | 3 | — | (attaches scsi0 disk) | `VM.Config.Disk` |
160
+ | 3 | — | (sets net0) | `VM.Config.Network` |
161
+ | 3 | — | (sets cloud-init: citype, ciuser, …) | `VM.Config.Cloudinit` |
162
+ | 3 | — | (sets bios, machine, boot order) | `VM.Config.Options` |
163
+ | 4 | POST | `/nodes/{node}/qemu/{vmid}/status/start` | `VM.PowerMgmt` |
164
+ | 5 | GET | `/nodes/{node}/qemu/{vmid}/status/current` | `VM.Audit` |
165
+
166
+ **Minimal role `PVECloudInitAdmin`**: `Sys.Audit`, `Datastore.Allocate`,
167
+ `Datastore.AllocateSpace`, `Datastore.AllocateTemplate`, `VM.Allocate`,
168
+ `VM.Audit`, `VM.Config.Cloudinit`, `VM.Config.Disk`, `VM.Config.Network`,
169
+ `VM.Config.Options`, `VM.PowerMgmt`.
170
+
171
+ ### Additional Privileges by Feature
172
+
173
+ | Feature | Extra Privileges Needed |
174
+ |---------|------------------------|
175
+ | Snapshots | `VM.Snapshot`, `VM.Snapshot.Rollback` |
176
+ | Clone VM | `VM.Clone` |
177
+ | Change memory/CPU | `VM.Config.Memory`, `VM.Config.CPU` |
178
+ | Attach ISOs | `VM.Config.CDROM` |
179
+ | Migrate VM | `VM.Migrate` |
180
+ | Backup VM | `VM.Backup` |
181
+ | Delete VM | `VM.Allocate` (already needed), `Datastore.AllocateSpace` |
182
+ | QEMU guest agent | `VM.GuestAgent.Audit`, `VM.GuestAgent.FileRead` |
183
+ | Containers | `VM.Allocate`, `VM.Audit`, `VM.PowerMgmt` |
184
+ | Pools | `Pool.Allocate`, `Pool.Audit` |
185
+ | Cluster/node/VM firewall | `Sys.Modify`, `Sys.Audit` |
186
+ | ACL management | `Permissions.Modify` (Administrator role) |
187
+ | User/role management | `Permissions.Modify` (Administrator role) |
188
+
189
+ ### Built-in Roles (for reference)
190
+
191
+ **PVEVMAdmin**: `VM.Allocate`, `VM.Audit`, `VM.Backup`, `VM.Clone`,
192
+ `VM.Config.CDROM`, `VM.Config.Cloudinit`, `VM.Config.CPU`,
193
+ `VM.Config.Disk`, `VM.Config.HWType`, `VM.Config.Memory`,
194
+ `VM.Config.Network`, `VM.Config.Options`, `VM.Console`, `VM.Migrate`,
195
+ `VM.PowerMgmt`, `VM.Snapshot`, `VM.Snapshot.Rollback`
196
+
197
+ **PVEDatastoreAdmin**: `Datastore.Allocate`, `Datastore.AllocateSpace`,
198
+ `Datastore.AllocateTemplate`, `Datastore.Audit`, `Datastore.Copy`
199
+
200
+ **PVEPoolAdmin**: `Pool.Allocate`, `Pool.Audit`
201
+
202
+ ## Verifying Permissions
203
+
204
+ Check your current effective permissions:
205
+
206
+ ```bash
207
+ proxmox auth permissions
208
+ ```
209
+
210
+ Or test a specific action with dry-run:
211
+
212
+ ```bash
213
+ proxmox --dry-run vm create --node <node> --memory 512 --cores 1
214
+ ```
215
+
216
+ If any privilege is missing, Proxmox returns a **403 Forbidden**.
@@ -0,0 +1,259 @@
1
+ """`proxmox auth` subcommand — authentication status and permission setup."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ from urllib.parse import urlencode
7
+
8
+ from proxmox.client.client import ProxmoxClient
9
+ from proxmox.client.exceptions import ProxmoxAPIError
10
+ from proxmox.config.config import ConfigLoader
11
+
12
+ # Recommended roles for proxcli (see docs/api-permissions.md)
13
+ PROXCLI_ROLES: dict[str, str] = {
14
+ "proxcli-sys": "Sys.Audit,Sys.Modify",
15
+ "proxcli-storage": "Datastore.Allocate,Datastore.AllocateSpace,"
16
+ "Datastore.AllocateTemplate,Datastore.Audit",
17
+ "proxcli-vm": "VM.Allocate,VM.Audit,VM.Backup,VM.Clone,"
18
+ "VM.Config.CDROM,VM.Config.Cloudinit,VM.Config.CPU,"
19
+ "VM.Config.Disk,VM.Config.HWType,VM.Config.Memory,"
20
+ "VM.Config.Network,VM.Config.Options,VM.Console,"
21
+ "VM.Migrate,VM.PowerMgmt,VM.Snapshot,"
22
+ "VM.Snapshot.Rollback,Pool.Allocate,Pool.Audit",
23
+ "proxcli-node": "VM.GuestAgent.Audit,VM.GuestAgent.FileRead",
24
+ }
25
+
26
+ # ACL paths for each role
27
+ PROXCLI_ACLS: list[tuple[str, str]] = [
28
+ ("/", "proxcli-sys"),
29
+ ("/storage", "proxcli-storage"),
30
+ ("/vms", "proxcli-vm"),
31
+ ("/nodes", "proxcli-node"),
32
+ ]
33
+
34
+
35
+ # Permission checks: (label, method, path, privilege_needed)
36
+ # The handler does a dry-run-like GET/POST to check if 403 is returned.
37
+ PERMISSION_CHECKS: list[tuple[str, str, str, str]] = [
38
+ # ── read-only / system ──
39
+ ("Cluster status", "GET", "/cluster/status", "Sys.Audit"),
40
+ ("Node list", "GET", "/nodes", "Sys.Audit"),
41
+ ("Task list", "GET", "/cluster/tasks", "Sys.Audit"),
42
+ ("Cluster log", "GET", "/cluster/log", "Sys.Audit"),
43
+ ("Ceph status", "GET", "/cluster/ceph/status", "Sys.Audit"),
44
+ ("Cluster options", "GET", "/cluster/options", "Sys.Audit"),
45
+
46
+ # ── storage ──
47
+ ("Storage list", "GET", "/storage", "Datastore.Audit"),
48
+ ("Storage upload", "POST", "/nodes/{node}/storage/{storage}/upload",
49
+ "Datastore.AllocateTemplate"),
50
+ ("Storage status", "GET", "/nodes/{node}/storage/{storage}/status",
51
+ "Datastore.Audit"),
52
+
53
+ # ── VMs (read) ──
54
+ ("VM list", "GET", "/cluster/resources", "VM.Audit"),
55
+ ("VM config", "GET", "/nodes/{node}/qemu/{vmid}/config",
56
+ "VM.Audit"),
57
+ ("VM status", "GET", "/nodes/{node}/qemu/{vmid}/status/current",
58
+ "VM.Audit"),
59
+
60
+ # ── VMs (lifecycle) ──
61
+ ("VM create (nextid)", "GET", "/cluster/nextid", "VM.Allocate"),
62
+ ("VM create (save)", "POST", "/nodes/{node}/qemu", "VM.Allocate"),
63
+ ("VM start", "POST", "/nodes/{node}/qemu/{vmid}/status/start",
64
+ "VM.PowerMgmt"),
65
+ ("VM stop", "POST", "/nodes/{node}/qemu/{vmid}/status/stop",
66
+ "VM.PowerMgmt"),
67
+ ("VM delete", "DELETE", "/nodes/{node}/qemu/{vmid}", "VM.Allocate"),
68
+
69
+ # ── VMs (config) ──
70
+ ("VM set memory/cores", "PUT", "/nodes/{node}/qemu/{vmid}/config",
71
+ "VM.Config.Memory"),
72
+ ("VM set network", "PUT", "/nodes/{node}/qemu/{vmid}/config",
73
+ "VM.Config.Network"),
74
+ ("VM set disk", "PUT", "/nodes/{node}/qemu/{vmid}/config",
75
+ "VM.Config.Disk"),
76
+ ("VM set cloud-init", "PUT", "/nodes/{node}/qemu/{vmid}/config",
77
+ "VM.Config.Cloudinit"),
78
+ ("VM set CDROM", "PUT", "/nodes/{node}/qemu/{vmid}/config",
79
+ "VM.Config.CDROM"),
80
+ ("VM set options", "PUT", "/nodes/{node}/qemu/{vmid}/config",
81
+ "VM.Config.Options"),
82
+
83
+ # ── VMs (snapshots) ──
84
+ ("VM snapshot list", "GET", "/nodes/{node}/qemu/{vmid}/snapshot",
85
+ "VM.Snapshot"),
86
+ ("VM snapshot create", "POST", "/nodes/{node}/qemu/{vmid}/snapshot",
87
+ "VM.Snapshot"),
88
+ ("VM snapshot rollback", "POST", "/nodes/{node}/qemu/{vmid}/snapshot/{snapname}/rollback",
89
+ "VM.Snapshot.Rollback"),
90
+
91
+ # ── VMs (backup/clone/migrate) ──
92
+ ("VM backup", "POST", "/nodes/{node}/vzdump", "VM.Backup"),
93
+ ("VM clone", "POST", "/nodes/{node}/qemu/{vmid}/clone",
94
+ "VM.Clone"),
95
+ ("VM migrate", "POST", "/nodes/{node}/qemu/{vmid}/migrate",
96
+ "VM.Migrate"),
97
+
98
+ # ── QEMU guest agent ──
99
+ ("VM guest agent", "GET", "/nodes/{node}/qemu/{vmid}/agent/network-get-interfaces",
100
+ "VM.GuestAgent.Audit"),
101
+
102
+ # ── containers ──
103
+ ("Container list", "GET", "/nodes/{node}/lxc", "VM.Audit"),
104
+ ("Container start", "POST", "/nodes/{node}/lxc/{vmid}/status/start",
105
+ "VM.PowerMgmt"),
106
+
107
+ # ── firewall ──
108
+ ("Cluster firewall rules", "GET", "/cluster/firewall/rules",
109
+ "Sys.Modify"),
110
+ ("VM firewall rules", "GET", "/nodes/{node}/qemu/{vmid}/firewall/rules",
111
+ "VM.Allocate"),
112
+
113
+ # ── pools ──
114
+ ("Pool list", "GET", "/pools", "Pool.Audit"),
115
+ ("Pool create", "POST", "/pools", "Pool.Allocate"),
116
+
117
+ # ── ACL / users / roles (admin-only) ──
118
+ ("User list", "GET", "/access/users", "Permissions.Modify"),
119
+ ("Role list", "GET", "/access/roles", "Permissions.Modify"),
120
+ ("ACL list", "GET", "/access/acl", "Permissions.Modify"),
121
+ ]
122
+
123
+
124
+ def _safe_encode(data: dict[str, str]) -> str:
125
+ """URL-encode dict as form data, preserving literal commas in values.
126
+
127
+ Proxmox expects literal commas in ``privs`` values, but httpx's
128
+ default form-encoding converts them to ``%2C``.
129
+ """
130
+ return urlencode(data, safe=",")
131
+
132
+
133
+ def register_auth_parser(subparsers: argparse._SubParsersAction) -> None:
134
+ """Register the `proxmox auth` subcommand tree."""
135
+ auth_parser = subparsers.add_parser("auth", help="Authentication and permissions")
136
+ auth_sub = auth_parser.add_subparsers(dest="action", title="actions", required=True)
137
+
138
+ # --- auth status ---
139
+ status = auth_sub.add_parser("status", help="Show current authentication status")
140
+ status.add_argument("--permissions", "-p", action="store_true",
141
+ help="Also show effective permissions of the current token")
142
+ status.set_defaults(func=_auth_status)
143
+
144
+ # --- auth setup ---
145
+ setup = auth_sub.add_parser("setup", help="Create recommended roles and ACLs for proxcli")
146
+ setup.set_defaults(func=_auth_setup)
147
+
148
+ # --- auth check ---
149
+ check = auth_sub.add_parser("check", help="Test each permission endpoint live")
150
+ check.set_defaults(func=_auth_check, output_format="table")
151
+
152
+
153
+ def _auth_status(args: argparse.Namespace, client: ProxmoxClient | None = None) -> dict:
154
+ """Display current authentication status."""
155
+ loader = ConfigLoader()
156
+ creds = loader.load_or_none()
157
+ if creds is None:
158
+ return {"status": "not authenticated"}
159
+
160
+ found_path = loader.find_file()
161
+ result: dict = {
162
+ "status": "authenticated",
163
+ "url": creds.url,
164
+ "username": creds.username,
165
+ "auth_method": creds.auth_method.value,
166
+ "verify_tls": creds.verify_tls,
167
+ "config_file": str(found_path) if found_path else "unknown",
168
+ }
169
+
170
+ if client is not None and args.permissions:
171
+ perms = client.get("/access/permissions")
172
+ result["permissions"] = perms
173
+
174
+ return result
175
+
176
+
177
+ def _auth_setup(args: argparse.Namespace, client: ProxmoxClient) -> dict:
178
+ """Create the recommended proxcli roles and ACLs."""
179
+ created_roles: list[str] = []
180
+ skipped_roles: list[str] = []
181
+ created_acls: list[str] = []
182
+ skipped_acls: list[str] = []
183
+
184
+ # 1. Create roles
185
+ for role_name, privs in PROXCLI_ROLES.items():
186
+ existing = client.get("/access/roles")
187
+ if any(r.get("roleid") == role_name for r in existing):
188
+ skipped_roles.append(role_name)
189
+ continue
190
+ content = _safe_encode({"roleid": role_name, "privs": privs})
191
+ client.request("POST", "/access/roles", content=content)
192
+ created_roles.append(role_name)
193
+
194
+ # 2. Create ACLs
195
+ # We need the username from config to assign ACLs
196
+ loader = ConfigLoader()
197
+ creds = loader.load()
198
+ ug = creds.username
199
+
200
+ for path, role in PROXCLI_ACLS:
201
+ existing_acls = client.get("/access/acl")
202
+ # Check if this exact ACL already exists
203
+ already = any(
204
+ a.get("path") == path
205
+ and a.get("roleid") == role
206
+ and a.get("ugid") == ug
207
+ for a in existing_acls
208
+ )
209
+ if already:
210
+ skipped_acls.append(f"{path} → {role} ({ug})")
211
+ continue
212
+ content = _safe_encode({"path": path, "roles": role, "users": ug})
213
+ client.request("PUT", "/access/acl", content=content)
214
+ created_acls.append(f"{path} → {role} ({ug})")
215
+
216
+ return {
217
+ "created_roles": created_roles,
218
+ "skipped_roles": skipped_roles,
219
+ "created_acls": created_acls,
220
+ "skipped_acls": skipped_acls,
221
+ }
222
+
223
+
224
+ def _auth_check(args: argparse.Namespace, client: ProxmoxClient) -> list[dict]:
225
+ """Test each proxcli endpoint and report permission status."""
226
+ results: list[dict] = []
227
+
228
+ # Resolve a real node name for paths that need it
229
+ nodes = client.get("/nodes")
230
+ node = nodes[0]["node"] if nodes else "pve"
231
+
232
+ for label, method, path, needed_priv in PERMISSION_CHECKS:
233
+ # Replace placeholders
234
+ real_path = path.replace("{node}", node).replace("{vmid}", "99999") \
235
+ .replace("{storage}", "local").replace("{snapname}", "test")
236
+
237
+ try:
238
+ if method in ("GET", "DELETE"):
239
+ client.request(method, real_path)
240
+ else:
241
+ # POST/PUT: send dummy body to check permission (not actual creation)
242
+ client.request(method, real_path, data={"dry": "1"})
243
+ status = "PASS"
244
+ except ProxmoxAPIError as exc:
245
+ if exc.status_code == 403:
246
+ status = "FAIL"
247
+ else:
248
+ # 404 (node not found), 500, etc. means we had permission
249
+ # to reach the endpoint but something else went wrong
250
+ status = "PASS"
251
+
252
+ results.append({
253
+ "feature": label,
254
+ "method": method,
255
+ "privilege": needed_priv,
256
+ "status": status,
257
+ })
258
+
259
+ return results
@@ -20,7 +20,8 @@ def register_ceph_parser(subparsers: argparse._SubParsersAction) -> None:
20
20
  log = ceph_sub.add_parser("log", help="Show recent Ceph log entries")
21
21
  log.add_argument("--node", help="Show logs for a specific node (default: all nodes)")
22
22
  log.add_argument("--limit", type=int, default=50, help="Number of log entries (default: 50)")
23
- log.set_defaults(func=_ceph_log)
23
+ log.add_argument("--follow", "-f", action="store_true", help="Follow log output")
24
+ log.set_defaults(func=_ceph_log, output_format="log")
24
25
 
25
26
  # --- ceph osd ---
26
27
  osd = ceph_sub.add_parser("osd", help="List Ceph OSDs")
@@ -79,8 +80,18 @@ def _ceph_status(args: argparse.Namespace, client: ProxmoxClient) -> dict:
79
80
  }
80
81
 
81
82
 
82
- def _ceph_log(args: argparse.Namespace, client: ProxmoxClient) -> list:
83
+ def _ceph_log(args: argparse.Namespace, client: ProxmoxClient) -> list | None:
83
84
  """Fetch Ceph log entries."""
85
+ if args.follow:
86
+ # Follow only works with --node for ceph log (per-node endpoint)
87
+ if not args.node:
88
+ return {"error": "--follow requires --node for Ceph logs"}
89
+ client.stream_log(
90
+ f"/nodes/{args.node}/ceph/log", follow=True,
91
+ params={"limit": args.limit},
92
+ )
93
+ return None
94
+
84
95
  if args.node:
85
96
  node_list = [args.node]
86
97
  else:
@@ -102,13 +113,13 @@ def _ceph_log(args: argparse.Namespace, client: ProxmoxClient) -> list:
102
113
  all_entries.sort(key=lambda e: e.get("t", ""), reverse=True)
103
114
  entries = all_entries[: args.limit]
104
115
 
105
- return [
116
+ return list(reversed([
106
117
  {
107
118
  "time": entry.get("t", ""),
108
119
  "node": entry.get("_node", ""),
109
120
  }
110
121
  for entry in entries
111
- ]
122
+ ]))
112
123
 
113
124
 
114
125
  def _ceph_osd(args: argparse.Namespace, client: ProxmoxClient) -> list:
@@ -20,7 +20,8 @@ def register_cluster_parser(subparsers: argparse._SubParsersAction) -> None:
20
20
  # --- cluster log ---
21
21
  cl_log = cl_sub.add_parser("log", help="Show cluster log")
22
22
  cl_log.add_argument("--limit", type=int, default=50, help="Number of entries (default: 50)")
23
- cl_log.set_defaults(func=_cl_log)
23
+ cl_log.add_argument("--follow", "-f", action="store_true", help="Follow log output")
24
+ cl_log.set_defaults(func=_cl_log, output_format="log")
24
25
 
25
26
  # --- cluster options ---
26
27
  cl_opts = cl_sub.add_parser("options", help="Show cluster-wide options")
@@ -142,8 +143,12 @@ def _cl_status(_args: argparse.Namespace, client: ProxmoxClient) -> dict | list:
142
143
  return client.get("/cluster/status")
143
144
 
144
145
 
145
- def _cl_log(args: argparse.Namespace, client: ProxmoxClient) -> list:
146
- return client.get("/cluster/log", params={"max": args.limit})
146
+ def _cl_log(args: argparse.Namespace, client: ProxmoxClient) -> list | None:
147
+ if args.follow:
148
+ client.stream_log("/cluster/log", follow=True, params={"max": args.limit})
149
+ return None
150
+ data = client.get("/cluster/log", params={"max": args.limit})
151
+ return list(reversed(data)) if isinstance(data, list) else data
147
152
 
148
153
 
149
154
  def _cl_options(_args: argparse.Namespace, client: ProxmoxClient) -> dict: