proxcli 0.2.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {proxcli-0.2.0 → proxcli-0.3.0}/.github/workflows/ci.yml +22 -3
- proxcli-0.3.0/AGENTS.md +123 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/CHANGELOG.md +21 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/PKG-INFO +1 -1
- proxcli-0.3.0/proxmox/cli/cluster.py +239 -0
- proxcli-0.3.0/proxmox/cli/firewall_helpers.py +60 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/proxmox/cli/main.py +18 -3
- proxcli-0.3.0/proxmox/cli/node.py +177 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/proxmox/cli/vm.py +159 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/pyproject.toml +1 -1
- {proxcli-0.2.0 → proxcli-0.3.0}/uv.lock +2 -2
- proxcli-0.2.0/proxmox/cli/cluster.py +0 -21
- proxcli-0.2.0/proxmox/cli/node.py +0 -55
- {proxcli-0.2.0 → proxcli-0.3.0}/.env.example +0 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/.gitignore +0 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/.python-version +0 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/PLAN.md +0 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/PROJECT.md +0 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/PROMPT.md +0 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/README.md +0 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/TODO.md +0 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/proxmox/__init__.py +0 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/proxmox/cli/__init__.py +0 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/proxmox/cli/auth.py +0 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/proxmox/cli/container.py +0 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/proxmox/cli/storage.py +0 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/proxmox/cli/tasks.py +0 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/proxmox/client/__init__.py +0 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/proxmox/client/auth.py +0 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/proxmox/client/client.py +0 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/proxmox/client/exceptions.py +0 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/proxmox/config/__init__.py +0 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/proxmox/config/config.py +0 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/proxmox/config/models.py +0 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/proxmox/output/__init__.py +0 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/proxmox/output/formatter.py +0 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/proxmox/output/json_fmt.py +0 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/proxmox/output/table_fmt.py +0 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/proxmox/output/yaml_fmt.py +0 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/proxmox/utils/__init__.py +0 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/proxmox/utils/helpers.py +0 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/proxmox/utils/logging.py +0 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/tests/__init__.py +0 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/tests/conftest.py +0 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/tests/test_auth.py +0 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/tests/test_cli/__init__.py +0 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/tests/test_cli/test_main.py +0 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/tests/test_client.py +0 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/tests/test_config.py +0 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/tests/test_integration/__init__.py +0 -0
- {proxcli-0.2.0 → proxcli-0.3.0}/tests/test_output/__init__.py +0 -0
- {proxcli-0.2.0 → proxcli-0.3.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@
|
|
15
|
+
- uses: astral-sh/setup-uv@4db96194c378173c656ce18a155ffc14a9fc4355 # v5.2.2
|
|
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@
|
|
30
|
+
- uses: astral-sh/setup-uv@4db96194c378173c656ce18a155ffc14a9fc4355 # v5.2.2
|
|
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@
|
|
44
|
+
- uses: astral-sh/setup-uv@4db96194c378173c656ce18a155ffc14a9fc4355 # v5.2.2
|
|
45
45
|
with:
|
|
46
46
|
enable-cache: true
|
|
47
47
|
|
|
@@ -52,3 +52,22 @@ jobs:
|
|
|
52
52
|
with:
|
|
53
53
|
name: dist
|
|
54
54
|
path: dist/
|
|
55
|
+
|
|
56
|
+
publish:
|
|
57
|
+
runs-on: ubuntu-24.04
|
|
58
|
+
if: github.ref == 'refs/heads/main'
|
|
59
|
+
needs: [build]
|
|
60
|
+
environment: pypi
|
|
61
|
+
steps:
|
|
62
|
+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
63
|
+
|
|
64
|
+
- uses: astral-sh/setup-uv@4db96194c378173c656ce18a155ffc14a9fc4355 # v5.2.2
|
|
65
|
+
with:
|
|
66
|
+
enable-cache: true
|
|
67
|
+
|
|
68
|
+
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
|
|
69
|
+
with:
|
|
70
|
+
name: dist
|
|
71
|
+
path: dist/
|
|
72
|
+
|
|
73
|
+
- run: uv publish --token "${{ secrets.PYPI_TOKEN }}"
|
proxcli-0.3.0/AGENTS.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Agent Guidelines for proxmox CLI
|
|
2
|
+
|
|
3
|
+
## CLI Command Convention
|
|
4
|
+
|
|
5
|
+
All commands follow a strict **`<resource> <action> [positional_id] [--flags]`** pattern:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
proxmox <resource> <action> [id_or_name] [--options]
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
### Resource-level (top-level subcommands)
|
|
12
|
+
|
|
13
|
+
Every resource is a noun: `vm`, `container`, `node`, `storage`, `cluster`, `task`, `auth`.
|
|
14
|
+
|
|
15
|
+
Each has actions as verbs: `list`, `show`, `create`, `start`, `stop`, `delete`, etc.
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
proxmox vm list
|
|
19
|
+
proxmox vm show 100
|
|
20
|
+
proxmox vm start 100
|
|
21
|
+
proxmox node show pve01
|
|
22
|
+
proxmox container list
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Nested resources (firewall, etc.)
|
|
26
|
+
|
|
27
|
+
When a resource has sub-resources, use the same **`<resource> <action> <subresource> [subaction]`** pattern:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
proxmox cluster firewall rules # list (shorthand)
|
|
31
|
+
proxmox cluster firewall rules list # list (explicit)
|
|
32
|
+
proxmox cluster firewall rules add --action ACCEPT ...
|
|
33
|
+
proxmox cluster firewall rules show <pos>
|
|
34
|
+
proxmox cluster firewall rules delete <pos>
|
|
35
|
+
proxmox vm firewall rules list <vmid>
|
|
36
|
+
proxmox vm firewall rules add <vmid> --action ACCEPT ...
|
|
37
|
+
proxmox node firewall rules list <node_name>
|
|
38
|
+
proxmox node firewall rules add <node_name> --action ACCEPT ...
|
|
39
|
+
proxmox cluster firewall aliases # list (shorthand)
|
|
40
|
+
proxmox cluster firewall aliases add <name> --cidr ...
|
|
41
|
+
proxmox cluster firewall ipsets # list (shorthand)
|
|
42
|
+
proxmox cluster firewall ipsets add <name> ...
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Key rules
|
|
46
|
+
|
|
47
|
+
1. **Nouns before verbs** — `proxmox vm firewall rules list`, NOT `proxmox vm firewall list-rules`.
|
|
48
|
+
2. **Resource identifiers are positional arguments**, placed after the action verb:
|
|
49
|
+
- `proxmox vm show 100` (vmid is positional)
|
|
50
|
+
- `proxmox node show pve01` (node_name is positional)
|
|
51
|
+
- `proxmox vm firewall rules show 100 3` (vmid then pos)
|
|
52
|
+
3. **`--node` is always an optional flag** for VM/container commands (auto-detected if omitted), except on `create` where it's required.
|
|
53
|
+
4. **No shorthand noun squeezing** — `proxmox vm fw` is NOT acceptable. Always use full resource names.
|
|
54
|
+
5. **Subcommands inherit `--flags` from parents** via `set_defaults(func=handler)`. Each handler function receives the merged `Namespace`.
|
|
55
|
+
|
|
56
|
+
## Parser Registration
|
|
57
|
+
|
|
58
|
+
Each CLI module has a `register_<resource>_parser(subparsers)` function that adds subparsers to the passed-in `_SubParsersAction`. Example:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
def register_vm_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
62
|
+
vm_parser = subparsers.add_parser("vm", help="Manage QEMU virtual machines")
|
|
63
|
+
vm_sub = vm_parser.add_subparsers(dest="action", title="actions", required=True)
|
|
64
|
+
|
|
65
|
+
vm_list = vm_sub.add_parser("list", help="List virtual machines")
|
|
66
|
+
vm_list.add_argument("--node", help="Filter by node name")
|
|
67
|
+
vm_list.set_defaults(func=_vm_list)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
For nested resources (like firewall), use a second `dest` name to track the sub-resource:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
fw = vm_sub.add_parser("firewall", help="Manage VM firewall")
|
|
74
|
+
fw_sub = fw.add_subparsers(dest="fw_resource", title="resources", required=True)
|
|
75
|
+
|
|
76
|
+
rules = fw_sub.add_parser("rules", help="Manage VM firewall rules")
|
|
77
|
+
rules_sub = rules.add_subparsers(dest="fw_action", title="rule actions", required=False)
|
|
78
|
+
rules_list = rules_sub.add_parser("list", help="List rules")
|
|
79
|
+
rules_list.add_argument("vmid", type=vmid_type, help="VM ID")
|
|
80
|
+
rules_list.set_defaults(func=_vm_fw_rules)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Handler Functions
|
|
84
|
+
|
|
85
|
+
Every handler has the signature:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
def _handler(args: argparse.Namespace, client: ProxmoxClient) -> dict | list:
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
- `args` contains all parsed arguments (global + resource-specific)
|
|
92
|
+
- `client` is the authenticated `ProxmoxClient` instance
|
|
93
|
+
- Returns a dict or list that will be formatted by the output system
|
|
94
|
+
- For errors, return `{"error": "message"}` dict
|
|
95
|
+
|
|
96
|
+
## Shared Helpers
|
|
97
|
+
|
|
98
|
+
Shared argument definitions go in helper modules (e.g., `firewall_helpers.py`). They export two functions:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
def add_firewall_rule_args(parser: argparse.ArgumentParser) -> None:
|
|
102
|
+
"""Add common rule-form arguments to a parser."""
|
|
103
|
+
|
|
104
|
+
def build_rule_data(args: argparse.Namespace) -> dict[str, Any]:
|
|
105
|
+
"""Convert parsed args into the POST/PUT body dict."""
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Testing
|
|
109
|
+
|
|
110
|
+
- Unit tests use `pytest-httpx` to mock API responses
|
|
111
|
+
- Integration tests use `subprocess.run` to invoke the CLI binary
|
|
112
|
+
- Dry-run tests verify the URL, method, and body without real network calls
|
|
113
|
+
|
|
114
|
+
## Versioning
|
|
115
|
+
|
|
116
|
+
The version string is read from `importlib.metadata.version('proxcli')` — never hardcoded in source.
|
|
117
|
+
|
|
118
|
+
## PyPI
|
|
119
|
+
|
|
120
|
+
- Package name: `proxcli`
|
|
121
|
+
- CLI binary: `proxmox`
|
|
122
|
+
- Publish via `uv publish --token $PYPI_TOKEN`
|
|
123
|
+
- CI auto-publishes on push to `main` (after lint + test + build)
|
|
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0] - 2026-06-20
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Cluster firewall management: options, enable/disable, policy, rules (CRUD), aliases, ipsets (with CIDR management), refs.
|
|
14
|
+
- Node firewall management: options, enable/disable, policy, rules (CRUD), refs.
|
|
15
|
+
- VM firewall management: options, enable/disable, policy, rules (CRUD), refs.
|
|
16
|
+
- Shared `firewall_helpers.py` for consistent rule argument building across all levels.
|
|
17
|
+
- CI `publish` job: auto-publishes to PyPI on push to main (uses `PYPI_TOKEN` repo secret with `environment: pypi`).
|
|
18
|
+
- `AGENTS.md` with CLI convention and contribution guidelines.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- Removed `.env` file with PyPI token; now uses GitHub Actions secrets.
|
|
22
|
+
- Firewall subcommands refactored to consistent `<resource> <action> <subresource> [subaction]` pattern.
|
|
23
|
+
|
|
24
|
+
## [0.2.1] - 2026-06-20
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- `--version` now reads from installed package metadata (`importlib.metadata`) instead of a hardcoded string.
|
|
28
|
+
|
|
10
29
|
## [0.2.0] - 2026-06-20
|
|
11
30
|
|
|
12
31
|
### Added
|
|
@@ -36,6 +55,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
36
55
|
- CSRF ticket auto-refresh on 401.
|
|
37
56
|
- AI-agent-friendly: default JSON output, strict exit codes, `--dry-run` mode.
|
|
38
57
|
|
|
58
|
+
[0.3.0]: https://github.com/xezpeleta/proxcli/releases/tag/v0.3.0
|
|
59
|
+
[0.2.1]: https://github.com/xezpeleta/proxcli/releases/tag/v0.2.1
|
|
39
60
|
[0.2.0]: https://github.com/xezpeleta/proxcli/releases/tag/v0.2.0
|
|
40
61
|
[0.1.1]: https://github.com/xezpeleta/proxcli/releases/tag/v0.1.1
|
|
41
62
|
[0.1.0]: https://github.com/xezpeleta/proxcli/releases/tag/v0.1.0
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""`proxmox cluster` subcommand — cluster 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
|
+
|
|
10
|
+
|
|
11
|
+
def register_cluster_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
12
|
+
"""Register the `proxmox cluster` subcommand tree."""
|
|
13
|
+
cl_parser = subparsers.add_parser("cluster", help="Manage Proxmox cluster")
|
|
14
|
+
cl_sub = cl_parser.add_subparsers(dest="action", title="actions", required=True)
|
|
15
|
+
|
|
16
|
+
# --- cluster status ---
|
|
17
|
+
cl_status = cl_sub.add_parser("status", help="Show cluster status")
|
|
18
|
+
cl_status.set_defaults(func=_cl_status)
|
|
19
|
+
|
|
20
|
+
# --- firewall ---
|
|
21
|
+
fw = cl_sub.add_parser("firewall", help="Manage cluster firewall")
|
|
22
|
+
fw_sub = fw.add_subparsers(dest="fw_resource", title="resources", required=True)
|
|
23
|
+
|
|
24
|
+
# firewall options
|
|
25
|
+
fw_opts = fw_sub.add_parser("options", help="Show cluster firewall options")
|
|
26
|
+
fw_opts.set_defaults(func=_cl_fw_options)
|
|
27
|
+
|
|
28
|
+
fw_opts_set = fw_sub.add_parser("enable", help="Enable cluster firewall")
|
|
29
|
+
fw_opts_set.set_defaults(func=_cl_fw_enable)
|
|
30
|
+
|
|
31
|
+
fw_opts_disable = fw_sub.add_parser("disable", help="Disable cluster firewall")
|
|
32
|
+
fw_opts_disable.set_defaults(func=_cl_fw_disable)
|
|
33
|
+
|
|
34
|
+
fw_policy = fw_sub.add_parser("policy", help="Set default firewall policy")
|
|
35
|
+
fw_policy.add_argument("--in-policy", choices=["ACCEPT", "DENY", "REJECT"], default=None,
|
|
36
|
+
help="Default input policy")
|
|
37
|
+
fw_policy.add_argument("--out-policy", choices=["ACCEPT", "DENY", "REJECT"], default=None,
|
|
38
|
+
help="Default output policy")
|
|
39
|
+
fw_policy.set_defaults(func=_cl_fw_policy)
|
|
40
|
+
|
|
41
|
+
# firewall rules
|
|
42
|
+
rules = fw_sub.add_parser("rules", help="Manage cluster firewall rules")
|
|
43
|
+
rules.set_defaults(func=_cl_fw_rules)
|
|
44
|
+
rules_sub = rules.add_subparsers(dest="fw_action", title="rule actions", required=False)
|
|
45
|
+
|
|
46
|
+
rules_list = rules_sub.add_parser("list", help="List rules")
|
|
47
|
+
rules_list.set_defaults(func=_cl_fw_rules)
|
|
48
|
+
|
|
49
|
+
rules_add = rules_sub.add_parser("add", help="Add a rule")
|
|
50
|
+
add_firewall_rule_args(rules_add)
|
|
51
|
+
rules_add.set_defaults(func=_cl_fw_rule_add)
|
|
52
|
+
|
|
53
|
+
rules_show = rules_sub.add_parser("show", help="Show a rule by position")
|
|
54
|
+
rules_show.add_argument("pos", type=int, help="Rule position")
|
|
55
|
+
rules_show.set_defaults(func=_cl_fw_rule_show)
|
|
56
|
+
|
|
57
|
+
rules_upd = rules_sub.add_parser("update", help="Update a rule by position")
|
|
58
|
+
rules_upd.add_argument("pos", type=int, help="Rule position")
|
|
59
|
+
add_firewall_rule_args(rules_upd)
|
|
60
|
+
for action in rules_upd._actions:
|
|
61
|
+
if action.dest == "action":
|
|
62
|
+
action.required = False
|
|
63
|
+
action.default = None
|
|
64
|
+
rules_upd.set_defaults(func=_cl_fw_rule_upd)
|
|
65
|
+
|
|
66
|
+
rules_del = rules_sub.add_parser("delete", help="Delete a rule by position")
|
|
67
|
+
rules_del.add_argument("pos", type=int, help="Rule position")
|
|
68
|
+
rules_del.set_defaults(func=_cl_fw_rule_del)
|
|
69
|
+
|
|
70
|
+
# firewall aliases
|
|
71
|
+
aliases = fw_sub.add_parser("aliases", help="Manage firewall aliases")
|
|
72
|
+
aliases.set_defaults(func=_cl_fw_aliases)
|
|
73
|
+
aliases_sub = aliases.add_subparsers(dest="fw_action", title="alias actions", required=False)
|
|
74
|
+
|
|
75
|
+
aliases_list = aliases_sub.add_parser("list", help="List aliases")
|
|
76
|
+
aliases_list.set_defaults(func=_cl_fw_aliases)
|
|
77
|
+
|
|
78
|
+
aliases_add = aliases_sub.add_parser("add", help="Add an alias")
|
|
79
|
+
aliases_add.add_argument("name", help="Alias name")
|
|
80
|
+
aliases_add.add_argument("--cidr", required=True, help="CIDR notation (e.g. 10.0.0.0/8)")
|
|
81
|
+
aliases_add.add_argument("--comment", default=None, help="Comment / description")
|
|
82
|
+
aliases_add.set_defaults(func=_cl_fw_alias_add)
|
|
83
|
+
|
|
84
|
+
aliases_del = aliases_sub.add_parser("delete", help="Delete an alias")
|
|
85
|
+
aliases_del.add_argument("name", help="Alias name")
|
|
86
|
+
aliases_del.set_defaults(func=_cl_fw_alias_del)
|
|
87
|
+
|
|
88
|
+
# firewall ipsets
|
|
89
|
+
ipsets = fw_sub.add_parser("ipsets", help="Manage firewall ipsets")
|
|
90
|
+
ipsets.set_defaults(func=_cl_fw_ipsets)
|
|
91
|
+
ipsets_sub = ipsets.add_subparsers(dest="fw_action", title="ipset actions", required=False)
|
|
92
|
+
|
|
93
|
+
ipsets_list = ipsets_sub.add_parser("list", help="List ipsets")
|
|
94
|
+
ipsets_list.set_defaults(func=_cl_fw_ipsets)
|
|
95
|
+
|
|
96
|
+
ipsets_add = ipsets_sub.add_parser("add", help="Add an ipset")
|
|
97
|
+
ipsets_add.add_argument("name", help="IPset name")
|
|
98
|
+
ipsets_add.add_argument("--comment", default=None, help="Comment / description")
|
|
99
|
+
ipsets_add.set_defaults(func=_cl_fw_ipset_add)
|
|
100
|
+
|
|
101
|
+
ipsets_show = ipsets_sub.add_parser("show", help="Show ipset contents")
|
|
102
|
+
ipsets_show.add_argument("name", help="IPset name")
|
|
103
|
+
ipsets_show.set_defaults(func=_cl_fw_ipset_show)
|
|
104
|
+
|
|
105
|
+
ipsets_del = ipsets_sub.add_parser("delete", help="Delete an ipset")
|
|
106
|
+
ipsets_del.add_argument("name", help="IPset name")
|
|
107
|
+
ipsets_del.set_defaults(func=_cl_fw_ipset_del)
|
|
108
|
+
|
|
109
|
+
ipsets_add_cidr = ipsets_sub.add_parser("add-cidr", help="Add a CIDR to an ipset")
|
|
110
|
+
ipsets_add_cidr.add_argument("name", help="IPset name")
|
|
111
|
+
ipsets_add_cidr.add_argument("--cidr", required=True, help="CIDR to add")
|
|
112
|
+
ipsets_add_cidr.add_argument("--comment", default=None, help="Comment")
|
|
113
|
+
ipsets_add_cidr.add_argument("--nomatch", action="store_true", help="Exclude match")
|
|
114
|
+
ipsets_add_cidr.set_defaults(func=_cl_fw_ipset_add_cidr)
|
|
115
|
+
|
|
116
|
+
ipsets_del_cidr = ipsets_sub.add_parser("delete-cidr", help="Remove a CIDR from an ipset")
|
|
117
|
+
ipsets_del_cidr.add_argument("name", help="IPset name")
|
|
118
|
+
ipsets_del_cidr.add_argument("--cidr", required=True, help="CIDR to remove")
|
|
119
|
+
ipsets_del_cidr.set_defaults(func=_cl_fw_ipset_del_cidr)
|
|
120
|
+
|
|
121
|
+
# firewall refs
|
|
122
|
+
fw_refs = fw_sub.add_parser("refs", help="List firewall references")
|
|
123
|
+
fw_refs.add_argument("--type", default=None, choices=["alias", "ipset", "group"],
|
|
124
|
+
help="Filter by reference type")
|
|
125
|
+
fw_refs.set_defaults(func=_cl_fw_refs)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# ---------------------------------------------------------------------------
|
|
129
|
+
# Handlers
|
|
130
|
+
# ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
def _cl_status(_args: argparse.Namespace, client: ProxmoxClient) -> dict | list:
|
|
133
|
+
return client.get("/cluster/status")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# --- Firewall options ---
|
|
137
|
+
|
|
138
|
+
def _cl_fw_options(_args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
139
|
+
return client.get("/cluster/firewall/options")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _cl_fw_enable(_args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
143
|
+
return client.put("/cluster/firewall/options", data={"enable": 1})
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _cl_fw_disable(_args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
147
|
+
return client.put("/cluster/firewall/options", data={"enable": 0})
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _cl_fw_policy(args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
151
|
+
data: dict = {}
|
|
152
|
+
if args.in_policy is not None:
|
|
153
|
+
data["policy_in"] = args.in_policy
|
|
154
|
+
if args.out_policy is not None:
|
|
155
|
+
data["policy_out"] = args.out_policy
|
|
156
|
+
if not data:
|
|
157
|
+
return {"error": "No policy specified. Use --in-policy or --out-policy"}
|
|
158
|
+
return client.put("/cluster/firewall/options", data=data)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# --- Firewall rules ---
|
|
162
|
+
|
|
163
|
+
def _cl_fw_rules(_args: argparse.Namespace, client: ProxmoxClient) -> dict | list:
|
|
164
|
+
return client.get("/cluster/firewall/rules")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _cl_fw_rule_add(args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
168
|
+
return client.post("/cluster/firewall/rules", data=build_rule_data(args))
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _cl_fw_rule_show(args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
172
|
+
return client.get(f"/cluster/firewall/rules/{args.pos}")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _cl_fw_rule_upd(args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
176
|
+
data = {k: v for k, v in build_rule_data(args).items() if v is not None and v != 0}
|
|
177
|
+
return client.put(f"/cluster/firewall/rules/{args.pos}", data=data)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _cl_fw_rule_del(args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
181
|
+
return client.delete(f"/cluster/firewall/rules/{args.pos}")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# --- Firewall aliases ---
|
|
185
|
+
|
|
186
|
+
def _cl_fw_aliases(_args: argparse.Namespace, client: ProxmoxClient) -> dict | list:
|
|
187
|
+
return client.get("/cluster/firewall/aliases")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _cl_fw_alias_add(args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
191
|
+
data = {"name": args.name, "cidr": args.cidr}
|
|
192
|
+
if args.comment:
|
|
193
|
+
data["comment"] = args.comment
|
|
194
|
+
return client.post("/cluster/firewall/aliases", data=data)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _cl_fw_alias_del(args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
198
|
+
return client.delete(f"/cluster/firewall/aliases/{args.name}")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# --- Firewall ipsets ---
|
|
202
|
+
|
|
203
|
+
def _cl_fw_ipsets(_args: argparse.Namespace, client: ProxmoxClient) -> dict | list:
|
|
204
|
+
return client.get("/cluster/firewall/ipset")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _cl_fw_ipset_add(args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
208
|
+
data: dict = {"name": args.name}
|
|
209
|
+
if args.comment:
|
|
210
|
+
data["comment"] = args.comment
|
|
211
|
+
return client.post("/cluster/firewall/ipset", data=data)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _cl_fw_ipset_show(args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
215
|
+
return client.get(f"/cluster/firewall/ipset/{args.name}")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _cl_fw_ipset_del(args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
219
|
+
return client.delete(f"/cluster/firewall/ipset/{args.name}")
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _cl_fw_ipset_add_cidr(args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
223
|
+
data: dict = {"cidr": args.cidr}
|
|
224
|
+
if args.comment:
|
|
225
|
+
data["comment"] = args.comment
|
|
226
|
+
if args.nomatch:
|
|
227
|
+
data["nomatch"] = 1
|
|
228
|
+
return client.post(f"/cluster/firewall/ipset/{args.name}", data=data)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _cl_fw_ipset_del_cidr(args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
232
|
+
return client.delete(f"/cluster/firewall/ipset/{args.name}/{args.cidr}")
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# --- Firewall refs ---
|
|
236
|
+
|
|
237
|
+
def _cl_fw_refs(args: argparse.Namespace, client: ProxmoxClient) -> dict | list:
|
|
238
|
+
params = {"type": args.type} if args.type else None
|
|
239
|
+
return client.get("/cluster/firewall/refs", params=params)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Firewall helpers shared across cluster, node, and VM subcommands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
# ---------------------------------------------------------------------------
|
|
9
|
+
# Rule argument builder — shared across all firewall levels
|
|
10
|
+
# ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def add_firewall_rule_args(parser: argparse.ArgumentParser) -> None:
|
|
14
|
+
"""Add common rule-form arguments to a parser."""
|
|
15
|
+
parser.add_argument("--action", required=True, choices=["ACCEPT", "DENY", "REJECT"],
|
|
16
|
+
help="Rule action")
|
|
17
|
+
parser.add_argument("--type", default="in", choices=["in", "out"],
|
|
18
|
+
help="Traffic direction (default: in)")
|
|
19
|
+
parser.add_argument("--iface", default=None, help="Network interface (e.g. net0)")
|
|
20
|
+
parser.add_argument("--source", default=None, help="Source IP/CIDR")
|
|
21
|
+
parser.add_argument("--dest", default=None, help="Destination IP/CIDR")
|
|
22
|
+
parser.add_argument("--dport", default=None, help="Destination port")
|
|
23
|
+
parser.add_argument("--sport", default=None, help="Source port")
|
|
24
|
+
parser.add_argument("--proto", default=None, choices=["tcp", "udp", "icmp", "any"],
|
|
25
|
+
help="Protocol")
|
|
26
|
+
parser.add_argument("--comment", default=None, help="Comment / description")
|
|
27
|
+
parser.add_argument("--enable", type=int, default=1, choices=[0, 1],
|
|
28
|
+
help="Enable the rule (default: 1)")
|
|
29
|
+
parser.add_argument("--macro", default=None, help="Pre-defined macro (e.g. SSH, HTTP)")
|
|
30
|
+
parser.add_argument("--log", default=None, choices=["emerg", "alert", "crit", "err",
|
|
31
|
+
"warning", "notice", "info", "debug", "nolog"],
|
|
32
|
+
help="Log level")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def build_rule_data(args: argparse.Namespace) -> dict[str, Any]:
|
|
36
|
+
"""Convert parsed firewall rule args into the POST body dict."""
|
|
37
|
+
data: dict[str, Any] = {
|
|
38
|
+
"action": args.action,
|
|
39
|
+
"type": args.type,
|
|
40
|
+
"enable": args.enable,
|
|
41
|
+
}
|
|
42
|
+
if args.macro:
|
|
43
|
+
data["macro"] = args.macro
|
|
44
|
+
if args.iface:
|
|
45
|
+
data["iface"] = args.iface
|
|
46
|
+
if args.source:
|
|
47
|
+
data["source"] = args.source
|
|
48
|
+
if args.dest:
|
|
49
|
+
data["dest"] = args.dest
|
|
50
|
+
if args.dport:
|
|
51
|
+
data["dport"] = args.dport
|
|
52
|
+
if args.sport:
|
|
53
|
+
data["sport"] = args.sport
|
|
54
|
+
if args.proto and not args.macro:
|
|
55
|
+
data["proto"] = args.proto
|
|
56
|
+
if args.comment:
|
|
57
|
+
data["comment"] = args.comment
|
|
58
|
+
if args.log:
|
|
59
|
+
data["log"] = args.log
|
|
60
|
+
return data
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import argparse
|
|
6
6
|
import os
|
|
7
7
|
import sys
|
|
8
|
+
from importlib.metadata import version
|
|
8
9
|
from typing import Any
|
|
9
10
|
|
|
10
11
|
from proxmox.client.auth import AuthManager
|
|
@@ -48,7 +49,7 @@ def build_root_parser() -> argparse.ArgumentParser:
|
|
|
48
49
|
)
|
|
49
50
|
parser.add_argument("--verbose", action="store_true", help="Enable debug output to stderr")
|
|
50
51
|
parser.add_argument(
|
|
51
|
-
"--version", action="version", version="proxmox
|
|
52
|
+
"--version", action="version", version=f"proxmox {version('proxcli')}"
|
|
52
53
|
)
|
|
53
54
|
|
|
54
55
|
subparsers = parser.add_subparsers(dest="resource", title="resources", required=False)
|
|
@@ -176,6 +177,20 @@ def _build_client(overrides: dict[str, Any], args: argparse.Namespace) -> Proxmo
|
|
|
176
177
|
return client
|
|
177
178
|
|
|
178
179
|
|
|
180
|
+
def _print_command_help(args: argparse.Namespace) -> None:
|
|
181
|
+
"""When a subcommand has no explicit handler set, print the parent parser's help."""
|
|
182
|
+
# Try to reconstruct the appropriate parser and show its help
|
|
183
|
+
cmd_parts = ["proxmox", args.resource]
|
|
184
|
+
if hasattr(args, "fw_resource"):
|
|
185
|
+
cmd_parts.append("firewall")
|
|
186
|
+
cmd_parts.append(args.fw_resource)
|
|
187
|
+
elif hasattr(args, "action"):
|
|
188
|
+
cmd_parts.append(args.action)
|
|
189
|
+
log_error(
|
|
190
|
+
f"Missing required argument. Run '{' '.join(cmd_parts)} --help' for usage."
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
179
194
|
def main(argv: list[str] | None = None) -> None:
|
|
180
195
|
"""Main entry point."""
|
|
181
196
|
parser = build_root_parser()
|
|
@@ -206,8 +221,8 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
206
221
|
output = format_output(result, args.output)
|
|
207
222
|
print(output)
|
|
208
223
|
else:
|
|
209
|
-
#
|
|
210
|
-
|
|
224
|
+
# No handler was set — show relevant help
|
|
225
|
+
_print_command_help(args)
|
|
211
226
|
|
|
212
227
|
except ConfigError as exc:
|
|
213
228
|
log_error(str(exc))
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""`proxmox node` subcommand — node 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
|
+
|
|
10
|
+
|
|
11
|
+
def register_node_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
12
|
+
"""Register the `proxmox node` subcommand tree."""
|
|
13
|
+
node_parser = subparsers.add_parser("node", help="Manage Proxmox nodes")
|
|
14
|
+
node_sub = node_parser.add_subparsers(dest="action", title="actions", required=True)
|
|
15
|
+
|
|
16
|
+
# --- node list ---
|
|
17
|
+
node_list = node_sub.add_parser("list", help="List all nodes")
|
|
18
|
+
node_list.set_defaults(func=_node_list)
|
|
19
|
+
|
|
20
|
+
# --- node show ---
|
|
21
|
+
node_show = node_sub.add_parser("show", help="Show node details")
|
|
22
|
+
node_show.add_argument("node_name", help="Node name")
|
|
23
|
+
node_show.set_defaults(func=_node_show)
|
|
24
|
+
|
|
25
|
+
# --- node status ---
|
|
26
|
+
node_status = node_sub.add_parser("status", help="Show node status")
|
|
27
|
+
node_status.add_argument("node_name", nargs="?", help="Node name (omit for all nodes)")
|
|
28
|
+
node_status.set_defaults(func=_node_status)
|
|
29
|
+
|
|
30
|
+
# --- firewall ---
|
|
31
|
+
fw = node_sub.add_parser("firewall", help="Manage node firewall")
|
|
32
|
+
fw_sub = fw.add_subparsers(dest="fw_resource", title="resources", required=True)
|
|
33
|
+
|
|
34
|
+
fw_opts = fw_sub.add_parser("options", help="Show node firewall options")
|
|
35
|
+
fw_opts.add_argument("node_name", help="Node name")
|
|
36
|
+
fw_opts.set_defaults(func=_node_fw_options)
|
|
37
|
+
|
|
38
|
+
fw_enable = fw_sub.add_parser("enable", help="Enable node firewall")
|
|
39
|
+
fw_enable.add_argument("node_name", help="Node name")
|
|
40
|
+
fw_enable.set_defaults(func=_node_fw_enable)
|
|
41
|
+
|
|
42
|
+
fw_disable = fw_sub.add_parser("disable", help="Disable node firewall")
|
|
43
|
+
fw_disable.add_argument("node_name", help="Node name")
|
|
44
|
+
fw_disable.set_defaults(func=_node_fw_disable)
|
|
45
|
+
|
|
46
|
+
fw_policy = fw_sub.add_parser("policy", help="Set default input/output policy")
|
|
47
|
+
fw_policy.add_argument("node_name", help="Node name")
|
|
48
|
+
fw_policy.add_argument("--in-policy", choices=["ACCEPT", "DENY", "REJECT"], default=None,
|
|
49
|
+
help="Default input policy")
|
|
50
|
+
fw_policy.add_argument("--out-policy", choices=["ACCEPT", "DENY", "REJECT"], default=None,
|
|
51
|
+
help="Default output policy")
|
|
52
|
+
fw_policy.set_defaults(func=_node_fw_policy)
|
|
53
|
+
|
|
54
|
+
# firewall rules
|
|
55
|
+
rules = fw_sub.add_parser("rules", help="Manage node firewall rules")
|
|
56
|
+
rules.set_defaults(func=_node_fw_rules)
|
|
57
|
+
rules_sub = rules.add_subparsers(dest="fw_action", title="rule actions", required=False)
|
|
58
|
+
|
|
59
|
+
rules_list = rules_sub.add_parser("list", help="List rules")
|
|
60
|
+
rules_list.add_argument("node_name", help="Node name")
|
|
61
|
+
rules_list.set_defaults(func=_node_fw_rules)
|
|
62
|
+
|
|
63
|
+
rules_add = rules_sub.add_parser("add", help="Add a rule")
|
|
64
|
+
rules_add.add_argument("node_name", help="Node name")
|
|
65
|
+
add_firewall_rule_args(rules_add)
|
|
66
|
+
rules_add.set_defaults(func=_node_fw_rule_add)
|
|
67
|
+
|
|
68
|
+
rules_show = rules_sub.add_parser("show", help="Show a rule by position")
|
|
69
|
+
rules_show.add_argument("node_name", help="Node name")
|
|
70
|
+
rules_show.add_argument("pos", type=int, help="Rule position")
|
|
71
|
+
rules_show.set_defaults(func=_node_fw_rule_show)
|
|
72
|
+
|
|
73
|
+
rules_upd = rules_sub.add_parser("update", help="Update a rule by position")
|
|
74
|
+
rules_upd.add_argument("node_name", help="Node name")
|
|
75
|
+
rules_upd.add_argument("pos", type=int, help="Rule position")
|
|
76
|
+
add_firewall_rule_args(rules_upd)
|
|
77
|
+
for action in rules_upd._actions:
|
|
78
|
+
if action.dest == "action":
|
|
79
|
+
action.required = False
|
|
80
|
+
action.default = None
|
|
81
|
+
rules_upd.set_defaults(func=_node_fw_rule_upd)
|
|
82
|
+
|
|
83
|
+
rules_del = rules_sub.add_parser("delete", help="Delete a rule by position")
|
|
84
|
+
rules_del.add_argument("node_name", help="Node name")
|
|
85
|
+
rules_del.add_argument("pos", type=int, help="Rule position")
|
|
86
|
+
rules_del.set_defaults(func=_node_fw_rule_del)
|
|
87
|
+
|
|
88
|
+
# firewall refs
|
|
89
|
+
fw_refs = fw_sub.add_parser("refs", help="List firewall references for node")
|
|
90
|
+
fw_refs.add_argument("node_name", help="Node name")
|
|
91
|
+
fw_refs.add_argument("--type", default=None, choices=["alias", "ipset", "group"],
|
|
92
|
+
help="Filter by reference type")
|
|
93
|
+
fw_refs.set_defaults(func=_node_fw_refs)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
# Node handlers
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
def _node_list(_args: argparse.Namespace, client: ProxmoxClient) -> dict | list:
|
|
101
|
+
return client.get("/nodes")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _node_show(args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
105
|
+
return client.get(f"/nodes/{args.node_name}/status")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _node_status(args: argparse.Namespace, client: ProxmoxClient) -> dict | list:
|
|
109
|
+
if args.node_name:
|
|
110
|
+
return client.get(f"/nodes/{args.node_name}/status")
|
|
111
|
+
nodes = client.get("/nodes")
|
|
112
|
+
if isinstance(nodes, list):
|
|
113
|
+
result = []
|
|
114
|
+
for n in nodes:
|
|
115
|
+
node_name = n.get("node") if isinstance(n, dict) else n
|
|
116
|
+
try:
|
|
117
|
+
status = client.get(f"/nodes/{node_name}/status")
|
|
118
|
+
if isinstance(status, dict):
|
|
119
|
+
status["node"] = node_name
|
|
120
|
+
result.append(status)
|
|
121
|
+
except Exception:
|
|
122
|
+
result.append({"node": node_name, "status": "error"})
|
|
123
|
+
return result
|
|
124
|
+
return nodes
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ---------------------------------------------------------------------------
|
|
128
|
+
# Node firewall handlers
|
|
129
|
+
# ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
def _node_fw_options(args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
132
|
+
return client.get(f"/nodes/{args.node_name}/firewall/options")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _node_fw_enable(args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
136
|
+
return client.put(f"/nodes/{args.node_name}/firewall/options", data={"enable": 1})
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _node_fw_disable(args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
140
|
+
return client.put(f"/nodes/{args.node_name}/firewall/options", data={"enable": 0})
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _node_fw_policy(args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
144
|
+
data: dict = {}
|
|
145
|
+
if args.in_policy:
|
|
146
|
+
data["policy_in"] = args.in_policy
|
|
147
|
+
if args.out_policy:
|
|
148
|
+
data["policy_out"] = args.out_policy
|
|
149
|
+
if not data:
|
|
150
|
+
return {"error": "No policy specified. Use --in-policy or --out-policy"}
|
|
151
|
+
return client.put(f"/nodes/{args.node_name}/firewall/options", data=data)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _node_fw_rules(args: argparse.Namespace, client: ProxmoxClient) -> dict | list:
|
|
155
|
+
return client.get(f"/nodes/{args.node_name}/firewall/rules")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _node_fw_rule_add(args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
159
|
+
return client.post(f"/nodes/{args.node_name}/firewall/rules", data=build_rule_data(args))
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _node_fw_rule_show(args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
163
|
+
return client.get(f"/nodes/{args.node_name}/firewall/rules/{args.pos}")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _node_fw_rule_upd(args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
167
|
+
data = {k: v for k, v in build_rule_data(args).items() if v is not None and v != 0}
|
|
168
|
+
return client.put(f"/nodes/{args.node_name}/firewall/rules/{args.pos}", data=data)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _node_fw_rule_del(args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
172
|
+
return client.delete(f"/nodes/{args.node_name}/firewall/rules/{args.pos}")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _node_fw_refs(args: argparse.Namespace, client: ProxmoxClient) -> dict | list:
|
|
176
|
+
params = {"type": args.type} if args.type else None
|
|
177
|
+
return client.get(f"/nodes/{args.node_name}/firewall/refs", params=params)
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
6
|
|
|
7
|
+
from proxmox.cli.firewall_helpers import add_firewall_rule_args, build_rule_data
|
|
7
8
|
from proxmox.client.client import ProxmoxClient
|
|
8
9
|
from proxmox.utils.helpers import resolve_vmid, vmid_type
|
|
9
10
|
|
|
@@ -74,6 +75,80 @@ def register_vm_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
|
74
75
|
vm_delete.add_argument("--purge", action="store_true", help="Purge VM from all configurations")
|
|
75
76
|
vm_delete.set_defaults(func=_vm_delete)
|
|
76
77
|
|
|
78
|
+
# --- firewall ---
|
|
79
|
+
fw = vm_sub.add_parser("firewall", help="Manage VM firewall")
|
|
80
|
+
fw_sub = fw.add_subparsers(dest="fw_resource", title="resources", required=True)
|
|
81
|
+
|
|
82
|
+
fw_opts = fw_sub.add_parser("options", help="Show VM firewall options")
|
|
83
|
+
fw_opts.add_argument("vmid", type=vmid_type, help="VM ID")
|
|
84
|
+
fw_opts.add_argument("--node", help="Node name (auto-detected if omitted)")
|
|
85
|
+
fw_opts.set_defaults(func=_vm_fw_options)
|
|
86
|
+
|
|
87
|
+
fw_enable = fw_sub.add_parser("enable", help="Enable VM firewall")
|
|
88
|
+
fw_enable.add_argument("vmid", type=vmid_type, help="VM ID")
|
|
89
|
+
fw_enable.add_argument("--node", help="Node name (auto-detected if omitted)")
|
|
90
|
+
fw_enable.set_defaults(func=_vm_fw_enable)
|
|
91
|
+
|
|
92
|
+
fw_disable = fw_sub.add_parser("disable", help="Disable VM firewall")
|
|
93
|
+
fw_disable.add_argument("vmid", type=vmid_type, help="VM ID")
|
|
94
|
+
fw_disable.add_argument("--node", help="Node name (auto-detected if omitted)")
|
|
95
|
+
fw_disable.set_defaults(func=_vm_fw_disable)
|
|
96
|
+
|
|
97
|
+
fw_policy = fw_sub.add_parser("policy", help="Set default input/output policy for VM")
|
|
98
|
+
fw_policy.add_argument("vmid", type=vmid_type, help="VM ID")
|
|
99
|
+
fw_policy.add_argument("--node", help="Node name (auto-detected if omitted)")
|
|
100
|
+
fw_policy.add_argument("--in-policy", choices=["ACCEPT", "DENY", "REJECT"], default=None,
|
|
101
|
+
help="Default input policy")
|
|
102
|
+
fw_policy.add_argument("--out-policy", choices=["ACCEPT", "DENY", "REJECT"], default=None,
|
|
103
|
+
help="Default output policy")
|
|
104
|
+
fw_policy.set_defaults(func=_vm_fw_policy)
|
|
105
|
+
|
|
106
|
+
# firewall rules
|
|
107
|
+
rules = fw_sub.add_parser("rules", help="Manage VM firewall rules")
|
|
108
|
+
rules_sub = rules.add_subparsers(dest="fw_action", title="rule actions", required=False)
|
|
109
|
+
|
|
110
|
+
rules_list = rules_sub.add_parser("list", help="List rules")
|
|
111
|
+
rules_list.add_argument("vmid", type=vmid_type, help="VM ID")
|
|
112
|
+
rules_list.add_argument("--node", help="Node name (auto-detected if omitted)")
|
|
113
|
+
rules_list.set_defaults(func=_vm_fw_rules)
|
|
114
|
+
|
|
115
|
+
rules_add = rules_sub.add_parser("add", help="Add a rule")
|
|
116
|
+
rules_add.add_argument("vmid", type=vmid_type, help="VM ID")
|
|
117
|
+
rules_add.add_argument("--node", help="Node name (auto-detected if omitted)")
|
|
118
|
+
add_firewall_rule_args(rules_add)
|
|
119
|
+
rules_add.set_defaults(func=_vm_fw_rule_add)
|
|
120
|
+
|
|
121
|
+
rules_show = rules_sub.add_parser("show", help="Show a rule by position")
|
|
122
|
+
rules_show.add_argument("vmid", type=vmid_type, help="VM ID")
|
|
123
|
+
rules_show.add_argument("--node", help="Node name (auto-detected if omitted)")
|
|
124
|
+
rules_show.add_argument("pos", type=int, help="Rule position")
|
|
125
|
+
rules_show.set_defaults(func=_vm_fw_rule_show)
|
|
126
|
+
|
|
127
|
+
rules_upd = rules_sub.add_parser("update", help="Update a rule by position")
|
|
128
|
+
rules_upd.add_argument("vmid", type=vmid_type, help="VM ID")
|
|
129
|
+
rules_upd.add_argument("--node", help="Node name (auto-detected if omitted)")
|
|
130
|
+
rules_upd.add_argument("pos", type=int, help="Rule position")
|
|
131
|
+
add_firewall_rule_args(rules_upd)
|
|
132
|
+
for action in rules_upd._actions:
|
|
133
|
+
if action.dest == "action":
|
|
134
|
+
action.required = False
|
|
135
|
+
action.default = None
|
|
136
|
+
rules_upd.set_defaults(func=_vm_fw_rule_upd)
|
|
137
|
+
|
|
138
|
+
rules_del = rules_sub.add_parser("delete", help="Delete a rule by position")
|
|
139
|
+
rules_del.add_argument("vmid", type=vmid_type, help="VM ID")
|
|
140
|
+
rules_del.add_argument("--node", help="Node name (auto-detected if omitted)")
|
|
141
|
+
rules_del.add_argument("pos", type=int, help="Rule position")
|
|
142
|
+
rules_del.set_defaults(func=_vm_fw_rule_del)
|
|
143
|
+
|
|
144
|
+
# firewall refs
|
|
145
|
+
fw_refs = fw_sub.add_parser("refs", help="List VM firewall references")
|
|
146
|
+
fw_refs.add_argument("vmid", type=vmid_type, help="VM ID")
|
|
147
|
+
fw_refs.add_argument("--node", help="Node name (auto-detected if omitted)")
|
|
148
|
+
fw_refs.add_argument("--type", default=None, choices=["alias", "ipset", "group"],
|
|
149
|
+
help="Filter by reference type")
|
|
150
|
+
fw_refs.set_defaults(func=_vm_fw_refs)
|
|
151
|
+
|
|
77
152
|
|
|
78
153
|
# ---------------------------------------------------------------------------
|
|
79
154
|
# Helpers
|
|
@@ -209,3 +284,87 @@ def _vm_delete(args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
|
209
284
|
params["purge"] = 1
|
|
210
285
|
result = client.delete(f"/nodes/{node}/qemu/{args.vmid}", params=params or None)
|
|
211
286
|
return result if isinstance(result, dict) else {"data": result}
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
# ---------------------------------------------------------------------------
|
|
290
|
+
# VM firewall handlers
|
|
291
|
+
# ---------------------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
def _vm_fw_options(args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
294
|
+
node = _resolve_node(client, args.node, args.vmid)
|
|
295
|
+
if not node:
|
|
296
|
+
return {"error": f"VM {args.vmid} not found"}
|
|
297
|
+
return client.get(f"/nodes/{node}/qemu/{args.vmid}/firewall/options")
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _vm_fw_enable(args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
301
|
+
node = _resolve_node(client, args.node, args.vmid)
|
|
302
|
+
if not node:
|
|
303
|
+
return {"error": f"VM {args.vmid} not found"}
|
|
304
|
+
return client.put(f"/nodes/{node}/qemu/{args.vmid}/firewall/options", data={"enable": 1})
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _vm_fw_disable(args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
308
|
+
node = _resolve_node(client, args.node, args.vmid)
|
|
309
|
+
if not node:
|
|
310
|
+
return {"error": f"VM {args.vmid} not found"}
|
|
311
|
+
return client.put(f"/nodes/{node}/qemu/{args.vmid}/firewall/options", data={"enable": 0})
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _vm_fw_policy(args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
315
|
+
node = _resolve_node(client, args.node, args.vmid)
|
|
316
|
+
if not node:
|
|
317
|
+
return {"error": f"VM {args.vmid} not found"}
|
|
318
|
+
data: dict = {}
|
|
319
|
+
if args.in_policy:
|
|
320
|
+
data["policy_in"] = args.in_policy
|
|
321
|
+
if args.out_policy:
|
|
322
|
+
data["policy_out"] = args.out_policy
|
|
323
|
+
if not data:
|
|
324
|
+
return {"error": "No policy specified. Use --in-policy or --out-policy"}
|
|
325
|
+
return client.put(f"/nodes/{node}/qemu/{args.vmid}/firewall/options", data=data)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _vm_fw_rules(args: argparse.Namespace, client: ProxmoxClient) -> dict | list:
|
|
329
|
+
node = _resolve_node(client, args.node, args.vmid)
|
|
330
|
+
if not node:
|
|
331
|
+
return {"error": f"VM {args.vmid} not found"}
|
|
332
|
+
return client.get(f"/nodes/{node}/qemu/{args.vmid}/firewall/rules")
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _vm_fw_rule_add(args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
336
|
+
node = _resolve_node(client, args.node, args.vmid)
|
|
337
|
+
if not node:
|
|
338
|
+
return {"error": f"VM {args.vmid} not found"}
|
|
339
|
+
return client.post(f"/nodes/{node}/qemu/{args.vmid}/firewall/rules",
|
|
340
|
+
data=build_rule_data(args))
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _vm_fw_rule_show(args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
344
|
+
node = _resolve_node(client, args.node, args.vmid)
|
|
345
|
+
if not node:
|
|
346
|
+
return {"error": f"VM {args.vmid} not found"}
|
|
347
|
+
return client.get(f"/nodes/{node}/qemu/{args.vmid}/firewall/rules/{args.pos}")
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _vm_fw_rule_upd(args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
351
|
+
node = _resolve_node(client, args.node, args.vmid)
|
|
352
|
+
if not node:
|
|
353
|
+
return {"error": f"VM {args.vmid} not found"}
|
|
354
|
+
data = {k: v for k, v in build_rule_data(args).items() if v is not None and v != 0}
|
|
355
|
+
return client.put(f"/nodes/{node}/qemu/{args.vmid}/firewall/rules/{args.pos}", data=data)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _vm_fw_rule_del(args: argparse.Namespace, client: ProxmoxClient) -> dict:
|
|
359
|
+
node = _resolve_node(client, args.node, args.vmid)
|
|
360
|
+
if not node:
|
|
361
|
+
return {"error": f"VM {args.vmid} not found"}
|
|
362
|
+
return client.delete(f"/nodes/{node}/qemu/{args.vmid}/firewall/rules/{args.pos}")
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _vm_fw_refs(args: argparse.Namespace, client: ProxmoxClient) -> dict | list:
|
|
366
|
+
node = _resolve_node(client, args.node, args.vmid)
|
|
367
|
+
if not node:
|
|
368
|
+
return {"error": f"VM {args.vmid} not found"}
|
|
369
|
+
params: dict | None = {"type": args.type} if args.type else None
|
|
370
|
+
return client.get(f"/nodes/{node}/qemu/{args.vmid}/firewall/refs", params=params)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
version = 1
|
|
2
|
-
revision =
|
|
2
|
+
revision = 3
|
|
3
3
|
requires-python = ">=3.10"
|
|
4
4
|
|
|
5
5
|
[[package]]
|
|
@@ -254,7 +254,7 @@ wheels = [
|
|
|
254
254
|
|
|
255
255
|
[[package]]
|
|
256
256
|
name = "proxcli"
|
|
257
|
-
version = "0.
|
|
257
|
+
version = "0.3.0"
|
|
258
258
|
source = { editable = "." }
|
|
259
259
|
dependencies = [
|
|
260
260
|
{ name = "httpx" },
|
|
@@ -1,21 +0,0 @@
|
|
|
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")
|
|
@@ -1,55 +0,0 @@
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|