aioproxmox 0.0.3a0__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.
- aioproxmox-0.0.3a0/LICENSE +21 -0
- aioproxmox-0.0.3a0/PKG-INFO +158 -0
- aioproxmox-0.0.3a0/README.md +137 -0
- aioproxmox-0.0.3a0/aioproxmox/__init__.py +300 -0
- aioproxmox-0.0.3a0/aioproxmox/const.py +3 -0
- aioproxmox-0.0.3a0/aioproxmox/endpoints.py +406 -0
- aioproxmox-0.0.3a0/aioproxmox/exceptions.py +23 -0
- aioproxmox-0.0.3a0/aioproxmox/helpers.py +53 -0
- aioproxmox-0.0.3a0/aioproxmox/model/__init__.py +62 -0
- aioproxmox-0.0.3a0/aioproxmox/model/pve.py +444 -0
- aioproxmox-0.0.3a0/aioproxmox.egg-info/PKG-INFO +158 -0
- aioproxmox-0.0.3a0/aioproxmox.egg-info/SOURCES.txt +22 -0
- aioproxmox-0.0.3a0/aioproxmox.egg-info/dependency_links.txt +1 -0
- aioproxmox-0.0.3a0/aioproxmox.egg-info/requires.txt +2 -0
- aioproxmox-0.0.3a0/aioproxmox.egg-info/top_level.txt +1 -0
- aioproxmox-0.0.3a0/pyproject.toml +759 -0
- aioproxmox-0.0.3a0/setup.cfg +4 -0
- aioproxmox-0.0.3a0/tests/test_aioproxmox.py +14 -0
- aioproxmox-0.0.3a0/tests/test_endpoints.py +377 -0
- aioproxmox-0.0.3a0/tests/test_helpers.py +42 -0
- aioproxmox-0.0.3a0/tests/test_init.py +263 -0
- aioproxmox-0.0.3a0/tests/test_model_init.py +43 -0
- aioproxmox-0.0.3a0/tests/test_pve_cluster.py +234 -0
- aioproxmox-0.0.3a0/tests/test_pve_migrate.py +62 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 by @CoMPaTech and @erwindouna
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aioproxmox
|
|
3
|
+
Version: 0.0.3a0
|
|
4
|
+
Summary: Asynchronous Python library for Proxmox products.
|
|
5
|
+
Maintainer: CoMPaTech, erwindouna
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Source Code, https://github.com/compatech/aioproxmox
|
|
8
|
+
Project-URL: Bug Reports, https://github.com/compatech/aioproxmox/issues
|
|
9
|
+
Keywords: home,automation,proxmox,pve,proxmoxve,pdm,proxmoxdm,pbs,proxmoxbs
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Topic :: Home Automation
|
|
15
|
+
Requires-Python: >=3.14
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: aiohttp>=3.14
|
|
19
|
+
Requires-Dist: mashumaro>=3.22
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# Asynchronous Python library for Proxmox products
|
|
23
|
+
|
|
24
|
+
An asynchronous, type-safe Python client tailored for extracting raw performance telemetry out of Proxmox VE clusters, nodes, and guests.
|
|
25
|
+
|
|
26
|
+
## Why another Proxmox library
|
|
27
|
+
|
|
28
|
+
This library was built out of necessity because existing ecosystem integrations and underlying client libraries have structural limitations. Whether they are synchronous bottlenecks, missing granular unnested endpoint layouts, or lacking strict runtime type enforcement. This library serves as our high-performance bridge today while we wait for any other mainstream library to catch up, open up, or accept modern async paradigms. Once they do, this phase can comfortably conclude.
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
* **Strict Asynchronous Execution:** Native `aiohttp` implementation designed never to block the event loop.
|
|
33
|
+
* **Unified REST Client:** Centralized HTTP architecture managing internal auth tickets, cookie construction, and CSRF token propagation seamlessly.
|
|
34
|
+
* **Granular Telemetry Models:** Distinct dataclasses separating cluster maps from deep host (`NodeStatus`), VM (`QemuStatus`), and container (`LXCStatus`) sensor profiles.
|
|
35
|
+
* **Smart Reconciliation:** Automated cache pruning to instantly evict vanished or decommissioned resources from tracking tables using fast set operations.
|
|
36
|
+
|
|
37
|
+
## Performance
|
|
38
|
+
|
|
39
|
+
PVE calls can be intense, the `live_test.py` script adds basic timing, especially storage is expensive. Example for a one-node cluster with only local storage:
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
Marker / Total time / Diff. time / Progress
|
|
43
|
+
Timing / 0.15 ms / 0.15 ms / Init
|
|
44
|
+
Timing / 0.20 ms / 0.02 ms / Setup
|
|
45
|
+
Timing / 69.37 ms / 69.15 ms / Connect
|
|
46
|
+
Timing / 69.52 ms / 0.07 ms / Cluster Resources
|
|
47
|
+
Timing / 69.67 ms / 0.14 ms / Resource printing done
|
|
48
|
+
Timing / 69.72 ms / 0.04 ms / Time
|
|
49
|
+
Timing / 80.22 ms / 10.49 ms / Qemu List
|
|
50
|
+
Timing / 101.04 ms / 20.75 ms / LXC List
|
|
51
|
+
Timing / 101.16 ms / 0.03 ms / Qemu/LXC listing done
|
|
52
|
+
Timing / 105.41 ms / 4.24 ms / Node Status
|
|
53
|
+
Timing / 110.88 ms / 5.42 ms / VM Status
|
|
54
|
+
Timing / 126.18 ms / 15.24 ms / LXC
|
|
55
|
+
Timing / 126.33 ms / 0.06 ms / Check existing capabilities
|
|
56
|
+
Timing / 126.39 ms / 0.05 ms / Mark time
|
|
57
|
+
Timing / 130.49 ms / 4.08 ms / Permissions fetch
|
|
58
|
+
Timing / 1784.85 ms / 1654.30 ms / Storage
|
|
59
|
+
Timing / 1793.08 ms / 8.13 ms / Backups
|
|
60
|
+
Timing / 1793.17 ms / 0.04 ms / Time
|
|
61
|
+
Timing / 1793.20 ms / 0.00 ms / Guest Agent running VM start
|
|
62
|
+
Timing / 1944.55 ms / 151.33 ms / Guest Agent running VM complete
|
|
63
|
+
Timing / 1944.75 ms / 0.08 ms / Guest Agent stopped VM start
|
|
64
|
+
Timing / 1950.67 ms / 5.90 ms / Guest Agent stopped VM complete
|
|
65
|
+
Timing / 1950.75 ms / 0.03 ms / Time
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Usage
|
|
69
|
+
|
|
70
|
+
The library provides a modern, fluent interface built on asyncio and aiohttp, designed to mirror the structure of the Proxmox VE API while maintaining strict type safety via dataclasses.
|
|
71
|
+
|
|
72
|
+
Instead of passing endpoints via string formatting or multi-level item lookups, aioproxmox exposes clean, chainable endpoint paths. All data responses are fully validated dataclass objects rather than raw nested dictionaries.
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
import asyncio
|
|
76
|
+
import aiohttp
|
|
77
|
+
import aioproxmox
|
|
78
|
+
|
|
79
|
+
async def main():
|
|
80
|
+
async with aiohttp.ClientSession() as session:
|
|
81
|
+
# Initialize the type-safe client backend
|
|
82
|
+
pve = aioproxmox.ProxmoxVE(
|
|
83
|
+
session=session,
|
|
84
|
+
host="192.168.1.100",
|
|
85
|
+
user="root@pam",
|
|
86
|
+
password="your_secure_password",
|
|
87
|
+
verify_ssl=False
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
cluster = await pve.connect()
|
|
91
|
+
print(f"Connected to cluster with: {len(cluster.resources)} resources")
|
|
92
|
+
|
|
93
|
+
lxc_status = await pve.nodes("pvex").lxc(501).status.current()
|
|
94
|
+
print(f"Container Memory: {lxc_status.mem} bytes")
|
|
95
|
+
|
|
96
|
+
cluster_resources = await pve.cluster.resources()
|
|
97
|
+
print(f"Total cluster resources tracked: {len(cluster_resources.resources)}")
|
|
98
|
+
|
|
99
|
+
node_status = await pve.nodes("pvex").status()
|
|
100
|
+
print(f"Node CPU usage: {node_status.cpu * 100}%")
|
|
101
|
+
|
|
102
|
+
vm_status = await pve.nodes("pvex").qemu(102).status.current()
|
|
103
|
+
print(f"VM {vm_status.name} Status: {vm_status.status}")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
if __name__ == "__main__":
|
|
107
|
+
asyncio.run(main())
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Contrast from Proxmoxer
|
|
111
|
+
|
|
112
|
+
| Feature | `proxmoxer` (Generic String Wrapper) | `aioproxmox` (Type-Safe Telemetry Client) |
|
|
113
|
+
| :--- | :--- | :--- |
|
|
114
|
+
| **I/O Strategy** | Synchronous (blocks the event loop) | Native Asynchronous (`aiohttp`) |
|
|
115
|
+
| **Path Navigation** | Verbatim endpoint strings / properties | Fluid, chainable endpoint paths |
|
|
116
|
+
| **Terminal Calls** | Explicit method tags required (`.get()`) | Direct execution or explicit telemetry targets |
|
|
117
|
+
| **Data Format** | Untyped primitives (`dict` / `list`) | Validated `Mashumaro` Dataclasses |
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
"""Example implementation, see scripts/live_test.py for more examples."""
|
|
121
|
+
from proxmoxer import ProxmoxAPI
|
|
122
|
+
|
|
123
|
+
# Initialize synchronous client
|
|
124
|
+
proxmox = ProxmoxAPI(
|
|
125
|
+
"192.168.1.100",
|
|
126
|
+
user="root@pam",
|
|
127
|
+
password="your_secure_password",
|
|
128
|
+
verify_ssl=False
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# 1. Fetch cluster resources (returns raw lists of dicts)
|
|
132
|
+
resources = proxmox.cluster.resources.get()
|
|
133
|
+
print(f"Total cluster resources tracked: {len(resources)}")
|
|
134
|
+
|
|
135
|
+
# 2. Fetch specific physical node status
|
|
136
|
+
node_status = proxmox.nodes("pvex").status.get()
|
|
137
|
+
print(f"Node CPU usage: {node_status.get('cpu', 0) * 100}%")
|
|
138
|
+
|
|
139
|
+
# 3. Fetch QEMU VM status
|
|
140
|
+
vm_status = proxmox.nodes("pvex").qemu(102).status.current.get()
|
|
141
|
+
print(f"VM Status: {vm_status.get('status')}")
|
|
142
|
+
|
|
143
|
+
# 4. Fetch LXC container status
|
|
144
|
+
lxc_status = proxmox.nodes("pvex").lxc(501).status.current.get()
|
|
145
|
+
print(f"Container Memory: {lxc_status.get('mem')} bytes")
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Development Diagnostics
|
|
149
|
+
|
|
150
|
+
The repository provides a diagnostic CLI utility to verify authentication mechanics, cookie/ticket attachment, and data serialization against live systems.
|
|
151
|
+
|
|
152
|
+
### Running the Live Test Script
|
|
153
|
+
|
|
154
|
+
Execute the script from the root directory by providing your targeted cluster parameters:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
scripts/live_test.py <host-ip-or-fqdn> "<user@realm>" "<password>"
|
|
158
|
+
```
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Asynchronous Python library for Proxmox products
|
|
2
|
+
|
|
3
|
+
An asynchronous, type-safe Python client tailored for extracting raw performance telemetry out of Proxmox VE clusters, nodes, and guests.
|
|
4
|
+
|
|
5
|
+
## Why another Proxmox library
|
|
6
|
+
|
|
7
|
+
This library was built out of necessity because existing ecosystem integrations and underlying client libraries have structural limitations. Whether they are synchronous bottlenecks, missing granular unnested endpoint layouts, or lacking strict runtime type enforcement. This library serves as our high-performance bridge today while we wait for any other mainstream library to catch up, open up, or accept modern async paradigms. Once they do, this phase can comfortably conclude.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
* **Strict Asynchronous Execution:** Native `aiohttp` implementation designed never to block the event loop.
|
|
12
|
+
* **Unified REST Client:** Centralized HTTP architecture managing internal auth tickets, cookie construction, and CSRF token propagation seamlessly.
|
|
13
|
+
* **Granular Telemetry Models:** Distinct dataclasses separating cluster maps from deep host (`NodeStatus`), VM (`QemuStatus`), and container (`LXCStatus`) sensor profiles.
|
|
14
|
+
* **Smart Reconciliation:** Automated cache pruning to instantly evict vanished or decommissioned resources from tracking tables using fast set operations.
|
|
15
|
+
|
|
16
|
+
## Performance
|
|
17
|
+
|
|
18
|
+
PVE calls can be intense, the `live_test.py` script adds basic timing, especially storage is expensive. Example for a one-node cluster with only local storage:
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
Marker / Total time / Diff. time / Progress
|
|
22
|
+
Timing / 0.15 ms / 0.15 ms / Init
|
|
23
|
+
Timing / 0.20 ms / 0.02 ms / Setup
|
|
24
|
+
Timing / 69.37 ms / 69.15 ms / Connect
|
|
25
|
+
Timing / 69.52 ms / 0.07 ms / Cluster Resources
|
|
26
|
+
Timing / 69.67 ms / 0.14 ms / Resource printing done
|
|
27
|
+
Timing / 69.72 ms / 0.04 ms / Time
|
|
28
|
+
Timing / 80.22 ms / 10.49 ms / Qemu List
|
|
29
|
+
Timing / 101.04 ms / 20.75 ms / LXC List
|
|
30
|
+
Timing / 101.16 ms / 0.03 ms / Qemu/LXC listing done
|
|
31
|
+
Timing / 105.41 ms / 4.24 ms / Node Status
|
|
32
|
+
Timing / 110.88 ms / 5.42 ms / VM Status
|
|
33
|
+
Timing / 126.18 ms / 15.24 ms / LXC
|
|
34
|
+
Timing / 126.33 ms / 0.06 ms / Check existing capabilities
|
|
35
|
+
Timing / 126.39 ms / 0.05 ms / Mark time
|
|
36
|
+
Timing / 130.49 ms / 4.08 ms / Permissions fetch
|
|
37
|
+
Timing / 1784.85 ms / 1654.30 ms / Storage
|
|
38
|
+
Timing / 1793.08 ms / 8.13 ms / Backups
|
|
39
|
+
Timing / 1793.17 ms / 0.04 ms / Time
|
|
40
|
+
Timing / 1793.20 ms / 0.00 ms / Guest Agent running VM start
|
|
41
|
+
Timing / 1944.55 ms / 151.33 ms / Guest Agent running VM complete
|
|
42
|
+
Timing / 1944.75 ms / 0.08 ms / Guest Agent stopped VM start
|
|
43
|
+
Timing / 1950.67 ms / 5.90 ms / Guest Agent stopped VM complete
|
|
44
|
+
Timing / 1950.75 ms / 0.03 ms / Time
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
The library provides a modern, fluent interface built on asyncio and aiohttp, designed to mirror the structure of the Proxmox VE API while maintaining strict type safety via dataclasses.
|
|
50
|
+
|
|
51
|
+
Instead of passing endpoints via string formatting or multi-level item lookups, aioproxmox exposes clean, chainable endpoint paths. All data responses are fully validated dataclass objects rather than raw nested dictionaries.
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
import asyncio
|
|
55
|
+
import aiohttp
|
|
56
|
+
import aioproxmox
|
|
57
|
+
|
|
58
|
+
async def main():
|
|
59
|
+
async with aiohttp.ClientSession() as session:
|
|
60
|
+
# Initialize the type-safe client backend
|
|
61
|
+
pve = aioproxmox.ProxmoxVE(
|
|
62
|
+
session=session,
|
|
63
|
+
host="192.168.1.100",
|
|
64
|
+
user="root@pam",
|
|
65
|
+
password="your_secure_password",
|
|
66
|
+
verify_ssl=False
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
cluster = await pve.connect()
|
|
70
|
+
print(f"Connected to cluster with: {len(cluster.resources)} resources")
|
|
71
|
+
|
|
72
|
+
lxc_status = await pve.nodes("pvex").lxc(501).status.current()
|
|
73
|
+
print(f"Container Memory: {lxc_status.mem} bytes")
|
|
74
|
+
|
|
75
|
+
cluster_resources = await pve.cluster.resources()
|
|
76
|
+
print(f"Total cluster resources tracked: {len(cluster_resources.resources)}")
|
|
77
|
+
|
|
78
|
+
node_status = await pve.nodes("pvex").status()
|
|
79
|
+
print(f"Node CPU usage: {node_status.cpu * 100}%")
|
|
80
|
+
|
|
81
|
+
vm_status = await pve.nodes("pvex").qemu(102).status.current()
|
|
82
|
+
print(f"VM {vm_status.name} Status: {vm_status.status}")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
if __name__ == "__main__":
|
|
86
|
+
asyncio.run(main())
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Contrast from Proxmoxer
|
|
90
|
+
|
|
91
|
+
| Feature | `proxmoxer` (Generic String Wrapper) | `aioproxmox` (Type-Safe Telemetry Client) |
|
|
92
|
+
| :--- | :--- | :--- |
|
|
93
|
+
| **I/O Strategy** | Synchronous (blocks the event loop) | Native Asynchronous (`aiohttp`) |
|
|
94
|
+
| **Path Navigation** | Verbatim endpoint strings / properties | Fluid, chainable endpoint paths |
|
|
95
|
+
| **Terminal Calls** | Explicit method tags required (`.get()`) | Direct execution or explicit telemetry targets |
|
|
96
|
+
| **Data Format** | Untyped primitives (`dict` / `list`) | Validated `Mashumaro` Dataclasses |
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
"""Example implementation, see scripts/live_test.py for more examples."""
|
|
100
|
+
from proxmoxer import ProxmoxAPI
|
|
101
|
+
|
|
102
|
+
# Initialize synchronous client
|
|
103
|
+
proxmox = ProxmoxAPI(
|
|
104
|
+
"192.168.1.100",
|
|
105
|
+
user="root@pam",
|
|
106
|
+
password="your_secure_password",
|
|
107
|
+
verify_ssl=False
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# 1. Fetch cluster resources (returns raw lists of dicts)
|
|
111
|
+
resources = proxmox.cluster.resources.get()
|
|
112
|
+
print(f"Total cluster resources tracked: {len(resources)}")
|
|
113
|
+
|
|
114
|
+
# 2. Fetch specific physical node status
|
|
115
|
+
node_status = proxmox.nodes("pvex").status.get()
|
|
116
|
+
print(f"Node CPU usage: {node_status.get('cpu', 0) * 100}%")
|
|
117
|
+
|
|
118
|
+
# 3. Fetch QEMU VM status
|
|
119
|
+
vm_status = proxmox.nodes("pvex").qemu(102).status.current.get()
|
|
120
|
+
print(f"VM Status: {vm_status.get('status')}")
|
|
121
|
+
|
|
122
|
+
# 4. Fetch LXC container status
|
|
123
|
+
lxc_status = proxmox.nodes("pvex").lxc(501).status.current.get()
|
|
124
|
+
print(f"Container Memory: {lxc_status.get('mem')} bytes")
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Development Diagnostics
|
|
128
|
+
|
|
129
|
+
The repository provides a diagnostic CLI utility to verify authentication mechanics, cookie/ticket attachment, and data serialization against live systems.
|
|
130
|
+
|
|
131
|
+
### Running the Live Test Script
|
|
132
|
+
|
|
133
|
+
Execute the script from the root directory by providing your targeted cluster parameters:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
scripts/live_test.py <host-ip-or-fqdn> "<user@realm>" "<password>"
|
|
137
|
+
```
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""Proxmox Home Assistant Integration Service."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import aiohttp
|
|
8
|
+
|
|
9
|
+
from .const import DEFAULT_PVE_PORT
|
|
10
|
+
from .endpoints import AccessEndpoint, ClusterEndpoint, NodeEndpoint
|
|
11
|
+
from .exceptions import ProxmoxAPIError, ProxmoxAuthError
|
|
12
|
+
from .model import PVECapabilities, PVEPermissions
|
|
13
|
+
from .model.pve import ClusterCache, ClusterResourcesCollection
|
|
14
|
+
|
|
15
|
+
_LOGGER = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
SERVICES: dict[str, dict[str, Any]] = {
|
|
18
|
+
"PVE": {"default_port": DEFAULT_PVE_PORT, "token_separator": "="}
|
|
19
|
+
# Future PDM, PBS
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ProxmoxHTTPAuthBase:
|
|
24
|
+
"""Base class for authentication structures."""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
session: aiohttp.ClientSession,
|
|
29
|
+
timeout: float = 5.0,
|
|
30
|
+
service: str = "PVE",
|
|
31
|
+
verify_ssl: bool = False,
|
|
32
|
+
):
|
|
33
|
+
"""Initialize ticket based authentication."""
|
|
34
|
+
self.session = session
|
|
35
|
+
self.timeout = timeout
|
|
36
|
+
self.service = service
|
|
37
|
+
self.verify_ssl = verify_ssl
|
|
38
|
+
self.capabilities: PVECapabilities
|
|
39
|
+
|
|
40
|
+
def get_cookies(self) -> dict[str, str]:
|
|
41
|
+
"""Return cookies."""
|
|
42
|
+
return {}
|
|
43
|
+
|
|
44
|
+
def get_headers(self) -> dict[str, str]:
|
|
45
|
+
"""Return headers."""
|
|
46
|
+
return {}
|
|
47
|
+
|
|
48
|
+
async def check_and_refresh(self, method: str) -> None:
|
|
49
|
+
"""Asynchronously refresh credentials if required before a request."""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ProxmoxHTTPAuth(ProxmoxHTTPAuthBase):
|
|
53
|
+
"""Ticket and Cookie based Authentication supporting TFA."""
|
|
54
|
+
|
|
55
|
+
renew_age = 3600
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
username: str,
|
|
60
|
+
password: str,
|
|
61
|
+
otp: bool | None = None,
|
|
62
|
+
base_url: str = "",
|
|
63
|
+
otptype: str = "totp",
|
|
64
|
+
**kwargs: Any,
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Initialize ticket based authentication."""
|
|
67
|
+
super().__init__(**kwargs)
|
|
68
|
+
self.base_url = base_url
|
|
69
|
+
self.username = username
|
|
70
|
+
self.password = password
|
|
71
|
+
self.otp = otp
|
|
72
|
+
self.otptype = otptype
|
|
73
|
+
self.pve_auth_ticket = ""
|
|
74
|
+
self.csrf_prevention_token = ""
|
|
75
|
+
self.birth_time = 0.0
|
|
76
|
+
|
|
77
|
+
async def async_init(self) -> Any:
|
|
78
|
+
"""Initial token acquisition loop."""
|
|
79
|
+
await self._get_new_tokens(
|
|
80
|
+
password=self.password, otp=self.otp, otptype=self.otptype
|
|
81
|
+
)
|
|
82
|
+
return self
|
|
83
|
+
|
|
84
|
+
async def _get_new_tokens(
|
|
85
|
+
self,
|
|
86
|
+
password: str | None = None,
|
|
87
|
+
otp: bool | None = None,
|
|
88
|
+
otptype: str | None = None,
|
|
89
|
+
) -> None:
|
|
90
|
+
"""Retrieve new tokens for session."""
|
|
91
|
+
target_password = password or self.pve_auth_ticket
|
|
92
|
+
data = {"username": self.username, "password": target_password}
|
|
93
|
+
timeout = aiohttp.ClientTimeout(total=self.timeout)
|
|
94
|
+
|
|
95
|
+
async with self.session.post(
|
|
96
|
+
f"{self.base_url}/access/ticket",
|
|
97
|
+
data=data,
|
|
98
|
+
timeout=timeout,
|
|
99
|
+
ssl=self.verify_ssl,
|
|
100
|
+
) as response:
|
|
101
|
+
if response.status != 200:
|
|
102
|
+
raise ProxmoxAuthError(
|
|
103
|
+
f"Couldn't authenticate user {self.username} to {self.base_url}/access/ticket: Code {response.status}"
|
|
104
|
+
)
|
|
105
|
+
res_json = await response.json()
|
|
106
|
+
response_data = res_json["data"]
|
|
107
|
+
|
|
108
|
+
self.birth_time = time.monotonic()
|
|
109
|
+
self.pve_auth_ticket = response_data["ticket"]
|
|
110
|
+
self.csrf_prevention_token = response_data["CSRFPreventionToken"]
|
|
111
|
+
|
|
112
|
+
if "cap" in response_data:
|
|
113
|
+
self.capabilities = PVECapabilities(**response_data["cap"])
|
|
114
|
+
|
|
115
|
+
# Secondary step if Two Factor Challenge is detected
|
|
116
|
+
if response_data.get("NeedTFA") is not None:
|
|
117
|
+
otpdata = {
|
|
118
|
+
"username": self.username,
|
|
119
|
+
"tfa-challenge": self.pve_auth_ticket,
|
|
120
|
+
"password": f"{otptype}:{otp}",
|
|
121
|
+
}
|
|
122
|
+
async with self.session.post(
|
|
123
|
+
f"{self.base_url}/access/ticket",
|
|
124
|
+
data=otpdata,
|
|
125
|
+
timeout=timeout,
|
|
126
|
+
ssl=self.verify_ssl,
|
|
127
|
+
) as otpresp:
|
|
128
|
+
otp_json = await otpresp.json()
|
|
129
|
+
otpresp_data = otp_json.get("data")
|
|
130
|
+
if not otpresp_data:
|
|
131
|
+
raise ProxmoxAuthError(
|
|
132
|
+
"Couldn't authenticate user: missing Two Factor Authentication (TFA)"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
self.birth_time = time.monotonic()
|
|
136
|
+
self.pve_auth_ticket = otpresp_data["ticket"]
|
|
137
|
+
self.csrf_prevention_token = otpresp_data["CSRFPreventionToken"]
|
|
138
|
+
|
|
139
|
+
def get_cookies(self) -> dict[str, str]:
|
|
140
|
+
"""Return cookies."""
|
|
141
|
+
return {f"{self.service}AuthCookie": self.pve_auth_ticket}
|
|
142
|
+
|
|
143
|
+
def get_headers(self) -> dict[str, str]:
|
|
144
|
+
"""Return headers."""
|
|
145
|
+
# Return CSRF prevention tokens strictly for mutation traffic
|
|
146
|
+
return {"CSRFPreventionToken": self.csrf_prevention_token}
|
|
147
|
+
|
|
148
|
+
async def check_and_refresh(self, method: str) -> None:
|
|
149
|
+
"""Asynchronously refresh credentials if required before a request."""
|
|
150
|
+
time_diff = time.monotonic() - self.birth_time
|
|
151
|
+
if time_diff >= self.renew_age:
|
|
152
|
+
_LOGGER.debug("Refreshing ticket (age %s)", time_diff)
|
|
153
|
+
await self._get_new_tokens()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class ProxmoxHTTPApiTokenAuth(ProxmoxHTTPAuthBase):
|
|
157
|
+
"""Stateless API Token based Authentication."""
|
|
158
|
+
|
|
159
|
+
def __init__(
|
|
160
|
+
self,
|
|
161
|
+
username: str,
|
|
162
|
+
token_name: str,
|
|
163
|
+
token_value: str,
|
|
164
|
+
**kwargs: Any,
|
|
165
|
+
) -> None:
|
|
166
|
+
"""Initialize token based authentication."""
|
|
167
|
+
super().__init__(**kwargs)
|
|
168
|
+
self.username = username
|
|
169
|
+
self.token_name = token_name
|
|
170
|
+
self.token_value = token_value
|
|
171
|
+
|
|
172
|
+
def get_headers(self) -> dict[str, str]:
|
|
173
|
+
"""Return headers."""
|
|
174
|
+
sep = SERVICES[self.service]["token_separator"]
|
|
175
|
+
auth_string = f"{self.service}APIToken={self.username}!{self.token_name}{sep}{self.token_value}"
|
|
176
|
+
return {"Authorization": auth_string}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class ProxmoxVE:
|
|
180
|
+
"""Backend Engine coordinating configuration endpoints and session mapping."""
|
|
181
|
+
|
|
182
|
+
def __init__(
|
|
183
|
+
self,
|
|
184
|
+
session: aiohttp.ClientSession,
|
|
185
|
+
host: str,
|
|
186
|
+
*,
|
|
187
|
+
user: str | None = None,
|
|
188
|
+
password: str | None = None,
|
|
189
|
+
otp: str | None = None,
|
|
190
|
+
port: int | None = None,
|
|
191
|
+
verify_ssl: bool = True,
|
|
192
|
+
timeout: float = 5.0,
|
|
193
|
+
token_name: str | None = None,
|
|
194
|
+
token_value: str | None = None,
|
|
195
|
+
service: str = "PVE",
|
|
196
|
+
) -> None:
|
|
197
|
+
"""HTTPS Backend for Proxmox Virtualisation Engine."""
|
|
198
|
+
if ":" in host and not host.startswith("["):
|
|
199
|
+
# Clean up base IPv4 parsing strings; ignore legacy bracket rules
|
|
200
|
+
host, _ = host.split(":", 1)
|
|
201
|
+
|
|
202
|
+
if not port:
|
|
203
|
+
port = int(SERVICES[service]["default_port"])
|
|
204
|
+
|
|
205
|
+
self.auth: ProxmoxHTTPAuthBase
|
|
206
|
+
self.base_url = f"https://{host}:{port}/api2/json"
|
|
207
|
+
self.verify_ssl = verify_ssl
|
|
208
|
+
self.timeout = timeout
|
|
209
|
+
self.permissions = PVEPermissions()
|
|
210
|
+
self.cluster_resources: ClusterResourcesCollection
|
|
211
|
+
self.cluster_cache = ClusterCache()
|
|
212
|
+
|
|
213
|
+
auth_kwargs: dict[str, Any] = {
|
|
214
|
+
"session": session,
|
|
215
|
+
"verify_ssl": verify_ssl,
|
|
216
|
+
"timeout": timeout,
|
|
217
|
+
"service": service,
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if token_name is not None:
|
|
221
|
+
self.auth = ProxmoxHTTPApiTokenAuth(
|
|
222
|
+
str(user), token_name, str(token_value), **auth_kwargs
|
|
223
|
+
)
|
|
224
|
+
elif password is not None:
|
|
225
|
+
self.auth = ProxmoxHTTPAuth(
|
|
226
|
+
str(user), password, bool(otp), base_url=self.base_url, **auth_kwargs
|
|
227
|
+
)
|
|
228
|
+
else:
|
|
229
|
+
raise ProxmoxAuthError("No valid authentication credentials were supplied")
|
|
230
|
+
|
|
231
|
+
async def connect(self) -> ClusterResourcesCollection:
|
|
232
|
+
"""Authenticate and gather cluster resources."""
|
|
233
|
+
if hasattr(self.auth, "async_init"):
|
|
234
|
+
await self.auth.async_init()
|
|
235
|
+
return await self.cluster.resources()
|
|
236
|
+
|
|
237
|
+
async def request(
|
|
238
|
+
self,
|
|
239
|
+
method: str,
|
|
240
|
+
path: str,
|
|
241
|
+
json_data: dict[str, Any] | None = None,
|
|
242
|
+
params: dict[str, Any] | None = None,
|
|
243
|
+
) -> dict[Any, Any] | list[Any] | dict[str, Any]:
|
|
244
|
+
"""Unified internal request pipeline managing tickets, CSRF tokens, and cookies."""
|
|
245
|
+
await self.auth.check_and_refresh(method=method)
|
|
246
|
+
|
|
247
|
+
headers: dict[str, str] = {
|
|
248
|
+
"Accept": "application/json",
|
|
249
|
+
"Connection": "keep-alive",
|
|
250
|
+
**self.auth.get_headers(),
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
# Only attach the Cookie header if cookies are present (e.g., Ticket Auth)
|
|
254
|
+
if cookies := self.auth.get_cookies():
|
|
255
|
+
headers["Cookie"] = "; ".join([f"{k}={v}" for k, v in cookies.items()])
|
|
256
|
+
|
|
257
|
+
request_kwargs: dict[str, Any] = {}
|
|
258
|
+
|
|
259
|
+
if method.upper() in ("GET", "DELETE"):
|
|
260
|
+
query_params = {**(params or {}), **(json_data or {})}
|
|
261
|
+
if query_params:
|
|
262
|
+
request_kwargs["params"] = {
|
|
263
|
+
k: (int(v) if isinstance(v, bool) else str(v))
|
|
264
|
+
for k, v in query_params.items()
|
|
265
|
+
}
|
|
266
|
+
elif json_data:
|
|
267
|
+
request_kwargs["json"] = json_data
|
|
268
|
+
|
|
269
|
+
url = f"{self.base_url.rstrip('/')}/{path.lstrip('/')}"
|
|
270
|
+
timeout = aiohttp.ClientTimeout(total=self.timeout)
|
|
271
|
+
|
|
272
|
+
async with self.auth.session.request(
|
|
273
|
+
method=method,
|
|
274
|
+
url=url,
|
|
275
|
+
headers=headers,
|
|
276
|
+
timeout=timeout,
|
|
277
|
+
ssl=self.verify_ssl,
|
|
278
|
+
**request_kwargs,
|
|
279
|
+
) as response:
|
|
280
|
+
if response.status not in (200, 201):
|
|
281
|
+
text = await response.text()
|
|
282
|
+
raise ProxmoxAPIError(response.status, text, path)
|
|
283
|
+
|
|
284
|
+
payload = await response.json()
|
|
285
|
+
data = payload.get("data", {})
|
|
286
|
+
return data if isinstance(data, (dict, list)) else {}
|
|
287
|
+
|
|
288
|
+
@property
|
|
289
|
+
def cluster(self) -> ClusterEndpoint:
|
|
290
|
+
"""Add cluster endpoint."""
|
|
291
|
+
return ClusterEndpoint(self)
|
|
292
|
+
|
|
293
|
+
@property
|
|
294
|
+
def access(self) -> AccessEndpoint:
|
|
295
|
+
"""Add access endpoint."""
|
|
296
|
+
return AccessEndpoint(self)
|
|
297
|
+
|
|
298
|
+
def nodes(self, node: str) -> NodeEndpoint:
|
|
299
|
+
"""Add nodes endpoint."""
|
|
300
|
+
return NodeEndpoint(self, node)
|