proxcli 0.1.0__py3-none-any.whl
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.1.0.dist-info/METADATA +262 -0
- proxcli-0.1.0.dist-info/RECORD +29 -0
- proxcli-0.1.0.dist-info/WHEEL +4 -0
- proxcli-0.1.0.dist-info/entry_points.txt +2 -0
- proxmox/__init__.py +0 -0
- proxmox/cli/__init__.py +0 -0
- proxmox/cli/auth.py +130 -0
- proxmox/cli/cluster.py +21 -0
- proxmox/cli/container.py +157 -0
- proxmox/cli/main.py +231 -0
- proxmox/cli/node.py +55 -0
- proxmox/cli/storage.py +63 -0
- proxmox/cli/tasks.py +65 -0
- proxmox/cli/vm.py +211 -0
- proxmox/client/__init__.py +0 -0
- proxmox/client/auth.py +113 -0
- proxmox/client/client.py +217 -0
- proxmox/client/exceptions.py +43 -0
- proxmox/config/__init__.py +0 -0
- proxmox/config/config.py +98 -0
- proxmox/config/models.py +51 -0
- proxmox/output/__init__.py +0 -0
- proxmox/output/formatter.py +26 -0
- proxmox/output/json_fmt.py +11 -0
- proxmox/output/table_fmt.py +64 -0
- proxmox/output/yaml_fmt.py +12 -0
- proxmox/utils/__init__.py +0 -0
- proxmox/utils/helpers.py +14 -0
- proxmox/utils/logging.py +15 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: proxcli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A CLI tool to interact with Proxmox VE nodes and clusters via the REST API
|
|
5
|
+
Author-email: Xabi Ezpeleta <xezpeleta@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Intended Audience :: System Administrators
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: System :: Systems Administration
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Requires-Dist: httpx>=0.27
|
|
19
|
+
Requires-Dist: pydantic>=2
|
|
20
|
+
Requires-Dist: pyyaml>=6
|
|
21
|
+
Requires-Dist: rich>=13
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# proxmox
|
|
25
|
+
|
|
26
|
+
A CLI tool to interact with [Proxmox VE](https://www.proxmox.com/) nodes and clusters via the REST API.
|
|
27
|
+
|
|
28
|
+
Designed to be easy for humans (table output, ergonomic flags) and AI agents (structured JSON, strict exit codes, `--dry-run`). Provides a higher-level abstraction over the raw Proxmox API.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
Requires Python 3.10+ and [uv](https://docs.astral.sh/uv/).
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# From PyPI
|
|
36
|
+
uv tool install proxmox
|
|
37
|
+
|
|
38
|
+
# From Git
|
|
39
|
+
uv tool install git+https://github.com/xezpeleta/proxmox-cli.git
|
|
40
|
+
|
|
41
|
+
# From local checkout
|
|
42
|
+
uv tool install .
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quickstart
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Authenticate (password-based)
|
|
49
|
+
proxmox auth login --url https://192.168.1.10:8006 --username root@pam --password your_password
|
|
50
|
+
|
|
51
|
+
# Or with an API token
|
|
52
|
+
proxmox auth login --url https://192.168.1.10:8006 --username root@pam --api-token 'root@pam!my-token=deadbeef...'
|
|
53
|
+
|
|
54
|
+
# Check auth status
|
|
55
|
+
proxmox auth status
|
|
56
|
+
|
|
57
|
+
# List VMs
|
|
58
|
+
proxmox vm list
|
|
59
|
+
|
|
60
|
+
# Show a specific VM
|
|
61
|
+
proxmox vm show 100
|
|
62
|
+
|
|
63
|
+
# Create a VM
|
|
64
|
+
proxmox vm create --node pve01 --vmid 110 --memory 2048 --cores 2 --name webserver
|
|
65
|
+
|
|
66
|
+
# Start / stop / reboot
|
|
67
|
+
proxmox vm start 110
|
|
68
|
+
proxmox vm stop 110
|
|
69
|
+
proxmox vm reboot 110
|
|
70
|
+
|
|
71
|
+
# Delete (with purge)
|
|
72
|
+
proxmox vm delete 110 --purge
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Authentication
|
|
76
|
+
|
|
77
|
+
Credentials are stored in `~/.config/proxmox-cli/credentials.json` with restrictive permissions (`0600`).
|
|
78
|
+
|
|
79
|
+
### Auth methods
|
|
80
|
+
|
|
81
|
+
| Method | Command |
|
|
82
|
+
|---|---|
|
|
83
|
+
| Password | `proxmox auth login --url ... --username ... --password ...` |
|
|
84
|
+
| Password (stdin) | `echo "$PASS" \| proxmox auth login --url ... --username ... --password-stdin` |
|
|
85
|
+
| API token | `proxmox auth login --url ... --username ... --api-token 'user!tokenid=secret'` |
|
|
86
|
+
|
|
87
|
+
### Override credentials per command
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
proxmox --url https://other-pve:8006 --username admin@pam --password pass123 vm list
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Environment variable
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
export PROXMOX_PASSWORD=mysecret
|
|
97
|
+
proxmox vm list --username root@pam --url https://pve:8006
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Self-signed certificates
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
proxmox --insecure vm list
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Command Reference
|
|
107
|
+
|
|
108
|
+
### Global flags
|
|
109
|
+
|
|
110
|
+
| Flag | Default | Description |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| `--url` | (config file) | Proxmox API URL |
|
|
113
|
+
| `--username` | (config file) | Username |
|
|
114
|
+
| `--password` | — | Password |
|
|
115
|
+
| `--password-stdin` | — | Read password from stdin |
|
|
116
|
+
| `--api-token` | — | API token (`user!tokenid=secret`) |
|
|
117
|
+
| `--output` | `json` | Output format: `json`, `table`, `yaml` |
|
|
118
|
+
| `--dry-run` | off | Print the API request without executing |
|
|
119
|
+
| `--insecure` | off | Skip TLS verification |
|
|
120
|
+
| `--timeout` | `30` | Request timeout in seconds |
|
|
121
|
+
| `--verbose` | off | Debug output to stderr |
|
|
122
|
+
| `--version` | — | Show version |
|
|
123
|
+
|
|
124
|
+
### Auth
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
proxmox auth login # Save credentials
|
|
128
|
+
proxmox auth status # Show current auth context
|
|
129
|
+
proxmox auth clear # Remove saved credentials
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### VM (QEMU)
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
proxmox vm list [--node <node>]
|
|
136
|
+
proxmox vm show <vmid> [--node <node>]
|
|
137
|
+
proxmox vm create --node <node> --vmid <id> --memory <mb> [--cores <n>] [--name <name>] [--storage <name>] [--net <config>]
|
|
138
|
+
proxmox vm start <vmid> [--node <node>]
|
|
139
|
+
proxmox vm stop <vmid> [--node <node>]
|
|
140
|
+
proxmox vm reboot <vmid> [--node <node>]
|
|
141
|
+
proxmox vm suspend <vmid> [--node <node>]
|
|
142
|
+
proxmox vm resume <vmid> [--node <node>]
|
|
143
|
+
proxmox vm delete <vmid> [--node <node>] [--force] [--purge]
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Container (LXC)
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
proxmox container list [--node <node>]
|
|
150
|
+
proxmox container show <vmid> [--node <node>]
|
|
151
|
+
proxmox container create --node <node> --vmid <id> --ostemplate <tmpl> [--memory <mb>] [--cores <n>] [--storage <name>]
|
|
152
|
+
proxmox container start <vmid> [--node <node>]
|
|
153
|
+
proxmox container stop <vmid> [--node <node>]
|
|
154
|
+
proxmox container delete <vmid> [--node <node>] [--force] [--purge]
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Node
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
proxmox node list
|
|
161
|
+
proxmox node show <node>
|
|
162
|
+
proxmox node status [<node>]
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Storage
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
proxmox storage list [--node <node>]
|
|
169
|
+
proxmox storage show <storage>
|
|
170
|
+
proxmox storage content <storage> [--node <node>]
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Cluster
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
proxmox cluster status
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Task
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
proxmox task list [--node <node>]
|
|
183
|
+
proxmox task show <upid>
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Output Formats
|
|
187
|
+
|
|
188
|
+
### JSON (default)
|
|
189
|
+
|
|
190
|
+
```json
|
|
191
|
+
[
|
|
192
|
+
{
|
|
193
|
+
"vmid": 100,
|
|
194
|
+
"name": "webserver",
|
|
195
|
+
"status": "running",
|
|
196
|
+
"cpu": 0.05,
|
|
197
|
+
"mem": 2048
|
|
198
|
+
}
|
|
199
|
+
]
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Table
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
┌──────┬───────────┬─────────┬───────┬──────┐
|
|
206
|
+
│ vmid │ name │ status │ cpu │ mem │
|
|
207
|
+
├──────┼───────────┼─────────┼───────┼──────┤
|
|
208
|
+
│ 100 │ webserver │ running │ 0.05 │ 2048 │
|
|
209
|
+
└──────┴───────────┴─────────┴───────┴──────┘
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### YAML
|
|
213
|
+
|
|
214
|
+
```yaml
|
|
215
|
+
- vmid: 100
|
|
216
|
+
name: webserver
|
|
217
|
+
status: running
|
|
218
|
+
cpu: 0.05
|
|
219
|
+
mem: 2048
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## AI Agent Usage
|
|
223
|
+
|
|
224
|
+
Every command emits valid JSON by default (stdout) and diagnostic messages on stderr. Exit codes follow Unix conventions.
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
# Dry-run to preview the API call
|
|
228
|
+
proxmox --dry-run vm create --node pve01 --vmid 110 --memory 1024
|
|
229
|
+
|
|
230
|
+
# Machine-parseable JSON output
|
|
231
|
+
proxmox --output json vm list | jq '.[] | {vmid, status}'
|
|
232
|
+
|
|
233
|
+
# Check exit code
|
|
234
|
+
proxmox vm show 999 || echo "VM not found"
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## Development
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
# Clone
|
|
241
|
+
git clone https://github.com/xezpeleta/proxmox-cli.git
|
|
242
|
+
cd proxmox-cli
|
|
243
|
+
|
|
244
|
+
# Install dev dependencies
|
|
245
|
+
uv sync
|
|
246
|
+
|
|
247
|
+
# Run tests
|
|
248
|
+
uv run pytest
|
|
249
|
+
|
|
250
|
+
# Run with coverage
|
|
251
|
+
uv run pytest --cov=proxmox --cov-report=term-missing
|
|
252
|
+
|
|
253
|
+
# Lint
|
|
254
|
+
uv run ruff check .
|
|
255
|
+
|
|
256
|
+
# Build
|
|
257
|
+
uv build
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## License
|
|
261
|
+
|
|
262
|
+
MIT
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
proxmox/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
proxmox/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
proxmox/cli/auth.py,sha256=9JAI4yW3THC0mWDgCjjWqSvcOTF07OsTuFdVDK7Zi7U,4779
|
|
4
|
+
proxmox/cli/cluster.py,sha256=zZOFYh9Sa9bO3UBP-qEXyJloan7tHmWPn2lRXgk6TRI,732
|
|
5
|
+
proxmox/cli/container.py,sha256=EpypF2MbqhpJX1zdtXYV6PrRjHwtxGV4sTWspruAA3k,6402
|
|
6
|
+
proxmox/cli/main.py,sha256=wyKx3cc-vGhoV5CrLedWvuVHaNDRwN1F3wUOi4QFIHc,8238
|
|
7
|
+
proxmox/cli/node.py,sha256=mKGShu89TQjSy8cH3ql901gQpZARLqVklIvNTfELlDs,2001
|
|
8
|
+
proxmox/cli/storage.py,sha256=n-TT61nqxOQhic1rnweXxJhmU5Ivth1fb281r49cZzk,2435
|
|
9
|
+
proxmox/cli/tasks.py,sha256=yGPGmm6GXuSiNBf1MH9KqOXpndOcVvnESqn-ebOR6wM,2353
|
|
10
|
+
proxmox/cli/vm.py,sha256=aj9OKBLrW0f4s6ZeRMZxpkSxF2ANbubT2e73t2QdlZ4,8743
|
|
11
|
+
proxmox/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
proxmox/client/auth.py,sha256=FFxPlE3dW-jusHxkZDLqP3Fu-53CiLJ0uW0b6H4_KZk,3797
|
|
13
|
+
proxmox/client/client.py,sha256=1-BkObMG0W0SJoJXLZ3jeyJOCQSjUH7orB-epuf-7fI,7067
|
|
14
|
+
proxmox/client/exceptions.py,sha256=LbhE_ZqMx9ESFi9Pwyarba9rceE2GaOdCH79HqSuOJk,1175
|
|
15
|
+
proxmox/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
proxmox/config/config.py,sha256=Jv1OlGDh5ExYchethHTRQV3g-OGqldfGXNeKGkKUBbY,3205
|
|
17
|
+
proxmox/config/models.py,sha256=XgAGFabGBT1WkhhROYO-ZCitovAJ84vK707wNe33wz0,2118
|
|
18
|
+
proxmox/output/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
+
proxmox/output/formatter.py,sha256=l8DI4GvtWHxD-GVqpQdZ39JGA_YqFxGVgsEI2ZBJJvo,757
|
|
20
|
+
proxmox/output/json_fmt.py,sha256=ltylV9vfs_-yu_QTW99fK68-NGSBqyFfnc7x-dTDTCo,277
|
|
21
|
+
proxmox/output/table_fmt.py,sha256=S9eGQwGogJm9F1HQHe-fkmCApSSu9_QZxVNAY3p6XYQ,1844
|
|
22
|
+
proxmox/output/yaml_fmt.py,sha256=lGS1HRtBYeibK0tRsLbd5jnia-Hhlv48SpWbEGpE6oo,260
|
|
23
|
+
proxmox/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
|
+
proxmox/utils/helpers.py,sha256=8mASmzgVozEQ_RA_V9db4V66-gT87wQi8lGlF658jVk,383
|
|
25
|
+
proxmox/utils/logging.py,sha256=Grld09w3zdsByacCOl_u1RvB07_bF-QMdG7KhyK3Zl8,329
|
|
26
|
+
proxcli-0.1.0.dist-info/METADATA,sha256=5cn7rVXcnpVUIapx3ceOnZjQP7QgudVCXtLWdQYFExw,6415
|
|
27
|
+
proxcli-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
28
|
+
proxcli-0.1.0.dist-info/entry_points.txt,sha256=v5r_PXC8CApMzQCZqgX80CeGgQdIDZv32hvpBNL2mOc,50
|
|
29
|
+
proxcli-0.1.0.dist-info/RECORD,,
|
proxmox/__init__.py
ADDED
|
File without changes
|
proxmox/cli/__init__.py
ADDED
|
File without changes
|
proxmox/cli/auth.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""`proxmox auth` subcommand — manage credentials."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from proxmox.client.auth import AuthManager
|
|
9
|
+
from proxmox.client.client import ProxmoxClient
|
|
10
|
+
from proxmox.config.config import ConfigLoader
|
|
11
|
+
from proxmox.config.models import AuthMethod, Credentials
|
|
12
|
+
from proxmox.utils.logging import log_error, log_info
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def register_auth_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
16
|
+
"""Register the `proxmox auth` subcommand tree."""
|
|
17
|
+
auth_parser = subparsers.add_parser("auth", help="Manage Proxmox credentials")
|
|
18
|
+
auth_sub = auth_parser.add_subparsers(dest="action", title="actions", required=True)
|
|
19
|
+
|
|
20
|
+
# --- auth login ---
|
|
21
|
+
login = auth_sub.add_parser("login", help="Authenticate and save credentials")
|
|
22
|
+
login.add_argument("--url", required=True, help="Proxmox API URL")
|
|
23
|
+
login.add_argument("--username", required=True, help="Username (e.g. root@pam)")
|
|
24
|
+
login.add_argument("--password", help="Password")
|
|
25
|
+
login.add_argument("--password-stdin", action="store_true", help="Read password from stdin")
|
|
26
|
+
login.add_argument("--api-token", help="API token (user!tokenid=secret)")
|
|
27
|
+
login.add_argument(
|
|
28
|
+
"--insecure", action="store_true", help="Skip TLS verification (save preference)"
|
|
29
|
+
)
|
|
30
|
+
login.set_defaults(func=_auth_login)
|
|
31
|
+
|
|
32
|
+
# --- auth status ---
|
|
33
|
+
status = auth_sub.add_parser("status", help="Show current authentication status")
|
|
34
|
+
status.set_defaults(func=_auth_status)
|
|
35
|
+
|
|
36
|
+
# --- auth clear ---
|
|
37
|
+
clear = auth_sub.add_parser("clear", help="Remove saved credentials")
|
|
38
|
+
clear.set_defaults(func=_auth_clear)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _auth_login(args: argparse.Namespace, client: ProxmoxClient | None = None) -> dict | None:
|
|
42
|
+
"""Authenticate and persist credentials."""
|
|
43
|
+
loader = ConfigLoader()
|
|
44
|
+
url = args.url.rstrip("/")
|
|
45
|
+
|
|
46
|
+
# Determine password
|
|
47
|
+
password = args.password
|
|
48
|
+
if args.password_stdin:
|
|
49
|
+
password = sys.stdin.readline().rstrip("\n")
|
|
50
|
+
|
|
51
|
+
verify_tls = not args.insecure
|
|
52
|
+
|
|
53
|
+
# Validate credentials
|
|
54
|
+
if args.api_token:
|
|
55
|
+
parts = args.api_token.split("=", 1)
|
|
56
|
+
if len(parts) != 2:
|
|
57
|
+
log_error("Invalid --api-token format. Expected: user!tokenid=secret")
|
|
58
|
+
sys.exit(1)
|
|
59
|
+
user_token, secret = parts
|
|
60
|
+
if "!" in user_token:
|
|
61
|
+
user, token_id = user_token.split("!", 1)
|
|
62
|
+
else:
|
|
63
|
+
user = args.username
|
|
64
|
+
token_id = user_token
|
|
65
|
+
creds = Credentials(
|
|
66
|
+
url=url,
|
|
67
|
+
username=user,
|
|
68
|
+
auth_method=AuthMethod.API_TOKEN,
|
|
69
|
+
api_token_id=token_id,
|
|
70
|
+
api_token_secret=secret,
|
|
71
|
+
verify_tls=verify_tls,
|
|
72
|
+
)
|
|
73
|
+
# Quick validation: attempt a version check
|
|
74
|
+
auth_mgr = AuthManager()
|
|
75
|
+
auth_mgr.set_api_token(user, token_id, secret)
|
|
76
|
+
test_client = ProxmoxClient(url, auth_mgr, verify_tls=verify_tls, timeout=10)
|
|
77
|
+
try:
|
|
78
|
+
test_client.get("/version")
|
|
79
|
+
except Exception as exc:
|
|
80
|
+
log_error(f"Failed to validate token: {exc}")
|
|
81
|
+
sys.exit(1)
|
|
82
|
+
elif password:
|
|
83
|
+
creds = Credentials(
|
|
84
|
+
url=url,
|
|
85
|
+
username=args.username,
|
|
86
|
+
auth_method=AuthMethod.PASSWORD,
|
|
87
|
+
password=password,
|
|
88
|
+
verify_tls=verify_tls,
|
|
89
|
+
)
|
|
90
|
+
# Quick validation: attempt a version check
|
|
91
|
+
auth_mgr = AuthManager()
|
|
92
|
+
auth_mgr.authenticate_password(url, args.username, password, verify=verify_tls)
|
|
93
|
+
test_client = ProxmoxClient(url, auth_mgr, verify_tls=verify_tls, timeout=10)
|
|
94
|
+
try:
|
|
95
|
+
test_client.get("/version")
|
|
96
|
+
except Exception as exc:
|
|
97
|
+
log_error(f"Failed to validate credentials: {exc}")
|
|
98
|
+
sys.exit(1)
|
|
99
|
+
else:
|
|
100
|
+
log_error("Either --password, --password-stdin, or --api-token is required")
|
|
101
|
+
sys.exit(1)
|
|
102
|
+
|
|
103
|
+
path = loader.save(creds)
|
|
104
|
+
log_info(f"Credentials saved to {path}")
|
|
105
|
+
return {"status": "authenticated", "url": url, "user": creds.username}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _auth_status(args: argparse.Namespace, client: ProxmoxClient | None = None) -> dict:
|
|
109
|
+
"""Display current authentication status."""
|
|
110
|
+
loader = ConfigLoader()
|
|
111
|
+
creds = loader.load_or_none()
|
|
112
|
+
if creds is None:
|
|
113
|
+
return {"status": "not authenticated"}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
"status": "authenticated",
|
|
117
|
+
"url": creds.url,
|
|
118
|
+
"username": creds.username,
|
|
119
|
+
"auth_method": creds.auth_method.value,
|
|
120
|
+
"verify_tls": creds.verify_tls,
|
|
121
|
+
"config_file": str(loader._user_dir / "credentials.json"),
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _auth_clear(args: argparse.Namespace, client: ProxmoxClient | None = None) -> dict:
|
|
126
|
+
"""Remove saved credentials."""
|
|
127
|
+
loader = ConfigLoader()
|
|
128
|
+
loader.clear()
|
|
129
|
+
log_info("Credentials cleared.")
|
|
130
|
+
return {"status": "cleared"}
|
proxmox/cli/cluster.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""`proxmox cluster` subcommand — cluster management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from proxmox.client.client import ProxmoxClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def register_cluster_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
11
|
+
"""Register the `proxmox cluster` subcommand tree."""
|
|
12
|
+
cl_parser = subparsers.add_parser("cluster", help="Manage Proxmox cluster")
|
|
13
|
+
cl_sub = cl_parser.add_subparsers(dest="action", title="actions", required=True)
|
|
14
|
+
|
|
15
|
+
# --- cluster status ---
|
|
16
|
+
cl_status = cl_sub.add_parser("status", help="Show cluster status")
|
|
17
|
+
cl_status.set_defaults(func=_cl_status)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _cl_status(args: argparse.Namespace, client: ProxmoxClient) -> dict | list:
|
|
21
|
+
return client.get("/cluster/status")
|
proxmox/cli/container.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""`proxmox container` subcommand — LXC container management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from proxmox.client.client import ProxmoxClient
|
|
8
|
+
from proxmox.utils.helpers import vmid_type
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def register_container_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
12
|
+
"""Register the `proxmox container` subcommand tree."""
|
|
13
|
+
ct_parser = subparsers.add_parser("container", help="Manage LXC containers")
|
|
14
|
+
ct_sub = ct_parser.add_subparsers(dest="action", title="actions", required=True)
|
|
15
|
+
|
|
16
|
+
# --- container list ---
|
|
17
|
+
ct_list = ct_sub.add_parser("list", help="List containers")
|
|
18
|
+
ct_list.add_argument("--node", help="Filter by node name")
|
|
19
|
+
ct_list.set_defaults(func=_ct_list)
|
|
20
|
+
|
|
21
|
+
# --- container show ---
|
|
22
|
+
ct_show = ct_sub.add_parser("show", help="Show container details")
|
|
23
|
+
ct_show.add_argument("vmid", type=vmid_type, help="Container ID")
|
|
24
|
+
ct_show.add_argument("--node", help="Node name (auto-detected if omitted)")
|
|
25
|
+
ct_show.set_defaults(func=_ct_show)
|
|
26
|
+
|
|
27
|
+
# --- container create ---
|
|
28
|
+
ct_create = ct_sub.add_parser("create", help="Create a new container")
|
|
29
|
+
ct_create.add_argument("--node", required=True, help="Target node")
|
|
30
|
+
ct_create.add_argument("--vmid", type=vmid_type, required=True, help="Container ID")
|
|
31
|
+
ct_create.add_argument("--ostemplate", required=True, help="OS template")
|
|
32
|
+
ct_create.add_argument("--storage", default=None, help="Storage for the container")
|
|
33
|
+
ct_create.add_argument("--memory", type=int, default=512, help="Memory in MB")
|
|
34
|
+
ct_create.add_argument("--cores", type=int, default=1, help="CPU cores")
|
|
35
|
+
ct_create.add_argument("--net", default=None, help="Network config (e.g. name=eth0,bridge=vmbr0,ip=dhcp)")
|
|
36
|
+
ct_create.add_argument("--password", default=None, help="Root password")
|
|
37
|
+
ct_create.set_defaults(func=_ct_create)
|
|
38
|
+
|
|
39
|
+
# --- container start ---
|
|
40
|
+
ct_start = ct_sub.add_parser("start", help="Start a container")
|
|
41
|
+
ct_start.add_argument("vmid", type=vmid_type, help="Container ID")
|
|
42
|
+
ct_start.add_argument("--node", help="Node name (auto-detected if omitted)")
|
|
43
|
+
ct_start.set_defaults(func=_ct_start)
|
|
44
|
+
|
|
45
|
+
# --- container stop ---
|
|
46
|
+
ct_stop = ct_sub.add_parser("stop", help="Stop a container")
|
|
47
|
+
ct_stop.add_argument("vmid", type=vmid_type, help="Container ID")
|
|
48
|
+
ct_stop.add_argument("--node", help="Node name (auto-detected if omitted)")
|
|
49
|
+
ct_stop.set_defaults(func=_ct_stop)
|
|
50
|
+
|
|
51
|
+
# --- container delete ---
|
|
52
|
+
ct_delete = ct_sub.add_parser("delete", help="Delete a container")
|
|
53
|
+
ct_delete.add_argument("vmid", type=vmid_type, help="Container ID")
|
|
54
|
+
ct_delete.add_argument("--node", help="Node name (auto-detected if omitted)")
|
|
55
|
+
ct_delete.add_argument("--force", action="store_true", help="Force removal")
|
|
56
|
+
ct_delete.add_argument("--purge", action="store_true", help="Purge from all configurations")
|
|
57
|
+
ct_delete.set_defaults(func=_ct_delete)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# Helpers
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
def _resolve_ct_node(client: ProxmoxClient, node: str | None, vmid: int) -> str | None:
|
|
65
|
+
if node:
|
|
66
|
+
return node
|
|
67
|
+
try:
|
|
68
|
+
resources = client.get("/cluster/resources", params={"type": "vm"})
|
|
69
|
+
if isinstance(resources, list):
|
|
70
|
+
for r in resources:
|
|
71
|
+
if r.get("vmid") == vmid:
|
|
72
|
+
return r.get("node")
|
|
73
|
+
except Exception:
|
|
74
|
+
pass
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
# Command handlers
|
|
80
|
+
# ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
def _ct_list(args: argparse.Namespace, client: ProxmoxClient) -> dict | list:
|
|
83
|
+
if args.node:
|
|
84
|
+
return client.get(f"/nodes/{args.node}/lxc")
|
|
85
|
+
nodes = client.get("/nodes")
|
|
86
|
+
if isinstance(nodes, dict):
|
|
87
|
+
nodes = nodes.get("data", [])
|
|
88
|
+
cts: list[dict] = []
|
|
89
|
+
for n in (nodes if isinstance(nodes, list) else []):
|
|
90
|
+
node_name = n.get("node") if isinstance(n, dict) else n
|
|
91
|
+
try:
|
|
92
|
+
node_cts = client.get(f"/nodes/{node_name}/lxc")
|
|
93
|
+
if isinstance(node_cts, list):
|
|
94
|
+
for ct in node_cts:
|
|
95
|
+
if isinstance(ct, dict):
|
|
96
|
+
ct["_node"] = node_name
|
|
97
|
+
cts.append(ct)
|
|
98
|
+
elif isinstance(node_cts, dict):
|
|
99
|
+
for ct in node_cts.get("data", []):
|
|
100
|
+
if isinstance(ct, dict):
|
|
101
|
+
ct["_node"] = node_name
|
|
102
|
+
cts.append(ct)
|
|
103
|
+
except Exception:
|
|
104
|
+
pass
|
|
105
|
+
return cts
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _ct_show(args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
109
|
+
node = _resolve_ct_node(client, args.node, args.vmid)
|
|
110
|
+
if not node:
|
|
111
|
+
return {"error": f"Container {args.vmid} not found"}
|
|
112
|
+
result = client.get(f"/nodes/{node}/lxc/{args.vmid}/status/current")
|
|
113
|
+
if isinstance(result, dict):
|
|
114
|
+
result["_node"] = node
|
|
115
|
+
return result
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _ct_create(args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
119
|
+
data: dict = {
|
|
120
|
+
"vmid": args.vmid,
|
|
121
|
+
"ostemplate": args.ostemplate,
|
|
122
|
+
"memory": args.memory,
|
|
123
|
+
"cores": args.cores,
|
|
124
|
+
}
|
|
125
|
+
if args.storage:
|
|
126
|
+
data["storage"] = args.storage
|
|
127
|
+
if args.net:
|
|
128
|
+
data["net0"] = args.net
|
|
129
|
+
if args.password:
|
|
130
|
+
data["password"] = args.password
|
|
131
|
+
return client.post(f"/nodes/{args.node}/lxc", data=data)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _ct_start(args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
135
|
+
node = _resolve_ct_node(client, args.node, args.vmid)
|
|
136
|
+
if not node:
|
|
137
|
+
return {"error": f"Container {args.vmid} not found"}
|
|
138
|
+
return client.post(f"/nodes/{node}/lxc/{args.vmid}/status/start")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _ct_stop(args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
142
|
+
node = _resolve_ct_node(client, args.node, args.vmid)
|
|
143
|
+
if not node:
|
|
144
|
+
return {"error": f"Container {args.vmid} not found"}
|
|
145
|
+
return client.post(f"/nodes/{node}/lxc/{args.vmid}/status/stop")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _ct_delete(args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
149
|
+
node = _resolve_ct_node(client, args.node, args.vmid)
|
|
150
|
+
if not node:
|
|
151
|
+
return {"error": f"Container {args.vmid} not found"}
|
|
152
|
+
params: dict = {}
|
|
153
|
+
if args.force:
|
|
154
|
+
params["force"] = 1
|
|
155
|
+
if args.purge:
|
|
156
|
+
params["purge"] = 1
|
|
157
|
+
return client.delete(f"/nodes/{node}/lxc/{args.vmid}", params=params or None)
|