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.
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ proxmox = proxmox.cli.main:main
proxmox/__init__.py ADDED
File without changes
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")
@@ -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)