async-xenapi 1.0.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.
- async_xenapi-1.0.1/.gitignore +7 -0
- async_xenapi-1.0.1/PKG-INFO +145 -0
- async_xenapi-1.0.1/README.md +124 -0
- async_xenapi-1.0.1/pyproject.toml +59 -0
- async_xenapi-1.0.1/src/async_xenapi/__init__.py +5 -0
- async_xenapi-1.0.1/src/async_xenapi/py.typed +0 -0
- async_xenapi-1.0.1/src/async_xenapi/session.py +127 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: async-xenapi
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: Async XenAPI session via JSON-RPC
|
|
5
|
+
Project-URL: Homepage, https://github.com/acefei/async-xenapi
|
|
6
|
+
Project-URL: Repository, https://github.com/acefei/async-xenapi
|
|
7
|
+
Author-email: Su Fei <acefei@163.com>
|
|
8
|
+
License: LGPL-2.1-only
|
|
9
|
+
Keywords: async,xen-api,xenserver
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: GNU Lesser General Public License v2 (LGPLv2)
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Topic :: System :: Systems Administration
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.14
|
|
19
|
+
Requires-Dist: aiohttp>=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# async-xenapi (Python)
|
|
23
|
+
|
|
24
|
+
An async Python library for [XenAPI](https://xapi-project.github.io/xen-api).
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
Requires **Python 3.14+**.
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
pip install async-xenapi
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Or with `uv`:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
uv add async-xenapi
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import asyncio
|
|
44
|
+
from async_xenapi import AsyncXenAPISession
|
|
45
|
+
|
|
46
|
+
async def main():
|
|
47
|
+
session = AsyncXenAPISession("https://xen-host")
|
|
48
|
+
await session.login_with_password("root", "password")
|
|
49
|
+
try:
|
|
50
|
+
vms = await session.xenapi.VM.get_all()
|
|
51
|
+
for vm in vms:
|
|
52
|
+
record = await session.xenapi.VM.get_record(vm)
|
|
53
|
+
print(record["name_label"])
|
|
54
|
+
finally:
|
|
55
|
+
await session.logout()
|
|
56
|
+
|
|
57
|
+
asyncio.run(main())
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Any dotted method path under `session.xenapi` maps directly to the corresponding JSON-RPC call.
|
|
61
|
+
See the [XenAPI Reference](https://xapi-project.github.io/xen-api) for all available classes and fields.
|
|
62
|
+
|
|
63
|
+
## Best Practices
|
|
64
|
+
|
|
65
|
+
### 1. Use `get_all_records()` instead of N+1 queries
|
|
66
|
+
|
|
67
|
+
The single biggest performance win. Instead of fetching a list of refs then querying each one individually, fetch everything in one call:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
# SLOW — N+1 round-trips (1 for get_all + N for each get_name_label)
|
|
71
|
+
vms = await session.xenapi.VM.get_all()
|
|
72
|
+
for vm in vms:
|
|
73
|
+
name = await session.xenapi.VM.get_name_label(vm)
|
|
74
|
+
print(name)
|
|
75
|
+
|
|
76
|
+
# FAST — 1 round-trip, returns {ref: {field: value, ...}, ...}
|
|
77
|
+
records = await session.xenapi.VM.get_all_records()
|
|
78
|
+
for ref, rec in records.items():
|
|
79
|
+
if not rec["is_a_template"] and not rec["is_a_snapshot"]:
|
|
80
|
+
print(f"{rec['name_label']} ({rec['power_state']})")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
This applies to every XenAPI class: `host`, `SR`, `network`, `VM`, `pool`, etc.
|
|
84
|
+
|
|
85
|
+
### 2. Use `asyncio.gather()` for independent calls
|
|
86
|
+
|
|
87
|
+
When you need results from multiple independent API calls, run them concurrently:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
# SLOW — sequential, each awaits the previous
|
|
91
|
+
major = await session.xenapi.host.get_API_version_major(host)
|
|
92
|
+
minor = await session.xenapi.host.get_API_version_minor(host)
|
|
93
|
+
|
|
94
|
+
# FAST — concurrent, both requests in flight at the same time
|
|
95
|
+
major, minor = await asyncio.gather(
|
|
96
|
+
session.xenapi.host.get_API_version_major(host),
|
|
97
|
+
session.xenapi.host.get_API_version_minor(host),
|
|
98
|
+
)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
You can also gather across different classes:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
hosts, vms, networks = await asyncio.gather(
|
|
105
|
+
session.xenapi.host.get_all_records(),
|
|
106
|
+
session.xenapi.VM.get_all_records(),
|
|
107
|
+
session.xenapi.network.get_all_records(),
|
|
108
|
+
)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 3. Always clean up the session
|
|
112
|
+
|
|
113
|
+
Use `try/finally` to ensure `logout()` is called, which closes both the server-side session and the underlying HTTP connection:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
session = AsyncXenAPISession("https://xen-host")
|
|
117
|
+
await session.login_with_password("root", "password")
|
|
118
|
+
try:
|
|
119
|
+
# ... your code ...
|
|
120
|
+
finally:
|
|
121
|
+
await session.logout()
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Key Takeaways
|
|
125
|
+
|
|
126
|
+
| Pattern | Calls | Approach |
|
|
127
|
+
|------------------------------|--------------|----------------------|
|
|
128
|
+
| List objects with fields | 1 | `get_all_records()` |
|
|
129
|
+
| Multiple independent values | N concurrent | `asyncio.gather()` |
|
|
130
|
+
| Lookup one field for one ref | 1 | `get_<field>(ref)` |
|
|
131
|
+
|
|
132
|
+
## Run Tests
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
git clone git@github.com:acefei/async-xenapi.git
|
|
136
|
+
cd async-xenapi
|
|
137
|
+
cp .env.example .env # then edit .env with your credentials
|
|
138
|
+
cd python
|
|
139
|
+
uv sync
|
|
140
|
+
uv run pytest tests/test_get_xapi_version.py -v
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## License
|
|
144
|
+
|
|
145
|
+
LGPL-2.1-only
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# async-xenapi (Python)
|
|
2
|
+
|
|
3
|
+
An async Python library for [XenAPI](https://xapi-project.github.io/xen-api).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Requires **Python 3.14+**.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
pip install async-xenapi
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or with `uv`:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
uv add async-xenapi
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
import asyncio
|
|
23
|
+
from async_xenapi import AsyncXenAPISession
|
|
24
|
+
|
|
25
|
+
async def main():
|
|
26
|
+
session = AsyncXenAPISession("https://xen-host")
|
|
27
|
+
await session.login_with_password("root", "password")
|
|
28
|
+
try:
|
|
29
|
+
vms = await session.xenapi.VM.get_all()
|
|
30
|
+
for vm in vms:
|
|
31
|
+
record = await session.xenapi.VM.get_record(vm)
|
|
32
|
+
print(record["name_label"])
|
|
33
|
+
finally:
|
|
34
|
+
await session.logout()
|
|
35
|
+
|
|
36
|
+
asyncio.run(main())
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Any dotted method path under `session.xenapi` maps directly to the corresponding JSON-RPC call.
|
|
40
|
+
See the [XenAPI Reference](https://xapi-project.github.io/xen-api) for all available classes and fields.
|
|
41
|
+
|
|
42
|
+
## Best Practices
|
|
43
|
+
|
|
44
|
+
### 1. Use `get_all_records()` instead of N+1 queries
|
|
45
|
+
|
|
46
|
+
The single biggest performance win. Instead of fetching a list of refs then querying each one individually, fetch everything in one call:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
# SLOW — N+1 round-trips (1 for get_all + N for each get_name_label)
|
|
50
|
+
vms = await session.xenapi.VM.get_all()
|
|
51
|
+
for vm in vms:
|
|
52
|
+
name = await session.xenapi.VM.get_name_label(vm)
|
|
53
|
+
print(name)
|
|
54
|
+
|
|
55
|
+
# FAST — 1 round-trip, returns {ref: {field: value, ...}, ...}
|
|
56
|
+
records = await session.xenapi.VM.get_all_records()
|
|
57
|
+
for ref, rec in records.items():
|
|
58
|
+
if not rec["is_a_template"] and not rec["is_a_snapshot"]:
|
|
59
|
+
print(f"{rec['name_label']} ({rec['power_state']})")
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
This applies to every XenAPI class: `host`, `SR`, `network`, `VM`, `pool`, etc.
|
|
63
|
+
|
|
64
|
+
### 2. Use `asyncio.gather()` for independent calls
|
|
65
|
+
|
|
66
|
+
When you need results from multiple independent API calls, run them concurrently:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
# SLOW — sequential, each awaits the previous
|
|
70
|
+
major = await session.xenapi.host.get_API_version_major(host)
|
|
71
|
+
minor = await session.xenapi.host.get_API_version_minor(host)
|
|
72
|
+
|
|
73
|
+
# FAST — concurrent, both requests in flight at the same time
|
|
74
|
+
major, minor = await asyncio.gather(
|
|
75
|
+
session.xenapi.host.get_API_version_major(host),
|
|
76
|
+
session.xenapi.host.get_API_version_minor(host),
|
|
77
|
+
)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
You can also gather across different classes:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
hosts, vms, networks = await asyncio.gather(
|
|
84
|
+
session.xenapi.host.get_all_records(),
|
|
85
|
+
session.xenapi.VM.get_all_records(),
|
|
86
|
+
session.xenapi.network.get_all_records(),
|
|
87
|
+
)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 3. Always clean up the session
|
|
91
|
+
|
|
92
|
+
Use `try/finally` to ensure `logout()` is called, which closes both the server-side session and the underlying HTTP connection:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
session = AsyncXenAPISession("https://xen-host")
|
|
96
|
+
await session.login_with_password("root", "password")
|
|
97
|
+
try:
|
|
98
|
+
# ... your code ...
|
|
99
|
+
finally:
|
|
100
|
+
await session.logout()
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Key Takeaways
|
|
104
|
+
|
|
105
|
+
| Pattern | Calls | Approach |
|
|
106
|
+
|------------------------------|--------------|----------------------|
|
|
107
|
+
| List objects with fields | 1 | `get_all_records()` |
|
|
108
|
+
| Multiple independent values | N concurrent | `asyncio.gather()` |
|
|
109
|
+
| Lookup one field for one ref | 1 | `get_<field>(ref)` |
|
|
110
|
+
|
|
111
|
+
## Run Tests
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
git clone git@github.com:acefei/async-xenapi.git
|
|
115
|
+
cd async-xenapi
|
|
116
|
+
cp .env.example .env # then edit .env with your credentials
|
|
117
|
+
cd python
|
|
118
|
+
uv sync
|
|
119
|
+
uv run pytest tests/test_get_xapi_version.py -v
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
LGPL-2.1-only
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "async-xenapi"
|
|
3
|
+
version = "1.0.1"
|
|
4
|
+
description = "Async XenAPI session via JSON-RPC"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "LGPL-2.1-only" }
|
|
7
|
+
authors = [{ name = "Su Fei", email = "acefei@163.com" }]
|
|
8
|
+
requires-python = ">=3.14"
|
|
9
|
+
dependencies = ["aiohttp>=3.9"]
|
|
10
|
+
keywords = ["xenserver", "xen-api", "async"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 4 - Beta",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"License :: OSI Approved :: GNU Lesser General Public License v2 (LGPLv2)",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.14",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Topic :: System :: Systems Administration",
|
|
19
|
+
"Typing :: Typed",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://github.com/acefei/async-xenapi"
|
|
24
|
+
Repository = "https://github.com/acefei/async-xenapi"
|
|
25
|
+
|
|
26
|
+
[build-system]
|
|
27
|
+
requires = ["hatchling"]
|
|
28
|
+
build-backend = "hatchling.build"
|
|
29
|
+
|
|
30
|
+
[tool.hatch.build.targets.wheel]
|
|
31
|
+
packages = ["src/async_xenapi"]
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.sdist]
|
|
34
|
+
only-include = ["src/", "README.md", "pyproject.toml"]
|
|
35
|
+
|
|
36
|
+
[tool.ruff]
|
|
37
|
+
line-length = 100
|
|
38
|
+
target-version = "py314"
|
|
39
|
+
|
|
40
|
+
[tool.ruff.lint]
|
|
41
|
+
select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
42
|
+
ignore = ["E501"]
|
|
43
|
+
|
|
44
|
+
[tool.ruff.lint.isort]
|
|
45
|
+
known-first-party = ["async_xenapi"]
|
|
46
|
+
|
|
47
|
+
[tool.pytest.ini_options]
|
|
48
|
+
asyncio_mode = "strict"
|
|
49
|
+
asyncio_default_fixture_loop_scope = "class"
|
|
50
|
+
asyncio_default_test_loop_scope = "class"
|
|
51
|
+
|
|
52
|
+
[dependency-groups]
|
|
53
|
+
dev = [
|
|
54
|
+
"pytest>=8.0",
|
|
55
|
+
"pytest-asyncio>=0.23",
|
|
56
|
+
"python-dotenv>=1.0",
|
|
57
|
+
"ruff>=0.15.2",
|
|
58
|
+
"xenapi>=26.5.0",
|
|
59
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Async XenAPI session via JSON-RPC
|
|
3
|
+
|
|
4
|
+
Usage mirrors the synchronous XenAPI SDK:
|
|
5
|
+
|
|
6
|
+
session = AsyncXenAPISession("https://host-ip")
|
|
7
|
+
await session.login_with_password("root", "password")
|
|
8
|
+
|
|
9
|
+
vms = await session.xenapi.VM.get_all()
|
|
10
|
+
for vm in vms:
|
|
11
|
+
record = await session.xenapi.VM.get_record(vm)
|
|
12
|
+
print(record["name_label"])
|
|
13
|
+
|
|
14
|
+
await session.logout()
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import contextlib
|
|
18
|
+
import ssl
|
|
19
|
+
import uuid
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
import aiohttp
|
|
23
|
+
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
# SSL / JSON-RPC helpers
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _create_ssl_ctx() -> ssl.SSLContext:
|
|
30
|
+
ctx = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_CLIENT)
|
|
31
|
+
ctx.check_hostname = False
|
|
32
|
+
ctx.verify_mode = ssl.CERT_NONE
|
|
33
|
+
return ctx
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
_ssl_ctx = _create_ssl_ctx()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _jsonrpc_req(method: str, params: list[Any]) -> dict[str, Any]:
|
|
40
|
+
return {
|
|
41
|
+
"jsonrpc": "2.0",
|
|
42
|
+
"method": method,
|
|
43
|
+
"params": params,
|
|
44
|
+
"id": str(uuid.uuid4()),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# Async XenAPI proxy
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class _MethodProxy:
|
|
54
|
+
"""Accumulates dotted attribute access (e.g. VM.get_all) then turns the
|
|
55
|
+
final call into an awaitable JSON-RPC request."""
|
|
56
|
+
|
|
57
|
+
def __init__(self, session: AsyncXenAPISession, name: str):
|
|
58
|
+
self._session = session
|
|
59
|
+
self._name = name
|
|
60
|
+
|
|
61
|
+
def __getattr__(self, attr: str) -> _MethodProxy:
|
|
62
|
+
return _MethodProxy(self._session, f"{self._name}.{attr}")
|
|
63
|
+
|
|
64
|
+
async def __call__(self, *args: Any) -> Any:
|
|
65
|
+
return await self._session._call(self._name, list(args))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class _XenAPINamespace:
|
|
69
|
+
"""The object returned by ``session.xenapi``."""
|
|
70
|
+
|
|
71
|
+
def __init__(self, session: AsyncXenAPISession):
|
|
72
|
+
self._session = session
|
|
73
|
+
|
|
74
|
+
def __getattr__(self, attr: str) -> _MethodProxy:
|
|
75
|
+
return _MethodProxy(self._session, attr)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class AsyncXenAPISession:
|
|
79
|
+
"""Lightweight async wrapper around XAPI's JSON-RPC endpoint using aiohttp."""
|
|
80
|
+
|
|
81
|
+
def __init__(self, url: str):
|
|
82
|
+
self._url = f"{url.rstrip('/')}/jsonrpc"
|
|
83
|
+
self._http: aiohttp.ClientSession | None = None
|
|
84
|
+
self._session_ref: str | None = None
|
|
85
|
+
self.xenapi = _XenAPINamespace(self)
|
|
86
|
+
|
|
87
|
+
def _ensure_http(self) -> aiohttp.ClientSession:
|
|
88
|
+
if self._http is None or self._http.closed:
|
|
89
|
+
connector = aiohttp.TCPConnector(ssl=_ssl_ctx)
|
|
90
|
+
self._http = aiohttp.ClientSession(connector=connector)
|
|
91
|
+
return self._http
|
|
92
|
+
|
|
93
|
+
async def _post(self, payload: dict[str, Any]) -> Any:
|
|
94
|
+
http = self._ensure_http()
|
|
95
|
+
async with http.post(self._url, json=payload) as resp:
|
|
96
|
+
return await resp.json()
|
|
97
|
+
|
|
98
|
+
async def login_with_password(self, user: str, password: str) -> str:
|
|
99
|
+
payload = _jsonrpc_req(
|
|
100
|
+
"session.login_with_password",
|
|
101
|
+
[user, password, "version", "originator"],
|
|
102
|
+
)
|
|
103
|
+
ret = await self._post(payload)
|
|
104
|
+
if "error" in ret:
|
|
105
|
+
raise RuntimeError(f"Login failed: {ret['error']}")
|
|
106
|
+
self._session_ref = ret["result"]
|
|
107
|
+
return self._session_ref
|
|
108
|
+
|
|
109
|
+
async def logout(self) -> None:
|
|
110
|
+
if self._session_ref:
|
|
111
|
+
payload = _jsonrpc_req("session.logout", [self._session_ref])
|
|
112
|
+
with contextlib.suppress(Exception):
|
|
113
|
+
await self._post(payload)
|
|
114
|
+
self._session_ref = None
|
|
115
|
+
if self._http and not self._http.closed:
|
|
116
|
+
await self._http.close()
|
|
117
|
+
self._http = None
|
|
118
|
+
|
|
119
|
+
async def _call(self, method: str, params: list[Any]) -> Any:
|
|
120
|
+
"""Send an authenticated JSON-RPC call and return the result."""
|
|
121
|
+
if not self._session_ref:
|
|
122
|
+
raise RuntimeError("Not logged in")
|
|
123
|
+
payload = _jsonrpc_req(method, [self._session_ref] + params)
|
|
124
|
+
ret = await self._post(payload)
|
|
125
|
+
if "error" in ret:
|
|
126
|
+
raise RuntimeError(f"XAPI {method} failed: {ret['error']}")
|
|
127
|
+
return ret["result"]
|