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.
@@ -0,0 +1,7 @@
1
+ **/.env
2
+ **/node_modules/
3
+ **/dist/
4
+ **/__pycache__/
5
+ **/*.egg-info/
6
+ **/.venv/
7
+ **/.pytest_cache/
@@ -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
+ ]
@@ -0,0 +1,5 @@
1
+ """async_xenapi — Async XenAPI session via JSON-RPC (stdlib only)."""
2
+
3
+ from .session import AsyncXenAPISession
4
+
5
+ __all__ = ["AsyncXenAPISession"]
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"]