proxcli 0.9.0__tar.gz → 0.9.1__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 (57) hide show
  1. {proxcli-0.9.0 → proxcli-0.9.1}/CHANGELOG.md +12 -0
  2. {proxcli-0.9.0 → proxcli-0.9.1}/PKG-INFO +1 -1
  3. {proxcli-0.9.0 → proxcli-0.9.1}/TODO.md +4 -5
  4. proxcli-0.9.1/docs/api-permissions.md +153 -0
  5. proxcli-0.9.1/proxmox/cli/acl.py +95 -0
  6. {proxcli-0.9.0 → proxcli-0.9.1}/proxmox/cli/main.py +7 -1
  7. proxcli-0.9.1/proxmox/cli/role.py +78 -0
  8. proxcli-0.9.1/proxmox/cli/user.py +124 -0
  9. {proxcli-0.9.0 → proxcli-0.9.1}/pyproject.toml +1 -1
  10. {proxcli-0.9.0 → proxcli-0.9.1}/uv.lock +1 -1
  11. {proxcli-0.9.0 → proxcli-0.9.1}/.env.example +0 -0
  12. {proxcli-0.9.0 → proxcli-0.9.1}/.github/workflows/ci.yml +0 -0
  13. {proxcli-0.9.0 → proxcli-0.9.1}/.gitignore +0 -0
  14. {proxcli-0.9.0 → proxcli-0.9.1}/.python-version +0 -0
  15. {proxcli-0.9.0 → proxcli-0.9.1}/AGENTS.md +0 -0
  16. {proxcli-0.9.0 → proxcli-0.9.1}/PLAN.md +0 -0
  17. {proxcli-0.9.0 → proxcli-0.9.1}/PROJECT.md +0 -0
  18. {proxcli-0.9.0 → proxcli-0.9.1}/PROMPT.md +0 -0
  19. {proxcli-0.9.0 → proxcli-0.9.1}/README.md +0 -0
  20. {proxcli-0.9.0 → proxcli-0.9.1}/docs/cloud-init.md +0 -0
  21. {proxcli-0.9.0 → proxcli-0.9.1}/proxmox/__init__.py +0 -0
  22. {proxcli-0.9.0 → proxcli-0.9.1}/proxmox/cli/__init__.py +0 -0
  23. {proxcli-0.9.0 → proxcli-0.9.1}/proxmox/cli/auth.py +0 -0
  24. {proxcli-0.9.0 → proxcli-0.9.1}/proxmox/cli/cluster.py +0 -0
  25. {proxcli-0.9.0 → proxcli-0.9.1}/proxmox/cli/completion.py +0 -0
  26. {proxcli-0.9.0 → proxcli-0.9.1}/proxmox/cli/container.py +0 -0
  27. {proxcli-0.9.0 → proxcli-0.9.1}/proxmox/cli/firewall_helpers.py +0 -0
  28. {proxcli-0.9.0 → proxcli-0.9.1}/proxmox/cli/node.py +0 -0
  29. {proxcli-0.9.0 → proxcli-0.9.1}/proxmox/cli/pool.py +0 -0
  30. {proxcli-0.9.0 → proxcli-0.9.1}/proxmox/cli/storage.py +0 -0
  31. {proxcli-0.9.0 → proxcli-0.9.1}/proxmox/cli/tasks.py +0 -0
  32. {proxcli-0.9.0 → proxcli-0.9.1}/proxmox/cli/vm.py +0 -0
  33. {proxcli-0.9.0 → proxcli-0.9.1}/proxmox/client/__init__.py +0 -0
  34. {proxcli-0.9.0 → proxcli-0.9.1}/proxmox/client/auth.py +0 -0
  35. {proxcli-0.9.0 → proxcli-0.9.1}/proxmox/client/client.py +0 -0
  36. {proxcli-0.9.0 → proxcli-0.9.1}/proxmox/client/exceptions.py +0 -0
  37. {proxcli-0.9.0 → proxcli-0.9.1}/proxmox/config/__init__.py +0 -0
  38. {proxcli-0.9.0 → proxcli-0.9.1}/proxmox/config/config.py +0 -0
  39. {proxcli-0.9.0 → proxcli-0.9.1}/proxmox/config/models.py +0 -0
  40. {proxcli-0.9.0 → proxcli-0.9.1}/proxmox/output/__init__.py +0 -0
  41. {proxcli-0.9.0 → proxcli-0.9.1}/proxmox/output/formatter.py +0 -0
  42. {proxcli-0.9.0 → proxcli-0.9.1}/proxmox/output/json_fmt.py +0 -0
  43. {proxcli-0.9.0 → proxcli-0.9.1}/proxmox/output/table_fmt.py +0 -0
  44. {proxcli-0.9.0 → proxcli-0.9.1}/proxmox/output/yaml_fmt.py +0 -0
  45. {proxcli-0.9.0 → proxcli-0.9.1}/proxmox/utils/__init__.py +0 -0
  46. {proxcli-0.9.0 → proxcli-0.9.1}/proxmox/utils/helpers.py +0 -0
  47. {proxcli-0.9.0 → proxcli-0.9.1}/proxmox/utils/logging.py +0 -0
  48. {proxcli-0.9.0 → proxcli-0.9.1}/tests/__init__.py +0 -0
  49. {proxcli-0.9.0 → proxcli-0.9.1}/tests/conftest.py +0 -0
  50. {proxcli-0.9.0 → proxcli-0.9.1}/tests/test_auth.py +0 -0
  51. {proxcli-0.9.0 → proxcli-0.9.1}/tests/test_cli/__init__.py +0 -0
  52. {proxcli-0.9.0 → proxcli-0.9.1}/tests/test_cli/test_main.py +0 -0
  53. {proxcli-0.9.0 → proxcli-0.9.1}/tests/test_client.py +0 -0
  54. {proxcli-0.9.0 → proxcli-0.9.1}/tests/test_config.py +0 -0
  55. {proxcli-0.9.0 → proxcli-0.9.1}/tests/test_integration/__init__.py +0 -0
  56. {proxcli-0.9.0 → proxcli-0.9.1}/tests/test_output/__init__.py +0 -0
  57. {proxcli-0.9.0 → proxcli-0.9.1}/tests/test_output/test_formatter.py +0 -0
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.9.1] - 2026-06-20
11
+
12
+ ### Added
13
+ - **user, role, and ACL management**: ``proxmox user`` (list, show, create,
14
+ update, delete), ``proxmox role`` (list, show, create, update, delete),
15
+ ``proxmox acl`` (list, show, add, delete). Wraps ``/access/users``,
16
+ ``/access/roles``, and ``/access/acl`` endpoints. ACL write operations
17
+ require ``Permissions.Modify`` (Administrator role).
18
+
10
19
  ## [0.9.0] - 2026-06-20
