proxcli 0.6.0__tar.gz → 0.7.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.6.0 → proxcli-0.7.0}/CHANGELOG.md +9 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/PKG-INFO +7 -3
- {proxcli-0.6.0 → proxcli-0.7.0}/README.md +6 -2
- {proxcli-0.6.0 → proxcli-0.7.0}/TODO.md +12 -11
- {proxcli-0.6.0 → proxcli-0.7.0}/proxmox/cli/tasks.py +16 -4
- {proxcli-0.6.0 → proxcli-0.7.0}/proxmox/client/client.py +78 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/pyproject.toml +1 -1
- {proxcli-0.6.0 → proxcli-0.7.0}/uv.lock +1 -1
- {proxcli-0.6.0 → proxcli-0.7.0}/.env.example +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/.github/workflows/ci.yml +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/.gitignore +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/.python-version +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/AGENTS.md +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/PLAN.md +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/PROJECT.md +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/PROMPT.md +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/proxmox/__init__.py +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/proxmox/cli/__init__.py +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/proxmox/cli/auth.py +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/proxmox/cli/cluster.py +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/proxmox/cli/completion.py +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/proxmox/cli/container.py +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/proxmox/cli/firewall_helpers.py +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/proxmox/cli/main.py +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/proxmox/cli/node.py +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/proxmox/cli/pool.py +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/proxmox/cli/storage.py +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/proxmox/cli/vm.py +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/proxmox/client/__init__.py +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/proxmox/client/auth.py +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/proxmox/client/exceptions.py +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/proxmox/config/__init__.py +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/proxmox/config/config.py +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/proxmox/config/models.py +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/proxmox/output/__init__.py +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/proxmox/output/formatter.py +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/proxmox/output/json_fmt.py +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/proxmox/output/table_fmt.py +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/proxmox/output/yaml_fmt.py +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/proxmox/utils/__init__.py +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/proxmox/utils/helpers.py +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/proxmox/utils/logging.py +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/tests/__init__.py +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/tests/conftest.py +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/tests/test_auth.py +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/tests/test_cli/__init__.py +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/tests/test_cli/test_main.py +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/tests/test_client.py +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/tests/test_config.py +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/tests/test_integration/__init__.py +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/tests/test_output/__init__.py +0 -0
- {proxcli-0.6.0 → proxcli-0.7.0}/tests/test_output/test_formatter.py +0 -0
|
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.7.0] - 2026-06-20
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Task log streaming: `proxmox task log <upid> [--follow]`.
|
|
14
|
+
Without `--follow`, prints available log lines. With `--follow`,
|
|
15
|
+
polls every second until the task exits (like `tail -f`).
|
|
16
|
+
Also added `ProxmoxClient._extract_node_from_upid()` as a static helper.
|
|
17
|
+
|
|
10
18
|
## [0.6.0] - 2026-06-20
|
|
11
19
|
|
|
12
20
|
### Added
|
|
@@ -74,6 +82,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
74
82
|
- CSRF ticket auto-refresh on 401.
|
|
75
83
|
- AI-agent-friendly: default JSON output, strict exit codes, `--dry-run` mode.
|
|
76
84
|
|
|
85
|
+
[0.7.0]: https://github.com/xezpeleta/proxcli/releases/tag/v0.7.0
|
|
77
86
|
[0.6.0]: https://github.com/xezpeleta/proxcli/releases/tag/v0.6.0
|
|
78
87
|
[0.5.0]: https://github.com/xezpeleta/proxcli/releases/tag/v0.5.0
|
|
79
88
|
[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.
|
|
3
|
+
Version: 0.7.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
|
|
@@ -21,7 +21,7 @@ Requires-Dist: pyyaml>=6
|
|
|
21
21
|
Requires-Dist: rich>=13
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
|
|
24
|
-
#
|
|
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
|
|
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
|
-
#
|
|
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
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
|
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`.
|
|
33
|
-
|
|
34
|
-
-
|
|
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
|
|
|
@@ -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
|
-
|
|
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)
|
|
@@ -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
|
|
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
|