Implement AWS authentication

This commit is contained in:
PapaTutuWawa 2025-03-30 16:14:06 +02:00
parent 6ee5778878
commit 6b4eaa6448
7 changed files with 308 additions and 3 deletions

View File

@ -6,6 +6,7 @@ readme = "README.md"
authors = []
requires-python = ">=3.13"
dependencies = [
"cryptography>=44.0.2",
"fastapi[standard]>=0.115.12",
"libvirt-python>=11.1.0",
"pydantic-xml>=2.14.3",

View File

@ -0,0 +1,45 @@
import argparse
import secrets
import base64
from openec2.db import get_session
from openec2.db.user import User
from sqlmodel import select
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--name", type=str, required=True)
args = parser.parse_args()
secret_access_key = base64.b64encode(
secrets.token_bytes(64),
).decode()
db = next(get_session())
access_key = base64.b64encode(
secrets.token_bytes(32),
).decode()
while True:
if db.exec(select(User).where(User.access_key == access_key)).first() is None:
break
access_key = base64.b64encode(
secrets.token_bytes(32),
).decode()
db.add(
User(
name=args.name,
access_key=access_key,
secret_access_key=secret_access_key,
)
)
db.commit()
print(f"Access key: {access_key}")
print(f"Secret access key: {secret_access_key}")
if __name__ == "__main__":
main()

View File

@ -30,6 +30,7 @@ class _OpenEC2Config(BaseModel):
libvirt: _OpenEC2LibvirtConfig
database: _OpenEC2DatabaseConfig
debug: bool
insecure: bool
def _get_config() -> _OpenEC2Config:
# TODO: Read from disk
@ -50,6 +51,7 @@ def _get_config() -> _OpenEC2Config:
connection="qemu:///system"
),
debug=True,
insecure=False,
database=_OpenEC2DatabaseConfig(
url="sqlite:////home/alexander/openec2/db2.sqlite3",
debug=True,

11
src/openec2/db/user.py Normal file
View File

@ -0,0 +1,11 @@
from sqlmodel import SQLModel, Field
class User(SQLModel, table=True):
id: int = Field(default=None, primary_key=True)
# Human-readable id
name: str
# For request signing
access_key: str
secret_access_key: str

View File

@ -1,7 +1,11 @@
from urllib.parse import parse_qs
from typing import cast
from fastapi import FastAPI, HTTPException, Request, Response
from sqlalchemy import select
from sqlmodel import SQLModel
from openec2.security.aws import AWSSignature
from openec2.utils.text import multiline_yaml_response
from openec2.config import OpenEC2Config
from openec2.db import DatabaseDep, engine
@ -28,8 +32,24 @@ def healthz():
}
@app.get("/Action", response_model=None)
def action(request: Request, config: OpenEC2Config, db: DatabaseDep):
action = request.query_params["Action"]
def action(request: Request, config: OpenEC2Config, db: DatabaseDep, _: AWSSignature):
return run_action(request, config, db, cast(dict, request.query_params))
@app.post("/Action", response_model=None)
async def test(request: Request, config: OpenEC2Config, db: DatabaseDep, _: AWSSignature):
query_params = {
key: value[0] for key, value in parse_qs((await request.body()).decode()).items()
}
return run_action(request, config, db, cast(dict, query_params))
def run_action(
request: Request,
config: OpenEC2Config,
db: DatabaseDep,
query_params: dict[str, str],
):
print(query_params)
action = query_params["Action"]
return {
"ImportImage": import_image,
"DescribeImages": describe_images,
@ -39,7 +59,8 @@ def action(request: Request, config: OpenEC2Config, db: DatabaseDep):
"StartInstances": start_instances,
"StopInstances": stop_instances,
"DeregisterImage": deregister_image,
}[action](request.query_params, config, db)
}[action](query_params, config, db)
@app.get("/private/cloudinit/{instance_id}/{entry}")
def cloud_init_data(instance_id: str, entry: str, db: DatabaseDep):

157
src/openec2/security/aws.py Normal file
View File

