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.
@@ -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)
@@ -0,0 +1,3 @@
1
+ """Constant definitions."""
2
+
3
+ DEFAULT_PVE_PORT = 8006