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.
Files changed (69) hide show
  1. {proxcli-0.13.0 → proxcli-0.13.2}/.github/workflows/ci.yml +4 -2
  2. {proxcli-0.13.0 → proxcli-0.13.2}/CHANGELOG.md +17 -0
  3. {proxcli-0.13.0 → proxcli-0.13.2}/PKG-INFO +1 -1
  4. {proxcli-0.13.0 → proxcli-0.13.2}/docs/api-permissions.md +15 -10
  5. {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/cli/auth.py +68 -31
  6. {proxcli-0.13.0 → proxcli-0.13.2}/pyproject.toml +1 -1
  7. {proxcli-0.13.0 → proxcli-0.13.2}/uv.lock +2 -2
  8. {proxcli-0.13.0 → proxcli-0.13.2}/.env.example +0 -0
  9. {proxcli-0.13.0 → proxcli-0.13.2}/.gitignore +0 -0
  10. {proxcli-0.13.0 → proxcli-0.13.2}/.python-version +0 -0
  11. {proxcli-0.13.0 → proxcli-0.13.2}/AGENTS.md +0 -0
  12. {proxcli-0.13.0 → proxcli-0.13.2}/PLAN.md +0 -0
  13. {proxcli-0.13.0 → proxcli-0.13.2}/PROJECT.md +0 -0
  14. {proxcli-0.13.0 → proxcli-0.13.2}/PROMPT.md +0 -0
  15. {proxcli-0.13.0 → proxcli-0.13.2}/README.md +0 -0
  16. {proxcli-0.13.0 → proxcli-0.13.2}/TODO.md +0 -0
  17. {proxcli-0.13.0 → proxcli-0.13.2}/docs/api-coverage.md +0 -0
  18. {proxcli-0.13.0 → proxcli-0.13.2}/docs/cloud-init.md +0 -0
  19. {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/__init__.py +0 -0
  20. {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/cli/__init__.py +0 -0
  21. {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/cli/acl.py +0 -0
  22. {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/cli/backup.py +0 -0
  23. {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/cli/ceph.py +0 -0
  24. {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/cli/cluster.py +0 -0
  25. {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/cli/completion.py +0 -0
  26. {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/cli/container.py +0 -0
  27. {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/cli/firewall_helpers.py +0 -0
  28. {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/cli/main.py +0 -0
  29. {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/cli/network.py +0 -0
  30. {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/cli/node.py +0 -0
  31. {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/cli/pool.py +0 -0
  32. {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/cli/role.py +0 -0
  33. {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/cli/storage.py +0 -0
  34. {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/cli/tasks.py +0 -0
  35. {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/cli/user.py +0 -0
  36. {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/cli/vm.py +0 -0
  37. {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/cli/vm_spec.py +0 -0
  38. {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/client/__init__.py +0 -0
  39. {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/client/auth.py +0 -0
  40. {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/client/client.py +0 -0
  41. {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/client/exceptions.py +0 -0
  42. {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/config/__init__.py +0 -0
  43. {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/config/config.py +0 -0
  44. {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/config/models.py +0 -0
  45. {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/output/__init__.py +0 -0
  46. {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/output/formatter.py +0 -0
  47. {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/output/json_fmt.py +0 -0
  48. {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/output/log_fmt.py +0 -0
  49. {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/output/table_fmt.py +0 -0
  50. {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/output/yaml_fmt.py +0 -0
  51. {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/utils/__init__.py +0 -0
  52. {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/utils/helpers.py +0 -0
  53. {proxcli-0.13.0 → proxcli-0.13.2}/proxmox/utils/logging.py +0 -0
  54. {proxcli-0.13.0 → proxcli-0.13.2}/tests/__init__.py +0 -0
  55. {proxcli-0.13.0 → proxcli-0.13.2}/tests/conftest.py +0 -0
  56. {proxcli-0.13.0 → proxcli-0.13.2}/tests/test_auth.py +0 -0
  57. {proxcli-0.13.0 → proxcli-0.13.2}/tests/test_cli/__init__.py +0 -0
  58. {proxcli-0.13.0 → proxcli-0.13.2}/tests/test_cli/test_backup.py +0 -0
  59. {proxcli-0.13.0 → proxcli-0.13.2}/tests/test_cli/test_ceph.py +0 -0
  60. {proxcli-0.13.0 → proxcli-0.13.2}/tests/test_cli/test_main.py +0 -0
  61. {proxcli-0.13.0 → proxcli-0.13.2}/tests/test_cli/test_network.py +0 -0
  62. {proxcli-0.13.0 → proxcli-0.13.2}/tests/test_cli/test_node_system.py +0 -0
  63. {proxcli-0.13.0 → proxcli-0.13.2}/tests/test_cli/test_role_acl.py +0 -0
  64. {proxcli-0.13.0 → proxcli-0.13.2}/tests/test_cli/test_user.py +0 -0
  65. {proxcli-0.13.0 → proxcli-0.13.2}/tests/test_client.py +0 -0
  66. {proxcli-0.13.0 → proxcli-0.13.2}/tests/test_config.py +0 -0
  67. {proxcli-0.13.0 → proxcli-0.13.2}/tests/test_integration/__init__.py +0 -0
  68. {proxcli-0.13.0 → proxcli-0.13.2}/tests/test_output/__init__.py +0 -0
  69. {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.ref == 'refs/heads/main'
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 }}" --check-url https://pypi.org/simple
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proxcli
3
- Version: 0.13.0
3
+ Version: 0.13.2
4
4
  Summary: A CLI tool to interact with Proxmox VE nodes and clusters via the REST API
5
5
  Author-email: Xabi Ezpeleta <xezpeleta@gmail.com>
6
6
  License: MIT
@@ -99,18 +99,23 @@ Role name: proxcli-node
99
99
  ```
100
100
 
101
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
- ```
102
+ # Assign each role to the token — use 'proxmox auth setup' (does this automatically):
103
+ proxmox auth setup
108
104
 
109
- That's it. Four roles, four ACLs, zero privilege creep — each path
110
- only gets what proxcli actually uses there.
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` does all of this automatically
113
- > if your current token has Administrator privileges.
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,Pool.Allocate,Pool.Audit",
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
- # We need the username from config to assign ACLs
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
- ug = creds.username
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") == ug
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} ({ug})")
211
+ skipped_acls.append(f"{path} → {role} ({token_ug})")
211
212
  continue
212
- content = _safe_encode({"path": path, "roles": role, "users": ug})
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} ({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
- }
215
+ created_acls.append(f"{path} → {role} ({token_ug})")
222
216
 
223
217
 
224
- def _auth_check(args: argparse.Namespace, client: ProxmoxClient) -> list[dict]:
218
+ def _auth_check(args: argparse.Namespace, client: ProxmoxClient) -> None:
225
219
  """Test each proxcli endpoint and report permission status."""
226
- results: list[dict] = []
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
- for label, method, path, needed_priv in PERMISSION_CHECKS:
233
- # Replace placeholders
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
- results.append({
253
- "feature": label,
254
- "method": method,
255
- "privilege": needed_priv,
256
- "status": status,
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
- return results
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,6 +1,6 @@
1
1
  [project]
2
2
  name = "proxcli"
3
- version = "0.13.0"
3
+ version = "0.13.2"
4
4
  description = "A CLI tool to interact with Proxmox VE nodes and clusters via the REST API"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -1,5 +1,5 @@
1
1
  version = 1
2
- revision = 2
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.0"
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