@ -0,0 +1,157 @@
from typing import Annotated, cast
from dataclasses import dataclass
import datetime
from hashlib import sha256
from urllib.parse import quote, parse_qs
from sqlmodel import select
from fastapi import Request, HTTPException, Depends
from fastapi.datastructures import QueryParams, URL, Headers
from cryptography.hazmat.primitives import hashes, hmac
from openec2.config import ConfigSingleton
from openec2.db import DatabaseDep
from openec2.db.user import User
def _hmac_sha256(key: bytes, payload: bytes) -> bytes:
h = hmac.HMAC(key, hashes.SHA256())
h.update(payload)
return h.finalize()
@dataclass
class AWSRequest:
# The entire used URL
url: URL
# Request method
method: str
# Query params
params: dict[str, str]
# Request headers
headers: Headers
# The payload, if we used a POST/PUT
payload: str | None
def sign(self, secret_access_key: str, region: str, product: str, credential_scope: str) -> str:
dt = datetime.datetime.fromisoformat(self.headers["X-Amz-Date"])
canonical_query_string_keys = sorted(self.params.keys())
canonical_query_string = "&".join([
f"{key}={quote(self.params[key][0])}" for key in canonical_query_string_keys if key not in (
"X-Amz-Signature",
)
])
canonical_header_string_keys = sorted([
name for name in self.headers.keys() if include_in_canonical_string(name)
])
canonical_header_string = "\n".join([
f"{name.lower()}:{self.headers[name].strip()}" for name in canonical_header_string_keys
]) + "\n"
signed_headers = ";".join(canonical_header_string_keys)
hashed_payload = sha256((self.payload or "").encode()).hexdigest()
canonical_request = "\n".join([
self.method.upper(),
self.url.path or "/",
#canonical_query_string,
"",
canonical_header_string,
signed_headers,
hashed_payload,
])
print("Canonical request")
print(canonical_request)
hashed_canonical_request = sha256(canonical_request.encode()).hexdigest()
date = dt.strftime("%Y%m%d")
string_to_sign = "\n".join([
"AWS4-HMAC-SHA256",
dt.strftime("%Y%m%dT%H%M%SZ"),
credential_scope,
hashed_canonical_request,
])
print("String to sign")
print(string_to_sign)
date_key = _hmac_sha256(f"AWS4{secret_access_key}".encode(), date.encode())
date_region_key = _hmac_sha256(date_key, region.encode())
date_region_service_key = _hmac_sha256(date_region_key, product.encode())
signing_key = _hmac_sha256(date_region_service_key, "aws4_request".encode())
return _hmac_sha256(signing_key, string_to_sign.encode()).hex()
@dataclass
class AWSAuthentication:
x_amz_algorithm: str
x_amz_credential: str
x_amz_signature: str
def include_in_canonical_string(name: str) -> bool:
lower = name.lower()
return lower in ("host", "content-type") or lower.startswith("x-amz")
def get_authentication_info(request: Request) -> AWSAuthentication:
if request.method == "POST":
algorithm, rest = request.headers["Authorization"].split(" ", 1)
auth = {}
for part in rest.split(","):
key, value = part.split("=", 1)
auth[key.strip()] = value.strip()
return AWSAuthentication(
x_amz_algorithm=algorithm,
x_amz_credential=auth["Credential"],
x_amz_signature=auth["Signature"],
)
return AWSAuthentication(
"", "", "",
)
async def check_request_signature(request: Request, db: DatabaseDep):
# Do not check if we don't care
if ConfigSingleton.of().config.insecure:
return
body = (await request.body()).decode()
query_params = cast(dict, parse_qs(body)) if request.method == "POST" else cast(dict, request.query_params)
auth_info = get_authentication_info(request)
if auth_info.x_amz_algorithm != "AWS4-HMAC-SHA256":
raise HTTPException(status_code=400, detail=f"Invalid signature algorithm: {x_amz_algorithm}")
x_amz_credential = auth_info.x_amz_credential
reversed_parts = x_amz_credential[::-1].split("/", 4)
access_key, date, region, service, key = [s[::-1] for s in reversed_parts][::-1]
user = db.exec(select(User).where(User.access_key == access_key)).first()
if user is None:
raise HTTPException(status_code=403)
x_amz_signature = auth_info.x_amz_signature
signature = AWSRequest(
url=request.url,
method=request.method,
params=query_params,
headers=request.headers,
payload=body,
).sign(
user.secret_access_key,
region,
service,
"/".join([
date, region, service, key
]),
)
print(x_amz_signature, signature)
if x_amz_signature != signature:
raise HTTPException(status_code=401)
AWSSignature = Annotated[None, Depends(check_request_signature)]

68
uv.lock
View File

