diff --git a/pyproject.toml b/pyproject.toml index cf14991..299a837 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,3 +21,8 @@ openec2 = "openec2:main" [build-system] requires = ["hatchling"] build-backend = "hatchling.build" + +[dependency-groups] +dev = [ + "ruff>=0.11.2", +] diff --git a/src/openec2/actions/deregister_image.py b/src/openec2/actions/deregister_image.py index 8bebcf7..7d6c724 100644 --- a/src/openec2/actions/deregister_image.py +++ b/src/openec2/actions/deregister_image.py @@ -7,6 +7,7 @@ from openec2.db import DatabaseDep from openec2.db.image import AMI from openec2.images import garbage_collect_image + def deregister_image( params: QueryParams, config: OpenEC2Config, diff --git a/src/openec2/actions/describe_images.py b/src/openec2/actions/describe_images.py index b17645d..ac367fa 100644 --- a/src/openec2/actions/describe_images.py +++ b/src/openec2/actions/describe_images.py @@ -30,7 +30,7 @@ def describe_images( requestId=uuid.uuid4().hex, imagesSet=ImagesSet( items=images, - ) + ), ).to_xml(), media_type="application/xml", ) diff --git a/src/openec2/actions/describe_instances.py b/src/openec2/actions/describe_instances.py index be2bf4f..fc3148f 100644 --- a/src/openec2/actions/describe_instances.py +++ b/src/openec2/actions/describe_instances.py @@ -5,12 +5,20 @@ from fastapi.datastructures import QueryParams from sqlmodel import select from openec2.libvirt import LibvirtSingleton -from openec2.api.describe_instances import InstanceDescription, DescribeInstancesResponse, DescribeInstancesResponseReservationSet, ReservationSet, ReservationSetInstancesSet, describe_instance +from openec2.api.describe_instances import ( + InstanceDescription, + DescribeInstancesResponse, + DescribeInstancesResponseReservationSet, + ReservationSet, + ReservationSetInstancesSet, + describe_instance, +) from openec2.api.shared import InstanceState from openec2.config import OpenEC2Config from openec2.db import DatabaseDep from openec2.db.instance import Instance + def describe_instances( params: QueryParams, config: OpenEC2Config, @@ -36,7 +44,8 @@ def describe_instances( instancesSet=ReservationSetInstancesSet( item=[instance], ), - ) for instance in response_items + ) + for instance in response_items ], ), ).to_xml(), diff --git a/src/openec2/actions/import_image.py b/src/openec2/actions/import_image.py index 14de2f6..d8c9c9f 100644 --- a/src/openec2/actions/import_image.py +++ b/src/openec2/actions/import_image.py @@ -11,6 +11,7 @@ from openec2.config import OpenEC2Config from openec2.db import DatabaseDep from openec2.db.image import AMI + def import_image( params: QueryParams, config: OpenEC2Config, diff --git a/src/openec2/actions/run_instances.py b/src/openec2/actions/run_instances.py index 6838a5f..d492e8d 100644 --- a/src/openec2/actions/run_instances.py +++ b/src/openec2/actions/run_instances.py @@ -15,9 +15,14 @@ from openec2.db.image import AMI from openec2.api.run_instances import RunInstanceResponse, RunInstanceInstanceSet from openec2.api.describe_instances import describe_instance from openec2.utils.array import parse_array_objects -from openec2.ipam import get_available_ipv4, is_ipv4_available, add_instance_dhcp_mapping +from openec2.ipam import ( + get_available_ipv4, + is_ipv4_available, + add_instance_dhcp_mapping, +) from openec2.utils.ip import generate_available_mac + def create_libvirt_domain( name: str, memory: int, @@ -84,6 +89,7 @@ def create_libvirt_domain( """ + def run_instances( params: QueryParams, config: OpenEC2Config, @@ -94,7 +100,7 @@ def run_instances( instance_type = config.instances.types.get(params["InstanceType"]) if instance_type is None: - raise Exception(f"Unknown instance type {params["InstanceType"]}") + raise Exception(f"Unknown instance type {params['InstanceType']}") ami = db.exec(select(AMI).where(AMI.id == image_id)).first() if ami is None: @@ -140,7 +146,9 @@ def run_instances( id=instance_id, imageId=image_id, tags=tags, - userData=base64.b64decode(value).decode() if (value := params.get("UserData")) is not None else None, + userData=base64.b64decode(value).decode() + if (value := params.get("UserData")) is not None + else None, privateIPv4=private_ipv4, interfaceMac=mac, ) diff --git a/src/openec2/actions/terminate_instances.py b/src/openec2/actions/terminate_instances.py index 042e179..faa4100 100644 --- a/src/openec2/actions/terminate_instances.py +++ b/src/openec2/actions/terminate_instances.py @@ -18,6 +18,7 @@ from openec2.api.terminate_instances import TerminateInstancesResponse, Instance logger = logging.getLogger() + def terminate_instances( params: QueryParams, config: OpenEC2Config, @@ -31,7 +32,7 @@ def terminate_instances( instance = db.exec(select(Instance).where(Instance.id == instance_id)).first() if instance is None: continue - #raise HTTPException(status_code=404, detail="Unknown instance") + # raise HTTPException(status_code=404, detail="Unknown instance") dom = conn.lookupByName(instance_id) prev_state = describe_instance_state(dom) @@ -54,7 +55,9 @@ def terminate_instances( instance_disk.unlink() image_ids.add(instance.imageId) - remove_instance_dhcp_mapping(instance.id, instance.interfaceMac, instance.privateIPv4, db) + remove_instance_dhcp_mapping( + instance.id, instance.interfaceMac, instance.privateIPv4, db + ) db.delete(instance) db.commit() diff --git a/src/openec2/api/describe_images.py b/src/openec2/api/describe_images.py index c17bf57..950ad0f 100644 --- a/src/openec2/api/describe_images.py +++ b/src/openec2/api/describe_images.py @@ -1,5 +1,6 @@ from pydantic_xml import BaseXmlModel, element + class Image(BaseXmlModel): imageId: str = element() @@ -7,13 +8,15 @@ class Image(BaseXmlModel): name: str = element() + class ImagesSet(BaseXmlModel, tag="imagesSet"): items: list[Image] = element(tag="item") + class DescribeImagesResponse( BaseXmlModel, tag="DescribeImagesResponse", - nsmap={"": "http://ec2.amazonaws.com/doc/2016-11-15/"} + nsmap={"": "http://ec2.amazonaws.com/doc/2016-11-15/"}, ): requestId: str = element() diff --git a/src/openec2/api/describe_instances.py b/src/openec2/api/describe_instances.py index e8738e8..3633664 100644 --- a/src/openec2/api/describe_instances.py +++ b/src/openec2/api/describe_instances.py @@ -9,9 +9,11 @@ class InstanceTag(BaseXmlModel): key: str = element() value: str = element() + class InstanceTagSet(BaseXmlModel): item: list[InstanceTag] = element() + class InstanceDescription( BaseXmlModel, tag="item", @@ -25,20 +27,23 @@ class InstanceDescription( class ReservationSetInstancesSet(BaseXmlModel): item: list[InstanceDescription] = element() + class ReservationSet(BaseXmlModel): reservationId: str = element() instancesSet: ReservationSetInstancesSet = element() + class DescribeInstancesResponseReservationSet( BaseXmlModel, tag="reservationSet", ): item: list[ReservationSet] = element("") + class DescribeInstancesResponse( BaseXmlModel, tag="DescribeInstancesResponse", - nsmap={"": "http://ec2.amazonaws.com/doc/2016-11-15/"} + nsmap={"": "http://ec2.amazonaws.com/doc/2016-11-15/"}, ): request_id: str = element(tag="requestId") reservation_set: DescribeInstancesResponseReservationSet = element("reservationSet") @@ -51,7 +56,10 @@ def describe_instance_state(domain: libvirt.virDomain) -> InstanceState: name="running" if running else "stopped", ) -def describe_instance(instance: Instance, domain: libvirt.virDomain) -> InstanceDescription: + +def describe_instance( + instance: Instance, domain: libvirt.virDomain +) -> InstanceDescription: return InstanceDescription( instance_id=instance.id, image_id=instance.imageId, @@ -61,7 +69,8 @@ def describe_instance(instance: Instance, domain: libvirt.virDomain) -> Instance InstanceTag( key=key, value=value, - ) for key, value in instance.tags.items() + ) + for key, value in instance.tags.items() ], ), ) diff --git a/src/openec2/api/run_instances.py b/src/openec2/api/run_instances.py index 1eed661..5e37066 100644 --- a/src/openec2/api/run_instances.py +++ b/src/openec2/api/run_instances.py @@ -1,12 +1,12 @@ from pydantic_xml import BaseXmlModel, element -from openec2.db.instance import Instance -from openec2.api.shared import InstanceState from openec2.api.describe_instances import InstanceDescription + class RunInstanceInstanceSet(BaseXmlModel): item: list[InstanceDescription] = element() + class RunInstanceResponse(BaseXmlModel): request_id: str = element(tag="requestId") diff --git a/src/openec2/api/start_instances.py b/src/openec2/api/start_instances.py index b199548..90a0550 100644 --- a/src/openec2/api/start_instances.py +++ b/src/openec2/api/start_instances.py @@ -6,7 +6,7 @@ from openec2.api.shared import InstancesSet class StartInstancesResponse( BaseXmlModel, tag="StartInstancesResponse", - nsmap={"": "http://ec2.amazonaws.com/doc/2016-11-15/"} + nsmap={"": "http://ec2.amazonaws.com/doc/2016-11-15/"}, ): request_id: str = element(tag="requestId") instancesSet: InstancesSet = element() diff --git a/src/openec2/api/stop_instances.py b/src/openec2/api/stop_instances.py index 932b150..ac34443 100644 --- a/src/openec2/api/stop_instances.py +++ b/src/openec2/api/stop_instances.py @@ -6,7 +6,7 @@ from openec2.api.shared import InstancesSet class StopInstancesResponse( BaseXmlModel, tag="StopInstancesResponse", - nsmap={"": "http://ec2.amazonaws.com/doc/2016-11-15/"} + nsmap={"": "http://ec2.amazonaws.com/doc/2016-11-15/"}, ): requestId: str = element(tag="requestId") instancesSet: InstancesSet = element() diff --git a/src/openec2/cli/add_user.py b/src/openec2/cli/add_user.py index 2b680c6..3e8bd0f 100644 --- a/src/openec2/cli/add_user.py +++ b/src/openec2/cli/add_user.py @@ -41,5 +41,6 @@ def main(): print(f"Access key: {access_key}") print(f"Secret access key: {secret_access_key}") + if __name__ == "__main__": main() diff --git a/src/openec2/config.py b/src/openec2/config.py index 9ef80b6..a04ff12 100644 --- a/src/openec2/config.py +++ b/src/openec2/config.py @@ -3,19 +3,23 @@ from fastapi import Depends from pydantic import BaseModel from typing import Annotated + class _OpenEC2InstanceType(BaseModel): - memory: int # MiB + memory: int # MiB vcpu: float - disk: int # Gi + disk: int # Gi + class _OpenEC2InstanceConfig(BaseModel): location: Path types: dict[str, _OpenEC2InstanceType] + class _OpenEC2LibvirtConfig(BaseModel): connection: str + class _OpenEC2DatabaseConfig(BaseModel): # DB URL for sqlmodel url: str @@ -23,6 +27,7 @@ class _OpenEC2DatabaseConfig(BaseModel): # Print SQL statements debug: bool + class _OpenEC2Config(BaseModel): images: Path seed: Path @@ -32,6 +37,7 @@ class _OpenEC2Config(BaseModel): debug: bool insecure: bool + def _get_config() -> _OpenEC2Config: # TODO: Read from disk return _OpenEC2Config( @@ -47,9 +53,7 @@ def _get_config() -> _OpenEC2Config: ), }, ), - libvirt=_OpenEC2LibvirtConfig( - connection="qemu:///system" - ), + libvirt=_OpenEC2LibvirtConfig(connection="qemu:///system"), debug=True, insecure=False, database=_OpenEC2DatabaseConfig( @@ -58,6 +62,7 @@ def _get_config() -> _OpenEC2Config: ), ) + class ConfigSingleton: __instance: "ConfigSingleton | None" = None @@ -75,4 +80,5 @@ class ConfigSingleton: ConfigSingleton.__instance = ConfigSingleton() return ConfigSingleton.__instance + OpenEC2Config = Annotated[_OpenEC2Config, Depends(ConfigSingleton.of().get_config)] diff --git a/src/openec2/db/__init__.py b/src/openec2/db/__init__.py index 7c2137a..27ac23c 100644 --- a/src/openec2/db/__init__.py +++ b/src/openec2/db/__init__.py @@ -12,8 +12,10 @@ engine = create_engine( echo=ConfigSingleton.of().config.database.debug, ) + def get_session() -> Generator[Session]: with Session(engine) as session: yield session + DatabaseDep = Annotated[Session, Depends(get_session)] diff --git a/src/openec2/db/image.py b/src/openec2/db/image.py index 60be1fc..e433aa3 100644 --- a/src/openec2/db/image.py +++ b/src/openec2/db/image.py @@ -1,5 +1,6 @@ from sqlmodel import SQLModel, Field + class AMI(SQLModel, table=True): # ID of the AMI id: str = Field(default=None, primary_key=True) diff --git a/src/openec2/db/instance.py b/src/openec2/db/instance.py index a6428e6..b0a0e33 100644 --- a/src/openec2/db/instance.py +++ b/src/openec2/db/instance.py @@ -1,5 +1,6 @@ from sqlmodel import SQLModel, Field, JSON, Column + class Instance(SQLModel, table=True): id: str = Field(default=None, primary_key=True) diff --git a/src/openec2/db/ipam.py b/src/openec2/db/ipam.py index b528291..0392d36 100644 --- a/src/openec2/db/ipam.py +++ b/src/openec2/db/ipam.py @@ -19,6 +19,4 @@ class IPAMEntry(SQLModel, table=True): def set_ipv4(self, addr: str): self.ipv4_addr_raw = ipv4_to_int(addr) - __table_args = ( - PrimaryKeyConstraint("ipv4_addr_raw", "vpc_id"), - ) + __table_args = (PrimaryKeyConstraint("ipv4_addr_raw", "vpc_id"),) diff --git a/src/openec2/db/user.py b/src/openec2/db/user.py index 7efe4b1..843684e 100644 --- a/src/openec2/db/user.py +++ b/src/openec2/db/user.py @@ -1,5 +1,6 @@ from sqlmodel import SQLModel, Field + class User(SQLModel, table=True): id: int = Field(default=None, primary_key=True) diff --git a/src/openec2/db/vpc.py b/src/openec2/db/vpc.py index 2253c9e..478f60b 100644 --- a/src/openec2/db/vpc.py +++ b/src/openec2/db/vpc.py @@ -1,5 +1,6 @@ from sqlmodel import SQLModel, Field, PrimaryKeyConstraint + class VPC(SQLModel, table=True): # ID of the VPC id: str = Field(default=None, primary_key=True) diff --git a/src/openec2/images.py b/src/openec2/images.py index 6c804ee..8d221d4 100644 --- a/src/openec2/images.py +++ b/src/openec2/images.py @@ -13,7 +13,9 @@ def garbage_collect_image(image_id: str, db: DatabaseDep): print(instances) return - ami = db.exec(select(AMI).where(AMI.id == image_id, AMI.deregistered == True)).first() + ami = db.exec( + select(AMI).where(AMI.id == image_id, AMI.deregistered == True) + ).first() if ami is not None: db.delete(ami) db.commit() diff --git a/src/openec2/ipam.py b/src/openec2/ipam.py index 8cb6539..1a3d5d5 100644 --- a/src/openec2/ipam.py +++ b/src/openec2/ipam.py @@ -34,9 +34,16 @@ def add_instance_dhcp_mapping(instance_id: str, mac: str, ipv4: str, db: Databas flags=libvirt.VIR_NETWORK_UPDATE_AFFECT_LIVE, ) -def remove_instance_dhcp_mapping(instance_id: str, mac: str ,ipv4: str, db: DatabaseDep): + +def remove_instance_dhcp_mapping( + instance_id: str, mac: str, ipv4: str, db: DatabaseDep +): i = ipv4_to_int(ipv4) - entry = db.exec(select(IPAMEntry).where(IPAMEntry.ipv4_addr_raw == i, IPAMEntry.instance_id == instance_id)).first() + entry = db.exec( + select(IPAMEntry).where( + IPAMEntry.ipv4_addr_raw == i, IPAMEntry.instance_id == instance_id + ) + ).first() db.delete(entry) # Tell libvirt about this mapping @@ -49,13 +56,21 @@ def remove_instance_dhcp_mapping(instance_id: str, mac: str ,ipv4: str, db: Data flags=libvirt.VIR_NETWORK_UPDATE_AFFECT_LIVE, ) + def is_ipv4_available(ipv4: str, db: DatabaseDep) -> bool: i = ipv4_to_int(ipv4) - return db.exec(select(IPAMEntry).where(IPAMEntry.ipv4_addr_raw == i)).first() is None + return ( + db.exec(select(IPAMEntry).where(IPAMEntry.ipv4_addr_raw == i)).first() is None + ) + def get_available_ipv4(db: DatabaseDep) -> str: entries = db.exec(select(IPAMEntry)).all() # TODO: Use the VPC's subnet - max_ip = max(e.ipv4_addr_raw for e in entries) if entries else ipv4_to_int("192.168.122.2") + max_ip = ( + max(e.ipv4_addr_raw for e in entries) + if entries + else ipv4_to_int("192.168.122.2") + ) # TODO: Check if we're still inside the subnet return int_to_ipv4(max_ip + 1) diff --git a/src/openec2/main.py b/src/openec2/main.py index e57fb90..ddf7841 100644 --- a/src/openec2/main.py +++ b/src/openec2/main.py @@ -21,27 +21,35 @@ from openec2.db.instance import Instance app = FastAPI() + @app.on_event("startup") def on_startup(): SQLModel.metadata.create_all(engine) + @app.get("/healthz", response_model=None) def healthz(): return { "status": "OK", } + @app.get("/Action", response_model=None) 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): +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() + 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, @@ -68,7 +76,9 @@ def cloud_init_data(instance_id: str, entry: str, db: DatabaseDep): raise HTTPException(status_code=404, detail="Unknown cloud-init file") if entry == "user-data": - instance = db.exec(select(Instance).where(Instance.id == instance_id)).first()[0] + instance = db.exec(select(Instance).where(Instance.id == instance_id)).first()[ + 0 + ] if instance is None: raise HTTPException(status_code=404, detail="Unknown instance") @@ -79,17 +89,21 @@ def cloud_init_data(instance_id: str, entry: str, db: DatabaseDep): media_type="application/yaml", ) elif entry == "meta-data": - return multiline_yaml_response([ - f"instance-id: {instance_id}", - f"local-hostname: {instance_id}", - ]) + return multiline_yaml_response( + [ + f"instance-id: {instance_id}", + f"local-hostname: {instance_id}", + ] + ) elif entry == "vendor-data": - return multiline_yaml_response([ - "#cloud-config", - "growpart:", - " devices: [/]", - " ignore_growroot_disabled: true", - ]) + return multiline_yaml_response( + [ + "#cloud-config", + "growpart:", + " devices: [/]", + " ignore_growroot_disabled: true", + ] + ) elif entry == "network-config": return Response( "", diff --git a/src/openec2/security/aws.py b/src/openec2/security/aws.py index eac55de..6012c4a 100644 --- a/src/openec2/security/aws.py +++ b/src/openec2/security/aws.py @@ -19,6 +19,7 @@ def _hmac_sha256(key: bytes, payload: bytes) -> bytes: h.update(payload) return h.finalize() + @dataclass class AWSRequest: # The entire used URL @@ -36,43 +37,57 @@ class AWSRequest: # 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: + 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_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 + ] ) - ]) - 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" + + "\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, - ]) + 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, - ]) + 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) @@ -83,6 +98,7 @@ class AWSRequest: 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 @@ -91,10 +107,12 @@ class AWSAuthentication: 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) @@ -110,21 +128,30 @@ def get_authentication_info(request: Request) -> AWSAuthentication: ) 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) + 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}") + raise HTTPException( + status_code=400, detail=f"Invalid signature algorithm: {x_amz_algorithm}" + ) x_amz_credential = auth_info.x_amz_credential @@ -145,13 +172,12 @@ async def check_request_signature(request: Request, db: DatabaseDep): user.secret_access_key, region, service, - "/".join([ - date, region, service, key - ]), + "/".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)] diff --git a/src/openec2/utils/array.py b/src/openec2/utils/array.py index 75f7d61..60ab58a 100644 --- a/src/openec2/utils/array.py +++ b/src/openec2/utils/array.py @@ -10,6 +10,7 @@ def parse_array_objects(prefix: str, params: dict) -> list[dict[str, str]]: items[parts[1]][".".join(parts[2:])] = value return list(items.values()) + def parse_array_plain(prefix: str, params: dict[str, str]) -> list[str]: items: dict[str, str] = {} for key, value in params.items(): diff --git a/src/openec2/utils/ip.py b/src/openec2/utils/ip.py index d12f0b3..4851821 100644 --- a/src/openec2/utils/ip.py +++ b/src/openec2/utils/ip.py @@ -9,17 +9,19 @@ from openec2.db.instance import Instance def ipv4_to_int(ip: str) -> int: i = 0 for idx, p in enumerate(ip.split(".")): - i += (int(p) << (3-idx)*8) + i += int(p) << (3 - idx) * 8 return i + def int_to_ipv4(ip: int) -> str: parts: list[int] = [] for i in reversed(range(4)): parts.append( - (ip >> i*8) & 255, + (ip >> i * 8) & 255, ) return ".".join(str(p) for p in parts) + def generate_mac() -> str: mac_bytes = random.randbytes(6) mac = "" @@ -35,10 +37,14 @@ def generate_mac() -> str: mac += f"{h}:" return mac[:-1] + def generate_available_mac(db: DatabaseDep) -> str: mac = "" while True: mac = generate_mac() - if db.exec(select(Instance).where(Instance.interfaceMac == mac)).first() is None: + if ( + db.exec(select(Instance).where(Instance.interfaceMac == mac)).first() + is None + ): break return mac diff --git a/src/openec2/utils/qemu.py b/src/openec2/utils/qemu.py index 542a0e6..b7722eb 100644 --- a/src/openec2/utils/qemu.py +++ b/src/openec2/utils/qemu.py @@ -3,12 +3,17 @@ import subprocess def create_cow_copy(src: Path, dst: Path, size: str): - subprocess.call([ - "qemu-img", - "create", - "-f", "qcow2", - "-b", str(src), - "-F", "qcow2", - str(dst), - size, - ]) + subprocess.call( + [ + "qemu-img", + "create", + "-f", + "qcow2", + "-b", + str(src), + "-F", + "qcow2", + str(dst), + size, + ] + ) diff --git a/src/openec2/utils/text.py b/src/openec2/utils/text.py index 6fa4f4d..6956e79 100644 --- a/src/openec2/utils/text.py +++ b/src/openec2/utils/text.py @@ -1,5 +1,6 @@ from fastapi import Response + def multiline_yaml_response(lines: list[str]) -> Response: return Response( "\n".join(lines), diff --git a/tests/openec2/utils/test_array.py b/tests/openec2/utils/test_array.py index 631db3e..b62b43e 100644 --- a/tests/openec2/utils/test_array.py +++ b/tests/openec2/utils/test_array.py @@ -14,6 +14,7 @@ def test_array_parsing_keys(): assert parsed[1]["a"] == "3" assert parsed[1]["b"] == "4" + def test_array_plain_parsing(): params = { "Key.1": "1", diff --git a/uv.lock b/uv.lock index f4206e0..daed187 100644 --- a/uv.lock +++ b/uv.lock @@ -373,6 +373,11 @@ dependencies = [ { name = "sqlmodel" }, ] +[package.dev-dependencies] +dev = [ + { name = "ruff" }, +] + [package.metadata] requires-dist = [ { name = "cryptography", specifier = ">=44.0.2" }, @@ -384,6 +389,9 @@ requires-dist = [ { name = "sqlmodel", specifier = ">=0.0.24" }, ] +[package.metadata.requires-dev] +dev = [{ name = "ruff", specifier = ">=0.11.2" }] + [[package]] name = "packaging" version = "24.2" @@ -568,6 +576,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/a2/dc0ae0b61d5fce9eec3763c98d5a471f7b07c891a2cbfb3fd6a0f632a9a1/rich_toolkit-0.14.0-py3-none-any.whl", hash = "sha256:75ff4b3e70e27e9cb145164bfe8d8e56758162fa3f87594067f4d85630b98bf9", size = 24062 }, ] +[[package]] +name = "ruff" +version = "0.11.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/61/fb87430f040e4e577e784e325351186976516faef17d6fcd921fe28edfd7/ruff-0.11.2.tar.gz", hash = "sha256:ec47591497d5a1050175bdf4e1a4e6272cddff7da88a2ad595e1e326041d8d94", size = 3857511 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/99/102578506f0f5fa29fd7e0df0a273864f79af044757aef73d1cae0afe6ad/ruff-0.11.2-py3-none-linux_armv6l.whl", hash = "sha256:c69e20ea49e973f3afec2c06376eb56045709f0212615c1adb0eda35e8a4e477", size = 10113146 }, + { url = "https://files.pythonhosted.org/packages/74/ad/5cd4ba58ab602a579997a8494b96f10f316e874d7c435bcc1a92e6da1b12/ruff-0.11.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2c5424cc1c4eb1d8ecabe6d4f1b70470b4f24a0c0171356290b1953ad8f0e272", size = 10867092 }, + { url = "https://files.pythonhosted.org/packages/fc/3e/d3f13619e1d152c7b600a38c1a035e833e794c6625c9a6cea6f63dbf3af4/ruff-0.11.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ecf20854cc73f42171eedb66f006a43d0a21bfb98a2523a809931cda569552d9", size = 10224082 }, + { url = "https://files.pythonhosted.org/packages/90/06/f77b3d790d24a93f38e3806216f263974909888fd1e826717c3ec956bbcd/ruff-0.11.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c543bf65d5d27240321604cee0633a70c6c25c9a2f2492efa9f6d4b8e4199bb", size = 10394818 }, + { url = "https://files.pythonhosted.org/packages/99/7f/78aa431d3ddebfc2418cd95b786642557ba8b3cb578c075239da9ce97ff9/ruff-0.11.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20967168cc21195db5830b9224be0e964cc9c8ecf3b5a9e3ce19876e8d3a96e3", size = 9952251 }, + { url = "https://files.pythonhosted.org/packages/30/3e/f11186d1ddfaca438c3bbff73c6a2fdb5b60e6450cc466129c694b0ab7a2/ruff-0.11.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:955a9ce63483999d9f0b8f0b4a3ad669e53484232853054cc8b9d51ab4c5de74", size = 11563566 }, + { url = "https://files.pythonhosted.org/packages/22/6c/6ca91befbc0a6539ee133d9a9ce60b1a354db12c3c5d11cfdbf77140f851/ruff-0.11.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:86b3a27c38b8fce73bcd262b0de32e9a6801b76d52cdb3ae4c914515f0cef608", size = 12208721 }, + { url = "https://files.pythonhosted.org/packages/19/b0/24516a3b850d55b17c03fc399b681c6a549d06ce665915721dc5d6458a5c/ruff-0.11.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3b66a03b248c9fcd9d64d445bafdf1589326bee6fc5c8e92d7562e58883e30f", size = 11662274 }, + { url = "https://files.pythonhosted.org/packages/d7/65/76be06d28ecb7c6070280cef2bcb20c98fbf99ff60b1c57d2fb9b8771348/ruff-0.11.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0397c2672db015be5aa3d4dac54c69aa012429097ff219392c018e21f5085147", size = 13792284 }, + { url = "https://files.pythonhosted.org/packages/ce/d2/4ceed7147e05852876f3b5f3fdc23f878ce2b7e0b90dd6e698bda3d20787/ruff-0.11.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:869bcf3f9abf6457fbe39b5a37333aa4eecc52a3b99c98827ccc371a8e5b6f1b", size = 11327861 }, + { url = "https://files.pythonhosted.org/packages/c4/78/4935ecba13706fd60ebe0e3dc50371f2bdc3d9bc80e68adc32ff93914534/ruff-0.11.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2a2b50ca35457ba785cd8c93ebbe529467594087b527a08d487cf0ee7b3087e9", size = 10276560 }, + { url = "https://files.pythonhosted.org/packages/81/7f/1b2435c3f5245d410bb5dc80f13ec796454c21fbda12b77d7588d5cf4e29/ruff-0.11.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7c69c74bf53ddcfbc22e6eb2f31211df7f65054bfc1f72288fc71e5f82db3eab", size = 9945091 }, + { url = "https://files.pythonhosted.org/packages/39/c4/692284c07e6bf2b31d82bb8c32f8840f9d0627d92983edaac991a2b66c0a/ruff-0.11.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6e8fb75e14560f7cf53b15bbc55baf5ecbe373dd5f3aab96ff7aa7777edd7630", size = 10977133 }, + { url = "https://files.pythonhosted.org/packages/94/cf/8ab81cb7dd7a3b0a3960c2769825038f3adcd75faf46dd6376086df8b128/ruff-0.11.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:842a472d7b4d6f5924e9297aa38149e5dcb1e628773b70e6387ae2c97a63c58f", size = 11378514 }, + { url = "https://files.pythonhosted.org/packages/d9/3a/a647fa4f316482dacf2fd68e8a386327a33d6eabd8eb2f9a0c3d291ec549/ruff-0.11.2-py3-none-win32.whl", hash = "sha256:aca01ccd0eb5eb7156b324cfaa088586f06a86d9e5314b0eb330cb48415097cc", size = 10319835 }, + { url = "https://files.pythonhosted.org/packages/86/54/3c12d3af58012a5e2cd7ebdbe9983f4834af3f8cbea0e8a8c74fa1e23b2b/ruff-0.11.2-py3-none-win_amd64.whl", hash = "sha256:3170150172a8f994136c0c66f494edf199a0bbea7a409f649e4bc8f4d7084080", size = 11373713 }, + { url = "https://files.pythonhosted.org/packages/d6/d4/dd813703af8a1e2ac33bf3feb27e8a5ad514c9f219df80c64d69807e7f71/ruff-0.11.2-py3-none-win_arm64.whl", hash = "sha256:52933095158ff328f4c77af3d74f0379e34fd52f175144cefc1b192e7ccd32b4", size = 10441990 }, +] + [[package]] name = "shellingham" version = "1.5.4"