proxcli 0.13.0__tar.gz → 0.13.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {proxcli-0.13.0 → proxcli-0.13.2}/.github/workflows/ci.yml +4 -2
- {proxcli-0.13.0 → proxcli-0.13.2}/CHANGELOG.md +17 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/PKG-INFO +1 -1
- {proxcli-0.13.0 → proxcli-0.13.2}/docs/api-permissions.md +15 -10
- {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/cli/auth.py +68 -31
- {proxcli-0.13.0 → proxcli-0.13.2}/pyproject.toml +1 -1
- {proxcli-0.13.0 → proxcli-0.13.2}/uv.lock +2 -2
- {proxcli-0.13.0 → proxcli-0.13.2}/.env.example +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/.gitignore +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/.python-version +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/AGENTS.md +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/PLAN.md +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/PROJECT.md +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/PROMPT.md +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/README.md +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/TODO.md +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/docs/api-coverage.md +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/docs/cloud-init.md +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/__init__.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/cli/__init__.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/cli/acl.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/cli/backup.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/cli/ceph.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/cli/cluster.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/cli/completion.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/cli/container.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/cli/firewall_helpers.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/cli/main.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/cli/network.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/cli/node.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/cli/pool.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/cli/role.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/cli/storage.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/cli/tasks.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/cli/user.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/cli/vm.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/cli/vm_spec.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/client/__init__.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/client/auth.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/client/client.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/client/exceptions.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/config/__init__.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/config/config.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/config/models.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/output/__init__.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/output/formatter.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/output/json_fmt.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/output/log_fmt.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/output/table_fmt.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/output/yaml_fmt.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/utils/__init__.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/utils/helpers.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/utils/logging.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/tests/__init__.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/tests/conftest.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/tests/test_auth.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/tests/test_cli/__init__.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/tests/test_cli/test_backup.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/tests/test_cli/test_ceph.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/tests/test_cli/test_main.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/tests/test_cli/test_network.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/tests/test_cli/test_node_system.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/tests/test_cli/test_role_acl.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/tests/test_cli/test_user.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/tests/test_client.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/tests/test_config.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/tests/test_integration/__init__.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/tests/test_output/__init__.py +0 -0
- {proxcli-0.13.0 → proxcli-0.13.2}/tests/test_output/test_formatter.py +0 -0
|
@@ -5,6 +5,8 @@ on:
|
|
|
5
5
|
branches: [main]
|
|
6
6
|
pull_request:
|
|
7
7
|
branches: [main]
|
|
8
|
+
release:
|
|
9
|
+
types: [published]
|
|
8
10
|
|
|
9
11
|
jobs:
|
|
10
12
|
lint:
|
|
@@ -55,7 +57,7 @@ jobs:
|
|
|
55
57
|
|
|
56
58
|
publish:
|
|
57
59
|
runs-on: ubuntu-24.04
|
|
58
|
-
if: github.
|
|
60
|
+
if: github.event_name == 'release'
|
|
59
61
|
needs: [build]
|
|
60
62
|
environment: pypi
|
|
61
63
|
steps:
|
|
@@ -70,4 +72,4 @@ jobs:
|
|
|
70
72
|
name: dist
|
|
71
73
|
path: dist/
|
|
72
74
|
|
|
73
|
-
- run: uv publish --token "${{ secrets.PYPI_TOKEN }}"
|
|
75
|
+
- run: uv publish --token "${{ secrets.PYPI_TOKEN }}"
|
|
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.13.1] - 2026-06-21
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **``proxmox auth setup``**: now uses the correct ``tokens`` (plural) form
|
|
14
|
+
parameter for token ACLs instead of ``tokenid``, which Proxmox's REST API
|
|
15
|
+
doesn't accept. Token-scoped ACLs are now created fully automatically.
|
|
16
|
+
- **Role permissions**: ``Pool.Allocate`` and ``Pool.Audit`` moved to
|
|
17
|
+
``proxcli-sys`` (pool operations check against ``/``, not ``/vms``).
|
|
18
|
+
``VM.GuestAgent.Audit`` added to ``proxcli-vm`` (guest agent checks
|
|
19
|
+
against ``/vms/{id}``, not ``/nodes/{node}``).
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- **``proxmox auth check``**: now prints each check inline with colored
|
|
23
|
+
PASS (green) / FAIL (red) as it runs instead of only at the end.
|
|
24
|
+
Also scans the token for leftover Administrator/PVEAdmin roles and
|
|
25
|
+
warns to remove them after the proxcli roles are confirmed working.
|
|
26
|
+
|
|
10
27
|
## [0.13.0] - 2026-06-21
|
|
11
28
|
|
|
12
29
|
### Added
|
|
@@ -99,18 +99,23 @@ Role name: proxcli-node
|
|
|
99
99
|
```
|
|
100
100
|
|
|
101
101
|
```bash
|
|
102
|
-
# Assign each role to
|
|
103
|
-
|
|
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
|
-
```
|
|
102
|
+
# Assign each role to the token — use 'proxmox auth setup' (does this automatically):
|
|
103
|
+
proxmox auth setup
|
|
108
104
|
|
|
109
|
-
|
|
110
|
-
|
|
105
|
+
# Or manually via pvesh:
|
|
106
|
+
pvesh set /access/acl --path / --roles proxcli-sys --tokenid proxcli --users xezpeleta@pve
|
|
107
|
+
pvesh set /access/acl --path /storage --roles proxcli-storage --tokenid proxcli --users xezpeleta@pve
|
|
108
|
+
pvesh set /access/acl --path /vms --roles proxcli-vm --tokenid proxcli --users xezpeleta@pve
|
|
109
|
+
pvesh set /access/acl --path /nodes --roles proxcli-node --tokenid proxcli --users xezpeleta@pve
|
|
110
|
+
```
|
|
111
111
|
|
|
112
|
-
> **One-liner**: `proxmox auth setup`
|
|
113
|
-
> if your
|
|
112
|
+
> **One-liner**: `proxmox auth setup` creates both roles and token ACLs
|
|
113
|
+
> automatically if your token has Administrator privileges.
|
|
114
|
+
>
|
|
115
|
+
> **Safe by design**: ACLs target the **token** (`--tokenid proxcli`),
|
|
116
|
+
> not the user. Your user account keeps whatever roles it already has
|
|
117
|
+
> (PVEAdmin, Administrator, etc.). The token gets exactly the proxcli
|
|
118
|
+
> privileges without touching anything else.
|
|
114
119
|
|
|
115
120
|
| Path | Role | Why |
|
|
116
121
|
|------|------|-----|
|
|
@@ -11,15 +11,16 @@ from proxmox.config.config import ConfigLoader
|
|
|
11
11
|
|
|
12
12
|
# Recommended roles for proxcli (see docs/api-permissions.md)
|
|
13
13
|
PROXCLI_ROLES: dict[str, str] = {
|
|
14
|
-
"proxcli-sys": "Sys.Audit,Sys.Modify",
|
|
14
|
+
"proxcli-sys": "Sys.Audit,Sys.Modify,Pool.Allocate,Pool.Audit",
|
|
15
15
|
"proxcli-storage": "Datastore.Allocate,Datastore.AllocateSpace,"
|
|
16
16
|
"Datastore.AllocateTemplate,Datastore.Audit",
|
|
17
17
|
"proxcli-vm": "VM.Allocate,VM.Audit,VM.Backup,VM.Clone,"
|
|
18
18
|
"VM.Config.CDROM,VM.Config.Cloudinit,VM.Config.CPU,"
|
|
19
19
|
"VM.Config.Disk,VM.Config.HWType,VM.Config.Memory,"
|
|
20
20
|
"VM.Config.Network,VM.Config.Options,VM.Console,"
|
|
21
|
+
"VM.GuestAgent.Audit,VM.GuestAgent.FileRead,"
|
|
21
22
|
"VM.Migrate,VM.PowerMgmt,VM.Snapshot,"
|
|
22
|
-
"VM.Snapshot.Rollback
|
|
23
|
+
"VM.Snapshot.Rollback",
|
|
23
24
|
"proxcli-node": "VM.GuestAgent.Audit,VM.GuestAgent.FileRead",
|
|
24
25
|
}
|
|
25
26
|
|
|
@@ -191,46 +192,46 @@ def _auth_setup(args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
|
191
192
|
client.request("POST", "/access/roles", content=content)
|
|
192
193
|
created_roles.append(role_name)
|
|
193
194
|
|
|
194
|
-
# 2. Create ACLs
|
|
195
|
-
#
|
|
195
|
+
# 2. Create ACLs for the token using 'tokens' parameter (plural —
|
|
196
|
+
# the API docs say 'tokenid' but the actual HTTP param is 'tokens')
|
|
196
197
|
loader = ConfigLoader()
|
|
197
198
|
creds = loader.load()
|
|
198
|
-
|
|
199
|
+
token_ug = f"{creds.username}!{creds.api_token_id}" if creds.api_token_id else creds.username
|
|
199
200
|
|
|
200
201
|
for path, role in PROXCLI_ACLS:
|
|
201
202
|
existing_acls = client.get("/access/acl")
|
|
202
|
-
# Check if this exact ACL already exists
|
|
203
203
|
already = any(
|
|
204
204
|
a.get("path") == path
|
|
205
205
|
and a.get("roleid") == role
|
|
206
|
-
and a.get("ugid") ==
|
|
206
|
+
and a.get("ugid") == token_ug
|
|
207
|
+
and a.get("type") == "token"
|
|
207
208
|
for a in existing_acls
|
|
208
209
|
)
|
|
209
210
|
if already:
|
|
210
|
-
skipped_acls.append(f"{path} → {role} ({
|
|
211
|
+
skipped_acls.append(f"{path} → {role} ({token_ug})")
|
|
211
212
|
continue
|
|
212
|
-
content = _safe_encode({"path": path, "roles": role, "
|
|
213
|
+
content = _safe_encode({"path": path, "roles": role, "tokens": token_ug})
|
|
213
214
|
client.request("PUT", "/access/acl", content=content)
|
|
214
|
-
created_acls.append(f"{path} → {role} ({
|
|
215
|
-
|
|
216
|
-
return {
|
|
217
|
-
"created_roles": created_roles,
|
|
218
|
-
"skipped_roles": skipped_roles,
|
|
219
|
-
"created_acls": created_acls,
|
|
220
|
-
"skipped_acls": skipped_acls,
|
|
221
|
-
}
|
|
215
|
+
created_acls.append(f"{path} → {role} ({token_ug})")
|
|
222
216
|
|
|
223
217
|
|
|
224
|
-
def _auth_check(args: argparse.Namespace, client: ProxmoxClient) ->
|
|
218
|
+
def _auth_check(args: argparse.Namespace, client: ProxmoxClient) -> None:
|
|
225
219
|
"""Test each proxcli endpoint and report permission status."""
|
|
226
|
-
|
|
220
|
+
# Rich for colored output
|
|
221
|
+
from rich.console import Console
|
|
222
|
+
from rich.text import Text
|
|
223
|
+
|
|
224
|
+
console = Console(highlight=False, force_terminal=True, width=120)
|
|
227
225
|
|
|
228
226
|
# Resolve a real node name for paths that need it
|
|
229
227
|
nodes = client.get("/nodes")
|
|
230
228
|
node = nodes[0]["node"] if nodes else "pve"
|
|
231
229
|
|
|
232
|
-
|
|
233
|
-
|
|
230
|
+
total = len(PERMISSION_CHECKS)
|
|
231
|
+
passed = 0
|
|
232
|
+
failed = 0
|
|
233
|
+
|
|
234
|
+
for idx, (label, method, path, needed_priv) in enumerate(PERMISSION_CHECKS, 1):
|
|
234
235
|
real_path = path.replace("{node}", node).replace("{vmid}", "99999") \
|
|
235
236
|
.replace("{storage}", "local").replace("{snapname}", "test")
|
|
236
237
|
|
|
@@ -238,22 +239,58 @@ def _auth_check(args: argparse.Namespace, client: ProxmoxClient) -> list[dict]:
|
|
|
238
239
|
if method in ("GET", "DELETE"):
|
|
239
240
|
client.request(method, real_path)
|
|
240
241
|
else:
|
|
241
|
-
# POST/PUT: send dummy body to check permission (not actual creation)
|
|
242
242
|
client.request(method, real_path, data={"dry": "1"})
|
|
243
243
|
status = "PASS"
|
|
244
|
+
passed += 1
|
|
244
245
|
except ProxmoxAPIError as exc:
|
|
245
246
|
if exc.status_code == 403:
|
|
246
247
|
status = "FAIL"
|
|
248
|
+
failed += 1
|
|
247
249
|
else:
|
|
248
|
-
# 404 (node not found), 500, etc. means we had permission
|
|
249
|
-
# to reach the endpoint but something else went wrong
|
|
250
250
|
status = "PASS"
|
|
251
|
+
passed += 1
|
|
252
|
+
|
|
253
|
+
# Print inline with colors
|
|
254
|
+
color = "green" if status == "PASS" else "red"
|
|
255
|
+
status_text = Text(status, style=f"bold {color}")
|
|
256
|
+
console.print(
|
|
257
|
+
f"[{idx}/{total}]",
|
|
258
|
+
status_text,
|
|
259
|
+
f"{needed_priv:<28s}",
|
|
260
|
+
label,
|
|
261
|
+
)
|
|
251
262
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
263
|
+
# Summary
|
|
264
|
+
console.print()
|
|
265
|
+
console.print(f"Passed: [bold green]{passed}[/], Failed: [bold red]{failed}[/], Total: {total}")
|
|
266
|
+
if failed == 0:
|
|
267
|
+
console.print("[bold green]✓ All permissions configured correctly!")
|
|
268
|
+
else:
|
|
269
|
+
console.print("[bold yellow]⚠ Some permissions are missing. Run 'proxmox auth setup' or check docs/api-permissions.md")
|
|
258
270
|
|
|
259
|
-
|
|
271
|
+
# ── Check for stray/leftover roles on the token ──
|
|
272
|
+
loader = ConfigLoader()
|
|
273
|
+
creds = loader.load()
|
|
274
|
+
token_ug = f"{creds.username}!{creds.api_token_id}" if creds.api_token_id else creds.username
|
|
275
|
+
|
|
276
|
+
acls = client.get("/access/acl")
|
|
277
|
+
token_acls = [a for a in acls if a.get("ugid") == token_ug and a.get("type") == "token"]
|
|
278
|
+
expected_roles = set("proxcli-" + suffix for suffix in ["sys", "storage", "vm", "node"])
|
|
279
|
+
stray = [a for a in token_acls if a.get("roleid") not in expected_roles]
|
|
280
|
+
|
|
281
|
+
if stray:
|
|
282
|
+
console.print()
|
|
283
|
+
console.print(
|
|
284
|
+
f"[bold yellow]⚠ The token [bold]{token_ug}[/] has extra roles[/] "
|
|
285
|
+
f"[dim](not needed after initial setup)[/]:"
|
|
286
|
+
)
|
|
287
|
+
for a in stray:
|
|
288
|
+
console.print(f" Path: [bold]{a['path']:<10s}[/] Role: [bold red]{a['roleid']}[/]")
|
|
289
|
+
console.print()
|
|
290
|
+
console.print(" Remove them in Datacenter → Permissions → API Token Permissions.")
|
|
291
|
+
console.print(" Keep only: [bold green]proxcli-sys, proxcli-storage, proxcli-vm, proxcli-node[/]")
|
|
292
|
+
console.print()
|
|
293
|
+
# Add note to summary
|
|
294
|
+
console.print(
|
|
295
|
+
f"[bold yellow]⚠ {len(stray)} stray role(s) — remove after verifying proxcli roles work.[/]"
|
|
296
|
+
)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
version = 1
|
|
2
|
-
revision =
|
|
2
|
+
revision = 3
|
|
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.13.
|
|
257
|
+
version = "0.13.2"
|
|
258
258
|
source = { editable = "." }
|
|
259
259
|
dependencies = [
|
|
260
260
|
{ name = "httpx" },
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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
|