proxcli 0.3.0__tar.gz → 0.4.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.
Files changed (51) hide show
  1. {proxcli-0.3.0 → proxcli-0.4.0}/.github/workflows/ci.yml +4 -4
  2. {proxcli-0.3.0 → proxcli-0.4.0}/CHANGELOG.md +7 -0
  3. {proxcli-0.3.0 → proxcli-0.4.0}/PKG-INFO +1 -1
  4. proxcli-0.4.0/proxmox/cli/container.py +316 -0
  5. {proxcli-0.3.0 → proxcli-0.4.0}/pyproject.toml +1 -1
  6. {proxcli-0.3.0 → proxcli-0.4.0}/uv.lock +1 -1
  7. proxcli-0.3.0/proxmox/cli/container.py +0 -157
  8. {proxcli-0.3.0 → proxcli-0.4.0}/.env.example +0 -0
  9. {proxcli-0.3.0 → proxcli-0.4.0}/.gitignore +0 -0
  10. {proxcli-0.3.0 → proxcli-0.4.0}/.python-version +0 -0
  11. {proxcli-0.3.0 → proxcli-0.4.0}/AGENTS.md +0 -0
  12. {proxcli-0.3.0 → proxcli-0.4.0}/PLAN.md +0 -0
  13. {proxcli-0.3.0 → proxcli-0.4.0}/PROJECT.md +0 -0
  14. {proxcli-0.3.0 → proxcli-0.4.0}/PROMPT.md +0 -0
  15. {proxcli-0.3.0 → proxcli-0.4.0}/README.md +0 -0
  16. {proxcli-0.3.0 → proxcli-0.4.0}/TODO.md +0 -0
  17. {proxcli-0.3.0 → proxcli-0.4.0}/proxmox/__init__.py +0 -0
  18. {proxcli-0.3.0 → proxcli-0.4.0}/proxmox/cli/__init__.py +0 -0
  19. {proxcli-0.3.0 → proxcli-0.4.0}/proxmox/cli/auth.py +0 -0
  20. {proxcli-0.3.0 → proxcli-0.4.0}/proxmox/cli/cluster.py +0 -0
  21. {proxcli-0.3.0 → proxcli-0.4.0}/proxmox/cli/firewall_helpers.py +0 -0
  22. {proxcli-0.3.0 → proxcli-0.4.0}/proxmox/cli/main.py +0 -0
  23. {proxcli-0.3.0 → proxcli-0.4.0}/proxmox/cli/node.py +0 -0
  24. {proxcli-0.3.0 → proxcli-0.4.0}/proxmox/cli/storage.py +0 -0
  25. {proxcli-0.3.0 → proxcli-0.4.0}/proxmox/cli/tasks.py +0 -0
  26. {proxcli-0.3.0 → proxcli-0.4.0}/proxmox/cli/vm.py +0 -0
  27. {proxcli-0.3.0 → proxcli-0.4.0}/proxmox/client/__init__.py +0 -0
  28. {proxcli-0.3.0 → proxcli-0.4.0}/proxmox/client/auth.py +0 -0
  29. {proxcli-0.3.0 → proxcli-0.4.0}/proxmox/client/client.py +0 -0
  30. {proxcli-0.3.0 → proxcli-0.4.0}/proxmox/client/exceptions.py +0 -0
  31. {proxcli-0.3.0 → proxcli-0.4.0}/proxmox/config/__init__.py +0 -0
  32. {proxcli-0.3.0 → proxcli-0.4.0}/proxmox/config/config.py +0 -0
  33. {proxcli-0.3.0 → proxcli-0.4.0}/proxmox/config/models.py +0 -0
  34. {proxcli-0.3.0 → proxcli-0.4.0}/proxmox/output/__init__.py +0 -0
  35. {proxcli-0.3.0 → proxcli-0.4.0}/proxmox/output/formatter.py +0 -0
  36. {proxcli-0.3.0 → proxcli-0.4.0}/proxmox/output/json_fmt.py +0 -0
  37. {proxcli-0.3.0 → proxcli-0.4.0}/proxmox/output/table_fmt.py +0 -0
  38. {proxcli-0.3.0 → proxcli-0.4.0}/proxmox/output/yaml_fmt.py +0 -0
  39. {proxcli-0.3.0 → proxcli-0.4.0}/proxmox/utils/__init__.py +0 -0
  40. {proxcli-0.3.0 → proxcli-0.4.0}/proxmox/utils/helpers.py +0 -0
  41. {proxcli-0.3.0 → proxcli-0.4.0}/proxmox/utils/logging.py +0 -0
  42. {proxcli-0.3.0 → proxcli-0.4.0}/tests/__init__.py +0 -0
  43. {proxcli-0.3.0 → proxcli-0.4.0}/tests/conftest.py +0 -0
  44. {proxcli-0.3.0 → proxcli-0.4.0}/tests/test_auth.py +0 -0
  45. {proxcli-0.3.0 → proxcli-0.4.0}/tests/test_cli/__init__.py +0 -0
  46. {proxcli-0.3.0 → proxcli-0.4.0}/tests/test_cli/test_main.py +0 -0
  47. {proxcli-0.3.0 → proxcli-0.4.0}/tests/test_client.py +0 -0
  48. {proxcli-0.3.0 → proxcli-0.4.0}/tests/test_config.py +0 -0
  49. {proxcli-0.3.0 → proxcli-0.4.0}/tests/test_integration/__init__.py +0 -0
  50. {proxcli-0.3.0 → proxcli-0.4.0}/tests/test_output/__init__.py +0 -0
  51. {proxcli-0.3.0 → proxcli-0.4.0}/tests/test_output/test_formatter.py +0 -0
