diff --git a/docker/vm_supervisor-dev.dockerfile b/docker/vm_supervisor-dev.dockerfile index da730aca8..77718f3da 100644 --- a/docker/vm_supervisor-dev.dockerfile +++ b/docker/vm_supervisor-dev.dockerfile @@ -19,7 +19,7 @@ RUN curl -fsSL -o /opt/firecracker/vmlinux.bin https://s3.amazonaws.com/spec.ccf RUN ln /opt/firecracker/release-*/firecracker-v* /opt/firecracker/firecracker RUN ln /opt/firecracker/release-*/jailer-v* /opt/firecracker/jailer -RUN pip3 install typing-extensions 'aleph-message==0.4.4' +RUN pip3 install typing-extensions 'aleph-message==0.4.7' RUN mkdir -p /var/lib/aleph/vm/jailer diff --git a/examples/confidential_instance_message_from_aleph.json b/examples/confidential_instance_message_from_aleph.json index 5ea2e1c00..e44ef2928 100644 --- a/examples/confidential_instance_message_from_aleph.json +++ b/examples/confidential_instance_message_from_aleph.json @@ -1,51 +1,55 @@ { - "chain": "ETH", - "item_hash": "fake-hash-fake-hash-fake-hash-fake-hash-fake-hash-fake-hash-hash", - "sender": "0x9319Ad3B7A8E0eE24f2E639c40D8eD124C5520Ba", - "type": "INSTANCE", - "channel": "Fun-dApps", - "confirmed": true, - "content": { - "address": "0x9319Ad3B7A8E0eE24f2E639c40D8eD124C5520Ba", - "allow_amend": false, - "variables": { - "VM_CUSTOM_NUMBER": "32" - }, - "environment": { - "reproducible": true, - "internet": true, - "aleph_api": true, - "shared_cache": true - }, - "resources": { - "vcpus": 1, - "memory": 512, - "seconds": 30 - }, - "rootfs": { - "parent": { - "ref": "549ec451d9b099cad112d4aaa2c00ac40fb6729a92ff252ff22eef0b5c3cb613", - "use_latest": true - }, - "persistence": "host", - "size_mib": 5000 - }, - "authorized_keys": [ - "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDj95BHGUx0/z2G/tTrEi8o49i70xvjcEUdSs3j4A33jE7pAphrfRVbuFMgFubcm8n9r5ftd/H8SjjTL4hY9YvWV5ZuMf92GUga3n4wgevvPlBszYZCy/idxFl0vtHYC1CcK9v4tVb9onhDt8FOJkf2m6PmDyvC+6tl6LwoerXTeeiKr5VnTB4KOBkammtFmix3d1X1SZd/cxdwZIHcQ7BNsqBm2w/YzVba6Z4ZnFUelBkQtMQqNs2aV51O1pFFqtZp2mM71D5d8vn9pOtqJ5QmY5IW6NypcyqKJZg5o6QguK5rdXLkc7AWro27BiaHIENl3w0wazp9EDO9zPAGJ6lz olivier@lanius" - ], - - "time": 1619017773.8950517 + "chain": "ETH", + "item_hash": "fake-hash-fake-hash-fake-hash-fake-hash-fake-hash-fake-hash-hash", + "sender": "0x9319Ad3B7A8E0eE24f2E639c40D8eD124C5520Ba", + "type": "INSTANCE", + "channel": "Fun-dApps", + "confirmed": true, + "content": { + "address": "0x9319Ad3B7A8E0eE24f2E639c40D8eD124C5520Ba", + "allow_amend": false, + "variables": { + "VM_CUSTOM_NUMBER": "32" }, - "item_content": "{\"address\":\"0x9319Ad3B7A8E0eE24f2E639c40D8eD124C5520Ba\",\"allow_amend\":false,\"variables\":{\"VM_CUSTOM_NUMBER\":\"32\"},\"environment\":{\"reproducible\":true,\"internet\":true,\"aleph_api\":true,\"shared_cache\":true},\"resources\":{\"vcpus\":1,\"memory\":128,\"seconds\":30},\"rootfs\":{\"parent\":{\"ref\":\"549ec451d9b099cad112d4aaa2c00ac40fb6729a92ff252ff22eef0b5c3cb613\",\"use_latest\":true},\"persistence\":\"host\",\"size_mib\":20000},\"cloud_config\":{\"password\":\"password\",\"chpasswd\":{\"expire\":\"False\"}},\"volumes\":[{\"mount\":\"/opt/venv\",\"ref\":\"5f31b0706f59404fad3d0bff97ef89ddf24da4761608ea0646329362c662ba51\",\"use_latest\":false},{\"comment\":\"Working data persisted on the VM supervisor, not available on other nodes\",\"mount\":\"/var/lib/example\",\"name\":\"data\",\"persistence\":\"host\",\"size_mib\":5}],\"replaces\":\"0x9319Ad3B7A8E0eE24f2E639c40D8eD124C5520Ba\",\"time\":1619017773.8950517}", - "item_type": "inline", - "signature": "0x372da8230552b8c3e65c05b31a0ff3a24666d66c575f8e11019f62579bf48c2b7fe2f0bbe907a2a5bf8050989cdaf8a59ff8a1cbcafcdef0656c54279b4aa0c71b", - "size": 749, - "time": 1619017773.8950577, - "confirmations": [ - { - "chain": "ETH", - "height": 12284734, - "hash": "0x67f2f3cde5e94e70615c92629c70d22dc959a118f46e9411b29659c2fce87cdc" - } - ] + "environment": { + "hypervisor": "qemu", + "reproducible": true, + "internet": true, + "aleph_api": true, + "shared_cache": true, + "trusted_execution": { + "firmware": "88978bb4c2ff54400ce5f51c3a109e1af1ab03d1ea4409666917317ac513846b", + "policy": 1 + } + }, + "resources": { + "vcpus": 1, + "memory": 512, + "seconds": 30 + }, + "rootfs": { + "parent": { + "ref": "549ec451d9b099cad112d4aaa2c00ac40fb6729a92ff252ff22eef0b5c3cb613", + "use_latest": true + }, + "persistence": "host", + "size_mib": 5000 + }, + "authorized_keys": [ + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDj95BHGUx0/z2G/tTrEi8o49i70xvjcEUdSs3j4A33jE7pAphrfRVbuFMgFubcm8n9r5ftd/H8SjjTL4hY9YvWV5ZuMf92GUga3n4wgevvPlBszYZCy/idxFl0vtHYC1CcK9v4tVb9onhDt8FOJkf2m6PmDyvC+6tl6LwoerXTeeiKr5VnTB4KOBkammtFmix3d1X1SZd/cxdwZIHcQ7BNsqBm2w/YzVba6Z4ZnFUelBkQtMQqNs2aV51O1pFFqtZp2mM71D5d8vn9pOtqJ5QmY5IW6NypcyqKJZg5o6QguK5rdXLkc7AWro27BiaHIENl3w0wazp9EDO9zPAGJ6lz olivier@lanius" + ], + "time": 1619017773.8950517 + }, + "item_content": "{\"address\":\"0x9319Ad3B7A8E0eE24f2E639c40D8eD124C5520Ba\",\"allow_amend\":false,\"variables\":{\"VM_CUSTOM_NUMBER\":\"32\"},\"environment\":{\"reproducible\":true,\"internet\":true,\"aleph_api\":true,\"shared_cache\":true},\"resources\":{\"vcpus\":1,\"memory\":128,\"seconds\":30},\"rootfs\":{\"parent\":{\"ref\":\"549ec451d9b099cad112d4aaa2c00ac40fb6729a92ff252ff22eef0b5c3cb613\",\"use_latest\":true},\"persistence\":\"host\",\"size_mib\":20000},\"cloud_config\":{\"password\":\"password\",\"chpasswd\":{\"expire\":\"False\"}},\"volumes\":[{\"mount\":\"/opt/venv\",\"ref\":\"5f31b0706f59404fad3d0bff97ef89ddf24da4761608ea0646329362c662ba51\",\"use_latest\":false},{\"comment\":\"Working data persisted on the VM supervisor, not available on other nodes\",\"mount\":\"/var/lib/example\",\"name\":\"data\",\"persistence\":\"host\",\"size_mib\":5}],\"replaces\":\"0x9319Ad3B7A8E0eE24f2E639c40D8eD124C5520Ba\",\"time\":1619017773.8950517}", + "item_type": "inline", + "signature": "0x372da8230552b8c3e65c05b31a0ff3a24666d66c575f8e11019f62579bf48c2b7fe2f0bbe907a2a5bf8050989cdaf8a59ff8a1cbcafcdef0656c54279b4aa0c71b", + "size": 749, + "time": 1619017773.8950577, + "confirmations": [ + { + "chain": "ETH", + "height": 12284734, + "hash": "0x67f2f3cde5e94e70615c92629c70d22dc959a118f46e9411b29659c2fce87cdc" + } + ] } diff --git a/examples/volumes/Dockerfile b/examples/volumes/Dockerfile index c5e67993f..f3aad1e18 100644 --- a/examples/volumes/Dockerfile +++ b/examples/volumes/Dockerfile @@ -6,6 +6,6 @@ RUN apt-get update && apt-get -y upgrade && apt-get install -y \ && rm -rf /var/lib/apt/lists/* RUN python3 -m venv /opt/venv -RUN /opt/venv/bin/pip install 'aleph-message==0.4.4' +RUN /opt/venv/bin/pip install 'aleph-message==0.4.7' CMD mksquashfs /opt/venv /mnt/volume-venv.squashfs diff --git a/packaging/Makefile b/packaging/Makefile index a1df5ecbd..8b497da13 100644 --- a/packaging/Makefile +++ b/packaging/Makefile @@ -15,7 +15,7 @@ debian-package-code: cp ../examples/instance_message_from_aleph.json ./aleph-vm/opt/aleph-vm/examples/instance_message_from_aleph.json cp -r ../examples/data ./aleph-vm/opt/aleph-vm/examples/data mkdir -p ./aleph-vm/opt/aleph-vm/examples/volumes - pip3 install --target ./aleph-vm/opt/aleph-vm/ 'aleph-message==0.4.4' 'eth-account==0.10' 'sentry-sdk==1.31.0' 'qmp==1.1.0' 'superfluid==0.2.1' 'sqlalchemy[asyncio]>=2.0' 'aiosqlite==0.19.0' 'alembic==1.13.1' 'aiohttp_cors==0.7.0' 'pyroute2==0.7.12' + pip3 install --target ./aleph-vm/opt/aleph-vm/ 'aleph-message==0.4.7' 'eth-account==0.10' 'sentry-sdk==1.31.0' 'qmp==1.1.0' 'superfluid==0.2.1' 'sqlalchemy[asyncio]>=2.0' 'aiosqlite==0.19.0' 'alembic==1.13.1' 'aiohttp_cors==0.7.0' 'pyroute2==0.7.12' python3 -m compileall ./aleph-vm/opt/aleph-vm/ debian-package-resources: firecracker-bins vmlinux download-ipfs-kubo target/bin/sevctl diff --git a/pyproject.toml b/pyproject.toml index bd26530db..e26b91e9f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ dependencies = [ "aiodns==3.1.0", "setproctitle==1.3.3", "pyyaml==6.0.1", - "aleph-message==0.4.4", + "aleph-message>=0.4.7", "eth-account~=0.10", "sentry-sdk==1.31.0", "aioredis==1.3.1", diff --git a/runtimes/aleph-debian-12-python/create_disk_image.sh b/runtimes/aleph-debian-12-python/create_disk_image.sh index bfaf050f1..859d678d5 100755 --- a/runtimes/aleph-debian-12-python/create_disk_image.sh +++ b/runtimes/aleph-debian-12-python/create_disk_image.sh @@ -36,7 +36,7 @@ locale-gen en_US.UTF-8 echo "Pip installing aleph-sdk-python" mkdir -p /opt/aleph/libs -pip3 install --target /opt/aleph/libs 'aleph-sdk-python==0.9.0' 'aleph-message==0.4.4' 'fastapi~=0.109.2' +pip3 install --target /opt/aleph/libs 'aleph-sdk-python==0.9.0' 'aleph-message==0.4.7' 'fastapi~=0.109.2' # Compile Python code to bytecode for faster execution # -o2 is needed to compile with optimization level 2 which is what we launch init1.py ("python -OO") diff --git a/src/aleph/vm/controllers/configuration.py b/src/aleph/vm/controllers/configuration.py index a03b381ab..32f869bea 100644 --- a/src/aleph/vm/controllers/configuration.py +++ b/src/aleph/vm/controllers/configuration.py @@ -1,7 +1,7 @@ import logging from enum import Enum from pathlib import Path -from typing import Optional, Union +from typing import List, Optional, Union from pydantic import BaseModel @@ -18,6 +18,12 @@ class VMConfiguration(BaseModel): init_timeout: float +class QemuVMHostVolume(BaseModel): + mount: str + path_on_host: Path + read_only: bool + + class QemuVMConfiguration(BaseModel): qemu_bin_path: str cloud_init_drive_path: Optional[str] @@ -27,6 +33,7 @@ class QemuVMConfiguration(BaseModel): vcpu_count: int mem_size_mb: int interface_name: Optional[str] + host_volumes: List[QemuVMHostVolume] class QemuConfidentialVMConfiguration(BaseModel): @@ -38,9 +45,11 @@ class QemuConfidentialVMConfiguration(BaseModel): vcpu_count: int mem_size_mb: int interface_name: Optional[str] + host_volumes: List[QemuVMHostVolume] ovmf_path: Path sev_session_file: Path sev_dh_cert_file: Path + sev_policy: int class HypervisorType(str, Enum): diff --git a/src/aleph/vm/controllers/qemu/instance.py b/src/aleph/vm/controllers/qemu/instance.py index b60aff4f0..8aea8f457 100644 --- a/src/aleph/vm/controllers/qemu/instance.py +++ b/src/aleph/vm/controllers/qemu/instance.py @@ -1,3 +1,4 @@ +import asyncio import json import logging import shutil @@ -17,6 +18,7 @@ Configuration, HypervisorType, QemuVMConfiguration, + QemuVMHostVolume, save_controller_configuration, ) from aleph.vm.controllers.firecracker.executable import ( @@ -34,11 +36,17 @@ class AlephQemuResources(AlephFirecrackerResources): - async def download_all(self) -> None: + async def download_runtime(self) -> None: volume = self.message_content.rootfs parent_image_path = await get_rootfs_base_path(volume.parent.ref) self.rootfs_path = await self.make_writable_volume(parent_image_path, volume) + async def download_all(self): + await asyncio.gather( + self.download_runtime(), + self.download_volumes(), + ) + async def make_writable_volume(self, parent_image_path, volume: Union[PersistentVolume, RootfsVolume]): """Create a new qcow2 image file based on the passed one, that we give to the VM to write onto""" qemu_img_path: Optional[str] = shutil.which("qemu-img") @@ -181,6 +189,14 @@ async def configure(self): vcpu_count=vcpu_count, mem_size_mb=mem_size_mb, interface_name=interface_name, + host_volumes=[ + QemuVMHostVolume( + mount=volume.mount, + path_on_host=volume.path_on_host, + read_only=volume.read_only, + ) + for volume in self.resources.volumes + ], ) configuration = Configuration( diff --git a/src/aleph/vm/controllers/qemu_confidential/instance.py b/src/aleph/vm/controllers/qemu_confidential/instance.py index fd371dfb0..82a5dd049 100644 --- a/src/aleph/vm/controllers/qemu_confidential/instance.py +++ b/src/aleph/vm/controllers/qemu_confidential/instance.py @@ -2,16 +2,18 @@ import logging import shutil from asyncio.subprocess import Process +from pathlib import Path from typing import Callable, Optional from aleph_message.models import ItemHash -from aleph_message.models.execution.environment import MachineResources +from aleph_message.models.execution.environment import AMDSEVPolicy, MachineResources from aleph.vm.conf import settings from aleph.vm.controllers.configuration import ( Configuration, HypervisorType, QemuConfidentialVMConfiguration, + QemuVMHostVolume, save_controller_configuration, ) from aleph.vm.controllers.qemu import AlephQemuInstance @@ -21,19 +23,30 @@ logger, ) from aleph.vm.network.interfaces import TapInterface +from aleph.vm.storage import get_existing_file logger = logging.getLogger(__name__) class AlephQemuConfidentialResources(AlephQemuResources): - # TODO: Implement download of the custom OVMF bootloader to use if specified, if not only use the default one. - pass + firmware_path: Path + + async def download_firmware(self): + firmware = self.message_content.environment.trusted_execution.firmware + self.firmware_path = await get_existing_file(firmware) + + async def download_all(self): + await asyncio.gather( + self.download_runtime(), + self.download_firmware(), + self.download_volumes(), + ) class AlephQemuConfidentialInstance(AlephQemuInstance): vm_id: int vm_hash: ItemHash - resources: AlephQemuResources + resources: AlephQemuConfidentialResources enable_console: bool enable_networking: bool hardware_resources: MachineResources @@ -45,6 +58,7 @@ class AlephQemuConfidentialInstance(AlephQemuInstance): persistent = True _queue_cancellers: dict[asyncio.Queue, Callable] = {} controller_configuration: Configuration + confidential_policy: int def __repr__(self): return f"" @@ -56,12 +70,17 @@ def __init__( self, vm_id: int, vm_hash: ItemHash, - resources: AlephQemuResources, + resources: AlephQemuConfidentialResources, enable_networking: bool = False, + confidential_policy: int = AMDSEVPolicy.NO_DBG, enable_console: Optional[bool] = None, hardware_resources: MachineResources = MachineResources(), tap_interface: Optional[TapInterface] = None, ): + super().__init__( + vm_id, vm_hash, resources, enable_networking, enable_console, hardware_resources, tap_interface + ) + self.confidential_policy = confidential_policy super().__init__(vm_id, vm_hash, resources, enable_networking, hardware_resources, tap_interface) async def setup(self): @@ -76,6 +95,7 @@ async def configure(self): cloud_init_drive = await self._create_cloud_init_drive() image_path = str(self.resources.rootfs_path) + firmware_path = str(self.resources.firmware_path) vcpu_count = self.hardware_resources.vcpus mem_size_mib = self.hardware_resources.memory mem_size_mb = str(int(mem_size_mib / 1024 / 1024 * 1000 * 1000)) @@ -98,9 +118,18 @@ async def configure(self): vcpu_count=vcpu_count, mem_size_mb=mem_size_mb, interface_name=interface_name, - ovmf_path="/home/olivier/custom-OVMF.fd", + ovmf_path=firmware_path, sev_session_file=session_file_path, sev_dh_cert_file=godh_file_path, + sev_policy=self.confidential_policy, + host_volumes=[ + QemuVMHostVolume( + mount=volume.mount, + path_on_host=volume.path_on_host, + read_only=volume.read_only, + ) + for volume in self.resources.volumes + ], ) configuration = Configuration( diff --git a/src/aleph/vm/hypervisors/qemu/qemuvm.py b/src/aleph/vm/hypervisors/qemu/qemuvm.py index 79d7c9d12..501505830 100644 --- a/src/aleph/vm/hypervisors/qemu/qemuvm.py +++ b/src/aleph/vm/hypervisors/qemu/qemuvm.py @@ -1,5 +1,6 @@ import asyncio from asyncio.subprocess import Process +from dataclasses import dataclass from pathlib import Path from typing import Optional, TextIO @@ -10,6 +11,12 @@ from aleph.vm.controllers.qemu.instance import logger +@dataclass +class HostVolume: + path_on_host: Path + read_only: bool + + class QemuVM: qemu_bin_path: str cloud_init_drive_path: Optional[str] @@ -20,6 +27,7 @@ class QemuVM: mem_size_mb: int interface_name: str qemu_process: Optional[Process] = None + host_volumes: list[HostVolume] def __repr__(self) -> str: if self.qemu_process: @@ -38,6 +46,14 @@ def __init__(self, vm_hash, config: QemuVMConfiguration): self.interface_name = config.interface_name self.vm_hash = vm_hash + self.host_volumes = [ + HostVolume( + path_on_host=volume.path_on_host, + read_only=volume.read_only, + ) + for volume in config.host_volumes + ] + @property def _journal_stdout_name(self) -> str: return f"vm-{self.vm_hash}-stdout" @@ -88,6 +104,11 @@ async def start( # "-serial", "telnet:localhost:4321,server,nowait", # "-snapshot", # Do not save anything to disk ] + for volume in self.host_volumes: + args += [ + "-drive", + f"file={volume.path_on_host},format=raw,readonly={'on' if volume.read_only else 'off'},media=disk,if=virtio", + ] if self.interface_name: # script=no, downscript=no tell qemu not to try to set up the network itself args += ["-net", "nic,model=virtio", "-net", f"tap,ifname={self.interface_name},script=no,downscript=no"] diff --git a/src/aleph/vm/hypervisors/qemu_confidential/qemuvm.py b/src/aleph/vm/hypervisors/qemu_confidential/qemuvm.py index dd31b2580..6b76f62f3 100644 --- a/src/aleph/vm/hypervisors/qemu_confidential/qemuvm.py +++ b/src/aleph/vm/hypervisors/qemu_confidential/qemuvm.py @@ -3,6 +3,7 @@ from pathlib import Path from typing import TextIO +from aleph_message.models.execution.environment import AMDSEVPolicy from cpuid.features import secure_encryption_info from systemd import journal @@ -13,9 +14,9 @@ class QemuConfidentialVM(QemuVM): - sev_policy: str = "0x1" # FIXME have it passed from guest + sev_policy: str = hex(AMDSEVPolicy.NO_DBG) sev_dh_cert_file: Path # "vm_godh.b64" - sev_session_file: Path # "vm_session.b64" + sev_session_file: Path # "vm_session.b64" def __repr__(self) -> str: if self.qemu_process: @@ -37,6 +38,7 @@ def __init__(self, vm_hash, config: QemuConfidentialVMConfiguration): self.ovmf_path: Path = config.ovmf_path self.sev_session_file = config.sev_session_file self.sev_dh_cert_file = config.sev_dh_cert_file + self.sev_policy = hex(config.sev_policy) def prepare_start(self): pass @@ -108,6 +110,11 @@ async def start( # "-serial", "telnet:localhost:4321,server,nowait", # "-snapshot", # Do not save anything to disk ] + for volume in self.host_volumes: + args += [ + "-drive", + f"file={volume.path_on_host},format=raw,readonly={'on' if volume.read_only else 'off'},media=disk,if=virtio", + ] if self.interface_name: # script=no, downscript=no tell qemu not to try to set up the network itself args += ["-net", "nic,model=virtio", "-net", f"tap,ifname={self.interface_name},script=no,downscript=no"] diff --git a/src/aleph/vm/models.py b/src/aleph/vm/models.py index b5ad64f75..6f87b2363 100644 --- a/src/aleph/vm/models.py +++ b/src/aleph/vm/models.py @@ -110,7 +110,7 @@ def is_instance(self) -> bool: @property def is_confidential(self) -> bool: - return self.uses_payment_stream # TODO: check also if the VM message is confidential + return True if self.message.environment.trusted_execution else False @property def hypervisor(self) -> HypervisorType: @@ -186,14 +186,19 @@ async def prepare(self) -> None: return self.times.preparing_at = datetime.now(tz=timezone.utc) - resources: Union[AlephProgramResources, AlephInstanceResources, AlephQemuResources] + resources: Union[ + AlephProgramResources, AlephInstanceResources, AlephQemuResources, AlephQemuConfidentialInstance + ] if self.is_program: resources = AlephProgramResources(self.message, namespace=self.vm_hash) elif self.is_instance: if self.hypervisor == HypervisorType.firecracker: resources = AlephInstanceResources(self.message, namespace=self.vm_hash) elif self.hypervisor == HypervisorType.qemu: - resources = AlephQemuResources(self.message, namespace=self.vm_hash) + if self.is_confidential: + resources = AlephQemuConfidentialResources(self.message, namespace=self.vm_hash) + else: + resources = AlephQemuResources(self.message, namespace=self.vm_hash) else: raise ValueError(f"Unknown hypervisor type {self.hypervisor}") else: diff --git a/src/aleph/vm/orchestrator/README.md b/src/aleph/vm/orchestrator/README.md index a9d9a3136..95430423a 100644 --- a/src/aleph/vm/orchestrator/README.md +++ b/src/aleph/vm/orchestrator/README.md @@ -86,7 +86,7 @@ is used to parse and validate Aleph messages. ```shell apt install -y --no-install-recommends --no-install-suggests python3-pip pip3 install pydantic[dotenv] -pip3 install 'aleph-message==0.4.4' +pip3 install 'aleph-message==0.4.7' ``` ### 2.f. Create the jailer working directory: diff --git a/tests/supervisor/test_operator.py b/tests/supervisor/test_operator.py deleted file mode 100644 index 24c81ae30..000000000 --- a/tests/supervisor/test_operator.py +++ /dev/null @@ -1,136 +0,0 @@ -import io -import tempfile -from pathlib import Path -from unittest import mock -from unittest.mock import MagicMock - -import aiohttp -import pytest -from aleph_message.models import ItemHash - -from aleph.vm.conf import settings -from aleph.vm.orchestrator.supervisor import setup_webapp -from aleph.vm.storage import get_message - - -@pytest.mark.asyncio -async def test_operator_confidential_initialize_not_authorized(aiohttp_client): - """Test that the confidential initialize endpoint rejects if the sender is not the good one. Auth needed""" - - settings.ENABLE_QEMU_SUPPORT = True - settings.ENABLE_CONFIDENTIAL_COMPUTING = True - settings.setup() - - class FakeExecution: - message = None - is_running: bool = True - is_confidential: bool = False - - class FakeVmPool: - executions: dict[ItemHash, FakeExecution] = {} - - def __init__(self): - self.executions[settings.FAKE_INSTANCE_ID] = FakeExecution() - - with mock.patch( - "aleph.vm.orchestrator.views.authentication.authenticate_jwk", - return_value="", - ): - with mock.patch( - "aleph.vm.orchestrator.views.operator.is_sender_authorized", - return_value=False, - ) as is_sender_authorized_mock: - app = setup_webapp() - app["vm_pool"] = FakeVmPool() - client = await aiohttp_client(app) - response = await client.post( - f"/control/machine/{settings.FAKE_INSTANCE_ID}/confidential/initialize", - ) - assert response.status == 403 - assert await response.text() == "Unauthorized sender" - is_sender_authorized_mock.assert_called_once() - - -@pytest.mark.asyncio -async def test_operator_confidential_initialize_already_running(aiohttp_client): - """Test that the confidential initialize endpoint rejects if the VM is already running. Auth needed""" - - settings.ENABLE_QEMU_SUPPORT = True - settings.ENABLE_CONFIDENTIAL_COMPUTING = True - settings.setup() - - vm_hash = ItemHash(settings.FAKE_INSTANCE_ID) - instance_message = await get_message(ref=vm_hash) - - class FakeExecution: - message = instance_message.content - is_running: bool = True - is_confidential: bool = False - - class FakeVmPool: - executions: dict[ItemHash, FakeExecution] = {} - - def __init__(self): - self.executions[vm_hash] = FakeExecution() - - with mock.patch( - "aleph.vm.orchestrator.views.authentication.authenticate_jwk", - return_value=instance_message.sender, - ): - app = setup_webapp() - app["vm_pool"] = FakeVmPool() - client = await aiohttp_client(app) - response = await client.post( - f"/control/machine/{vm_hash}/confidential/initialize", - json={"persistent_vms": []}, - ) - assert response.status == 403 - assert await response.text() == f"VM with ref {vm_hash} already running" - - -@pytest.mark.asyncio -async def test_operator_confidential_initialize(aiohttp_client): - """Test that the certificates system endpoint responds. No auth needed""" - - settings.ENABLE_QEMU_SUPPORT = True - settings.ENABLE_CONFIDENTIAL_COMPUTING = True - settings.setup() - - vm_hash = ItemHash(settings.FAKE_INSTANCE_ID) - instance_message = await get_message(ref=vm_hash) - - class FakeExecution: - message = instance_message.content - is_running: bool = False - is_confidential: bool = True - controller_service: str = "" - - class MockSystemDManager: - enable_and_start = MagicMock(return_value=True) - - class FakeVmPool: - executions: dict[ItemHash, FakeExecution] = {} - - def __init__(self): - self.executions[vm_hash] = FakeExecution() - self.systemd_manager = MockSystemDManager() - - with tempfile.NamedTemporaryFile() as temp_file: - form_data = aiohttp.FormData() - form_data.add_field("session", open(temp_file.name, "rb"), filename="session.b64") - form_data.add_field("godh", open(temp_file.name, "rb"), filename="godh.b64") - - with mock.patch( - "aleph.vm.orchestrator.views.authentication.authenticate_jwk", - return_value=instance_message.sender, - ): - app = setup_webapp() - app["vm_pool"] = FakeVmPool() - client = await aiohttp_client(app) - response = await client.post( - f"/control/machine/{vm_hash}/confidential/initialize", - data=form_data, - ) - assert response.status == 200 - assert await response.text() == f"Started VM with ref {vm_hash}" - app["vm_pool"].systemd_manager.enable_and_start.assert_called_once() diff --git a/tests/supervisor/views/test_operator.py b/tests/supervisor/views/test_operator.py index 3e50fe53e..102fdc171 100644 --- a/tests/supervisor/views/test_operator.py +++ b/tests/supervisor/views/test_operator.py @@ -1,6 +1,137 @@ +import tempfile +from unittest import mock +from unittest.mock import MagicMock + +import aiohttp import pytest +from aleph_message.models import ItemHash +from aleph.vm.conf import settings from aleph.vm.orchestrator.supervisor import setup_webapp +from aleph.vm.storage import get_message + + +@pytest.mark.asyncio +async def test_operator_confidential_initialize_not_authorized(aiohttp_client): + """Test that the confidential initialize endpoint rejects if the sender is not the good one. Auth needed""" + + settings.ENABLE_QEMU_SUPPORT = True + settings.ENABLE_CONFIDENTIAL_COMPUTING = True + settings.setup() + + class FakeExecution: + message = None + is_running: bool = True + is_confidential: bool = False + + class FakeVmPool: + executions: dict[ItemHash, FakeExecution] = {} + + def __init__(self): + self.executions[settings.FAKE_INSTANCE_ID] = FakeExecution() + + with mock.patch( + "aleph.vm.orchestrator.views.authentication.authenticate_jwk", + return_value="", + ): + with mock.patch( + "aleph.vm.orchestrator.views.operator.is_sender_authorized", + return_value=False, + ) as is_sender_authorized_mock: + app = setup_webapp() + app["vm_pool"] = FakeVmPool() + client = await aiohttp_client(app) + response = await client.post( + f"/control/machine/{settings.FAKE_INSTANCE_ID}/confidential/initialize", + ) + assert response.status == 403 + assert await response.text() == "Unauthorized sender" + is_sender_authorized_mock.assert_called_once() + + +@pytest.mark.asyncio +async def test_operator_confidential_initialize_already_running(aiohttp_client): + """Test that the confidential initialize endpoint rejects if the VM is already running. Auth needed""" + + settings.ENABLE_QEMU_SUPPORT = True + settings.ENABLE_CONFIDENTIAL_COMPUTING = True + settings.setup() + + vm_hash = ItemHash(settings.FAKE_INSTANCE_ID) + instance_message = await get_message(ref=vm_hash) + + class FakeExecution: + message = instance_message.content + is_running: bool = True + is_confidential: bool = False + + class FakeVmPool: + executions: dict[ItemHash, FakeExecution] = {} + + def __init__(self): + self.executions[vm_hash] = FakeExecution() + + with mock.patch( + "aleph.vm.orchestrator.views.authentication.authenticate_jwk", + return_value=instance_message.sender, + ): + app = setup_webapp() + app["vm_pool"] = FakeVmPool() + client = await aiohttp_client(app) + response = await client.post( + f"/control/machine/{vm_hash}/confidential/initialize", + json={"persistent_vms": []}, + ) + assert response.status == 403 + assert await response.text() == f"VM with ref {vm_hash} already running" + + +@pytest.mark.asyncio +async def test_operator_confidential_initialize(aiohttp_client): + """Test that the certificates system endpoint responds. No auth needed""" + + settings.ENABLE_QEMU_SUPPORT = True + settings.ENABLE_CONFIDENTIAL_COMPUTING = True + settings.setup() + + vm_hash = ItemHash(settings.FAKE_INSTANCE_ID) + instance_message = await get_message(ref=vm_hash) + + class FakeExecution: + message = instance_message.content + is_running: bool = False + is_confidential: bool = True + controller_service: str = "" + + class MockSystemDManager: + enable_and_start = MagicMock(return_value=True) + + class FakeVmPool: + executions: dict[ItemHash, FakeExecution] = {} + + def __init__(self): + self.executions[vm_hash] = FakeExecution() + self.systemd_manager = MockSystemDManager() + + with tempfile.NamedTemporaryFile() as temp_file: + form_data = aiohttp.FormData() + form_data.add_field("session", open(temp_file.name, "rb"), filename="session.b64") + form_data.add_field("godh", open(temp_file.name, "rb"), filename="godh.b64") + + with mock.patch( + "aleph.vm.orchestrator.views.authentication.authenticate_jwk", + return_value=instance_message.sender, + ): + app = setup_webapp() + app["vm_pool"] = FakeVmPool() + client = await aiohttp_client(app) + response = await client.post( + f"/control/machine/{vm_hash}/confidential/initialize", + data=form_data, + ) + assert response.status == 200 + assert await response.text() == f"Started VM with ref {vm_hash}" + app["vm_pool"].systemd_manager.enable_and_start.assert_called_once() @pytest.mark.asyncio