@ -33,6 +33,28 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 },
]
[[package]]
name = "cffi"
version = "1.17.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pycparser" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 },
{ url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 },
{ url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 },
{ url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 },
{ url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 },
{ url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 },
{ url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 },
{ url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 },
{ url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 },
{ url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 },
{ url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 },
]
[[package]]
name = "charset-normalizer"
version = "3.4.1"
@ -76,6 +98,41 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
]
[[package]]
name = "cryptography"
version = "44.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cd/25/4ce80c78963834b8a9fd1cc1266be5ed8d1840785c0f2e1b73b8d128d505/cryptography-44.0.2.tar.gz", hash = "sha256:c63454aa261a0cf0c5b4718349629793e9e634993538db841165b3df74f37ec0", size = 710807 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/92/ef/83e632cfa801b221570c5f58c0369db6fa6cef7d9ff859feab1aae1a8a0f/cryptography-44.0.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:efcfe97d1b3c79e486554efddeb8f6f53a4cdd4cf6086642784fa31fc384e1d7", size = 6676361 },
{ url = "https://files.pythonhosted.org/packages/30/ec/7ea7c1e4c8fc8329506b46c6c4a52e2f20318425d48e0fe597977c71dbce/cryptography-44.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1", size = 3952350 },
{ url = "https://files.pythonhosted.org/packages/27/61/72e3afdb3c5ac510330feba4fc1faa0fe62e070592d6ad00c40bb69165e5/cryptography-44.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc821e161ae88bfe8088d11bb39caf2916562e0a2dc7b6d56714a48b784ef0bb", size = 4166572 },
{ url = "https://files.pythonhosted.org/packages/26/e4/ba680f0b35ed4a07d87f9e98f3ebccb05091f3bf6b5a478b943253b3bbd5/cryptography-44.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3c00b6b757b32ce0f62c574b78b939afab9eecaf597c4d624caca4f9e71e7843", size = 3958124 },
{ url = "https://files.pythonhosted.org/packages/9c/e8/44ae3e68c8b6d1cbc59040288056df2ad7f7f03bbcaca6b503c737ab8e73/cryptography-44.0.2-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7bdcd82189759aba3816d1f729ce42ffded1ac304c151d0a8e89b9996ab863d5", size = 3678122 },
{ url = "https://files.pythonhosted.org/packages/27/7b/664ea5e0d1eab511a10e480baf1c5d3e681c7d91718f60e149cec09edf01/cryptography-44.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4973da6ca3db4405c54cd0b26d328be54c7747e89e284fcff166132eb7bccc9c", size = 4191831 },
{ url = "https://files.pythonhosted.org/packages/2a/07/79554a9c40eb11345e1861f46f845fa71c9e25bf66d132e123d9feb8e7f9/cryptography-44.0.2-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4e389622b6927d8133f314949a9812972711a111d577a5d1f4bee5e58736b80a", size = 3960583 },
{ url = "https://files.pythonhosted.org/packages/bb/6d/858e356a49a4f0b591bd6789d821427de18432212e137290b6d8a817e9bf/cryptography-44.0.2-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f514ef4cd14bb6fb484b4a60203e912cfcb64f2ab139e88c2274511514bf7308", size = 4191753 },
{ url = "https://files.pythonhosted.org/packages/b2/80/62df41ba4916067fa6b125aa8c14d7e9181773f0d5d0bd4dcef580d8b7c6/cryptography-44.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1bc312dfb7a6e5d66082c87c34c8a62176e684b6fe3d90fcfe1568de675e6688", size = 4079550 },
{ url = "https://files.pythonhosted.org/packages/f3/cd/2558cc08f7b1bb40683f99ff4327f8dcfc7de3affc669e9065e14824511b/cryptography-44.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b721b8b4d948b218c88cb8c45a01793483821e709afe5f622861fc6182b20a7", size = 4298367 },
{ url = "https://files.pythonhosted.org/packages/71/59/94ccc74788945bc3bd4cf355d19867e8057ff5fdbcac781b1ff95b700fb1/cryptography-44.0.2-cp37-abi3-win32.whl", hash = "sha256:51e4de3af4ec3899d6d178a8c005226491c27c4ba84101bfb59c901e10ca9f79", size = 2772843 },
{ url = "https://files.pythonhosted.org/packages/ca/2c/0d0bbaf61ba05acb32f0841853cfa33ebb7a9ab3d9ed8bb004bd39f2da6a/cryptography-44.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:c505d61b6176aaf982c5717ce04e87da5abc9a36a5b39ac03905c4aafe8de7aa", size = 3209057 },
{ url = "https://files.pythonhosted.org/packages/9e/be/7a26142e6d0f7683d8a382dd963745e65db895a79a280a30525ec92be890/cryptography-44.0.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8e0ddd63e6bf1161800592c71ac794d3fb8001f2caebe0966e77c5234fa9efc3", size = 6677789 },
{ url = "https://files.pythonhosted.org/packages/06/88/638865be7198a84a7713950b1db7343391c6066a20e614f8fa286eb178ed/cryptography-44.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81276f0ea79a208d961c433a947029e1a15948966658cf6710bbabb60fcc2639", size = 3951919 },
{ url = "https://files.pythonhosted.org/packages/d7/fc/99fe639bcdf58561dfad1faa8a7369d1dc13f20acd78371bb97a01613585/cryptography-44.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1e657c0f4ea2a23304ee3f964db058c9e9e635cc7019c4aa21c330755ef6fd", size = 4167812 },
{ url = "https://files.pythonhosted.org/packages/53/7b/aafe60210ec93d5d7f552592a28192e51d3c6b6be449e7fd0a91399b5d07/cryptography-44.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6210c05941994290f3f7f175a4a57dbbb2afd9273657614c506d5976db061181", size = 3958571 },
{ url = "https://files.pythonhosted.org/packages/16/32/051f7ce79ad5a6ef5e26a92b37f172ee2d6e1cce09931646eef8de1e9827/cryptography-44.0.2-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1c3572526997b36f245a96a2b1713bf79ce99b271bbcf084beb6b9b075f29ea", size = 3679832 },
{ url = "https://files.pythonhosted.org/packages/78/2b/999b2a1e1ba2206f2d3bca267d68f350beb2b048a41ea827e08ce7260098/cryptography-44.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b042d2a275c8cee83a4b7ae30c45a15e6a4baa65a179a0ec2d78ebb90e4f6699", size = 4193719 },
{ url = "https://files.pythonhosted.org/packages/72/97/430e56e39a1356e8e8f10f723211a0e256e11895ef1a135f30d7d40f2540/cryptography-44.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d03806036b4f89e3b13b6218fefea8d5312e450935b1a2d55f0524e2ed7c59d9", size = 3960852 },
{ url = "https://files.pythonhosted.org/packages/89/33/c1cf182c152e1d262cac56850939530c05ca6c8d149aa0dcee490b417e99/cryptography-44.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c7362add18b416b69d58c910caa217f980c5ef39b23a38a0880dfd87bdf8cd23", size = 4193906 },
{ url = "https://files.pythonhosted.org/packages/e1/99/87cf26d4f125380dc674233971069bc28d19b07f7755b29861570e513650/cryptography-44.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8cadc6e3b5a1f144a039ea08a0bdb03a2a92e19c46be3285123d32029f40a922", size = 4081572 },
{ url = "https://files.pythonhosted.org/packages/b3/9f/6a3e0391957cc0c5f84aef9fbdd763035f2b52e998a53f99345e3ac69312/cryptography-44.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f101b1f780f7fc613d040ca4bdf835c6ef3b00e9bd7125a4255ec574c7916e4", size = 4298631 },
{ url = "https://files.pythonhosted.org/packages/e2/a5/5bc097adb4b6d22a24dea53c51f37e480aaec3465285c253098642696423/cryptography-44.0.2-cp39-abi3-win32.whl", hash = "sha256:3dc62975e31617badc19a906481deacdeb80b4bb454394b4098e3f2525a488c5", size = 2773792 },
{ url = "https://files.pythonhosted.org/packages/33/cf/1f7649b8b9a3543e042d3f348e398a061923ac05b507f3f4d95f11938aa9/cryptography-44.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:5f6f90b72d8ccadb9c6e311c775c8305381db88374c65fa1a68250aa8a9cb3a6", size = 3210957 },
]
[[package]]
name = "dnspython"
version = "2.7.0"
@ -307,6 +364,7 @@ name = "openec2"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "cryptography" },
{ name = "fastapi", extra = ["standard"] },
{ name = "libvirt-python" },
{ name = "pydantic-xml" },
@ -317,6 +375,7 @@ dependencies = [
[package.metadata]
requires-dist = [
{ name = "cryptography", specifier = ">=44.0.2" },
{ name = "fastapi", extras = ["standard"], specifier = ">=0.115.12" },
{ name = "libvirt-python", specifier = ">=11.1.0" },
{ name = "pydantic-xml", specifier = ">=2.14.3" },
@ -343,6 +402,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 },
]
[[package]]
name = "pycparser"
version = "2.22"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 },
]
[[package]]
name = "pydantic"
version = "2.11.0"