11
20
 
12
21
  ### Added
@@ -25,6 +34,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
25
34
  - **docs/cloud-init.md**: complete guide on creating and managing
26
35
  cloud-init VMs with proxcli, including prerequisites, examples,
27
36
  custom user-data, and troubleshooting.
37
+ - **docs/api-permissions.md**: minimum API privilege reference for the
38
+ cloud-init VM workflow and other proxcli operations.
28
39
 
29
40
  ## [0.8.2] - 2026-06-20
30
41
 
@@ -155,6 +166,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
155
166
  - CSRF ticket auto-refresh on 401.
156
167
  - AI-agent-friendly: default JSON output, strict exit codes, `--dry-run` mode.
157
168
 
169
+ [0.9.1]: https://github.com/xezpeleta/proxcli/releases/tag/v0.9.1
158
170
  [0.9.0]: https://github.com/xezpeleta/proxcli/releases/tag/v0.9.0
159
171
  [0.8.2]: https://github.com/xezpeleta/proxcli/releases/tag/v0.8.2
160
172
  [0.8.1]: https://github.com/xezpeleta/proxcli/releases/tag/v0.8.1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proxcli
3
- Version: 0.9.0
3
+ Version: 0.9.1
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
@@ -15,6 +15,10 @@ Completed items are marked with a check. Implementation notes are preserved for
15
15
  - [x] **QEMU guest agent interfaces** — `proxmox vm agent interfaces <vmid>`. Wraps `/nodes/{node}/qemu/{vmid}/agent/network-get-interfaces`.