@@ -12,7 +12,7 @@ jobs:
12
12
  steps:
13
13
  - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
14
14
 
15
- - uses: astral-sh/setup-uv@4db96194c378173c656ce18a155ffc14a9fc4355 # v5.2.2
15
+ - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
16
16
  with:
17
17
  enable-cache: true
18
18
 
@@ -27,7 +27,7 @@ jobs:
27
27
  steps:
28
28
  - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
29
29
 
30
- - uses: astral-sh/setup-uv@4db96194c378173c656ce18a155ffc14a9fc4355 # v5.2.2
30
+ - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
31
31
  with:
32
32
  enable-cache: true
33
33
 
@@ -41,7 +41,7 @@ jobs:
41
41
  steps:
42
42
  - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
43
43
 
44
- - uses: astral-sh/setup-uv@4db96194c378173c656ce18a155ffc14a9fc4355 # v5.2.2
44
+ - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
45
45
  with:
46
46
  enable-cache: true
47
47
 
@@ -61,7 +61,7 @@ jobs:
61
61
  steps:
62
62
  - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
63
63
 
64
- - uses: astral-sh/setup-uv@4db96194c378173c656ce18a155ffc14a9fc4355 # v5.2.2
64
+ - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
65
65
  with:
66
66
  enable-cache: true
67
67
 
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.0] - 2026-06-20
11
+
12
+ ### Added
13
+ - Container firewall management: options, enable/disable, policy, rules (CRUD), refs.
14
+ Uses `/nodes/{node}/lxc/{vmid}/firewall/*` endpoints.
15
+
10
16
  ## [0.3.0] - 2026-06-20
11
17
 
12
18
  ### Added
@@ -55,6 +61,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
55
61
  - CSRF ticket auto-refresh on 401.
56
62
  - AI-agent-friendly: default JSON output, strict exit codes, `--dry-run` mode.
57
63
 
64
+ [0.4.0]: https://github.com/xezpeleta/proxcli/releases/tag/v0.4.0
58
65
  [0.3.0]: https://github.com/xezpeleta/proxcli/releases/tag/v0.3.0
59
66
  [0.2.1]: https://github.com/xezpeleta/proxcli/releases/tag/v0.2.1
60
67
  [0.2.0]: https://github.com/xezpeleta/proxcli/releases/tag/v0.2.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proxcli
