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.
- {proxcli-0.12.0 → proxcli-0.13.0}/CHANGELOG.md +31 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/PKG-INFO +8 -4
- {proxcli-0.12.0 → proxcli-0.13.0}/README.md +7 -3
- proxcli-0.13.0/docs/api-permissions.md +216 -0
- proxcli-0.13.0/proxmox/cli/auth.py +259 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/cli/ceph.py +15 -4
- {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/cli/cluster.py +8 -3
- {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/cli/main.py +31 -7
- {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/client/client.py +84 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/output/formatter.py +4 -1
- proxcli-0.13.0/proxmox/output/log_fmt.py +69 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/pyproject.toml +1 -1
- {proxcli-0.12.0 → proxcli-0.13.0}/uv.lock +1 -1
- proxcli-0.12.0/docs/api-permissions.md +0 -153
- proxcli-0.12.0/proxmox/cli/auth.py +0 -36
- {proxcli-0.12.0 → proxcli-0.13.0}/.env.example +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/.github/workflows/ci.yml +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/.gitignore +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/.python-version +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/AGENTS.md +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/PLAN.md +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/PROJECT.md +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/PROMPT.md +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/TODO.md +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/docs/api-coverage.md +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/docs/cloud-init.md +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/__init__.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/cli/__init__.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/cli/acl.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/cli/backup.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/cli/completion.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/cli/container.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/cli/firewall_helpers.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/cli/network.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/cli/node.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/cli/pool.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/cli/role.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/cli/storage.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/cli/tasks.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/cli/user.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/cli/vm.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/cli/vm_spec.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/client/__init__.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/client/auth.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/client/exceptions.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/config/__init__.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/config/config.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/config/models.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/output/__init__.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/output/json_fmt.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/output/table_fmt.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/output/yaml_fmt.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/utils/__init__.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/utils/helpers.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/proxmox/utils/logging.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/tests/__init__.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/tests/conftest.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/tests/test_auth.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/tests/test_cli/__init__.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/tests/test_cli/test_backup.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/tests/test_cli/test_ceph.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/tests/test_cli/test_main.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/tests/test_cli/test_network.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/tests/test_cli/test_node_system.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/tests/test_cli/test_role_acl.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/tests/test_cli/test_user.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/tests/test_client.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/tests/test_config.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/tests/test_integration/__init__.py +0 -0
- {proxcli-0.12.0 → proxcli-0.13.0}/tests/test_output/__init__.py +0 -0
- {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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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:
|