16
16
  - [x] **Streaming task logs** — `proxmox task log <upid> [--follow]`. Polls `/nodes/{node}/tasks/{upid}/log`.
17
17
  - [x] **Global flag hint** — If user places `--output` / `--dry-run` / etc. after the resource, a hint suggests the correct order.
18
+ - [x] **User & permission management** — `proxmox user` (list/show/create/update/delete), `proxmox role` (list/show/create/update/delete), `proxmox acl` (list/show/add/delete). Wraps `/access/users`, `/access/roles`, `/access/acl`. ACL write requires `Permissions.Modify` (Administrator).
19
+ - [x] **VM cloud-init support** — `vm create` flags for citype, ciuser, cipassword, sshkeys, nameserver, searchdomain, cicustom + auto cloud-init drive creation. `vm cloudinit generate` for regeneration.
20
+ - [x] **VM disk import** — `vm create --import-from <storage:path>` imports an existing disk image as VM boot disk.
21
+ - [x] **Docs** — `docs/cloud-init.md` (cloud-init VM workflow), `docs/api-permissions.md` (minimum API privileges).
18
22
 
19
23
  ## v1.1 — Polish & Usability
20
24
 
@@ -34,11 +38,6 @@ Completed items are marked with a check. Implementation notes are preserved for
34
38
  - [ ] **Backup (`vzdump`) management**
35
39
  - `proxmox backup` subcommand: `list`, `create`, `show`, `delete`. Wrap `/nodes/{node}/vzdump` and `/nodes/{node}/storage/{storage}/content` for backup files.
36
40
 
37
- - [ ] **User & permission management**
38
- - `proxmox user` subcommand: `list`, `show`, `create`, `update`, `delete`.
39
- - `proxmox role` subcommand: `list`, `show`, `create`, `update`, `delete`.
40
- - `proxmox acl` subcommand: `list`, `show`. Wraps `/access/users`, `/access/roles`, `/access/acl`, `/access/groups`.
41
-
42
41
  - [ ] **Network management**
43
42
  - `proxmox network` subcommand: `list`, `show`, `update` for bridges, bonds, VLANs. Wraps `/nodes/{node}/network`.
44
43
 
