proxcli 0.5.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.5.0 → proxcli-0.7.0}/CHANGELOG.md +17 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/PKG-INFO +33 -3
- {proxcli-0.5.0 → proxcli-0.7.0}/README.md +32 -2
- {proxcli-0.5.0 → proxcli-0.7.0}/TODO.md +12 -11
- proxcli-0.7.0/proxmox/cli/completion.py +214 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/cli/main.py +10 -4
- {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/cli/tasks.py +16 -4
- {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/client/client.py +78 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/pyproject.toml +1 -1
- {proxcli-0.5.0 → proxcli-0.7.0}/uv.lock +1 -1
- {proxcli-0.5.0 → proxcli-0.7.0}/.env.example +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/.github/workflows/ci.yml +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/.gitignore +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/.python-version +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/AGENTS.md +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/PLAN.md +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/PROJECT.md +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/PROMPT.md +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/__init__.py +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/cli/__init__.py +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/cli/auth.py +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/cli/cluster.py +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/cli/container.py +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/cli/firewall_helpers.py +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/cli/node.py +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/cli/pool.py +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/cli/storage.py +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/cli/vm.py +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/client/__init__.py +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/client/auth.py +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/client/exceptions.py +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/config/__init__.py +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/config/config.py +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/config/models.py +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/output/__init__.py +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/output/formatter.py +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/output/json_fmt.py +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/output/table_fmt.py +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/output/yaml_fmt.py +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/utils/__init__.py +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/utils/helpers.py +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/utils/logging.py +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/tests/__init__.py +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/tests/conftest.py +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/tests/test_auth.py +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/tests/test_cli/__init__.py +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/tests/test_cli/test_main.py +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/tests/test_client.py +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/tests/test_config.py +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/tests/test_integration/__init__.py +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/tests/test_output/__init__.py +0 -0
- {proxcli-0.5.0 → proxcli-0.7.0}/tests/test_output/test_formatter.py +0 -0
|
@@ -7,6 +7,21 @@ 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
|
+
|
|
18
|
+
## [0.6.0] - 2026-06-20
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- Shell completion support: `proxmox completion bash|zsh|fish`.
|
|
22
|
+
Generated scripts introspect the parser tree and stay in sync
|
|
23
|
+
with all registered resources and actions.
|
|
24
|
+
|
|
10
25
|
## [0.5.0] - 2026-06-20
|
|
11
26
|
|
|
12
27
|
### Added
|
|
@@ -67,6 +82,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
67
82
|
- CSRF ticket auto-refresh on 401.
|
|
68
83
|
- AI-agent-friendly: default JSON output, strict exit codes, `--dry-run` mode.
|
|
69
84
|
|
|
85
|
+
[0.7.0]: https://github.com/xezpeleta/proxcli/releases/tag/v0.7.0
|
|
86
|
+
[0.6.0]: https://github.com/xezpeleta/proxcli/releases/tag/v0.6.0
|
|
70
87
|
[0.5.0]: https://github.com/xezpeleta/proxcli/releases/tag/v0.5.0
|
|
71
88
|
[0.4.0]: https://github.com/xezpeleta/proxcli/releases/tag/v0.4.0
|
|
72
89
|
[0.3.0]: https://github.com/xezpeleta/proxcli/releases/tag/v0.3.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
|
|
@@ -51,6 +51,11 @@ proxmox auth login --url https://192.168.1.10:8006 --username root@pam --passwor
|
|
|
51
51
|
# Or with an API token
|
|
52
52
|
proxmox auth login --url https://192.168.1.10:8006 --username root@pam --api-token 'root@pam!my-token=deadbeef...'
|
|
53
53
|
|
|
54
|
+
# Enable shell completions
|
|
55
|
+
source <(proxmox completion bash) # bash
|
|
56
|
+
source <(proxmox completion zsh) # zsh
|
|
57
|
+
proxmox completion fish | source # fish (or save to ~/.config/fish/completions/proxmox.fish)
|
|
58
|
+
|
|
54
59
|
# Check auth status
|
|
55
60
|
proxmox auth status
|
|
56
61
|
|
|
@@ -129,6 +134,27 @@ proxmox auth status # Show current auth context
|
|
|
129
134
|
proxmox auth clear # Remove saved credentials
|
|
130
135
|
```
|
|
131
136
|
|
|
137
|
+
### Completion
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
proxmox completion bash # Emit bash completion script
|
|
141
|
+
proxmox completion zsh # Emit zsh completion script
|
|
142
|
+
proxmox completion fish # Emit fish completion script
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Add to your shell's rc file:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
# bash (~/.bashrc)
|
|
149
|
+
source <(proxmox completion bash)
|
|
150
|
+
|
|
151
|
+
# zsh (~/.zshrc)
|
|
152
|
+
source <(proxmox completion zsh)
|
|
153
|
+
|
|
154
|
+
# fish (~/.config/fish/completions/proxmox.fish)
|
|
155
|
+
proxmox completion fish > ~/.config/fish/completions/proxmox.fish
|
|
156
|
+
```
|
|
157
|
+
|
|
132
158
|
### VM (QEMU)
|
|
133
159
|
|
|
134
160
|
```bash
|
|
@@ -250,8 +276,12 @@ proxmox cluster firewall refs [--type alias|ipset|group]
|
|
|
250
276
|
```bash
|
|
251
277
|
proxmox task list [--node <node>]
|
|
252
278
|
proxmox task show <upid>
|
|
279
|
+
proxmox task log <upid> [--follow]
|
|
253
280
|
```
|
|
254
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
|
+
|
|
255
285
|
## Output Formats
|
|
256
286
|
|
|
257
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
|
|
@@ -28,6 +28,11 @@ proxmox auth login --url https://192.168.1.10:8006 --username root@pam --passwor
|
|
|
28
28
|
# Or with an API token
|
|
29
29
|
proxmox auth login --url https://192.168.1.10:8006 --username root@pam --api-token 'root@pam!my-token=deadbeef...'
|
|
30
30
|
|
|
31
|
+
# Enable shell completions
|
|
32
|
+
source <(proxmox completion bash) # bash
|
|
33
|
+
source <(proxmox completion zsh) # zsh
|
|
34
|
+
proxmox completion fish | source # fish (or save to ~/.config/fish/completions/proxmox.fish)
|
|
35
|
+
|
|
31
36
|
# Check auth status
|
|
32
37
|
proxmox auth status
|
|
33
38
|
|
|
@@ -106,6 +111,27 @@ proxmox auth status # Show current auth context
|
|
|
106
111
|
proxmox auth clear # Remove saved credentials
|
|
107
112
|
```
|
|
108
113
|
|
|
114
|
+
### Completion
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
proxmox completion bash # Emit bash completion script
|
|
118
|
+
proxmox completion zsh # Emit zsh completion script
|
|
119
|
+
proxmox completion fish # Emit fish completion script
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Add to your shell's rc file:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# bash (~/.bashrc)
|
|
126
|
+
source <(proxmox completion bash)
|
|
127
|
+
|
|
128
|
+
# zsh (~/.zshrc)
|
|
129
|
+
source <(proxmox completion zsh)
|
|
130
|
+
|
|
131
|
+
# fish (~/.config/fish/completions/proxmox.fish)
|
|
132
|
+
proxmox completion fish > ~/.config/fish/completions/proxmox.fish
|
|
133
|
+
```
|
|
134
|
+
|
|
109
135
|
### VM (QEMU)
|
|
110
136
|
|
|
111
137
|
```bash
|
|
@@ -227,8 +253,12 @@ proxmox cluster firewall refs [--type alias|ipset|group]
|
|
|
227
253
|
```bash
|
|
228
254
|
proxmox task list [--node <node>]
|
|
229
255
|
proxmox task show <upid>
|
|
256
|
+
proxmox task log <upid> [--follow]
|
|
230
257
|
```
|
|
231
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
|
+
|
|
232
262
|
## Output Formats
|
|
233
263
|
|
|
234
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
|
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Shell completion script generation for bash, zsh, and fish."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import textwrap
|
|
7
|
+
|
|
8
|
+
# Each resource has a known list of actions and optional flag completions.
|
|
9
|
+
# We introspect from the parser instead of hardcoding, keeping this DRY.
|
|
10
|
+
|
|
11
|
+
BASH_SCRIPT = textwrap.dedent("""\
|
|
12
|
+
_proxmox_completion() {{
|
|
13
|
+
local cur prev words cword
|
|
14
|
+
_init_completion || return
|
|
15
|
+
|
|
16
|
+
# Global flags
|
|
17
|
+
local global_flags="--url --username --password --password-stdin --api-token --output --dry-run --insecure --timeout --verbose --version --help"
|
|
18
|
+
|
|
19
|
+
# Resources and their actions (autogenerated)
|
|
20
|
+
declare -A _proxmox_resources
|
|
21
|
+
{resource_map}
|
|
22
|
+
|
|
23
|
+
# If we're at the first positional (after global flags), complete resources
|
|
24
|
+
if [[ $cword -eq 1 ]] || [[ "${{words[$((cword-1))]}}" =~ ^(--.*) ]]; then
|
|
25
|
+
COMPREPLY=($(compgen -W "${{!_proxmox_resources[*]}}" -- "$cur"))
|
|
26
|
+
return
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# Find the resource word
|
|
30
|
+
local resource=""
|
|
31
|
+
for w in "${{words[@]:1}}"; do
|
|
32
|
+
if [[ -n "${{_proxmox_resources[$w]}}" ]]; then
|
|
33
|
+
resource="$w"
|
|
34
|
+
break
|
|
35
|
+
fi
|
|
36
|
+
done
|
|
37
|
+
|
|
38
|
+
if [[ -n "$resource" ]]; then
|
|
39
|
+
local actions="${{_proxmox_resources[$resource]}}"
|
|
40
|
+
# Complete actions
|
|
41
|
+
COMPREPLY=($(compgen -W "$actions" -- "$cur"))
|
|
42
|
+
fi
|
|
43
|
+
}}
|
|
44
|
+
|
|
45
|
+
complete -F _proxmox_completion proxmox
|
|
46
|
+
""")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
ZSH_SCRIPT = textwrap.dedent("""\
|
|
50
|
+
#compdef proxmox
|
|
51
|
+
|
|
52
|
+
_proxmox() {{
|
|
53
|
+
local -a resources
|
|
54
|
+
resources=({resources})
|
|
55
|
+
|
|
56
|
+
local -A resource_actions
|
|
57
|
+
{resource_zsh_map}
|
|
58
|
+
|
|
59
|
+
local curcontext="$curcontext" state line
|
|
60
|
+
typeset -A opt_args
|
|
61
|
+
|
|
62
|
+
_arguments -C \\
|
|
63
|
+
{resource_zsh_flags} \\
|
|
64
|
+
'1:resource:($resources)' \\
|
|
65
|
+
'*::arg:->args'
|
|
66
|
+
|
|
67
|
+
case $state in
|
|
68
|
+
args)
|
|
69
|
+
local resource="$line[1]"
|
|
70
|
+
if [[ -n "${{resource_actions[$resource]}}" ]]; then
|
|
71
|
+
_arguments "*:action:(${{resource_actions[$resource]}})"
|
|
72
|
+
fi
|
|
73
|
+
;;
|
|
74
|
+
esac
|
|
75
|
+
}}
|
|
76
|
+
|
|
77
|
+
_proxmox "$@"
|
|
78
|
+
""")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
FISH_SCRIPT = textwrap.dedent("""\
|
|
82
|
+
function __fish_proxmox_resources
|
|
83
|
+
echo {resources}
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
function __fish_proxmox_actions
|
|
87
|
+
set -l resource (commandline -opc)[1]
|
|
88
|
+
switch $resource
|
|
89
|
+
{fish_cases}
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Global flags
|
|
94
|
+
complete -c proxmox -f
|
|
95
|
+
complete -c proxmox -l url -d 'Proxmox API URL'
|
|
96
|
+
complete -c proxmox -l username -d 'Username'
|
|
97
|
+
complete -c proxmox -l password -d 'Password'
|
|
98
|
+
complete -c proxmox -l password-stdin -d 'Read password from stdin'
|
|
99
|
+
complete -c proxmox -l api-token -d 'API token'
|
|
100
|
+
complete -c proxmox -l output -a 'json table yaml' -d 'Output format'
|
|
101
|
+
complete -c proxmox -l dry-run -d 'Print request without executing'
|
|
102
|
+
complete -c proxmox -l insecure -d 'Skip TLS verification'
|
|
103
|
+
complete -c proxmox -l timeout -d 'Request timeout in seconds'
|
|
104
|
+
complete -c proxmox -l verbose -d 'Enable debug output'
|
|
105
|
+
complete -c proxmox -l version -d 'Show version'
|
|
106
|
+
complete -c proxmox -l help -d 'Show help'
|
|
107
|
+
|
|
108
|
+
# Resource completion at position 1
|
|
109
|
+
complete -c proxmox -n 'not __fish_seen_subcommand_from {resources}' \\
|
|
110
|
+
-a '(__fish_proxmox_resources)'
|
|
111
|
+
|
|
112
|
+
# Action completion per resource
|
|
113
|
+
{fish_action_completions}
|
|
114
|
+
""")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _collect_parser_tree(parser: argparse.ArgumentParser) -> dict[str, list[str]]:
|
|
118
|
+
"""Walk the root parser and collect resource -> action mapping."""
|
|
119
|
+
resource_actions: dict[str, list[str]] = {}
|
|
120
|
+
|
|
121
|
+
for action in parser._actions:
|
|
122
|
+
if not isinstance(action, argparse._SubParsersAction):
|
|
123
|
+
continue
|
|
124
|
+
for name, sub_parser in action.choices.items():
|
|
125
|
+
sub_actions: list[str] = []
|
|
126
|
+
for sa in sub_parser._actions:
|
|
127
|
+
if isinstance(sa, argparse._SubParsersAction):
|
|
128
|
+
sub_actions.extend(sa.choices.keys())
|
|
129
|
+
resource_actions[name] = sorted(sub_actions)
|
|
130
|
+
|
|
131
|
+
return resource_actions
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def generate_bash(parser: argparse.ArgumentParser) -> str:
|
|
135
|
+
"""Generate a bash completion script."""
|
|
136
|
+
resources = _collect_parser_tree(parser)
|
|
137
|
+
lines = []
|
|
138
|
+
for name, actions in sorted(resources.items()):
|
|
139
|
+
lines.append(f' _proxmox_resources[{name}]="{ " ".join(actions) }"')
|
|
140
|
+
return BASH_SCRIPT.format(resource_map="\n".join(lines))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def generate_zsh(parser: argparse.ArgumentParser) -> str:
|
|
144
|
+
"""Generate a zsh completion script."""
|
|
145
|
+
resources = _collect_parser_tree(parser)
|
|
146
|
+
resources_list = " ".join(sorted(resources.keys()))
|
|
147
|
+
|
|
148
|
+
zsh_lines = []
|
|
149
|
+
for name, actions in sorted(resources.items()):
|
|
150
|
+
zsh_lines.append(f' resource_actions[{name}]="{ " ".join(actions) }"')
|
|
151
|
+
|
|
152
|
+
return ZSH_SCRIPT.format(
|
|
153
|
+
resources=resources_list,
|
|
154
|
+
resource_zsh_map="\n".join(zsh_lines),
|
|
155
|
+
resource_zsh_flags="",
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def generate_fish(parser: argparse.ArgumentParser) -> str:
|
|
160
|
+
"""Generate a fish completion script."""
|
|
161
|
+
resources = _collect_parser_tree(parser)
|
|
162
|
+
resources_list = " ".join(sorted(resources.keys()))
|
|
163
|
+
|
|
164
|
+
fish_cases = []
|
|
165
|
+
for name, actions in sorted(resources.items()):
|
|
166
|
+
fish_cases.append(f" case {name}\n echo {' '.join(actions)}")
|
|
167
|
+
|
|
168
|
+
fish_completions = []
|
|
169
|
+
for name, actions in sorted(resources.items()):
|
|
170
|
+
fish_completions.append(
|
|
171
|
+
f"complete -c proxmox -n '__fish_seen_subcommand_from {name}' "
|
|
172
|
+
f"-a '{' '.join(actions)}'"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
return FISH_SCRIPT.format(
|
|
176
|
+
resources=resources_list,
|
|
177
|
+
fish_cases="\n".join(fish_cases),
|
|
178
|
+
fish_action_completions="\n".join(fish_completions),
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# ---------------------------------------------------------------------------
|
|
183
|
+
# CLI registration
|
|
184
|
+
# ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def register_completion_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
188
|
+
"""Register the `proxmox completion` subcommand."""
|
|
189
|
+
comp_parser = subparsers.add_parser(
|
|
190
|
+
"completion", help="Generate shell completion script"
|
|
191
|
+
)
|
|
192
|
+
comp_parser.add_argument(
|
|
193
|
+
"shell",
|
|
194
|
+
choices=["bash", "zsh", "fish"],
|
|
195
|
+
help="Target shell",
|
|
196
|
+
)
|
|
197
|
+
comp_parser.set_defaults(func=_emit_completion)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _emit_completion(args: argparse.Namespace, _client: object) -> str:
|
|
201
|
+
"""Emit the completion script for the requested shell.
|
|
202
|
+
|
|
203
|
+
We need access to the root parser to introspect the subcommand tree,
|
|
204
|
+
so we import the builder here.
|
|
205
|
+
"""
|
|
206
|
+
from proxmox.cli.main import build_root_parser
|
|
207
|
+
|
|
208
|
+
parser = build_root_parser()
|
|
209
|
+
generators = {
|
|
210
|
+
"bash": generate_bash,
|
|
211
|
+
"zsh": generate_zsh,
|
|
212
|
+
"fish": generate_fish,
|
|
213
|
+
}
|
|
214
|
+
return generators[args.shell](parser)
|
|
@@ -57,6 +57,7 @@ def build_root_parser() -> argparse.ArgumentParser:
|
|
|
57
57
|
# Import and register subcommands
|
|
58
58
|
from proxmox.cli.auth import register_auth_parser
|
|
59
59
|
from proxmox.cli.cluster import register_cluster_parser
|
|
60
|
+
from proxmox.cli.completion import register_completion_parser
|
|
60
61
|
from proxmox.cli.container import register_container_parser
|
|
61
62
|
from proxmox.cli.node import register_node_parser
|
|
62
63
|
from proxmox.cli.pool import register_pool_parser
|
|
@@ -71,6 +72,7 @@ def build_root_parser() -> argparse.ArgumentParser:
|
|
|
71
72
|
register_container_parser(subparsers)
|
|
72
73
|
register_storage_parser(subparsers)
|
|
73
74
|
register_cluster_parser(subparsers)
|
|
75
|
+
register_completion_parser(subparsers)
|
|
74
76
|
register_task_parser(subparsers)
|
|
75
77
|
|
|
76
78
|
return parser
|
|
@@ -204,13 +206,17 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
204
206
|
return
|
|
205
207
|
|
|
206
208
|
try:
|
|
207
|
-
# auth status and
|
|
208
|
-
if args.resource == "auth" and args.action in ("status", "clear"):
|
|
209
|
+
# auth status, clear, and completion don't need a client
|
|
210
|
+
if (args.resource == "auth" and args.action in ("status", "clear")) or args.resource == "completion":
|
|
209
211
|
if hasattr(args, "func"):
|
|
210
212
|
result = args.func(args, None)
|
|
211
213
|
if result is not None:
|
|
212
|
-
|
|
213
|
-
|
|
214
|
+
if args.resource == "completion":
|
|
215
|
+
# Completion scripts are raw shell code, not JSON
|
|
216
|
+
print(result)
|
|
217
|
+
else:
|
|
218
|
+
output = format_output(result, args.output)
|
|
219
|
+
print(output)
|
|
214
220
|
return
|
|
215
221
|
|
|
216
222
|
_, overrides = _merge_config(args)
|
|
@@ -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
|