proxcli 0.6.0__tar.gz → 0.7.1__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 (52) hide show
  1. {proxcli-0.6.0 → proxcli-0.7.1}/CHANGELOG.md +21 -0
  2. {proxcli-0.6.0 → proxcli-0.7.1}/PKG-INFO +7 -3
  3. {proxcli-0.6.0 → proxcli-0.7.1}/README.md +6 -2
  4. {proxcli-0.6.0 → proxcli-0.7.1}/TODO.md +12 -11
  5. {proxcli-0.6.0 → proxcli-0.7.1}/proxmox/cli/main.py +13 -15
  6. {proxcli-0.6.0 → proxcli-0.7.1}/proxmox/cli/tasks.py +16 -4
  7. {proxcli-0.6.0 → proxcli-0.7.1}/proxmox/client/auth.py +1 -4
  8. {proxcli-0.6.0 → proxcli-0.7.1}/proxmox/client/client.py +78 -0
  9. {proxcli-0.6.0 → proxcli-0.7.1}/pyproject.toml +1 -1
  10. {proxcli-0.6.0 → proxcli-0.7.1}/tests/test_auth.py +1 -5
  11. {proxcli-0.6.0 → proxcli-0.7.1}/uv.lock +2 -2
  12. {proxcli-0.6.0 → proxcli-0.7.1}/.env.example +0 -0
  13. {proxcli-0.6.0 → proxcli-0.7.1}/.github/workflows/ci.yml +0 -0
  14. {proxcli-0.6.0 → proxcli-0.7.1}/.gitignore +0 -0
  15. {proxcli-0.6.0 → proxcli-0.7.1}/.python-version +0 -0
  16. {proxcli-0.6.0 → proxcli-0.7.1}/AGENTS.md +0 -0
  17. {proxcli-0.6.0 → proxcli-0.7.1}/PLAN.md +0 -0
  18. {proxcli-0.6.0 → proxcli-0.7.1}/PROJECT.md +0 -0
  19. {proxcli-0.6.0 → proxcli-0.7.1}/PROMPT.md +0 -0
  20. {proxcli-0.6.0 → proxcli-0.7.1}/proxmox/__init__.py +0 -0
  21. {proxcli-0.6.0 → proxcli-0.7.1}/proxmox/cli/__init__.py +0 -0
  22. {proxcli-0.6.0 → proxcli-0.7.1}/proxmox/cli/auth.py +0 -0
  23. {proxcli-0.6.0 → proxcli-0.7.1}/proxmox/cli/cluster.py +0 -0
  24. {proxcli-0.6.0 → proxcli-0.7.1}/proxmox/cli/completion.py +0 -0
  25. {proxcli-0.6.0 → proxcli-0.7.1}/proxmox/cli/container.py +0 -0
  26. {proxcli-0.6.0 → proxcli-0.7.1}/proxmox/cli/firewall_helpers.py +0 -0
  27. {proxcli-0.6.0 → proxcli-0.7.1}/proxmox/cli/node.py +0 -0
  28. {proxcli-0.6.0 → proxcli-0.7.1}/proxmox/cli/pool.py +0 -0
  29. {proxcli-0.6.0 → proxcli-0.7.1}/proxmox/cli/storage.py +0 -0
  30. {proxcli-0.6.0 → proxcli-0.7.1}/proxmox/cli/vm.py +0 -0
  31. {proxcli-0.6.0 → proxcli-0.7.1}/proxmox/client/__init__.py +0 -0
  32. {proxcli-0.6.0 → proxcli-0.7.1}/proxmox/client/exceptions.py +0 -0
  33. {proxcli-0.6.0 → proxcli-0.7.1}/proxmox/config/__init__.py +0 -0
  34. {proxcli-0.6.0 → proxcli-0.7.1}/proxmox/config/config.py +0 -0
  35. {proxcli-0.6.0 → proxcli-0.7.1}/proxmox/config/models.py +0 -0
  36. {proxcli-0.6.0 → proxcli-0.7.1}/proxmox/output/__init__.py +0 -0
  37. {proxcli-0.6.0 → proxcli-0.7.1}/proxmox/output/formatter.py +0 -0
  38. {proxcli-0.6.0 → proxcli-0.7.1}/proxmox/output/json_fmt.py +0 -0
  39. {proxcli-0.6.0 → proxcli-0.7.1}/proxmox/output/table_fmt.py +0 -0
  40. {proxcli-0.6.0 → proxcli-0.7.1}/proxmox/output/yaml_fmt.py +0 -0
  41. {proxcli-0.6.0 → proxcli-0.7.1}/proxmox/utils/__init__.py +0 -0
  42. {proxcli-0.6.0 → proxcli-0.7.1}/proxmox/utils/helpers.py +0 -0
  43. {proxcli-0.6.0 → proxcli-0.7.1}/proxmox/utils/logging.py +0 -0
  44. {proxcli-0.6.0 → proxcli-0.7.1}/tests/__init__.py +0 -0
  45. {proxcli-0.6.0 → proxcli-0.7.1}/tests/conftest.py +0 -0
  46. {proxcli-0.6.0 → proxcli-0.7.1}/tests/test_cli/__init__.py +0 -0
  47. {proxcli-0.6.0 → proxcli-0.7.1}/tests/test_cli/test_main.py +0 -0
  48. {proxcli-0.6.0 → proxcli-0.7.1}/tests/test_client.py +0 -0
  49. {proxcli-0.6.0 → proxcli-0.7.1}/tests/test_config.py +0 -0
  50. {proxcli-0.6.0 → proxcli-0.7.1}/tests/test_integration/__init__.py +0 -0
  51. {proxcli-0.6.0 → proxcli-0.7.1}/tests/test_output/__init__.py +0 -0
  52. {proxcli-0.6.0 → proxcli-0.7.1}/tests/test_output/test_formatter.py +0 -0
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.7.1] - 2026-06-20
11
+
12
+ ### Fixed
13
+ - API token authentication: Removed incorrect base64 encoding.
14
+ Proxmox expects `PVEAPIToken=user@realm!tokenid=secret` as plain text
15
+ in the Authorization header, not base64-encoded.
16
+ - Dry-run mode now always sets API token headers so that
17
+ `--dry-run` output accurately reflects the Authorization header
18
+ that would be sent. (Password auth is still skipped in dry-run
19
+ since it requires a network call.)
20
+
21
+ ## [0.7.0] - 2026-06-20
22
+
23
+ ### Added
24
+ - Task log streaming: `proxmox task log <upid> [--follow]`.
25
+ Without `--follow`, prints available log lines. With `--follow`,
26
+ polls every second until the task exits (like `tail -f`).
27
+ Also added `ProxmoxClient._extract_node_from_upid()` as a static helper.
28
+
10
29
  ## [0.6.0] - 2026-06-20