@@ -0,0 +1,153 @@
1
+ # API Token Permissions
2
+
3
+ proxcli uses the Proxmox VE REST API. This document describes how to create
4
+ a minimal-permission API token for common workflows.
5
+
6
+ ## Creating an API Token
7
+
8
+ In the Proxmox VE UI: **Datacenter → Permissions → API Tokens → Add**
9
+
10
+ 1. Select **User**
11
+ 2. Enter **Token ID** (e.g. `proxcli`)
12
+ 3. Uncheck **Privilege Separation** (for simplicity) or leave it checked
13
+ if you want to limit the token's permissions
14
+
15
+ The **Secret** is shown only once — save it immediately.
16
+
17
+ ## Permission Model
18
+
19
+ Proxmox permissions follow the pattern:
20
+
21
+ ```
22
+ /path/to/resource PrivilegeName[,PrivilegeName...]
23
+ ```
24
+
25
+ - Paths can be broad (`/`) or specific (`/vms/100`)
26
+ - Privileges are inherited — permissions on `/` propagate to all sub-paths
27
+ - An API token's effective permissions are the **intersection** of:
28
+ 1. The token's own ACLs
29
+ 2. The user's ACLs (if privilege separation is enabled)
30
+
31
+ ## Step-by-Step: Cloud-Init VM Workflow
32
+
33
+ The complete workflow of uploading a cloud image, creating a VM with
34
+ cloud-init, and starting it uses these endpoints:
35
+
36
+ | Step | Method | Endpoint | Privilege |
37
+ |------|--------|----------|-----------|
38
+ | 1 | GET | `/cluster/nextid` | `Sys.Audit` |
39
+ | 2 | POST | `/nodes/{node}/storage/{storage}/upload` | `Datastore.AllocateTemplate` |
40
+ | 3 | POST | `/nodes/{node}/qemu` | `VM.Allocate` |
41
+ | 3 | — | (reads imported image from source storage) | `Datastore.Allocate` |
42
+ | 3 | — | (allocates disk on target storage) | `Datastore.AllocateSpace` |
43
+ | 3 | — | (attaches scsi0 disk) | `VM.Config.Disk` |
44
+ | 3 | — | (sets net0) | `VM.Config.Network` |
45
+ | 3 | — | (sets cloud-init: citype, ciuser, sshkeys, etc.) | `VM.Config.Cloudinit` |
46
+ | 3 | — | (sets bios, machine, boot order) | `VM.Config.Options` |
47
+ | 4 | POST | `/nodes/{node}/qemu/{vmid}/status/start` | `VM.PowerMgmt` |
48
+ | 5 | GET | `/nodes/{node}/qemu/{vmid}/status/current` | `VM.Audit` |
49
+ | 5 | GET | `/cluster/resources` | `Sys.Audit` |
50
+
51
+ ## Minimal Role: PVECloudInitAdmin
52
+
53
+ Create a role with only the 11 required privileges:
54
+
55
+ ```
56
+ Role name: PVECloudInitAdmin
57
+
58
+ Privileges:
59
+ Sys.Audit
60
+ Datastore.Allocate
61
+ Datastore.AllocateSpace
62
+ Datastore.AllocateTemplate
63
+ VM.Allocate
64
+ VM.Audit
65
+ VM.Config.Cloudinit
66
+ VM.Config.Disk
67
+ VM.Config.Network
68
+ VM.Config.Options
69
+ VM.PowerMgmt
70
+ ```
71
+
72
+ ### ACLs (broad — covers all storages and VMs)
73
+
74
+ ```
75
+ Add → Path: /
76
+ Add → Path: /storage
77
+ Add → Path: /vms
78
+ ```
79
+
80
+ Assign the `PVECloudInitAdmin` role to all three paths for your user or
81
+ token. Since permissions inherit, `/vms` covers all VMs and
82
+ `/storage` covers all storages.
83
+
84
+ ### ACLs (narrow — single storage, single node)
85
+
86
+ If you know exactly which storages you'll use, lock it down further:
87
+
88
+ ```
89
+ Path: / Role: PVECloudInitAdmin (only for Sys.Audit)
90
+ Path: /storage/local Role: PVECloudInitAdmin (for upload + import)
91
+ Path: /storage/rbd_ssd Role: PVECloudInitAdmin (for disk allocation)
92
+ Path: /vms Role: PVECloudInitAdmin (for VM operations)
93
+ ```
94
+
95
+ Or even narrower per-VM:
96
+
97
+ ```
98
+ Path: /vms/100-199 Role: PVECloudInitAdmin
99
+ ```
100
+
101
+ ## Additional Privileges for Other Workflows
102
+
103
+ | Feature | Extra Privileges |
104
+ |---------|-----------------|
105
+ | Snapshots | `VM.Snapshot`, `VM.Snapshot.Rollback` |
106
+ | Clone VM | `VM.Clone` |
107
+ | Change memory/CPU | `VM.Config.Memory`, `VM.Config.CPU` |
108
+ | Attach ISOs | `VM.Config.CDROM` |
109
+ | Migrate VM | `VM.Migrate` |
110
+ | Backup VM | `VM.Backup` |
111
+ | Delete VM | `VM.Allocate` (already included), `Datastore.AllocateSpace` |
112
+ | QEMU guest agent | `VM.GuestAgent.Audit`, `VM.GuestAgent.FileRead` |
113
+ | Containers | `VM.Allocate` (LXC creation), `VM.Audit`, `VM.PowerMgmt` |
114
+ | Pools | `Pool.Allocate`, `Pool.Audit` |
115
+ | Cluster firewall | `Sys.Modify`, `Sys.Audit` |
116
+ | Node firewall | `Sys.Modify` |
117
+ | VM firewall | `VM.Allocate` (already included) |
118
+ | ACL management | `Permissions.Modify` (Administrator role) |
119
+
120
+ ## Full Admin Role (for comparison)
121
+
122
+ The built-in **PVEVMAdmin** role includes:
123
+
124
+ ```
125
+ VM.Allocate, VM.Audit, VM.Backup, VM.Clone, VM.Config.CDROM,
126
+ VM.Config.Cloudinit, VM.Config.CPU, VM.Config.Disk, VM.Config.HWType,
127
+ VM.Config.Memory, VM.Config.Network, VM.Config.Options, VM.Console,
128
+ VM.Migrate, VM.Monitor, VM.PowerMgmt, VM.Snapshot, VM.Snapshot.Rollback
129
+ ```
130
+
131
+ The built-in **PVEDatastoreAdmin** adds:
132
+
133
+ ```
134
+ Datastore.Allocate, Datastore.AllocateSpace, Datastore.AllocateTemplate,
135
+ Datastore.Audit, Datastore.Copy
136
+ ```
137
+
138
+ ## Verifying Permissions
139
+
140
+ Check your current effective permissions:
141
+
142
+ ```bash
143
+ proxmox auth permissions
144
+ ```
145
+
146
+ Or test a specific action with dry-run:
147
+
148
+ ```bash
149
+ proxmox --dry-run vm create --node <node> --memory 512 --cores 1
150
+ ```
151
+
152
+ The dry-run output shows the exact endpoint and HTTP method — if any
153
+ privilege is missing, Proxmox will return a **403 Forbidden**.
@@ -0,0 +1,95 @@
1
+ """CLI subcommand for ACL management (`proxmox acl`)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+
7
+ from ..client.client import ProxmoxClient
8
+
9
+ # ---------------------------------------------------------------------------
10
+ # Handlers
11
+ # ---------------------------------------------------------------------------
12
+
13
+ def _acl_list(args: argparse.Namespace, client: ProxmoxClient) -> list | dict:
14
+ result = client.get("/access/acl")
15
+ if isinstance(result, list):
16
+ return result
17
+ if isinstance(result, dict) and "data" in result:
18
+ return result["data"]
19
+ return result
20
+
21
+
22
+ def _acl_show(args: argparse.Namespace, client: ProxmoxClient) -> dict | list:
23
+ params = {"path": args.path}
24
+ result = client.get("/access/acl", params=params)
25
+ if isinstance(result, dict) and "data" in result:
26
+ return result["data"]
27
+ return result
28
+
29
+
30
+ def _acl_create(args: argparse.Namespace, client: ProxmoxClient) -> dict:
31
+ data: dict = {
32
+ "path": args.path,
33
+ "roles": args.roles,
34
+ }
35
+ if args.users:
36
+ data["users"] = args.users
37
+ if args.groups:
38
+ data["groups"] = args.groups
39
+ if args.tokens:
40
+ data["tokens"] = args.tokens
41
+ if args.propagate is False:
42
+ data["propagate"] = 0
43
+ return client.put("/access/acl", data=data)
44
+
45
+
46
+ def _acl_delete(args: argparse.Namespace, client: ProxmoxClient) -> dict:
47
+ data: dict = {"path": args.path}
48
+ if args.roles:
49
+ data["roles"] = args.roles
50
+ if args.users:
51
+ data["users"] = args.users
52
+ if args.groups:
53
+ data["groups"] = args.groups
54
+ if args.tokens:
55
+ data["tokens"] = args.tokens
56
+ return client.put("/access/acl", data={"delete": 1, **data})
57
+
58
+
59
+ # ---------------------------------------------------------------------------
60
+ # Parser registration
61
+ # ---------------------------------------------------------------------------
62
+
63
+ def register_acl_parser(subparsers: argparse._SubParsersAction) -> None:
64
+ acl_parser = subparsers.add_parser("acl", help="Manage ACLs")
65
+ acl_sub = acl_parser.add_subparsers(dest="action", title="actions", required=True)
66
+
67
+ # acl list
68
+ acl_list = acl_sub.add_parser("list", help="List all ACLs")
69
+ acl_list.set_defaults(func=_acl_list)
70
+
71
+ # acl show
72
+ acl_show = acl_sub.add_parser("show", help="Show ACLs for a path")
73
+ acl_show.add_argument("path", help="ACL path (e.g. /vms/100)")
74
+ acl_show.set_defaults(func=_acl_show)
75
+
76
+ # acl create
77
+ acl_create = acl_sub.add_parser("add", help="Add ACL entry")
78
+ acl_create.add_argument("path", help="ACL path (e.g. /vms)")
79
+ acl_create.add_argument("--roles", help="Comma-separated role IDs")
80
+ acl_create.add_argument("--users", help="Comma-separated user IDs")
81
+ acl_create.add_argument("--groups", help="Comma-separated group IDs")
82
+ acl_create.add_argument("--tokens", help="Comma-separated token IDs")
83
+ acl_create.add_argument("--no-propagate", dest="propagate", action="store_false",
84
+ default=True,
85
+ help="Disable permission propagation to children")
86
+ acl_create.set_defaults(func=_acl_create)
87
+
88
+ # acl delete
89
+ acl_delete = acl_sub.add_parser("delete", help="Remove ACL entry")
90
+ acl_delete.add_argument("path", help="ACL path (e.g. /vms)")
91
+ acl_delete.add_argument("--roles", help="Comma-separated role IDs")
92
+ acl_delete.add_argument("--users", help="Comma-separated user IDs")
93
+ acl_delete.add_argument("--groups", help="Comma-separated group IDs")
94
+ acl_delete.add_argument("--tokens", help="Comma-separated token IDs")
95
+ acl_delete.set_defaults(func=_acl_delete)
@@ -55,16 +55,20 @@ def build_root_parser() -> argparse.ArgumentParser:
55
55
  subparsers = parser.add_subparsers(dest="resource", title="resources", required=False)
56
56
 
57
57
  # Import and register subcommands
58
+ from proxmox.cli.acl import register_acl_parser
58
59
  from proxmox.cli.auth import register_auth_parser
59
60
  from proxmox.cli.cluster import register_cluster_parser
60
61
  from proxmox.cli.completion import register_completion_parser
61
62
  from proxmox.cli.container import register_container_parser
62
63
  from proxmox.cli.node import register_node_parser
63
64
  from proxmox.cli.pool import register_pool_parser
65
+ from proxmox.cli.role import register_role_parser
64
66
  from proxmox.cli.storage import register_storage_parser
65
67
  from proxmox.cli.tasks import register_task_parser
68
+ from proxmox.cli.user import register_user_parser
66
69
  from proxmox.cli.vm import register_vm_parser
67
70
 
71
+ register_acl_parser(subparsers)
68
72
  register_auth_parser(subparsers)
69
73
  register_vm_parser(subparsers)
70
74
  register_node_parser(subparsers)
@@ -74,6 +78,8 @@ def build_root_parser() -> argparse.ArgumentParser:
74
78
  register_cluster_parser(subparsers)
75
79
  register_completion_parser(subparsers)
76
80
  register_task_parser(subparsers)
81
+ register_role_parser(subparsers)
82
+ register_user_parser(subparsers)
77
83
 
78
84
  return parser
79
85
 
@@ -206,7 +212,7 @@ GLOBAL_FLAGS_WITH_VALUE = {"--url", "--username", "--password", "--api-token", "
206
212
  def _hint_global_flags_order(argv: list[str]) -> None:
207
213
  """If user placed global flags after the resource, show a helpful hint."""
208
214
  resource_pos = -1
209
- resources = {"auth", "vm", "node", "pool", "container", "storage", "cluster", "completion", "task"}
215
+ resources = {"auth", "vm", "node", "pool", "container", "storage", "cluster", "completion", "task", "user", "role", "acl"}
210
216
  for i, arg in enumerate(argv):
211
217
  if arg in resources:
212
218
  resource_pos = i
@@ -0,0 +1,78 @@
1
+ """CLI subcommand for role management (`proxmox role`)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+
7
+ from ..client.client import ProxmoxClient
8
+
9
+ # ---------------------------------------------------------------------------
10
+ # Handlers
11
+ # ---------------------------------------------------------------------------
12
+
13
+ def _role_list(args: argparse.Namespace, client: ProxmoxClient) -> list | dict:
14
+ result = client.get("/access/roles")
15
+ if isinstance(result, list):
16
+ return result
17
+ if isinstance(result, dict) and "data" in result:
18
+ return result["data"]
19
+ return result
20
+
21
+
22
+ def _role_show(args: argparse.Namespace, client: ProxmoxClient) -> dict:
23
+ return client.get(f"/access/roles/{args.roleid}")
24
+
25
+
26
+ def _role_create(args: argparse.Namespace, client: ProxmoxClient) -> dict:
27
+ data: dict = {"roleid": args.roleid}
28
+ if args.privs:
29
+ data["privs"] = args.privs
30
+ return client.post("/access/roles", data=data)
31
+
32
+
33
+ def _role_update(args: argparse.Namespace, client: ProxmoxClient) -> dict:
34
+ data: dict = {}
35
+ if args.privs:
36
+ data["privs"] = args.privs
37
+ if not data:
38
+ return {"error": "No fields to update"}
39
+ return client.put(f"/access/roles/{args.roleid}", data=data)
40
+
41
+
42
+ def _role_delete(args: argparse.Namespace, client: ProxmoxClient) -> dict:
43
+ return client.delete(f"/access/roles/{args.roleid}")
44
+
45
+
46
+ # ---------------------------------------------------------------------------
47
+ # Parser registration
48
+ # ---------------------------------------------------------------------------
49
+
50
+ def register_role_parser(subparsers: argparse._SubParsersAction) -> None:
51
+ role_parser = subparsers.add_parser("role", help="Manage roles")
52
+ role_sub = role_parser.add_subparsers(dest="action", title="actions", required=True)
53
+
54
+ # role list
55
+ role_list = role_sub.add_parser("list", help="List roles")
56
+ role_list.set_defaults(func=_role_list)
57
+
58
+ # role show
59
+ role_show = role_sub.add_parser("show", help="Show role details")
60
+ role_show.add_argument("roleid", help="Role ID (e.g. PVEAdmin)")
61
+ role_show.set_defaults(func=_role_show)
62
+
63
+ # role create
64
+ role_create = role_sub.add_parser("create", help="Create role")
65
+ role_create.add_argument("roleid", help="Role ID")
66
+ role_create.add_argument("--privs", help="Comma-separated privilege list")
67
+ role_create.set_defaults(func=_role_create)
68
+
69
+ # role update
70
+ role_update = role_sub.add_parser("update", help="Update role privileges")
71
+ role_update.add_argument("roleid", help="Role ID")
72
+ role_update.add_argument("--privs", help="Comma-separated privilege list")
73
+ role_update.set_defaults(func=_role_update)
74
+
75
+ # role delete
76
+ role_delete = role_sub.add_parser("delete", help="Delete role")
77
+ role_delete.add_argument("roleid", help="Role ID")
78
+ role_delete.set_defaults(func=_role_delete)
@@ -0,0 +1,124 @@
1
+ """CLI subcommand for user management (`proxmox user`)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+
7
+ from ..client.client import ProxmoxClient
8
+
9
+ # ---------------------------------------------------------------------------
10
+ # Handlers
11
+ # ---------------------------------------------------------------------------
12
+
13
+ def _user_list(args: argparse.Namespace, client: ProxmoxClient) -> list | dict:
14
+ result = client.get("/access/users")
15
+ if isinstance(result, list):
16
+ return result
17
+ if isinstance(result, dict) and "data" in result:
18
+ return result["data"]
19
+ return result
20
+
21
+
22
+ def _user_show(args: argparse.Namespace, client: ProxmoxClient) -> dict:
23
+ return client.get(f"/access/users/{args.userid}")
24
+
25
+
26
+ def _user_create(args: argparse.Namespace, client: ProxmoxClient) -> dict:
27
+ data: dict = {"userid": args.userid}
28
+ if args.password:
29
+ data["password"] = args.password
30
+ if args.email:
31
+ data["email"] = args.email
32
+ if args.firstname:
33
+ data["firstname"] = args.firstname
34
+ if args.lastname:
35
+ data["lastname"] = args.lastname
36
+ if args.enable is False:
37
+ data["enable"] = 0
38
+ if args.expire is not None:
39
+ data["expire"] = args.expire
40
+ if args.group:
41
+ data["groups"] = ",".join(args.group)
42
+ return client.post("/access/users", data=data)
43
+
44
+
45
+ def _user_update(args: argparse.Namespace, client: ProxmoxClient) -> dict:
46
+ data: dict = {}
47
+ if args.password:
48
+ data["password"] = args.password
49
+ if args.email:
50
+ data["email"] = args.email
51
+ if args.firstname:
52
+ data["firstname"] = args.firstname
53
+ if args.lastname:
54
+ data["lastname"] = args.lastname
55
+ if args.enable is not False:
56
+ data["enable"] = 0
57
+ if args.expire is not None:
58
+ data["expire"] = args.expire
59
+ if args.group:
60
+ data["groups"] = ",".join(args.group)
61
+ if not data:
62
+ return {"error": "No fields to update"}
63
+ return client.put(f"/access/users/{args.userid}", data=data)
64
+
65
+
66
+ def _user_delete(args: argparse.Namespace, client: ProxmoxClient) -> dict:
67
+ return client.delete(f"/access/users/{args.userid}")
68
+
69
+
70
+ # ---------------------------------------------------------------------------
71
+ # Parser registration
72
+ # ---------------------------------------------------------------------------
73
+
74
+ def register_user_parser(subparsers: argparse._SubParsersAction) -> None:
75
+ user_parser = subparsers.add_parser("user", help="Manage users")
76
+ user_sub = user_parser.add_subparsers(dest="action", title="actions", required=True)
77
+
78
+ # user list
79
+ user_list = user_sub.add_parser("list", help="List users")
80
+ user_list.set_defaults(func=_user_list)
81
+
82
+ # user show
83
+ user_show = user_sub.add_parser("show", help="Show user details")
84
+ user_show.add_argument("userid", help="User ID (e.g. xezpeleta@pve)")
85
+ user_show.set_defaults(func=_user_show)
86
+
87
+ # user create
88
+ user_create = user_sub.add_parser("create", help="Create user")
89
+ user_create.add_argument("userid", help="User ID (e.g. newuser@pve)")
90
+ user_create.add_argument("--password", help="User password")
91
+ user_create.add_argument("--enable", action="store_true", default=True,
92
+ help="Enable user (default)")
93
+ user_create.add_argument("--disable", dest="enable", action="store_false",
94
+ help="Disable user account")
95
+ user_create.add_argument("--firstname", help="First name")
96
+ user_create.add_argument("--lastname", help="Last name")
97
+ user_create.add_argument("--email", help="Email address")
98
+ user_create.add_argument("--expire", type=int, default=0,
99
+ help="Account expiration (Unix timestamp, 0 = never)")
100
+ user_create.add_argument("--group", action="append",
101
+ help="Group to add user to (repeatable)")
102
+ user_create.set_defaults(func=_user_create)
103
+
104
+ # user update
105
+ user_update = user_sub.add_parser("update", help="Update user")
106
+ user_update.add_argument("userid", help="User ID (e.g. xezpeleta@pve)")
107
+ user_update.add_argument("--password", help="New password")
108
+ user_update.add_argument("--enable", action="store_true", default=None,
109
+ help="Enable user")
110
+ user_update.add_argument("--disable", dest="enable", action="store_false",
111
+ help="Disable user account")
112
+ user_update.add_argument("--firstname", help="First name")
113
+ user_update.add_argument("--lastname", help="Last name")
114
+ user_update.add_argument("--email", help="Email address")
115
+ user_update.add_argument("--expire", type=int, default=None,
116
+ help="Account expiration (Unix timestamp, 0 = never)")
117
+ user_update.add_argument("--group", action="append",
118
+ help="Group to add user to (repeatable)")
119
+ user_update.set_defaults(func=_user_update)
120
+
121
+ # user delete
122
+ user_delete = user_sub.add_parser("delete", help="Delete user")
123
+ user_delete.add_argument("userid", help="User ID (e.g. newuser@pve)")
124
+ user_delete.set_defaults(func=_user_delete)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "proxcli"
3
- version = "0.9.0"
3
+ version = "0.9.1"
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.9.0"
257
+ version = "0.9.1"
258
258
  source = { editable = "." }
259
259
  dependencies = [
260
260
  { name = "httpx" },
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