diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7975dcb..22f51e8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -68,7 +68,7 @@ jobs: - name: Build run: | pip install --user -r dev-requirements.txt - pylint --rcfile ${GITHUB_WORKSPACE}/.pylintrc ${GITHUB_WORKSPACE}/src/mrmat_python_cli --exit-zero + pylint --rcfile ${GITHUB_WORKSPACE}/.pylintrc ${GITHUB_WORKSPACE}/src/kaso_mashin --exit-zero python -m build --wheel -n PYTHONPATH=${GITHUB_WORKSPACE}/src python -m pytest diff --git a/.idea/misc.xml b/.idea/misc.xml index 91444ca..712f985 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,7 +3,7 @@ diff --git a/dev-requirements.txt b/dev-requirements.txt index 8dd3dce..b4653c4 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -4,23 +4,23 @@ # Build/Test requirements -setuptools>=42.0.0 -build>=0.9.0 # MIT -wheel>=0.36.0 # MIT -pylint~=2.17.5 # MIT -pytest~=7.4.2 # GPL-2.0-or-later -pytest-cov~=4.1.0 # MIT +setuptools==69.0.2 +build==0.9.0 # MIT +wheel==0.41.3 # MIT +pylint==3.0.2 # MIT +pytest==7.4.3 # GPL-2.0-or-later +pytest-cov==4.1.0 # MIT # Runtime requirements -rich~=13.5.3 # MIT -requests~=2.31.0 # Apache 2.0 -pyyaml~=6.0 # MIT -netifaces~=0.11.0 # MIT -sqlalchemy~=2.0.20 # MIT -fastapi~=0.103.1 # MIT -uvicorn~=0.23.2 # BSD 3-Clause -httpx~=0.25.0 # BSD 3-Clause -aiofiles~=23.2.1 # Apache 2.0 -qemu.qmp~=0.0.3 # ? -passlib~=1.7.4 # BSD +rich==13.7.0 # MIT +requests==2.31.0 # Apache 2.0 +pyyaml==6.0 # MIT +netifaces==0.11.0 # MIT +sqlalchemy==2.0.23 # MIT +fastapi==0.104.1 # MIT +uvicorn==0.23.2 # BSD 3-Clause +httpx==0.24.1 # BSD 3-Clause +aiofiles==23.2.1 # Apache 2.0 +qemu.qmp==0.0.3 # ? +passlib==1.7.4 # BSD \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 011a2e2..d465cfd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [build-system] requires = [ - 'setuptools>=42.0.0', - 'wheel >= 0.36.0' + 'setuptools==69.0.2', + 'wheel==0.41.3' ] build-backend = 'setuptools.build_meta' @@ -9,7 +9,7 @@ build-backend = 'setuptools.build_meta' name = "kaso-mashin" description = "Building a mini-cloud as a playground" urls = { "Sources" = "https://github.com/MrMatAP/kaso-mashin" } -keywords = ["experimental"] +keywords = ["mac", "virtualization", "virtualisation", "arm64"] readme = "README.md" license = { text = "MIT" } authors = [ @@ -25,17 +25,17 @@ classifiers = [ ] requires-python = ">=3.10" dependencies = [ - "rich~=13.5.2", # MIT - "requests~=2.31.0", # Apache 2.0 - "pyyaml~=6.0", # MIT - "netifaces~=0.11.0", # MIT - "sqlalchemy~=2.0.20", # MIT - "fastapi~=0.103.1", # MIT - "uvicorn~=0.23.2", # BSD 3-Clause - "httpx~=0.25.0", # BSD 3-Clause - "aiofiles~=23.2.1", # Apache 2.0 - "qemu.qmp~=0.0.3", # ? - "passlib~=1.7.4" # BSD + "rich==13.7.0", # MIT + "requests==2.31.0", # Apache 2.0 + "pyyaml==6.0", # MIT + "netifaces==0.11.0", # MIT + "sqlalchemy==2.0.23", # MIT + "fastapi==0.104.1", # MIT + "uvicorn==0.23.2", # BSD 3-Clause + "httpx==0.24.1", # BSD 3-Clause + "aiofiles==23.2.1", # Apache 2.0 + "qemu.qmp==0.0.3", # ? + "passlib==1.7.4" # BSD ] dynamic = ["version"] diff --git a/src/kaso_mashin/cli/commands/identity_commands.py b/src/kaso_mashin/cli/commands/identity_commands.py index 55698ea..c0ed584 100644 --- a/src/kaso_mashin/cli/commands/identity_commands.py +++ b/src/kaso_mashin/cli/commands/identity_commands.py @@ -139,7 +139,7 @@ def create(self, args: argparse.Namespace) -> int: homedir=args.homedir, shell=args.shell) if not args.pubkey and not args.passwd: - console.print(f'[red]ERROR[/red]: You must either provide the path to a public key or a password') + console.print('[red]ERROR[/red]: You must either provide the path to a public key or a password') return 1 if args.pubkey: schema.kind = IdentityKind.PUBKEY diff --git a/src/kaso_mashin/common/config.py b/src/kaso_mashin/common/config.py index d06c096..f1cd32c 100644 --- a/src/kaso_mashin/common/config.py +++ b/src/kaso_mashin/common/config.py @@ -41,7 +41,7 @@ def load(self, config_file: pathlib.Path): if not config_file.exists(): self._logger.debug('No configuration file exists, using defaults') return - self._logger.debug(f'Loading config file at {config_file}') + self._logger.debug('Loading config file at %s', config_file) configurable = {field.name: field.type for field in dataclasses.fields(self)} try: with open(config_file, 'r', encoding='UTF-8') as c: @@ -53,7 +53,7 @@ def load(self, config_file: pathlib.Path): setattr(self, key, pathlib.Path(value)) else: setattr(self, key, value) - self._logger.debug(f'Config file overrides {key} to {value}') + self._logger.debug('Config file overrides %s to %s', key, value) except yaml.YAMLError as exc: raise KasoMashinException(status=400, msg='Invalid config file') from exc @@ -69,10 +69,10 @@ def cli_override(self, args: argparse.Namespace): value = configured.get(key) if value != getattr(self, key): setattr(self, key, value) - self._logger.debug(f'CLI overrides {key} to {value}') + self._logger.debug('CLI overrides %s to %s', key, value) def save(self, config_file: pathlib.Path): - self._logger.debug(f'Saving configuration at {config_file}') + self._logger.debug('Saving configuration at %s', config_file) configured = {field.name: getattr(self, field.name) for field in dataclasses.fields(self)} try: with open(config_file, 'w+', encoding='UTF-8') as c: diff --git a/src/kaso_mashin/common/model/instance_model.py b/src/kaso_mashin/common/model/instance_model.py index 02da562..3a93d5a 100644 --- a/src/kaso_mashin/common/model/instance_model.py +++ b/src/kaso_mashin/common/model/instance_model.py @@ -58,7 +58,9 @@ class InstanceCreateSchema(pydantic.BaseModel): image_id: int = pydantic.Field(description='Image ID to use as the backing OS disk', examples=[1]) network_id: int = pydantic.Field(description='Network ID to connect the instance to', examples=[1]) os_disk_size: str = pydantic.Field(description='The OS disk size in GB', default='5G', examples=['5G']) - identities: list = pydantic.Field(description='The identities on that instance', default_factory=list, examples=['1']) + identities: list = pydantic.Field(description='The identities on that instance', + default_factory=list, + examples=['1']) class InstanceModifySchema(pydantic.BaseModel): diff --git a/src/kaso_mashin/common/model/qemu_model.py b/src/kaso_mashin/common/model/qemu_model.py index b44d970..1580e45 100644 --- a/src/kaso_mashin/common/model/qemu_model.py +++ b/src/kaso_mashin/common/model/qemu_model.py @@ -63,14 +63,16 @@ def __init__(self, model: InstanceModel): self._cmd = self._generate_cmd() self._process: subprocess.Popen | None = None self._logger = logging.getLogger(f'{self.__class__.__module__}.{self.__class__.__name__}') - self._logger.info(f'Initialised for {model.name}') + self._logger.info('Initialised for %s', model.name) def _generate_cmd(self) -> str: - cmd = (f'{self.emulator} -name {self.model.name} -machine virt -cpu host -accel hvf -smp {self.model.vcpu} -m {self.model.ram} ' + cmd = (f'{self.emulator} -name {self.model.name} -machine virt -cpu host -accel hvf ' + f'-smp {self.model.vcpu} -m {self.model.ram} ' f'-bios /opt/homebrew/share/qemu/edk2-aarch64-code.fd ' '-device virtio-rng-pci -device nec-usb-xhci,id=usb-bus -device usb-kbd,bus=usb-bus.0 ' f'-drive if=virtio,file={self.model.os_disk_path},format=qcow2,cache=writethrough ' - f'-smbios type=3,manufacturer=MrMat,version=0,serial=instance_{self.model.instance_id},asset={self.model.name},sku=MrMat ' + f'-smbios type=3,manufacturer=MrMat,version=0,serial=instance_{self.model.instance_id},' + f'asset={self.model.name},sku=MrMat ' f'-chardev socket,id=char0,server=on,wait=off,path={self.model.console_path} ' f'-qmp unix:{self.model.qmp_path},server=on,wait=off ') match self.model.display: @@ -120,7 +122,8 @@ def _generate_cmd(self) -> str: def generate_script(self): with open(self.model.vm_script_path, mode='w', encoding='UTF-8') as v: v.write(f'#!/bin/bash\n# ' - f'This script can be used to manually start the instance it is located in\n\n{self._generate_cmd()}') + f'This script can be used to manually start the instance it is located in' + f'\n\n{self._generate_cmd()}') self.model.vm_script_path.chmod(0o755) def _wait_for_bridge(self) -> bool | None: @@ -137,13 +140,14 @@ def _wait_for_bridge(self) -> bool | None: return self.model.network.host_ip4 in addr2if def start(self): + # pylint: disable=consider-using-with self.process = subprocess.Popen(args=shlex.split(self._generate_cmd()), encoding='UTF-8') self._logger.info('Started QEmu process') # TODO: Cloud-init will only ever phone home once per instance, unless we make it a runcmd # Wait for the bridge to come up attempt = 0 while attempt < 15 and not self._wait_for_bridge(): - self._logger.info(f'Waiting for bridge to come up ({attempt}/15)') + self._logger.info('Waiting for bridge to come up (%s/15)', attempt) attempt += 1 time.sleep(1) if attempt == 9: @@ -151,10 +155,11 @@ def start(self): self._logger.info('Bridge has come up') def _instance_phoned_home(actual_ip: str): - self._logger.info(f'Instance has phoned home. Actual IP: ${actual_ip}') + self._logger.info('Instance has phoned home. Actual IP: %s', actual_ip) - self._logger.info(f'Starting phone home server on host {self.model.network.host_ip4}') - httpd = PhoneHomeServer(server_address=(str(self.model.network.host_ip4), self.model.network.host_phone_home_port), + self._logger.info('Starting phone home server on host %s', self.model.network.host_ip4) + httpd = PhoneHomeServer(server_address=(str(self.model.network.host_ip4), + self.model.network.host_phone_home_port), callback=_instance_phoned_home, RequestHandlerClass=PhoneHomeHandler) httpd.timeout = 60 diff --git a/src/kaso_mashin/common/model/relation_tables.py b/src/kaso_mashin/common/model/relation_tables.py index e182bd8..45b44fc 100644 --- a/src/kaso_mashin/common/model/relation_tables.py +++ b/src/kaso_mashin/common/model/relation_tables.py @@ -7,4 +7,4 @@ Base.metadata, Column('instance_id', ForeignKey('instances.instance_id')), Column('identity_id', ForeignKey('identities.identity_id')) -) \ No newline at end of file +) diff --git a/src/kaso_mashin/server/controllers/identity_controller.py b/src/kaso_mashin/server/controllers/identity_controller.py index bbc48d5..9b8fa77 100644 --- a/src/kaso_mashin/server/controllers/identity_controller.py +++ b/src/kaso_mashin/server/controllers/identity_controller.py @@ -43,7 +43,7 @@ def create(self, model: IdentityModel) -> IdentityModel: return model except sqlalchemy.exc.SQLAlchemyError as sae: self.db.session.rollback() - raise KasoMashinException(status=500, msg=f'Database exception: {sae}') + raise KasoMashinException(status=500, msg=f'Database exception: {sae}') from sae def modify(self, identity_id: int, update: IdentityModel) -> IdentityModel: try: @@ -69,7 +69,7 @@ def modify(self, identity_id: int, update: IdentityModel) -> IdentityModel: return current except sqlalchemy.exc.SQLAlchemyError as sae: self.db.session.rollback() - raise KasoMashinException(status=500, msg=f'Database exception: {sae}') + raise KasoMashinException(status=500, msg=f'Database exception: {sae}') from sae def remove(self, identity_id: int): try: @@ -87,4 +87,4 @@ def remove(self, identity_id: int): return False except sqlalchemy.exc.SQLAlchemyError as sae: self.db.session.rollback() - raise KasoMashinException(status=500, msg=f'Database exception: {sae}') + raise KasoMashinException(status=500, msg=f'Database exception: {sae}') from sae diff --git a/src/kaso_mashin/server/run.py b/src/kaso_mashin/server/run.py index 79bd38f..7df0262 100644 --- a/src/kaso_mashin/server/run.py +++ b/src/kaso_mashin/server/run.py @@ -31,7 +31,7 @@ def create_server(runtime: Runtime) -> fastapi.applications.FastAPI: @app.exception_handler(KasoMashinException) # pylint: disable=unused-argument async def kaso_mashin_exception_handler(request: fastapi.Request, exc: KasoMashinException): - logging.getLogger('kaso_mashin.server').error(f'({exc.status}) {exc.msg}') + logging.getLogger('kaso_mashin.server').error('(%s) %s', exc.status, exc.msg) return fastapi.responses.JSONResponse(status_code=exc.status, content=ExceptionSchema(status=exc.status, msg=exc.msg) .model_dump()) @@ -39,7 +39,7 @@ async def kaso_mashin_exception_handler(request: fastapi.Request, exc: KasoMashi @app.exception_handler(sqlalchemy.exc.SQLAlchemyError) # pylint: disable=unused-argument async def sqlalchemy_exception_handler(request: fastapi.Request, exc: sqlalchemy.exc.SQLAlchemyError): - logging.getLogger('kaso_mashin.server').error(f'(500) Database exception {exc}') + logging.getLogger('kaso_mashin.server').error('(500) Database exception %s', str(exc)) return fastapi.responses.JSONResponse(status_code=500, content=ExceptionSchema(status=500, msg=f'Database exception {exc}') .model_dump()) @@ -87,8 +87,8 @@ def main(args: typing.Optional[typing.List] = None) -> int: # TODO: We should move this into config runtime.late_init(server=True) try: - logger.info(f'Effective user {runtime.effective_user}') - logger.info(f'Owning user {runtime.owning_user}') + logger.info('Effective user %s', runtime.effective_user) + logger.info('Owning user %s', runtime.owning_user) app = create_server(runtime) uvicorn.run(app, host=config.default_server_host, port=config.default_server_port) return 0