3
- Version: 0.3.0
3
+ Version: 0.4.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
@@ -0,0 +1,316 @@
1
+ """`proxmox container` subcommand — LXC container management."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+
7
+ from proxmox.cli.firewall_helpers import add_firewall_rule_args, build_rule_data
8
+ from proxmox.client.client import ProxmoxClient
9
+ from proxmox.utils.helpers import resolve_vmid, vmid_type
10
+
11
+
12
+ def register_container_parser(subparsers: argparse._SubParsersAction) -> None:
13
+ """Register the `proxmox container` subcommand tree."""
14
+ ct_parser = subparsers.add_parser("container", help="Manage LXC containers")
15
+ ct_sub = ct_parser.add_subparsers(dest="action", title="actions", required=True)
16
+
17
+ # --- container list ---
18
+ ct_list = ct_sub.add_parser("list", help="List containers")
19
+ ct_list.add_argument("--node", help="Filter by node name")
20
+ ct_list.set_defaults(func=_ct_list)
21
+
22
+ # --- container show ---
23
+ ct_show = ct_sub.add_parser("show", help="Show container details")
24
+ ct_show.add_argument("vmid", type=vmid_type, help="Container ID")
25
+ ct_show.add_argument("--node", help="Node name (auto-detected if omitted)")
26
+ ct_show.set_defaults(func=_ct_show)
27
+
28
+ # --- container create ---
29
+ ct_create = ct_sub.add_parser("create", help="Create a new container")
30
+ ct_create.add_argument("--node", required=True, help="Target node")
31
+ ct_create.add_argument("--vmid", type=vmid_type, default=None, help="Container ID (auto-assigned if omitted)")
32
+ ct_create.add_argument("--ostemplate", required=True, help="OS template")
33
+ ct_create.add_argument("--storage", default=None, help="Storage for the container")
34
+ ct_create.add_argument("--memory", type=int, default=512, help="Memory in MB")
35
+ ct_create.add_argument("--cores", type=int, default=1, help="CPU cores")
36
+ ct_create.add_argument("--net", default=None, help="Network config (e.g. name=eth0,bridge=vmbr0,ip=dhcp)")
37
+ ct_create.add_argument("--password", default=None, help="Root password")
38
+ ct_create.set_defaults(func=_ct_create)
39
+
40
+ # --- container start ---
41
+ ct_start = ct_sub.add_parser("start", help="Start a container")
42
+ ct_start.add_argument("vmid", type=vmid_type, help="Container ID")
43
+ ct_start.add_argument("--node", help="Node name (auto-detected if omitted)")
44
+ ct_start.set_defaults(func=_ct_start)
45
+
46
+ # --- container stop ---
47
+ ct_stop = ct_sub.add_parser("stop", help="Stop a container")
48
+ ct_stop.add_argument("vmid", type=vmid_type, help="Container ID")
49
+ ct_stop.add_argument("--node", help="Node name (auto-detected if omitted)")
50
+ ct_stop.set_defaults(func=_ct_stop)
51
+
52
+ # --- container delete ---
53
+ ct_delete = ct_sub.add_parser("delete", help="Delete a container")
54
+ ct_delete.add_argument("vmid", type=vmid_type, help="Container ID")
55
+ ct_delete.add_argument("--node", help="Node name (auto-detected if omitted)")
56
+ ct_delete.add_argument("--force", action="store_true", help="Force removal")
57
+ ct_delete.add_argument("--purge", action="store_true", help="Purge from all configurations")
58
+ ct_delete.set_defaults(func=_ct_delete)
59
+
60
+ # --- firewall ---
61
+ fw = ct_sub.add_parser("firewall", help="Manage container firewall")
62
+ fw_sub = fw.add_subparsers(dest="fw_resource", title="resources", required=True)
63
+
64
+ fw_opts = fw_sub.add_parser("options", help="Show container firewall options")
65
+ fw_opts.add_argument("vmid", type=vmid_type, help="Container ID")
66
+ fw_opts.add_argument("--node", help="Node name (auto-detected if omitted)")
67
+ fw_opts.set_defaults(func=_ct_fw_options)
68
+
69
+ fw_enable = fw_sub.add_parser("enable", help="Enable container firewall")
70
+ fw_enable.add_argument("vmid", type=vmid_type, help="Container ID")
71
+ fw_enable.add_argument("--node", help="Node name (auto-detected if omitted)")
72
+ fw_enable.set_defaults(func=_ct_fw_enable)
73
+
74
+ fw_disable = fw_sub.add_parser("disable", help="Disable container firewall")
75
+ fw_disable.add_argument("vmid", type=vmid_type, help="Container ID")
76
+ fw_disable.add_argument("--node", help="Node name (auto-detected if omitted)")
77
+ fw_disable.set_defaults(func=_ct_fw_disable)
78
+
79
+ fw_policy = fw_sub.add_parser("policy", help="Set default input/output policy for container")
80
+ fw_policy.add_argument("vmid", type=vmid_type, help="Container ID")
81
+ fw_policy.add_argument("--node", help="Node name (auto-detected if omitted)")
82
+ fw_policy.add_argument("--in-policy", choices=["ACCEPT", "DENY", "REJECT"], default=None,
83
+ help="Default input policy")
84
+ fw_policy.add_argument("--out-policy", choices=["ACCEPT", "DENY", "REJECT"], default=None,
85
+ help="Default output policy")
86
+ fw_policy.set_defaults(func=_ct_fw_policy)
87
+
88
+ # firewall rules
89
+ rules = fw_sub.add_parser("rules", help="Manage container firewall rules")
90
+ rules_sub = rules.add_subparsers(dest="fw_action", title="rule actions", required=False)
91
+
92
+ rules_list = rules_sub.add_parser("list", help="List rules")
93
+ rules_list.add_argument("vmid", type=vmid_type, help="Container ID")
94
+ rules_list.add_argument("--node", help="Node name (auto-detected if omitted)")
95
+ rules_list.set_defaults(func=_ct_fw_rules)
96
+
97
+ rules_add = rules_sub.add_parser("add", help="Add a rule")
98
+ rules_add.add_argument("vmid", type=vmid_type, help="Container ID")
99
+ rules_add.add_argument("--node", help="Node name (auto-detected if omitted)")
100
+ add_firewall_rule_args(rules_add)
101
+ rules_add.set_defaults(func=_ct_fw_rule_add)
102
+
103
+ rules_show = rules_sub.add_parser("show", help="Show a rule by position")
104
+ rules_show.add_argument("vmid", type=vmid_type, help="Container ID")
105
+ rules_show.add_argument("--node", help="Node name (auto-detected if omitted)")
106
+ rules_show.add_argument("pos", type=int, help="Rule position")
107
+ rules_show.set_defaults(func=_ct_fw_rule_show)
108
+
109
+ rules_upd = rules_sub.add_parser("update", help="Update a rule by position")
110
+ rules_upd.add_argument("vmid", type=vmid_type, help="Container ID")
111
+ rules_upd.add_argument("--node", help="Node name (auto-detected if omitted)")
112
+ rules_upd.add_argument("pos", type=int, help="Rule position")
113
+ add_firewall_rule_args(rules_upd)
114
+ for action in rules_upd._actions:
115
+ if action.dest == "action":
116
+ action.required = False
117
+ action.default = None
118
+ rules_upd.set_defaults(func=_ct_fw_rule_upd)
119
+
120
+ rules_del = rules_sub.add_parser("delete", help="Delete a rule by position")
121
+ rules_del.add_argument("vmid", type=vmid_type, help="Container ID")
122
+ rules_del.add_argument("--node", help="Node name (auto-detected if omitted)")
123
+ rules_del.add_argument("pos", type=int, help="Rule position")
124
+ rules_del.set_defaults(func=_ct_fw_rule_del)
125
+
126
+ # firewall refs
127
+ fw_refs = fw_sub.add_parser("refs", help="List container firewall references")
128
+ fw_refs.add_argument("vmid", type=vmid_type, help="Container ID")
129
+ fw_refs.add_argument("--node", help="Node name (auto-detected if omitted)")
130
+ fw_refs.add_argument("--type", default=None, choices=["alias", "ipset", "group"],
131
+ help="Filter by reference type")
132
+ fw_refs.set_defaults(func=_ct_fw_refs)
133
+
134
+
135
+ # ---------------------------------------------------------------------------
136
+ # Helpers
137
+ # ---------------------------------------------------------------------------
138
+
139
+ def _resolve_ct_node(client: ProxmoxClient, node: str | None, vmid: int) -> str | None:
140
+ if node:
141
+ return node
142
+ try:
143
+ resources = client.get("/cluster/resources", params={"type": "vm"})
144
+ if isinstance(resources, list):
145
+ for r in resources:
146
+ if r.get("vmid") == vmid:
147
+ return r.get("node")
148
+ except Exception:
149
+ pass
150
+ return None
151
+
152
+
153
+ # ---------------------------------------------------------------------------
154
+ # Command handlers
155
+ # ---------------------------------------------------------------------------
156
+
157
+ def _ct_list(args: argparse.Namespace, client: ProxmoxClient) -> dict | list:
158
+ if args.node:
159
+ return client.get(f"/nodes/{args.node}/lxc")
160
+ nodes = client.get("/nodes")
161
+ if isinstance(nodes, dict):
162
+ nodes = nodes.get("data", [])
163
+ cts: list[dict] = []
164
+ for n in (nodes if isinstance(nodes, list) else []):
165
+ node_name = n.get("node") if isinstance(n, dict) else n
166
+ try:
167
+ node_cts = client.get(f"/nodes/{node_name}/lxc")
168
+ if isinstance(node_cts, list):
169
+ for ct in node_cts:
170
+ if isinstance(ct, dict):
171
+ ct["_node"] = node_name
172
+ cts.append(ct)
173
+ elif isinstance(node_cts, dict):
174
+ for ct in node_cts.get("data", []):
175
+ if isinstance(ct, dict):
176
+ ct["_node"] = node_name
177
+ cts.append(ct)
178
+ except Exception:
179
+ pass
180
+ return cts
181
+
182
+
183
+ def _ct_show(args: argparse.Namespace, client: ProxmoxClient) -> dict:
184
+ node = _resolve_ct_node(client, args.node, args.vmid)
185
+ if not node:
186
+ return {"error": f"Container {args.vmid} not found"}
187
+ result = client.get(f"/nodes/{node}/lxc/{args.vmid}/status/current")
188
+ if isinstance(result, dict):
189
+ result["_node"] = node
190
+ return result
191
+
192
+
193
+ def _ct_create(args: argparse.Namespace, client: ProxmoxClient) -> dict:
194
+ data: dict = {
195
+ "vmid": resolve_vmid(client, args.vmid),
196
+ "ostemplate": args.ostemplate,
197
+ "memory": args.memory,
198
+ "cores": args.cores,
199
+ }
200
+ if args.storage:
201
+ data["storage"] = args.storage
202
+ if args.net:
203
+ data["net0"] = args.net
204
+ if args.password:
205
+ data["password"] = args.password
206
+ return client.post(f"/nodes/{args.node}/lxc", data=data)
207
+
208
+
209
+ def _ct_start(args: argparse.Namespace, client: ProxmoxClient) -> dict:
210
+ node = _resolve_ct_node(client, args.node, args.vmid)
211
+ if not node:
212
+ return {"error": f"Container {args.vmid} not found"}
213
+ return client.post(f"/nodes/{node}/lxc/{args.vmid}/status/start")
214
+
215
+
216
+ def _ct_stop(args: argparse.Namespace, client: ProxmoxClient) -> dict:
217
+ node = _resolve_ct_node(client, args.node, args.vmid)
218
+ if not node:
219
+ return {"error": f"Container {args.vmid} not found"}
220
+ return client.post(f"/nodes/{node}/lxc/{args.vmid}/status/stop")
221
+
222
+
223
+ def _ct_delete(args: argparse.Namespace, client: ProxmoxClient) -> dict:
224
+ node = _resolve_ct_node(client, args.node, args.vmid)
225
+ if not node:
226
+ return {"error": f"Container {args.vmid} not found"}
227
+ params: dict = {}
228
+ if args.force:
229
+ params["force"] = 1
230
+ if args.purge:
231
+ params["purge"] = 1
232
+ return client.delete(f"/nodes/{node}/lxc/{args.vmid}", params=params or None)
233
+
234
+
235
+ # ---------------------------------------------------------------------------
236
+ # Container firewall handlers
237
+ # ---------------------------------------------------------------------------
238
+
239
+ def _ct_fw_options(args: argparse.Namespace, client: ProxmoxClient) -> dict:
240
+ node = _resolve_ct_node(client, args.node, args.vmid)
241
+ if not node:
242
+ return {"error": f"Container {args.vmid} not found"}
243
+ return client.get(f"/nodes/{node}/lxc/{args.vmid}/firewall/options")
244
+
245
+
246
+ def _ct_fw_enable(args: argparse.Namespace, client: ProxmoxClient) -> dict:
247
+ node = _resolve_ct_node(client, args.node, args.vmid)
248
+ if not node:
249
+ return {"error": f"Container {args.vmid} not found"}
250
+ return client.put(f"/nodes/{node}/lxc/{args.vmid}/firewall/options", data={"enable": 1})
251
+
252
+
253
+ def _ct_fw_disable(args: argparse.Namespace, client: ProxmoxClient) -> dict:
254
+ node = _resolve_ct_node(client, args.node, args.vmid)
255
+ if not node:
256
+ return {"error": f"Container {args.vmid} not found"}
257
+ return client.put(f"/nodes/{node}/lxc/{args.vmid}/firewall/options", data={"enable": 0})
258
+
259
+
260
+ def _ct_fw_policy(args: argparse.Namespace, client: ProxmoxClient) -> dict:
261
+ node = _resolve_ct_node(client, args.node, args.vmid)
262
+ if not node:
263
+ return {"error": f"Container {args.vmid} not found"}
264
+ data: dict = {}
265
+ if args.in_policy:
266
+ data["policy_in"] = args.in_policy
267
+ if args.out_policy:
268
+ data["policy_out"] = args.out_policy
269
+ if not data:
270
+ return {"error": "No policy specified. Use --in-policy or --out-policy"}
271
+ return client.put(f"/nodes/{node}/lxc/{args.vmid}/firewall/options", data=data)
272
+
273
+
274
+ def _ct_fw_rules(args: argparse.Namespace, client: ProxmoxClient) -> dict | list:
275
+ node = _resolve_ct_node(client, args.node, args.vmid)
276
+ if not node:
277
+ return {"error": f"Container {args.vmid} not found"}
278
+ return client.get(f"/nodes/{node}/lxc/{args.vmid}/firewall/rules")
279
+
280
+
281
+ def _ct_fw_rule_add(args: argparse.Namespace, client: ProxmoxClient) -> dict:
282
+ node = _resolve_ct_node(client, args.node, args.vmid)
283
+ if not node:
284
+ return {"error": f"Container {args.vmid} not found"}
285
+ return client.post(f"/nodes/{node}/lxc/{args.vmid}/firewall/rules",
286
+ data=build_rule_data(args))
287
+
288
+
289
+ def _ct_fw_rule_show(args: argparse.Namespace, client: ProxmoxClient) -> dict:
290
+ node = _resolve_ct_node(client, args.node, args.vmid)
291
+ if not node:
292
+ return {"error": f"Container {args.vmid} not found"}
293
+ return client.get(f"/nodes/{node}/lxc/{args.vmid}/firewall/rules/{args.pos}")
294
+
295
+
296
+ def _ct_fw_rule_upd(args: argparse.Namespace, client: ProxmoxClient) -> dict:
297
+ node = _resolve_ct_node(client, args.node, args.vmid)
298
+ if not node:
299
+ return {"error": f"Container {args.vmid} not found"}
300
+ data = {k: v for k, v in build_rule_data(args).items() if v is not None and v != 0}
301
+ return client.put(f"/nodes/{node}/lxc/{args.vmid}/firewall/rules/{args.pos}", data=data)
302
+
303
+
304
+ def _ct_fw_rule_del(args: argparse.Namespace, client: ProxmoxClient) -> dict:
305
+ node = _resolve_ct_node(client, args.node, args.vmid)
306
+ if not node:
307
+ return {"error": f"Container {args.vmid} not found"}
308
+ return client.delete(f"/nodes/{node}/lxc/{args.vmid}/firewall/rules/{args.pos}")
309
+
310
+
311
+ def _ct_fw_refs(args: argparse.Namespace, client: ProxmoxClient) -> dict | list:
312
+ node = _resolve_ct_node(client, args.node, args.vmid)
313
+ if not node:
314
+ return {"error": f"Container {args.vmid} not found"}
315
+ params: dict | None = {"type": args.type} if args.type else None
316
+ return client.get(f"/nodes/{node}/lxc/{args.vmid}/firewall/refs", params=params)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "proxcli"
3
- version = "0.3.0"
3
+ version = "0.4.0"
4
4
  description = "A CLI tool to interact with Proxmox VE nodes and clusters via the REST API"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -254,7 +254,7 @@ wheels = [
254
254
 
255
255
  [[package]]
256
256
  name = "proxcli"
257
- version = "0.3.0"
257
+ version = "0.4.0"
258
258
  source = { editable = "." }
259
259
  dependencies = [
260
260
  { name = "httpx" },
@@ -1,157 +0,0 @@
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 resolve_vmid, 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, default=None, help="Container ID (auto-assigned if omitted)")
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": resolve_vmid(client, 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)
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