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.
proxmox/cli/main.py ADDED
@@ -0,0 +1,231 @@
1
+ """Root CLI parser and main entry point."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ import sys
8
+ from typing import Any
9
+
10
+ from proxmox.client.auth import AuthManager
11
+ from proxmox.client.client import ProxmoxClient
12
+ from proxmox.client.exceptions import ConfigError, ProxmoxAPIError, ProxmoxError
13
+ from proxmox.config.config import ConfigLoader
14
+ from proxmox.config.models import AuthMethod as AuthMethodModel
15
+ from proxmox.config.models import Credentials
16
+ from proxmox.output.formatter import format_output
17
+ from proxmox.utils.logging import log_error
18
+
19
+
20
+ def build_root_parser() -> argparse.ArgumentParser:
21
+ """Build the root argument parser with global flags and subcommands."""
22
+ parser = argparse.ArgumentParser(
23
+ prog="proxmox",
24
+ description="CLI tool to interact with Proxmox VE via the REST API",
25
+ formatter_class=argparse.RawDescriptionHelpFormatter,
26
+ )
27
+
28
+ # Global flags
29
+ parser.add_argument("--url", help="Proxmox API URL (e.g. https://pve:8006)")
30
+ parser.add_argument("--username", help="Username (e.g. root@pam)")
31
+ parser.add_argument("--password", help="Password (for password auth)")
32
+ parser.add_argument("--password-stdin", action="store_true", help="Read password from stdin")
33
+ parser.add_argument("--api-token", help="API token in format: user!tokenid=secret")
34
+ parser.add_argument(
35
+ "--output",
36
+ choices=["json", "table", "yaml"],
37
+ default="json",
38
+ help="Output format (default: json)",
39
+ )
40
+ parser.add_argument(
41
+ "--dry-run", action="store_true", help="Print the API request without executing it"
42
+ )
43
+ parser.add_argument(
44
+ "--insecure", action="store_true", help="Skip TLS certificate verification"
45
+ )
46
+ parser.add_argument(
47
+ "--timeout", type=int, default=30, help="Request timeout in seconds (default: 30)"
48
+ )
49
+ parser.add_argument("--verbose", action="store_true", help="Enable debug output to stderr")
50
+ parser.add_argument(
51
+ "--version", action="version", version="proxmox 0.1.0"
52
+ )
53
+
54
+ subparsers = parser.add_subparsers(dest="resource", title="resources", required=False)
55
+
56
+ # Import and register subcommands
57
+ from proxmox.cli.auth import register_auth_parser
58
+ from proxmox.cli.cluster import register_cluster_parser
59
+ from proxmox.cli.container import register_container_parser
60
+ from proxmox.cli.node import register_node_parser
61
+ from proxmox.cli.storage import register_storage_parser
62
+ from proxmox.cli.tasks import register_task_parser
63
+ from proxmox.cli.vm import register_vm_parser
64
+
65
+ register_auth_parser(subparsers)
66
+ register_vm_parser(subparsers)
67
+ register_node_parser(subparsers)
68
+ register_container_parser(subparsers)
69
+ register_storage_parser(subparsers)
70
+ register_cluster_parser(subparsers)
71
+ register_task_parser(subparsers)
72
+
73
+ return parser
74
+
75
+
76
+ def _merge_config(args: argparse.Namespace) -> tuple[Credentials | None, dict[str, Any]]:
77
+ """Merge config file, env vars, and CLI flags.
78
+
79
+ Returns (credentials, overrides) where overrides has keys: url, username, password,
80
+ api_token_id, api_token_secret, verify_tls. CLI flags win over env vars and config file.
81
+ """
82
+ loader = ConfigLoader()
83
+ creds = loader.load_or_none()
84
+
85
+ overrides: dict[str, Any] = {
86
+ "url": None,
87
+ "username": None,
88
+ "password": None,
89
+ "api_token_id": None,
90
+ "api_token_secret": None,
91
+ }
92
+
93
+ # Apply config file values first
94
+ if creds:
95
+ overrides["url"] = creds.url
96
+ overrides["username"] = creds.username
97
+ overrides["verify_tls"] = creds.verify_tls
98
+ if creds.auth_method == AuthMethodModel.PASSWORD:
99
+ overrides["password"] = creds.password
100
+ elif creds.auth_method == AuthMethodModel.API_TOKEN:
101
+ overrides["api_token_id"] = creds.api_token_id
102
+ overrides["api_token_secret"] = creds.api_token_secret
103
+
104
+ # Env var override
105
+ env_password = os.environ.get("PROXMOX_PASSWORD")
106
+ if env_password:
107
+ overrides["password"] = env_password
108
+
109
+ # CLI flag overrides (highest priority)
110
+ if args.url:
111
+ overrides["url"] = args.url
112
+ if args.username:
113
+ overrides["username"] = args.username
114
+ if args.password:
115
+ overrides["password"] = args.password
116
+ if args.password_stdin:
117
+ overrides["password"] = sys.stdin.readline().rstrip("\n")
118
+ if args.api_token:
119
+ parts = args.api_token.split("=", 1)
120
+ if len(parts) == 2:
121
+ user_token, secret = parts
122
+ if "!" in user_token:
123
+ user, token_id = user_token.split("!", 1)
124
+ overrides["username"] = user
125
+ overrides["api_token_id"] = token_id
126
+ overrides["api_token_secret"] = secret
127
+ else:
128
+ overrides["username"] = user_token
129
+ overrides["api_token_secret"] = secret
130
+ else:
131
+ log_error("Invalid --api-token format. Expected: user!tokenid=secret")
132
+ if args.insecure:
133
+ overrides["verify_tls"] = False
134
+ elif "verify_tls" not in overrides:
135
+ overrides["verify_tls"] = True
136
+
137
+ return creds, overrides
138
+
139
+
140
+ def _build_client(overrides: dict[str, Any], args: argparse.Namespace) -> ProxmoxClient:
141
+ """Build a ProxmoxClient from merged config overrides."""
142
+ if not overrides["url"]:
143
+ raise ConfigError("No Proxmox URL configured. Use --url or run 'proxmox auth login'.")
144
+
145
+ auth_mgr = AuthManager()
146
+
147
+ # Don't authenticate if dry-run (no actual API calls will be made)
148
+ if not args.dry_run:
149
+ if overrides["api_token_id"] and overrides["api_token_secret"]:
150
+ auth_mgr.set_api_token(
151
+ overrides["username"] or "root@pam",
152
+ overrides["api_token_id"],
153
+ overrides["api_token_secret"],
154
+ )
155
+ elif overrides["password"]:
156
+ auth_mgr.authenticate_password(
157
+ overrides["url"],
158
+ overrides["username"] or "root@pam",
159
+ overrides["password"],
160
+ verify=overrides["verify_tls"],
161
+ )
162
+
163
+ client = ProxmoxClient(
164
+ base_url=overrides["url"],
165
+ auth_manager=auth_mgr,
166
+ timeout=args.timeout,
167
+ verify_tls=overrides["verify_tls"],
168
+ dry_run=args.dry_run,
169
+ verbose=args.verbose,
170
+ )
171
+
172
+ # Store for lazy re-auth
173
+ if overrides["password"]:
174
+ client.set_credentials(overrides["username"] or "root@pam", overrides["password"])
175
+
176
+ return client
177
+
178
+
179
+ def main(argv: list[str] | None = None) -> None:
180
+ """Main entry point."""
181
+ parser = build_root_parser()
182
+ args = parser.parse_args(argv)
183
+
184
+ # --help or no subcommand: just show help
185
+ if args.resource is None:
186
+ parser.print_help()
187
+ return
188
+
189
+ try:
190
+ # auth status and clear don't need a client
191
+ if args.resource == "auth" and args.action in ("status", "clear"):
192
+ if hasattr(args, "func"):
193
+ result = args.func(args, None)
194
+ if result is not None:
195
+ output = format_output(result, args.output)
196
+ print(output)
197
+ return
198
+
199
+ _, overrides = _merge_config(args)
200
+ client = _build_client(overrides, args)
201
+
202
+ # Each subcommand sets args.func during registration
203
+ if hasattr(args, "func"):
204
+ result = args.func(args, client)
205
+ if result is not None:
206
+ output = format_output(result, args.output)
207
+ print(output)
208
+ else:
209
+ # Subcommand registered but no func set (cli module not implemented yet)
210
+ log_error(f"Command 'proxmox {args.resource}' is not yet implemented.")
211
+
212
+ except ConfigError as exc:
213
+ log_error(str(exc))
214
+ sys.exit(1)
215
+ except ProxmoxAPIError as exc:
216
+ if args.output == "json":
217
+ print(
218
+ format_output(
219
+ {"error": exc.message, "status_code": exc.status_code}, "json"
220
+ )
221
+ )
222
+ else:
223
+ log_error(str(exc))
224
+ sys.exit(exc.status_code if exc.status_code > 0 else 1)
225
+ except ProxmoxError as exc:
226
+ log_error(str(exc))
227
+ sys.exit(1)
228
+
229
+
230
+ if __name__ == "__main__":
231
+ main()
proxmox/cli/node.py ADDED
@@ -0,0 +1,55 @@
1
+ """`proxmox node` subcommand — node management."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+
7
+ from proxmox.client.client import ProxmoxClient
8
+
9
+
10
+ def register_node_parser(subparsers: argparse._SubParsersAction) -> None:
11
+ """Register the `proxmox node` subcommand tree."""
12
+ node_parser = subparsers.add_parser("node", help="Manage Proxmox nodes")
13
+ node_sub = node_parser.add_subparsers(dest="action", title="actions", required=True)
14
+
15
+ # --- node list ---
16
+ node_list = node_sub.add_parser("list", help="List all nodes")
17
+ node_list.set_defaults(func=_node_list)
18
+
19
+ # --- node show ---
20
+ node_show = node_sub.add_parser("show", help="Show node details")
21
+ node_show.add_argument("node_name", help="Node name")
22
+ node_show.set_defaults(func=_node_show)
23
+
24
+ # --- node status ---
25
+ node_status = node_sub.add_parser("status", help="Show node status")
26
+ node_status.add_argument("node_name", nargs="?", help="Node name (omit for all nodes)")
27
+ node_status.set_defaults(func=_node_status)
28
+
29
+
30
+ def _node_list(args: argparse.Namespace, client: ProxmoxClient) -> dict | list:
31
+ return client.get("/nodes")
32
+
33
+
34
+ def _node_show(args: argparse.Namespace, client: ProxmoxClient) -> dict:
35
+ return client.get(f"/nodes/{args.node_name}/status")
36
+
37
+
38
+ def _node_status(args: argparse.Namespace, client: ProxmoxClient) -> dict | list:
39
+ if args.node_name:
40
+ return client.get(f"/nodes/{args.node_name}/status")
41
+ # Return all nodes' statuses
42
+ nodes = client.get("/nodes")
43
+ if isinstance(nodes, list):
44
+ result = []
45
+ for n in nodes:
46
+ node_name = n.get("node") if isinstance(n, dict) else n
47
+ try:
48
+ status = client.get(f"/nodes/{node_name}/status")
49
+ if isinstance(status, dict):
50
+ status["node"] = node_name
51
+ result.append(status)
52
+ except Exception:
53
+ result.append({"node": node_name, "status": "error"})
54
+ return result
55
+ return nodes
proxmox/cli/storage.py ADDED
@@ -0,0 +1,63 @@
1
+ """`proxmox storage` subcommand — storage management."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+
7
+ from proxmox.client.client import ProxmoxClient
8
+
9
+
10
+ def register_storage_parser(subparsers: argparse._SubParsersAction) -> None:
11
+ """Register the `proxmox storage` subcommand tree."""
12
+ st_parser = subparsers.add_parser("storage", help="Manage Proxmox storage")
13
+ st_sub = st_parser.add_subparsers(dest="action", title="actions", required=True)
14
+
15
+ # --- storage list ---
16
+ st_list = st_sub.add_parser("list", help="List all storages")
17
+ st_list.add_argument("--node", help="Filter by node name")
18
+ st_list.set_defaults(func=_st_list)
19
+
20
+ # --- storage show ---
21
+ st_show = st_sub.add_parser("show", help="Show storage details")
22
+ st_show.add_argument("storage_name", help="Storage name")
23
+ st_show.set_defaults(func=_st_show)
24
+
25
+ # --- storage content ---
26
+ st_content = st_sub.add_parser("content", help="List storage contents")
27
+ st_content.add_argument("storage_name", help="Storage name")
28
+ st_content.add_argument("--node", help="Node name (auto-detected if omitted)")
29
+ st_content.set_defaults(func=_st_content)
30
+
31
+
32
+ def _st_list(args: argparse.Namespace, client: ProxmoxClient) -> dict | list:
33
+ if args.node:
34
+ return client.get(f"/nodes/{args.node}/storage")
35
+ return client.get("/storage")
36
+
37
+
38
+ def _st_show(args: argparse.Namespace, client: ProxmoxClient) -> dict:
39
+ return client.get(f"/storage/{args.storage_name}/status")
40
+
41
+
42
+ def _resolve_storage_node(client: ProxmoxClient, storage: str) -> str | None:
43
+ """Find which node a storage belongs to."""
44
+ try:
45
+ storages = client.get("/storage")
46
+ if isinstance(storages, list):
47
+ for s in storages:
48
+ if isinstance(s, dict) and s.get("storage") == storage:
49
+ return s.get("node")
50
+ elif isinstance(storages, dict):
51
+ for s in storages.get("data", []):
52
+ if isinstance(s, dict) and s.get("storage") == storage:
53
+ return s.get("node")
54
+ except Exception:
55
+ pass
56
+ return None
57
+
58
+
59
+ def _st_content(args: argparse.Namespace, client: ProxmoxClient) -> dict | list:
60
+ node = args.node or _resolve_storage_node(client, args.storage_name)
61
+ if not node:
62
+ return {"error": f"Could not determine node for storage '{args.storage_name}'"}
63
+ return client.get(f"/nodes/{node}/storage/{args.storage_name}/content")
proxmox/cli/tasks.py ADDED
@@ -0,0 +1,65 @@
1
+ """`proxmox task` subcommand — task management."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+
7
+ from proxmox.client.client import ProxmoxClient
8
+
9
+
10
+ def register_task_parser(subparsers: argparse._SubParsersAction) -> None:
11
+ """Register the `proxmox task` subcommand tree."""
12
+ task_parser = subparsers.add_parser("task", help="Manage Proxmox tasks/logs")
13
+ task_sub = task_parser.add_subparsers(dest="action", title="actions", required=True)
14
+
15
+ # --- task list ---
16
+ task_list = task_sub.add_parser("list", help="List recent tasks")
17
+ task_list.add_argument("--node", help="Filter by node name")
18
+ task_list.set_defaults(func=_task_list)
19
+
20
+ # --- task show ---
21
+ task_show = task_sub.add_parser("show", help="Show task details")
22
+ task_show.add_argument("upid", help="Task UPID")
23
+ task_show.set_defaults(func=_task_show)
24
+
25
+
26
+ def _extract_node_from_upid(upid: str) -> str | None:
27
+ """Parse node name from a Proxmox UPID string: UPID:{node}:..."""
28
+ parts = upid.split(":")
29
+ if len(parts) >= 2:
30
+ return parts[1]
31
+ return None
32
+
33
+
34
+ def _task_list(args: argparse.Namespace, client: ProxmoxClient) -> dict | list:
35
+ if args.node:
36
+ return client.get(f"/nodes/{args.node}/tasks")
37
+ # Iterate all nodes
38
+ nodes = client.get("/nodes")
39
+ if isinstance(nodes, dict):
40
+ nodes = nodes.get("data", [])
41
+ tasks: list[dict] = []
42
+ for n in (nodes if isinstance(nodes, list) else []):
43
+ node_name = n.get("node") if isinstance(n, dict) else n
44
+ try:
45
+ node_tasks = client.get(f"/nodes/{node_name}/tasks")
46
+ if isinstance(node_tasks, list):
47
+ for t in node_tasks:
48
+ if isinstance(t, dict):
49
+ t["_node"] = node_name
50
+ tasks.append(t)
51
+ elif isinstance(node_tasks, dict):
52
+ for t in node_tasks.get("data", []):
53
+ if isinstance(t, dict):
54
+ t["_node"] = node_name
55
+ tasks.append(t)
56
+ except Exception:
57
+ pass
58
+ return tasks
59
+
60
+
61
+ def _task_show(args: argparse.Namespace, client: ProxmoxClient) -> dict:
62
+ node = _extract_node_from_upid(args.upid)
63
+ if not node:
64
+ return {"error": f"Could not extract node from UPID: {args.upid}"}
65
+ return client.get(f"/nodes/{node}/tasks/{args.upid}/status")
proxmox/cli/vm.py ADDED
@@ -0,0 +1,211 @@
1
+ """`proxmox vm` subcommand — QEMU virtual machine 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_vm_parser(subparsers: argparse._SubParsersAction) -> None:
12
+ """Register the `proxmox vm` subcommand tree."""
13
+ vm_parser = subparsers.add_parser("vm", help="Manage QEMU virtual machines")
14
+ vm_sub = vm_parser.add_subparsers(dest="action", title="actions", required=True)
15
+
16
+ # --- vm list ---
17
+ vm_list = vm_sub.add_parser("list", help="List virtual machines")
18
+ vm_list.add_argument("--node", help="Filter by node name")
19
+ vm_list.set_defaults(func=_vm_list)
20
+
21
+ # --- vm show ---
22
+ vm_show = vm_sub.add_parser("show", help="Show VM details")
23
+ vm_show.add_argument("vmid", type=vmid_type, help="VM ID")
24
+ vm_show.add_argument("--node", help="Node name (auto-detected if omitted)")
25
+ vm_show.set_defaults(func=_vm_show)
26
+
27
+ # --- vm create ---
28
+ vm_create = vm_sub.add_parser("create", help="Create a new VM")
29
+ vm_create.add_argument("--node", required=True, help="Target node")
30
+ vm_create.add_argument("--vmid", type=vmid_type, required=True, help="VM ID")
31
+ vm_create.add_argument("--memory", type=int, required=True, help="Memory in MB")
32
+ vm_create.add_argument("--cores", type=int, default=1, help="CPU cores (default: 1)")
33
+ vm_create.add_argument("--net", default=None, help="Network config (e.g. model=virtio,bridge=vmbr0)")
34
+ vm_create.add_argument("--storage", default=None, help="Storage for the VM disk")
35
+ vm_create.add_argument("--ostemplate", default=None, help="OS template/ISO")
36
+ vm_create.add_argument("--name", default=None, help="VM name")
37
+ vm_create.set_defaults(func=_vm_create)
38
+
39
+ # --- vm start ---
40
+ vm_start = vm_sub.add_parser("start", help="Start a VM")
41
+ vm_start.add_argument("vmid", type=vmid_type, help="VM ID")
42
+ vm_start.add_argument("--node", help="Node name (auto-detected if omitted)")
43
+ vm_start.set_defaults(func=_vm_start)
44
+
45
+ # --- vm stop ---
46
+ vm_stop = vm_sub.add_parser("stop", help="Stop a VM")
47
+ vm_stop.add_argument("vmid", type=vmid_type, help="VM ID")
48
+ vm_stop.add_argument("--node", help="Node name (auto-detected if omitted)")
49
+ vm_stop.set_defaults(func=_vm_stop)
50
+
51
+ # --- vm reboot ---
52
+ vm_reboot = vm_sub.add_parser("reboot", help="Reboot a VM")
53
+ vm_reboot.add_argument("vmid", type=vmid_type, help="VM ID")
54
+ vm_reboot.add_argument("--node", help="Node name (auto-detected if omitted)")
55
+ vm_reboot.set_defaults(func=_vm_reboot)
56
+
57
+ # --- vm suspend ---
58
+ vm_suspend = vm_sub.add_parser("suspend", help="Suspend a VM")
59
+ vm_suspend.add_argument("vmid", type=vmid_type, help="VM ID")
60
+ vm_suspend.add_argument("--node", help="Node name (auto-detected if omitted)")
61
+ vm_suspend.set_defaults(func=_vm_suspend)
62
+
63
+ # --- vm resume ---
64
+ vm_resume = vm_sub.add_parser("resume", help="Resume a VM")
65
+ vm_resume.add_argument("vmid", type=vmid_type, help="VM ID")
66
+ vm_resume.add_argument("--node", help="Node name (auto-detected if omitted)")
67
+ vm_resume.set_defaults(func=_vm_resume)
68
+
69
+ # --- vm delete ---
70
+ vm_delete = vm_sub.add_parser("delete", help="Delete a VM")
71
+ vm_delete.add_argument("vmid", type=vmid_type, help="VM ID")
72
+ vm_delete.add_argument("--node", help="Node name (auto-detected if omitted)")
73
+ vm_delete.add_argument("--force", action="store_true", help="Force removal")
74
+ vm_delete.add_argument("--purge", action="store_true", help="Purge VM from all configurations")
75
+ vm_delete.set_defaults(func=_vm_delete)
76
+
77
+
78
+ # ---------------------------------------------------------------------------
79
+ # Helpers
80
+ # ---------------------------------------------------------------------------
81
+
82
+ def _resolve_node(client: ProxmoxClient, node: str | None, vmid: int) -> str | None:
83
+ """Resolve which node hosts a given VMID, unless node is already provided."""
84
+ if node:
85
+ return node
86
+ # Try cluster resources lookup
87
+ try:
88
+ resources = client.get("/cluster/resources", params={"type": "vm"})
89
+ if isinstance(resources, list):
90
+ for r in resources:
91
+ if r.get("vmid") == vmid:
92
+ return r.get("node")
93
+ except Exception:
94
+ pass
95
+ return None
96
+
97
+
98
+ # ---------------------------------------------------------------------------
99
+ # Command handlers
100
+ # ---------------------------------------------------------------------------
101
+
102
+ def _vm_list(args: argparse.Namespace, client: ProxmoxClient) -> dict | list:
103
+ if args.node:
104
+ result = client.get(f"/nodes/{args.node}/qemu")
105
+ return result if isinstance(result, list) else result.get("data", result)
106
+ # All nodes: iterate
107
+ nodes = client.get("/nodes")
108
+ if isinstance(nodes, dict):
109
+ nodes = nodes.get("data", [])
110
+ vms: list[dict] = []
111
+ for n in (nodes if isinstance(nodes, list) else []):
112
+ node_name = n.get("node") if isinstance(n, dict) else n
113
+ try:
114
+ node_vms = client.get(f"/nodes/{node_name}/qemu")
115
+ if isinstance(node_vms, list):
116
+ for vm in node_vms:
117
+ if isinstance(vm, dict):
118
+ vm["_node"] = node_name
119
+ vms.append(vm)
120
+ elif isinstance(node_vms, dict):
121
+ for vm in node_vms.get("data", []):
122
+ if isinstance(vm, dict):
123
+ vm["_node"] = node_name
124
+ vms.append(vm)
125
+ except Exception:
126
+ pass
127
+ return vms
128
+
129
+
130
+ def _vm_show(args: argparse.Namespace, client: ProxmoxClient) -> dict:
131
+ node = _resolve_node(client, args.node, args.vmid)
132
+ if not node:
133
+ return {"error": f"VM {args.vmid} not found on any node"}
134
+ resources = client.get(f"/nodes/{node}/qemu/{args.vmid}/status/current")
135
+ if isinstance(resources, dict):
136
+ resources["_node"] = node
137
+ else:
138
+ resources = {"data": resources, "_node": node}
139
+ return resources
140
+
141
+
142
+ def _vm_create(args: argparse.Namespace, client: ProxmoxClient) -> dict:
143
+ data: dict = {
144
+ "vmid": args.vmid,
145
+ "memory": args.memory,
146
+ "cores": args.cores,
147
+ }
148
+ if args.net:
149
+ data["net0"] = args.net
150
+ if args.name:
151
+ data["name"] = args.name
152
+ if args.ostemplate:
153
+ data["ostemplate"] = args.ostemplate
154
+ if args.storage:
155
+ data["storage"] = args.storage
156
+
157
+ result = client.post(f"/nodes/{args.node}/qemu", data=data)
158
+ return result if isinstance(result, dict) else {"data": result}
159
+
160
+
161
+ def _vm_start(args: argparse.Namespace, client: ProxmoxClient) -> dict:
162
+ node = _resolve_node(client, args.node, args.vmid)
163
+ if not node:
164
+ return {"error": f"VM {args.vmid} not found"}
165
+ result = client.post(f"/nodes/{node}/qemu/{args.vmid}/status/start")
166
+ return result if isinstance(result, dict) else {"data": result}
167
+
168
+
169
+ def _vm_stop(args: argparse.Namespace, client: ProxmoxClient) -> dict:
170
+ node = _resolve_node(client, args.node, args.vmid)
171
+ if not node:
172
+ return {"error": f"VM {args.vmid} not found"}
173
+ result = client.post(f"/nodes/{node}/qemu/{args.vmid}/status/stop")
174
+ return result if isinstance(result, dict) else {"data": result}
175
+
176
+
177
+ def _vm_reboot(args: argparse.Namespace, client: ProxmoxClient) -> dict:
178
+ node = _resolve_node(client, args.node, args.vmid)
179
+ if not node:
180
+ return {"error": f"VM {args.vmid} not found"}
181
+ result = client.post(f"/nodes/{node}/qemu/{args.vmid}/status/reboot")
182
+ return result if isinstance(result, dict) else {"data": result}
183
+
184
+
185
+ def _vm_suspend(args: argparse.Namespace, client: ProxmoxClient) -> dict:
186
+ node = _resolve_node(client, args.node, args.vmid)
187
+ if not node:
188
+ return {"error": f"VM {args.vmid} not found"}
189
+ result = client.post(f"/nodes/{node}/qemu/{args.vmid}/status/suspend")
190
+ return result if isinstance(result, dict) else {"data": result}
191
+
192
+
193
+ def _vm_resume(args: argparse.Namespace, client: ProxmoxClient) -> dict:
194
+ node = _resolve_node(client, args.node, args.vmid)
195
+ if not node:
196
+ return {"error": f"VM {args.vmid} not found"}
197
+ result = client.post(f"/nodes/{node}/qemu/{args.vmid}/status/resume")
198
+ return result if isinstance(result, dict) else {"data": result}
199
+
200
+
201
+ def _vm_delete(args: argparse.Namespace, client: ProxmoxClient) -> dict:
202
+ node = _resolve_node(client, args.node, args.vmid)
203
+ if not node:
204
+ return {"error": f"VM {args.vmid} not found"}
205
+ params: dict = {}
206
+ if args.force:
207
+ params["force"] = 1
208
+ if args.purge:
209
+ params["purge"] = 1
210
+ result = client.delete(f"/nodes/{node}/qemu/{args.vmid}", params=params or None)
211
+ return result if isinstance(result, dict) else {"data": result}
File without changes