11
30
 
12
31
  ### Added
@@ -74,6 +93,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
74
93
  - CSRF ticket auto-refresh on 401.
75
94
  - AI-agent-friendly: default JSON output, strict exit codes, `--dry-run` mode.
76
95
 
96
+ [0.7.1]: https://github.com/xezpeleta/proxcli/releases/tag/v0.7.1
97
+ [0.7.0]: https://github.com/xezpeleta/proxcli/releases/tag/v0.7.0
77
98
  [0.6.0]: https://github.com/xezpeleta/proxcli/releases/tag/v0.6.0
78
99
  [0.5.0]: https://github.com/xezpeleta/proxcli/releases/tag/v0.5.0
79
100
  [0.4.0]: https://github.com/xezpeleta/proxcli/releases/tag/v0.4.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proxcli
3
- Version: 0.6.0
3
+ Version: 0.7.1
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
@@ -21,7 +21,7 @@ Requires-Dist: pyyaml>=6
21
21
  Requires-Dist: rich>=13
22
22
  Description-Content-Type: text/markdown
23
23
 
24
- # proxmox
24
+ # proxcli
25
25
 
26
26
  A CLI tool to interact with [Proxmox VE](https://www.proxmox.com/) nodes and clusters via the REST API.
27
27
 
@@ -33,7 +33,7 @@ Requires Python 3.10+ and [uv](https://docs.astral.sh/uv/).
33
33
 
34
34
  ```bash
35
35
  # From PyPI
36
- uv tool install proxmox
36
+ uv tool install proxcli
37
37
 
38
38
  # From Git
39
39
  uv tool install git+https://github.com/xezpeleta/proxcli.git
@@ -276,8 +276,12 @@ proxmox cluster firewall refs [--type alias|ipset|group]
276
276
  ```bash
277
277
  proxmox task list [--node <node>]
278
278
  proxmox task show <upid>
279
+ proxmox task log <upid> [--follow]
279
280
  ```
280
281
 
282
+ `proxmox task log --follow` polls `/nodes/{node}/tasks/{upid}/log` every second
283
+ and streams new lines until the task completes (like `tail -f`).
284
+
281
285
  ## Output Formats
282
286
 
283
287
  ### JSON (default)
@@ -1,4 +1,4 @@
1
- # proxmox
1
+ # proxcli
2
2
 
3
3
  A CLI tool to interact with [Proxmox VE](https://www.proxmox.com/) nodes and clusters via the REST API.
4
4
 
@@ -10,7 +10,7 @@ Requires Python 3.10+ and [uv](https://docs.astral.sh/uv/).
10
10
 
11
11
  ```bash
12
12
  # From PyPI
13
- uv tool install proxmox
13
+ uv tool install proxcli
14
14
 
15
15
  # From Git
16
16
  uv tool install git+https://github.com/xezpeleta/proxcli.git
@@ -253,8 +253,12 @@ proxmox cluster firewall refs [--type alias|ipset|group]
253
253
  ```bash
254
254
  proxmox task list [--node <node>]
255
255
  proxmox task show <upid>
256
+ proxmox task log <upid> [--follow]
256
257
  ```
257
258
 
259
+ `proxmox task log --follow` polls `/nodes/{node}/tasks/{upid}/log` every second
260
+ and streams new lines until the task completes (like `tail -f`).
261
+
258
262
  ## Output Formats
259
263
 
260
264
  ### JSON (default)
@@ -2,15 +2,20 @@
2
2
 
3
3
  Planned improvements for future releases. Items are roughly ordered by priority.
4
4
 
5
+ Completed items are marked with a check. Implementation notes are preserved for context.
6
+
5
7
  ---
6
8
 
7
- ## v1.1 — Polish & Usability
9
+ ## Done
10
+
11
+ - [x] **Firewall management** — cluster, node, VM, and container. Options, enable/disable, policy, rules (CRUD), aliases (cluster), ipsets with CIDR mgmt (cluster), refs.
12
+ - [x] **Pool management** — `proxmox pool`: list, show, create, update, delete. Wraps `/pools`.
13
+ - [x] **Shell completions** — `proxmox completion bash|zsh|fish`. Dynamic, introspects the parser tree.
8
14
 
9
- - [ ] **Shell completions**
10
- - Add `proxmox completion bash|zsh|fish` subcommand that emits a completion script. Use argparse's built-in completion or a lightweight generator. Makes tab-completion work for all subcommands and flags.
15
+ ## v1.1 Polish & Usability
11
16
 
12
17
  - [ ] **Streaming task logs (`--follow`)**
13
- - `proxmox task log <upid>` that streams task output in real time (like `tail -f`). Requires httpx streaming support (already viable — httpx is the chosen HTTP client).
18
+ - `proxmox task log <upid> --follow` that streams task output in real time (like `tail -f`). Requires httpx streaming.
14
19
 
15
20
  - [ ] **Startup time optimization**
16
21
  - Current `proxmox --help` takes ~350ms. Lazy-load subcommand modules so only the requested resource's code is imported. Move `import rich`, `import yaml` inside formatter functions. Target: <200ms.
@@ -29,17 +34,13 @@ Planned improvements for future releases. Items are roughly ordered by priority.
29
34
  - `proxmox backup` subcommand: `list`, `create`, `show`, `delete`. Wrap `/nodes/{node}/vzdump` and `/nodes/{node}/storage/{storage}/content` for backup files.
30
35
 
31
36
  - [ ] **User & permission management**
32
- - `proxmox user` subcommand: `list`, `show`, `create`, `update`, `delete`. Wraps `/access/users`, `/access/acl`, `/access/roles`.
33
-
34
- - [ ] **Pool management**
35
- - `proxmox pool` subcommand: `list`, `show`, `create`, `update`, `delete`. Wraps `/pools`.
37
+ - `proxmox user` subcommand: `list`, `show`, `create`, `update`, `delete`.
38
+ - `proxmox role` subcommand: `list`, `show`, `create`, `update`, `delete`.
39
+ - `proxmox acl` subcommand: `list`, `show`. Wraps `/access/users`, `/access/roles`, `/access/acl`, `/access/groups`.
36
40
 
37
41
  - [ ] **Network management**
38
42
  - `proxmox network` subcommand: `list`, `show`, `update` for bridges, bonds, VLANs. Wraps `/nodes/{node}/network`.
39
43
 
40
- - [ ] **Firewall management**
41
- - `proxmox firewall` subcommand: `list`, `show`, `enable`, `disable`, `create rule`, `delete rule`. Wraps `/nodes/{node}/firewall`, `/cluster/firewall`.
42
-
43
44
  - [ ] **SDN (Software-Defined Networking)**
44
45
  - `proxmox sdn` subcommand: `zones`, `vnets`, `subnets`. Wraps `/cluster/sdn/*` endpoints.
45
46
 
@@ -149,21 +149,19 @@ def _build_client(overrides: dict[str, Any], args: argparse.Namespace) -> Proxmo
149
149
 
150
150
  auth_mgr = AuthManager()
151
151
 
152
- # Don't authenticate if dry-run (no actual API calls will be made)
153
- if not args.dry_run:
154
- if overrides["api_token_id"] and overrides["api_token_secret"]:
155
- auth_mgr.set_api_token(
156
- overrides["username"] or "root@pam",
157
- overrides["api_token_id"],
158
- overrides["api_token_secret"],
159
- )
160
- elif overrides["password"]:
161
- auth_mgr.authenticate_password(
162
- overrides["url"],
163
- overrides["username"] or "root@pam",
164
- overrides["password"],
165
- verify=overrides["verify_tls"],
166
- )
152
+ if overrides["api_token_id"] and overrides["api_token_secret"]:
153
+ auth_mgr.set_api_token(
154
+ overrides["username"] or "root@pam",
155
+ overrides["api_token_id"],
156
+ overrides["api_token_secret"],
157
+ )
158
+ elif overrides["password"] and not args.dry_run:
159
+ auth_mgr.authenticate_password(
160
+ overrides["url"],
161
+ overrides["username"] or "root@pam",
162
+ overrides["password"],
163
+ verify=overrides["verify_tls"],
164
+ )
167
165
 
168
166
  client = ProxmoxClient(
169
167
  base_url=overrides["url"],
@@ -22,13 +22,20 @@ def register_task_parser(subparsers: argparse._SubParsersAction) -> None:
22
22
  task_show.add_argument("upid", help="Task UPID")
23
23
  task_show.set_defaults(func=_task_show)
24
24
 
25
+ # --- task log ---
26
+ task_log = task_sub.add_parser("log", help="Show task log output")
27
+ task_log.add_argument("upid", help="Task UPID")
28
+ task_log.add_argument(
29
+ "--follow", "-f",
30
+ action="store_true",
31
+ help="Follow log output until task completes (like tail -f)",
32
+ )
33
+ task_log.set_defaults(func=_task_log)
34
+
25
35
 
26
36
  def _extract_node_from_upid(upid: str) -> str | None:
27
37
  """Parse node name from a Proxmox UPID string: UPID:{node}:..."""
28
- parts = upid.split(":")
29
- if len(parts) >= 2:
30
- return parts[1]
31
- return None
38
+ return ProxmoxClient._extract_node_from_upid(upid)
32
39
 
33
40
 
34
41
  def _task_list(args: argparse.Namespace, client: ProxmoxClient) -> dict | list:
@@ -63,3 +70,8 @@ def _task_show(args: argparse.Namespace, client: ProxmoxClient) -> dict:
63
70
  if not node:
64
71
  return {"error": f"Could not extract node from UPID: {args.upid}"}
65
72
  return client.get(f"/nodes/{node}/tasks/{args.upid}/status")
73
+
74
+
75
+ def _task_log(args: argparse.Namespace, client: ProxmoxClient) -> None:
76
+ """Stream task log (returns None so main.py skips JSON formatting)."""
77
+ client.stream_task_log(args.upid, follow=args.follow)
@@ -2,7 +2,6 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import base64
6
5
  from enum import Enum
7
6
  from typing import Any
8
7
 
@@ -63,9 +62,7 @@ class AuthManager:
63
62
 
64
63
  def set_api_token(self, user: str, token_id: str, secret: str) -> None:
65
64
  """Use a Proxmox API token for authentication."""
66
- raw = f"{user}!{token_id}={secret}"
67
- encoded = base64.b64encode(raw.encode()).decode()
68
- self._auth_header = f"PVEAPIToken {encoded}"
65
+ self._auth_header = f"PVEAPIToken={user}!{token_id}={secret}"
69
66
  self._method = AuthMethod.API_TOKEN
70
67
  self._ticket = None
71
68
  self._csrf_token = None
@@ -280,3 +280,81 @@ class ProxmoxClient:
280
280
  import sys
281
281
 
282
282
  print(f"[proxmox] {message}", file=sys.stderr)
283
+
284
+ # ------------------------------------------------------------------
285
+ # Task log streaming
286
+ # ------------------------------------------------------------------
287
+
288
+ def stream_task_log(self, upid: str, *, follow: bool = False) -> None:
289
+ """Stream task log lines to stdout.
290
+
291
+ Args:
292
+ upid: Task UPID string (e.g. UPID:pve01:00000010:...).
293
+ follow: If True, keep polling until task exits.
294
+ """
295
+ import sys
296
+
297
+ node = self._extract_node_from_upid(upid)
298
+ if not node:
299
+ print(f"Error: could not extract node from UPID: {upid}", file=sys.stderr)
300
+ return
301
+
302
+ if self._dry_run:
303
+ self._print_dry_run("GET", f"{self._base_url}/api2/json/nodes/{node}/tasks/{upid}/log", None)
304
+ if follow:
305
+ print("[dry-run] --follow would poll /tasks/{upid}/status until stopped")
306
+ return
307
+
308
+ start = 0
309
+ seen_lines: set[int] = set()
310
+
311
+ while True:
312
+ try:
313
+ log_data = self.request("GET", f"/nodes/{node}/tasks/{upid}/log", params={"start": start})
314
+ except ProxmoxAPIError:
315
+ if not follow:
316
+ break
317
+ time.sleep(1)
318
+ continue
319
+
320
+ if isinstance(log_data, dict):
321
+ log_data = log_data.get("data", []) if "data" in log_data else []
322
+
323
+ if isinstance(log_data, list):
324
+ for entry in log_data:
325
+ if not isinstance(entry, dict):
326
+ continue
327
+ n = entry.get("n", 0)
328
+ if n in seen_lines:
329
+ continue
330
+ seen_lines.add(n)
331
+ line = entry.get("t", "")
332
+ print(line)
333
+
334
+ # Check if task is done
335
+ if follow:
336
+ try:
337
+ status_data = self.request("GET", f"/nodes/{node}/tasks/{upid}/status")
338
+ if isinstance(status_data, dict):
339
+ status = status_data.get("status")
340
+ if status == "stopped":
341
+ exit_code = status_data.get("exitstatus")
342
+ if exit_code is not None:
343
+ print(f"\nTask completed with exit code: {exit_code}")
344
+ break
345
+ except ProxmoxAPIError:
346
+ pass
347
+
348
+ if not follow:
349
+ break
350
+
351
+ start = len(seen_lines)
352
+ time.sleep(1)
353
+
354
+ @staticmethod
355
+ def _extract_node_from_upid(upid: str) -> str | None:
356
+ """Parse node name from a Proxmox UPID string: UPID:{node}:..."""
357
+ parts = upid.split(":")
358
+ if len(parts) >= 2:
359
+ return parts[1]
360
+ return None
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "proxcli"
3
- version = "0.6.0"
3
+ version = "0.7.1"
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 = [
@@ -2,8 +2,6 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import base64
6
-
7
5
  import pytest
8
6
 
9
7
  from proxmox.client.auth import AuthMethod
@@ -60,9 +58,7 @@ class TestAuthManager:
60
58
  assert auth_manager.method == AuthMethod.API_TOKEN
61
59
 
62
60
  headers = auth_manager.get_headers()
63
- expected_raw = "root@pam!my-token=my-secret"
64
- expected_encoded = base64.b64encode(expected_raw.encode()).decode()
65
- assert headers["Authorization"] == f"PVEAPIToken {expected_encoded}"
61
+ assert headers["Authorization"] == "PVEAPIToken=root@pam!my-token=my-secret"
66
62
  assert "Cookie" not in headers
67
63
  assert "CSRFPreventionToken" not in headers
68
64
 
@@ -1,5 +1,5 @@
1
1
  version = 1
2
- revision = 3
2
+ revision = 2
3
3
  requires-python = ">=3.10"
4
4
 
5
5
  [[package]]
@@ -254,7 +254,7 @@ wheels = [
254
254
 
255
255
  [[package]]
256
256
  name = "proxcli"
257
- version = "0.6.0"
257
+ version = "0.7.1"
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