From 82e83e42a6e72f4c384712fa452f3813eef4ab7c Mon Sep 17 00:00:00 2001 From: "Andres D. Molins" Date: Tue, 28 May 2024 21:01:20 +0200 Subject: [PATCH 01/19] Problem: The server don't have a directory to save the platform certificates generated by sevctl. Solution: Set that directory field on settings class and ensure to create the folder on initialization step. --- src/aleph/vm/conf.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/aleph/vm/conf.py b/src/aleph/vm/conf.py index ef69ffb14..9a5c938ba 100644 --- a/src/aleph/vm/conf.py +++ b/src/aleph/vm/conf.py @@ -267,6 +267,11 @@ class Settings(BaseSettings): "with SEV and SEV-ES", ) + CONFIDENTIAL_DIRECTORY: Path = Field( + None, + description="Confidential Computing default directory. Default to EXECUTION_ROOT/confidential", + ) + # Tests on programs FAKE_DATA_PROGRAM: Optional[Path] = None @@ -409,6 +414,7 @@ def setup(self): os.makedirs(self.EXECUTION_LOG_DIRECTORY, exist_ok=True) os.makedirs(self.PERSISTENT_VOLUMES_DIR, exist_ok=True) + os.makedirs(self.CONFIDENTIAL_DIRECTORY, exist_ok=True) self.API_SERVER = self.API_SERVER.rstrip("/") @@ -467,6 +473,8 @@ def __init__( self.EXECUTION_LOG_DIRECTORY = self.EXECUTION_ROOT / "executions" if not self.JAILER_BASE_DIR: self.JAILER_BASE_DIR = self.EXECUTION_ROOT / "jailer" + if not self.CONFIDENTIAL_DIRECTORY: + self.CONFIDENTIAL_DIRECTORY = self.EXECUTION_ROOT / "confidential" class Config: env_prefix = "ALEPH_VM_" From 39bee9a13a0a3c29c50f4670dd53ec2342f87e82 Mon Sep 17 00:00:00 2001 From: "Andres D. Molins" Date: Tue, 28 May 2024 21:07:15 +0200 Subject: [PATCH 02/19] Problem: The aren't an endpoint to be able to get the confidential platform certificates to start the VM key exchange. Solution: Create that endpoint and return the platform certificates generated by the `sevctl` command. --- src/aleph/vm/conf.py | 1 + src/aleph/vm/orchestrator/resources.py | 15 +++++++++++++++ src/aleph/vm/orchestrator/supervisor.py | 10 +++++++++- src/aleph/vm/sevclient.py | 22 ++++++++++++++++++++++ 4 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 src/aleph/vm/sevclient.py diff --git a/src/aleph/vm/conf.py b/src/aleph/vm/conf.py index 9a5c938ba..53d1a1d27 100644 --- a/src/aleph/vm/conf.py +++ b/src/aleph/vm/conf.py @@ -387,6 +387,7 @@ def check(self): assert ( check_system_module("kvm_amd/parameters/sev_es") == "Y" ), "SEV-ES feature isn't enabled, enable it in BIOS" + assert is_command_available("sevctl"), "Command `sevctl` not found, run `cargo install sevctl`" assert self.ENABLE_QEMU_SUPPORT, "Qemu Support is needed for confidential computing and it's disabled, " "enable it setting the env variable `ENABLE_QEMU_SUPPORT=True` in configuration" diff --git a/src/aleph/vm/orchestrator/resources.py b/src/aleph/vm/orchestrator/resources.py index 448a822c5..e1b00f79e 100644 --- a/src/aleph/vm/orchestrator/resources.py +++ b/src/aleph/vm/orchestrator/resources.py @@ -122,6 +122,21 @@ async def about_system_usage(_: web.Request): return web.json_response(text=usage.json(exclude_none=True)) +@cors_allow_all +async def about_certificates(request: web.Request): + """Public endpoint to expose platform certificates for confidential computing.""" + + if not settings.ENABLE_CONFIDENTIAL_COMPUTING: + return web.HTTPBadRequest(reason="Confidential computing setting not enabled on that server") + + sev_client = request.app["sev_client"] + + if not sev_client.certificates_archive.is_file(): + sev_client.export_certificates() + + return web.FileResponse(sev_client.certificates_archive) + + class Allocation(BaseModel): """An allocation is the set of resources that are currently allocated on this orchestrator. It contains the item_hashes of all persistent VMs, instances, on-demand VMs and jobs. diff --git a/src/aleph/vm/orchestrator/supervisor.py b/src/aleph/vm/orchestrator/supervisor.py index 892106ba0..0bd55beef 100644 --- a/src/aleph/vm/orchestrator/supervisor.py +++ b/src/aleph/vm/orchestrator/supervisor.py @@ -21,7 +21,7 @@ from aleph.vm.version import __version__ from .metrics import create_tables, setup_engine -from .resources import about_system_usage +from .resources import about_system_usage, about_certificates from .tasks import ( start_payment_monitoring_task, start_watch_for_messages_task, @@ -52,6 +52,7 @@ operate_stop, stream_logs, ) +from ..sevclient import SevClient logger = logging.getLogger(__name__) @@ -95,6 +96,7 @@ def setup_webapp(): web.get("/about/executions/details", about_executions), web.get("/about/executions/records", about_execution_records), web.get("/about/usage/system", about_system_usage), + web.get("/about/certificates", about_certificates), web.get("/about/config", about_config), # /control APIs are used to control the VMs and access their logs web.post("/control/allocation/notify", notify_allocation), @@ -159,6 +161,12 @@ def run(): app["secret_token"] = secret_token app["vm_pool"] = pool + # Store sevctl app singleton only if confidential feature is enabled + if settings.ENABLE_CONFIDENTIAL_COMPUTING: + sev_client = SevClient(settings.CONFIDENTIAL_DIRECTORY) + app["sev_client"] = sev_client + # TODO: Review and check sevctl first initialization steps, like (sevctl generate and sevctl provision) + logger.debug(f"Login to /about pages {protocol}://{hostname}/about/login?token={secret_token}") try: diff --git a/src/aleph/vm/sevclient.py b/src/aleph/vm/sevclient.py new file mode 100644 index 000000000..aa5b43a73 --- /dev/null +++ b/src/aleph/vm/sevclient.py @@ -0,0 +1,22 @@ +import subprocess +from pathlib import Path + + +class SevClient: + def __init__(self, sev_dir: Path): + self.sev_dir = sev_dir + self.certificates_dir = sev_dir / "platform" + self.certificates_dir.mkdir(exist_ok=True, parents=True) + self.certificates_archive = self.certificates_dir / "certs_export.cert" + + def sevctl_cmd(self, *args) -> subprocess.CompletedProcess: + result = subprocess.run( + ["sevctl", *args], + capture_output=True, + text=True, + ) + + return result + + def export_certificates(self): + _ = self.sevctl_cmd("export", self.certificates_archive) From 5f2ef4995db1297fdc645356032e41eab023426c Mon Sep 17 00:00:00 2001 From: "Andres D. Molins" Date: Tue, 28 May 2024 21:11:26 +0200 Subject: [PATCH 03/19] Fix: Solved code quality issues. --- src/aleph/vm/orchestrator/supervisor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/aleph/vm/orchestrator/supervisor.py b/src/aleph/vm/orchestrator/supervisor.py index 0bd55beef..054188838 100644 --- a/src/aleph/vm/orchestrator/supervisor.py +++ b/src/aleph/vm/orchestrator/supervisor.py @@ -20,8 +20,9 @@ from aleph.vm.pool import VmPool from aleph.vm.version import __version__ +from ..sevclient import SevClient from .metrics import create_tables, setup_engine -from .resources import about_system_usage, about_certificates +from .resources import about_certificates, about_system_usage from .tasks import ( start_payment_monitoring_task, start_watch_for_messages_task, @@ -52,7 +53,6 @@ operate_stop, stream_logs, ) -from ..sevclient import SevClient logger = logging.getLogger(__name__) From 143909631041dd529538aa71060a5130f6e768de Mon Sep 17 00:00:00 2001 From: "Andres D. Molins" Date: Tue, 28 May 2024 22:27:49 +0200 Subject: [PATCH 04/19] Fix: Added 2 test cases for that endpoint. --- src/aleph/vm/orchestrator/supervisor.py | 2 +- tests/supervisor/test_views.py | 54 +++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/aleph/vm/orchestrator/supervisor.py b/src/aleph/vm/orchestrator/supervisor.py index 054188838..a2a712445 100644 --- a/src/aleph/vm/orchestrator/supervisor.py +++ b/src/aleph/vm/orchestrator/supervisor.py @@ -18,9 +18,9 @@ from aleph.vm.conf import settings from aleph.vm.pool import VmPool +from aleph.vm.sevclient import SevClient from aleph.vm.version import __version__ -from ..sevclient import SevClient from .metrics import create_tables, setup_engine from .resources import about_certificates, about_system_usage from .tasks import ( diff --git a/tests/supervisor/test_views.py b/tests/supervisor/test_views.py index 254e326df..6d9ddd80d 100644 --- a/tests/supervisor/test_views.py +++ b/tests/supervisor/test_views.py @@ -1,8 +1,13 @@ +from pathlib import Path +from unittest import mock +from unittest.mock import call + import pytest from aiohttp import web from aleph.vm.conf import settings from aleph.vm.orchestrator.supervisor import setup_webapp +from aleph.vm.sevclient import SevClient @pytest.mark.asyncio @@ -121,3 +126,52 @@ def get_persistent_executions(self): ) assert response.status == 200 assert await response.json() == {"success": True, "successful": [], "failing": [], "errors": {}} + + +@pytest.mark.asyncio +async def test_about_certificates_missing_setting(aiohttp_client): + """Test that the certificates system endpoint returns an error if the setting isn't enabled""" + settings.ENABLE_CONFIDENTIAL_COMPUTING = False + + app = setup_webapp() + app["sev_client"] = SevClient(Path().resolve()) + client = await aiohttp_client(app) + response: web.Response = await client.get("/about/certificates") + assert response.status == 400 + assert await response.text() == "400: Confidential computing setting not enabled on that server" + + +@pytest.mark.asyncio +async def test_about_certificates(aiohttp_client): + """Test that the certificates system endpoint responds. No auth needed""" + + settings.ENABLE_QEMU_SUPPORT = True + settings.ENABLE_CONFIDENTIAL_COMPUTING = True + settings.CONFIDENTIAL_DIRECTORY = Path().resolve() + settings.setup() + + with mock.patch( + "pathlib.Path.is_file", + return_value=False, + ) as is_file_mock: + with mock.patch( + "subprocess.run", + return_value=True, + ) as export_mock: + app = setup_webapp() + sev_client = SevClient(settings.CONFIDENTIAL_DIRECTORY) + app["sev_client"] = sev_client + # Create mock file to return it + Path(sev_client.certificates_archive).touch(exist_ok=True) + + client = await aiohttp_client(app) + response: web.Response = await client.get("/about/certificates") + assert response.status == 200 + is_file_mock.assert_has_calls([call(), call()]) + certificates_expected_dir = sev_client.certificates_archive + export_mock.assert_called_once_with( + ["sevctl", "export", certificates_expected_dir], capture_output=True, text=True + ) + + # Remove file mock + Path(sev_client.certificates_archive).unlink() From 3c294777b7c99802dee27386e690b2adba0b9a7f Mon Sep 17 00:00:00 2001 From: "Andres D. Molins" Date: Tue, 4 Jun 2024 18:31:58 +0200 Subject: [PATCH 05/19] Fix: Added PR suggestions. --- src/aleph/vm/orchestrator/resources.py | 5 +++-- src/aleph/vm/sevclient.py | 14 +++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/aleph/vm/orchestrator/resources.py b/src/aleph/vm/orchestrator/resources.py index e1b00f79e..1babc9542 100644 --- a/src/aleph/vm/orchestrator/resources.py +++ b/src/aleph/vm/orchestrator/resources.py @@ -11,6 +11,7 @@ from pydantic import BaseModel, Field from aleph.vm.conf import settings +from aleph.vm.sevclient import SevClient from aleph.vm.utils import cors_allow_all @@ -129,10 +130,10 @@ async def about_certificates(request: web.Request): if not settings.ENABLE_CONFIDENTIAL_COMPUTING: return web.HTTPBadRequest(reason="Confidential computing setting not enabled on that server") - sev_client = request.app["sev_client"] + sev_client: SevClient = request.app["sev_client"] if not sev_client.certificates_archive.is_file(): - sev_client.export_certificates() + await sev_client.export_certificates() return web.FileResponse(sev_client.certificates_archive) diff --git a/src/aleph/vm/sevclient.py b/src/aleph/vm/sevclient.py index aa5b43a73..8ac2b6477 100644 --- a/src/aleph/vm/sevclient.py +++ b/src/aleph/vm/sevclient.py @@ -1,6 +1,7 @@ -import subprocess from pathlib import Path +from aleph.vm.utils import run_in_subprocess + class SevClient: def __init__(self, sev_dir: Path): @@ -9,14 +10,13 @@ def __init__(self, sev_dir: Path): self.certificates_dir.mkdir(exist_ok=True, parents=True) self.certificates_archive = self.certificates_dir / "certs_export.cert" - def sevctl_cmd(self, *args) -> subprocess.CompletedProcess: - result = subprocess.run( + async def sevctl_cmd(self, *args) -> bytes: + result = await run_in_subprocess( ["sevctl", *args], - capture_output=True, - text=True, + check=True, ) return result - def export_certificates(self): - _ = self.sevctl_cmd("export", self.certificates_archive) + async def export_certificates(self): + _ = await self.sevctl_cmd("export", self.certificates_archive) From 7c6c3ac74a6a71dd445fa3a37bd383bdf6401e0e Mon Sep 17 00:00:00 2001 From: "Andres D. Molins" Date: Tue, 4 Jun 2024 18:33:39 +0200 Subject: [PATCH 06/19] Fix: Modified test mock to let the tests work --- tests/supervisor/test_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/supervisor/test_views.py b/tests/supervisor/test_views.py index 6d9ddd80d..c87ea9e08 100644 --- a/tests/supervisor/test_views.py +++ b/tests/supervisor/test_views.py @@ -155,7 +155,7 @@ async def test_about_certificates(aiohttp_client): return_value=False, ) as is_file_mock: with mock.patch( - "subprocess.run", + "aleph.vm.utils.run_in_subprocess", return_value=True, ) as export_mock: app = setup_webapp() From d1454a483e8e963a353ae2b445b6e1c82e3cf4cb Mon Sep 17 00:00:00 2001 From: "Andres D. Molins" Date: Tue, 4 Jun 2024 19:41:35 +0200 Subject: [PATCH 07/19] Problem: Now isn't possible as a VM operator to get the client session certificates to initialize a confidential VM. Solution: Create an operator start endpoint that receive the confidential session files and starts the qemu VM to continue with the certificate exchange methods. --- src/aleph/vm/conf.py | 5 +++ src/aleph/vm/models.py | 4 ++ src/aleph/vm/orchestrator/supervisor.py | 2 + src/aleph/vm/orchestrator/views/operator.py | 49 ++++++++++++++++++++- src/aleph/vm/pool.py | 2 +- src/aleph/vm/utils.py | 7 +++ 6 files changed, 67 insertions(+), 2 deletions(-) diff --git a/src/aleph/vm/conf.py b/src/aleph/vm/conf.py index 53d1a1d27..a54ea07a7 100644 --- a/src/aleph/vm/conf.py +++ b/src/aleph/vm/conf.py @@ -272,6 +272,8 @@ class Settings(BaseSettings): description="Confidential Computing default directory. Default to EXECUTION_ROOT/confidential", ) + CONFIDENTIAL_SESSION_DIRECTORY: Path = Field(None, description="Default to EXECUTION_ROOT/sessions") + # Tests on programs FAKE_DATA_PROGRAM: Optional[Path] = None @@ -416,6 +418,7 @@ def setup(self): os.makedirs(self.EXECUTION_LOG_DIRECTORY, exist_ok=True) os.makedirs(self.PERSISTENT_VOLUMES_DIR, exist_ok=True) os.makedirs(self.CONFIDENTIAL_DIRECTORY, exist_ok=True) + os.makedirs(self.CONFIDENTIAL_SESSION_DIRECTORY, exist_ok=True) self.API_SERVER = self.API_SERVER.rstrip("/") @@ -476,6 +479,8 @@ def __init__( self.JAILER_BASE_DIR = self.EXECUTION_ROOT / "jailer" if not self.CONFIDENTIAL_DIRECTORY: self.CONFIDENTIAL_DIRECTORY = self.EXECUTION_ROOT / "confidential" + if not self.CONFIDENTIAL_SESSION_DIRECTORY: + self.CONFIDENTIAL_SESSION_DIRECTORY = self.EXECUTION_ROOT / "sessions" class Config: env_prefix = "ALEPH_VM_" diff --git a/src/aleph/vm/models.py b/src/aleph/vm/models.py index 5a44c132a..3dfa738db 100644 --- a/src/aleph/vm/models.py +++ b/src/aleph/vm/models.py @@ -104,6 +104,10 @@ def is_program(self) -> bool: def is_instance(self) -> bool: return isinstance(self.message, InstanceContent) + @property + def is_confidential(self) -> bool: + return self.uses_payment_stream # TODO: check also if the VM message is confidential + @property def hypervisor(self) -> HypervisorType: if self.is_program: diff --git a/src/aleph/vm/orchestrator/supervisor.py b/src/aleph/vm/orchestrator/supervisor.py index a2a712445..99dc5daa7 100644 --- a/src/aleph/vm/orchestrator/supervisor.py +++ b/src/aleph/vm/orchestrator/supervisor.py @@ -50,6 +50,7 @@ operate_erase, operate_expire, operate_reboot, + operate_start, operate_stop, stream_logs, ) @@ -102,6 +103,7 @@ def setup_webapp(): web.post("/control/allocation/notify", notify_allocation), web.get("/control/machine/{ref}/logs", stream_logs), web.post("/control/machine/{ref}/expire", operate_expire), + web.post("/control/machine/{ref}/start", operate_start), web.post("/control/machine/{ref}/stop", operate_stop), web.post("/control/machine/{ref}/erase", operate_erase), web.post("/control/machine/{ref}/reboot", operate_reboot), diff --git a/src/aleph/vm/orchestrator/views/operator.py b/src/aleph/vm/orchestrator/views/operator.py index 298486b73..805284f19 100644 --- a/src/aleph/vm/orchestrator/views/operator.py +++ b/src/aleph/vm/orchestrator/views/operator.py @@ -1,4 +1,5 @@ import logging +import os from datetime import timedelta import aiohttp.web_exceptions @@ -7,7 +8,9 @@ from aleph_message.exceptions import UnknownHashError from aleph_message.models import ItemHash from aleph_message.models.execution import BaseExecutableContent +from anyio import asyncPath +from aleph.vm.conf import settings from aleph.vm.models import VmExecution from aleph.vm.orchestrator.run import create_vm_execution from aleph.vm.orchestrator.views import authenticate_api_request @@ -16,7 +19,7 @@ require_jwk_authentication, ) from aleph.vm.pool import VmPool -from aleph.vm.utils import cors_allow_all +from aleph.vm.utils import cors_allow_all, write_bytes_file logger = logging.getLogger(__name__) @@ -133,6 +136,50 @@ async def operate_expire(request: web.Request, authenticated_sender: str) -> web return web.Response(status=200, body=f"Expiring VM with ref {vm_hash} in {timeout} seconds") +@cors_allow_all +@require_jwk_authentication +async def operate_start(request: web.Request, authenticated_sender: str) -> web.Response: + """Start the confidential virtual machine if possible.""" + # TODO: Add user authentication + vm_hash = get_itemhash_or_400(request.match_info) + + pool: VmPool = request.app["vm_pool"] + logger.debug(f"Iterating through running executions... {pool.executions}") + execution = get_execution_or_404(vm_hash, pool=pool) + + if not is_sender_authorized(authenticated_sender, execution.message): + return web.Response(status=403, body="Unauthorized sender") + + if execution.is_running: + return web.Response(status=403, body=f"VM with ref {vm_hash} already running") + + if not execution.is_confidential: + return web.Response(status=403, body=f"Operation not allowed for VM {vm_hash} because it isn't confidential") + + post = await request.post() + + vm_session_path = settings.CONFIDENTIAL_SESSION_DIRECTORY / vm_hash + os.makedirs(vm_session_path, exist_ok=True) + + session_file_content = post.get("session") + if session_file_content: + return web.Response(status=403, body=f"Session file required for VM with ref {vm_hash}") + + session_file_path = asyncPath(vm_session_path / "vm_session.b64") + await session_file_path.write_bytes(session_file_content.file.read()) + + godh_file_content = post.get("godh") + if godh_file_content: + return web.Response(status=403, body=f"GODH file required for VM with ref {vm_hash}") + + godh_file_path = asyncPath(vm_session_path / "vm_godh.b64") + await godh_file_path.write_bytes(godh_file_content.file.read()) + + pool.systemd_manager.enable_and_start(execution.controller_service) + + return web.Response(status=200, body=f"Started VM with ref {vm_hash}") + + @cors_allow_all @require_jwk_authentication async def operate_stop(request: web.Request, authenticated_sender: str) -> web.Response: diff --git a/src/aleph/vm/pool.py b/src/aleph/vm/pool.py index 3e5c5f3ec..ffc24c124 100644 --- a/src/aleph/vm/pool.py +++ b/src/aleph/vm/pool.py @@ -123,7 +123,7 @@ async def create_a_vm( await execution.start() # Start VM and snapshots automatically - if execution.persistent: + if execution.persistent and not execution.is_confidential: self.systemd_manager.enable_and_start(execution.controller_service) await execution.wait_for_init() if execution.is_program and execution.vm: diff --git a/src/aleph/vm/utils.py b/src/aleph/vm/utils.py index 296af0c58..ff150bde3 100644 --- a/src/aleph/vm/utils.py +++ b/src/aleph/vm/utils.py @@ -3,6 +3,7 @@ import hashlib import json import logging +import pathlib import subprocess from base64 import b16encode, b32decode from collections.abc import Coroutine @@ -13,6 +14,7 @@ from typing import Any, Callable, Optional import aiodns +import aiofiles import msgpack from aiohttp_cors import ResourceOptions, custom_cors from aleph_message.models import ExecutableContent, InstanceContent, ProgramContent @@ -218,3 +220,8 @@ def file_hashes_differ(source: Path, destination: Path, checksum: Callable[[Path return True return checksum(source) != checksum(destination) + + +def write_bytes_file(file_path: Path, file_content: bytes): + """Save a file on disk in async way.""" + file_path.write_bytes(file_content) From a6559c25e91e44dbd22429f1ab1cb6f737fba415 Mon Sep 17 00:00:00 2001 From: "Andres D. Molins" Date: Wed, 5 Jun 2024 11:00:16 +0200 Subject: [PATCH 08/19] Fix: Remove useless aiofiles import --- src/aleph/vm/utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/aleph/vm/utils.py b/src/aleph/vm/utils.py index ff150bde3..7e98d699e 100644 --- a/src/aleph/vm/utils.py +++ b/src/aleph/vm/utils.py @@ -3,7 +3,6 @@ import hashlib import json import logging -import pathlib import subprocess from base64 import b16encode, b32decode from collections.abc import Coroutine @@ -14,7 +13,6 @@ from typing import Any, Callable, Optional import aiodns -import aiofiles import msgpack from aiohttp_cors import ResourceOptions, custom_cors from aleph_message.models import ExecutableContent, InstanceContent, ProgramContent From 5d7044bd282da7b5d807a51278fa9ef9b9473e07 Mon Sep 17 00:00:00 2001 From: "Andres D. Molins" Date: Wed, 5 Jun 2024 11:22:48 +0200 Subject: [PATCH 09/19] Fix: Solve test issues after code quality fixes --- src/aleph/vm/sevclient.py | 2 +- tests/supervisor/test_views.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/aleph/vm/sevclient.py b/src/aleph/vm/sevclient.py index 8ac2b6477..0139c572a 100644 --- a/src/aleph/vm/sevclient.py +++ b/src/aleph/vm/sevclient.py @@ -19,4 +19,4 @@ async def sevctl_cmd(self, *args) -> bytes: return result async def export_certificates(self): - _ = await self.sevctl_cmd("export", self.certificates_archive) + _ = await self.sevctl_cmd("export", str(self.certificates_archive)) diff --git a/tests/supervisor/test_views.py b/tests/supervisor/test_views.py index c87ea9e08..b957a21f7 100644 --- a/tests/supervisor/test_views.py +++ b/tests/supervisor/test_views.py @@ -155,7 +155,7 @@ async def test_about_certificates(aiohttp_client): return_value=False, ) as is_file_mock: with mock.patch( - "aleph.vm.utils.run_in_subprocess", + "aleph.vm.sevclient.run_in_subprocess", return_value=True, ) as export_mock: app = setup_webapp() @@ -170,7 +170,7 @@ async def test_about_certificates(aiohttp_client): is_file_mock.assert_has_calls([call(), call()]) certificates_expected_dir = sev_client.certificates_archive export_mock.assert_called_once_with( - ["sevctl", "export", certificates_expected_dir], capture_output=True, text=True + ["sevctl", "export", str(certificates_expected_dir)], check=True ) # Remove file mock From e37ba5909ee94747c4c6a6811019d03c99ecbf33 Mon Sep 17 00:00:00 2001 From: "Andres D. Molins" Date: Wed, 5 Jun 2024 11:28:08 +0200 Subject: [PATCH 10/19] Fix: Solve code quality issues. --- tests/supervisor/test_views.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/supervisor/test_views.py b/tests/supervisor/test_views.py index b957a21f7..41b609ab3 100644 --- a/tests/supervisor/test_views.py +++ b/tests/supervisor/test_views.py @@ -169,9 +169,7 @@ async def test_about_certificates(aiohttp_client): assert response.status == 200 is_file_mock.assert_has_calls([call(), call()]) certificates_expected_dir = sev_client.certificates_archive - export_mock.assert_called_once_with( - ["sevctl", "export", str(certificates_expected_dir)], check=True - ) + export_mock.assert_called_once_with(["sevctl", "export", str(certificates_expected_dir)], check=True) # Remove file mock Path(sev_client.certificates_archive).unlink() From 017115edccedbb351fb051131c35e178a01e62e9 Mon Sep 17 00:00:00 2001 From: "Andres D. Molins" Date: Wed, 5 Jun 2024 11:59:30 +0200 Subject: [PATCH 11/19] Fix: Solve code quality issues. --- tests/supervisor/test_views.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/supervisor/test_views.py b/tests/supervisor/test_views.py index b957a21f7..41b609ab3 100644 --- a/tests/supervisor/test_views.py +++ b/tests/supervisor/test_views.py @@ -169,9 +169,7 @@ async def test_about_certificates(aiohttp_client): assert response.status == 200 is_file_mock.assert_has_calls([call(), call()]) certificates_expected_dir = sev_client.certificates_archive - export_mock.assert_called_once_with( - ["sevctl", "export", str(certificates_expected_dir)], check=True - ) + export_mock.assert_called_once_with(["sevctl", "export", str(certificates_expected_dir)], check=True) # Remove file mock Path(sev_client.certificates_archive).unlink() From 8fc195cab8fb42a43f07eb9309215ed7e8989d47 Mon Sep 17 00:00:00 2001 From: "Andres D. Molins" Date: Wed, 5 Jun 2024 12:14:54 +0200 Subject: [PATCH 12/19] Fix: Write file in sync mode to avoid adding a new dependency. Files to write should be so small, so any blocking issue should be here. --- src/aleph/vm/orchestrator/views/operator.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/aleph/vm/orchestrator/views/operator.py b/src/aleph/vm/orchestrator/views/operator.py index 805284f19..fb2c4a44e 100644 --- a/src/aleph/vm/orchestrator/views/operator.py +++ b/src/aleph/vm/orchestrator/views/operator.py @@ -8,7 +8,6 @@ from aleph_message.exceptions import UnknownHashError from aleph_message.models import ItemHash from aleph_message.models.execution import BaseExecutableContent -from anyio import asyncPath from aleph.vm.conf import settings from aleph.vm.models import VmExecution @@ -165,15 +164,17 @@ async def operate_start(request: web.Request, authenticated_sender: str) -> web. if session_file_content: return web.Response(status=403, body=f"Session file required for VM with ref {vm_hash}") - session_file_path = asyncPath(vm_session_path / "vm_session.b64") - await session_file_path.write_bytes(session_file_content.file.read()) + session_file_path = vm_session_path / "vm_session.b64" + with open(session_file_path, "wb") as session_file: + session_file.write(session_file_content.file.read()) godh_file_content = post.get("godh") if godh_file_content: return web.Response(status=403, body=f"GODH file required for VM with ref {vm_hash}") - godh_file_path = asyncPath(vm_session_path / "vm_godh.b64") - await godh_file_path.write_bytes(godh_file_content.file.read()) + godh_file_path = vm_session_path / "vm_godh.b64" + with open(godh_file_path, "wb") as godh_file: + godh_file.write(godh_file_content.file.read()) pool.systemd_manager.enable_and_start(execution.controller_service) From 5d2eb5f548e50d8942d7eec43a2f8c77b0f3496f Mon Sep 17 00:00:00 2001 From: "Andres D. Molins" Date: Wed, 5 Jun 2024 12:52:33 +0200 Subject: [PATCH 13/19] Fix: Solved PR comments and wrong conditionals. --- src/aleph/vm/orchestrator/views/operator.py | 13 +++++-------- src/aleph/vm/utils.py | 5 ----- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/src/aleph/vm/orchestrator/views/operator.py b/src/aleph/vm/orchestrator/views/operator.py index fb2c4a44e..29de51a9a 100644 --- a/src/aleph/vm/orchestrator/views/operator.py +++ b/src/aleph/vm/orchestrator/views/operator.py @@ -1,5 +1,4 @@ import logging -import os from datetime import timedelta import aiohttp.web_exceptions @@ -158,23 +157,21 @@ async def operate_start(request: web.Request, authenticated_sender: str) -> web. post = await request.post() vm_session_path = settings.CONFIDENTIAL_SESSION_DIRECTORY / vm_hash - os.makedirs(vm_session_path, exist_ok=True) + vm_session_path.mkdir(exist_ok=True) session_file_content = post.get("session") - if session_file_content: + if not session_file_content: return web.Response(status=403, body=f"Session file required for VM with ref {vm_hash}") session_file_path = vm_session_path / "vm_session.b64" - with open(session_file_path, "wb") as session_file: - session_file.write(session_file_content.file.read()) + session_file_path.write_bytes(session_file_content.file.read()) godh_file_content = post.get("godh") - if godh_file_content: + if not godh_file_content: return web.Response(status=403, body=f"GODH file required for VM with ref {vm_hash}") godh_file_path = vm_session_path / "vm_godh.b64" - with open(godh_file_path, "wb") as godh_file: - godh_file.write(godh_file_content.file.read()) + godh_file_path.write_bytes(godh_file_content.file.read()) pool.systemd_manager.enable_and_start(execution.controller_service) diff --git a/src/aleph/vm/utils.py b/src/aleph/vm/utils.py index 7e98d699e..296af0c58 100644 --- a/src/aleph/vm/utils.py +++ b/src/aleph/vm/utils.py @@ -218,8 +218,3 @@ def file_hashes_differ(source: Path, destination: Path, checksum: Callable[[Path return True return checksum(source) != checksum(destination) - - -def write_bytes_file(file_path: Path, file_content: bytes): - """Save a file on disk in async way.""" - file_path.write_bytes(file_content) From 68413be5a28e8f7cb30279505758785085891333 Mon Sep 17 00:00:00 2001 From: "Andres D. Molins" Date: Wed, 5 Jun 2024 13:04:33 +0200 Subject: [PATCH 14/19] Fix: Solved more PR comments. --- src/aleph/vm/orchestrator/resources.py | 5 +---- src/aleph/vm/sevclient.py | 9 ++++---- tests/supervisor/test_views.py | 30 ++++++++++++-------------- 3 files changed, 20 insertions(+), 24 deletions(-) diff --git a/src/aleph/vm/orchestrator/resources.py b/src/aleph/vm/orchestrator/resources.py index 1babc9542..fe9deab26 100644 --- a/src/aleph/vm/orchestrator/resources.py +++ b/src/aleph/vm/orchestrator/resources.py @@ -132,10 +132,7 @@ async def about_certificates(request: web.Request): sev_client: SevClient = request.app["sev_client"] - if not sev_client.certificates_archive.is_file(): - await sev_client.export_certificates() - - return web.FileResponse(sev_client.certificates_archive) + return web.FileResponse(await sev_client.get_certificates()) class Allocation(BaseModel): diff --git a/src/aleph/vm/sevclient.py b/src/aleph/vm/sevclient.py index 0139c572a..fe9eb1c00 100644 --- a/src/aleph/vm/sevclient.py +++ b/src/aleph/vm/sevclient.py @@ -11,12 +11,13 @@ def __init__(self, sev_dir: Path): self.certificates_archive = self.certificates_dir / "certs_export.cert" async def sevctl_cmd(self, *args) -> bytes: - result = await run_in_subprocess( + return await run_in_subprocess( ["sevctl", *args], check=True, ) - return result + async def get_certificates(self) -> Path: + if not self.certificates_archive.is_file(): + _ = await self.sevctl_cmd("export", str(self.certificates_archive)) - async def export_certificates(self): - _ = await self.sevctl_cmd("export", str(self.certificates_archive)) + return self.certificates_archive diff --git a/tests/supervisor/test_views.py b/tests/supervisor/test_views.py index 41b609ab3..52426d48c 100644 --- a/tests/supervisor/test_views.py +++ b/tests/supervisor/test_views.py @@ -1,3 +1,4 @@ +import tempfile from pathlib import Path from unittest import mock from unittest.mock import call @@ -147,7 +148,6 @@ async def test_about_certificates(aiohttp_client): settings.ENABLE_QEMU_SUPPORT = True settings.ENABLE_CONFIDENTIAL_COMPUTING = True - settings.CONFIDENTIAL_DIRECTORY = Path().resolve() settings.setup() with mock.patch( @@ -158,18 +158,16 @@ async def test_about_certificates(aiohttp_client): "aleph.vm.sevclient.run_in_subprocess", return_value=True, ) as export_mock: - app = setup_webapp() - sev_client = SevClient(settings.CONFIDENTIAL_DIRECTORY) - app["sev_client"] = sev_client - # Create mock file to return it - Path(sev_client.certificates_archive).touch(exist_ok=True) - - client = await aiohttp_client(app) - response: web.Response = await client.get("/about/certificates") - assert response.status == 200 - is_file_mock.assert_has_calls([call(), call()]) - certificates_expected_dir = sev_client.certificates_archive - export_mock.assert_called_once_with(["sevctl", "export", str(certificates_expected_dir)], check=True) - - # Remove file mock - Path(sev_client.certificates_archive).unlink() + with tempfile.TemporaryDirectory() as tmp_dir: + app = setup_webapp() + sev_client = SevClient(Path(tmp_dir)) + app["sev_client"] = sev_client + # Create mock file to return it + Path(sev_client.certificates_archive).touch(exist_ok=True) + + client = await aiohttp_client(app) + response: web.Response = await client.get("/about/certificates") + assert response.status == 200 + is_file_mock.assert_has_calls([call(), call()]) + certificates_expected_dir = sev_client.certificates_archive + export_mock.assert_called_once_with(["sevctl", "export", str(certificates_expected_dir)], check=True) From 1b3a7c73da5d7e966bb330a115ac3fb06d016469 Mon Sep 17 00:00:00 2001 From: "Andres D. Molins" Date: Wed, 5 Jun 2024 13:07:18 +0200 Subject: [PATCH 15/19] Fix: Removed unexisting import --- src/aleph/vm/orchestrator/views/operator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aleph/vm/orchestrator/views/operator.py b/src/aleph/vm/orchestrator/views/operator.py index 29de51a9a..c7c7e7735 100644 --- a/src/aleph/vm/orchestrator/views/operator.py +++ b/src/aleph/vm/orchestrator/views/operator.py @@ -17,7 +17,7 @@ require_jwk_authentication, ) from aleph.vm.pool import VmPool -from aleph.vm.utils import cors_allow_all, write_bytes_file +from aleph.vm.utils import cors_allow_all logger = logging.getLogger(__name__) From e983c4e1d0b7ed53dc9ee532d7029f015455204c Mon Sep 17 00:00:00 2001 From: "Andres D. Molins" Date: Wed, 5 Jun 2024 17:03:52 +0200 Subject: [PATCH 16/19] Fix: Added useless command requested on the PR review. --- src/aleph/vm/pool.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/aleph/vm/pool.py b/src/aleph/vm/pool.py index ffc24c124..61975b1a3 100644 --- a/src/aleph/vm/pool.py +++ b/src/aleph/vm/pool.py @@ -123,6 +123,8 @@ async def create_a_vm( await execution.start() # Start VM and snapshots automatically + # If the execution is confidential, don't start it because we need to wait for the session certificate + # files, use the endpoint /control/machine/{ref}/start to get session files and start the VM if execution.persistent and not execution.is_confidential: self.systemd_manager.enable_and_start(execution.controller_service) await execution.wait_for_init() From ffd469eab42004962cb8cddd7fc72963f6458aa0 Mon Sep 17 00:00:00 2001 From: "Andres D. Molins" Date: Tue, 11 Jun 2024 14:32:42 +0200 Subject: [PATCH 17/19] Fix: Changed endpoint path and added automatic tests for that endpoint. --- src/aleph/vm/orchestrator/supervisor.py | 4 +- src/aleph/vm/orchestrator/views/operator.py | 2 +- tests/supervisor/test_operator.py | 136 ++++++++++++++++++++ 3 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 tests/supervisor/test_operator.py diff --git a/src/aleph/vm/orchestrator/supervisor.py b/src/aleph/vm/orchestrator/supervisor.py index 99dc5daa7..2786c6c4b 100644 --- a/src/aleph/vm/orchestrator/supervisor.py +++ b/src/aleph/vm/orchestrator/supervisor.py @@ -47,10 +47,10 @@ update_allocations, ) from .views.operator import ( + operate_confidential_initialize, operate_erase, operate_expire, operate_reboot, - operate_start, operate_stop, stream_logs, ) @@ -103,7 +103,7 @@ def setup_webapp(): web.post("/control/allocation/notify", notify_allocation), web.get("/control/machine/{ref}/logs", stream_logs), web.post("/control/machine/{ref}/expire", operate_expire), - web.post("/control/machine/{ref}/start", operate_start), + web.post("/control/machine/{ref}/confidential/initialize", operate_confidential_initialize), web.post("/control/machine/{ref}/stop", operate_stop), web.post("/control/machine/{ref}/erase", operate_erase), web.post("/control/machine/{ref}/reboot", operate_reboot), diff --git a/src/aleph/vm/orchestrator/views/operator.py b/src/aleph/vm/orchestrator/views/operator.py index c7c7e7735..399159d00 100644 --- a/src/aleph/vm/orchestrator/views/operator.py +++ b/src/aleph/vm/orchestrator/views/operator.py @@ -136,7 +136,7 @@ async def operate_expire(request: web.Request, authenticated_sender: str) -> web @cors_allow_all @require_jwk_authentication -async def operate_start(request: web.Request, authenticated_sender: str) -> web.Response: +async def operate_confidential_initialize(request: web.Request, authenticated_sender: str) -> web.Response: """Start the confidential virtual machine if possible.""" # TODO: Add user authentication vm_hash = get_itemhash_or_400(request.match_info) diff --git a/tests/supervisor/test_operator.py b/tests/supervisor/test_operator.py new file mode 100644 index 000000000..24c81ae30 --- /dev/null +++ b/tests/supervisor/test_operator.py @@ -0,0 +1,136 @@ +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() From e1c0afa9fb76aba4d1178d405bbc5924972dc38a Mon Sep 17 00:00:00 2001 From: "Andres D. Molins" Date: Wed, 12 Jun 2024 16:34:02 +0200 Subject: [PATCH 18/19] Fix: Solved settings singleton issue with testing, adding an `initialize_settings` method. --- src/aleph/vm/conf.py | 5 +++++ tests/supervisor/test_qemu_instance.py | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/aleph/vm/conf.py b/src/aleph/vm/conf.py index 8c4c145d6..c63f2069c 100644 --- a/src/aleph/vm/conf.py +++ b/src/aleph/vm/conf.py @@ -492,5 +492,10 @@ def make_db_url(): return f"sqlite+aiosqlite:///{settings.EXECUTION_DATABASE}" +def initialize_settings(): + global settings + settings = Settings() + + # Settings singleton settings = Settings() diff --git a/tests/supervisor/test_qemu_instance.py b/tests/supervisor/test_qemu_instance.py index 3792aaa87..ed3be5252 100644 --- a/tests/supervisor/test_qemu_instance.py +++ b/tests/supervisor/test_qemu_instance.py @@ -7,7 +7,7 @@ import pytest from aleph_message.models import ItemHash -from aleph.vm.conf import settings +from aleph.vm.conf import initialize_settings, settings from aleph.vm.controllers.__main__ import configuration_from_file, execute_persistent_vm from aleph.vm.controllers.qemu import AlephQemuInstance from aleph.vm.hypervisors.qemu.qemuvm import QemuVM @@ -50,6 +50,7 @@ async def test_create_qemu_instance(): Create an instance and check that it start / init / stop properly. """ + initialize_settings() settings.USE_FAKE_INSTANCE_BASE = True settings.FAKE_INSTANCE_MESSAGE = settings.FAKE_INSTANCE_QEMU_MESSAGE settings.FAKE_INSTANCE_BASE = settings.FAKE_QEMU_INSTANCE_BASE @@ -105,6 +106,7 @@ async def test_create_qemu_instance_online(): Create an instance and check that it start / init / stop properly. """ + initialize_settings() settings.USE_FAKE_INSTANCE_BASE = True settings.FAKE_INSTANCE_MESSAGE = settings.FAKE_INSTANCE_QEMU_MESSAGE settings.FAKE_INSTANCE_BASE = settings.FAKE_QEMU_INSTANCE_BASE From e3259f735650f2c0eff5741ca876d331ed366069 Mon Sep 17 00:00:00 2001 From: "Andres D. Molins" Date: Wed, 12 Jun 2024 17:01:35 +0200 Subject: [PATCH 19/19] Fix: Just disable the setting that is failing and remove previous method to initialize the singleton. --- src/aleph/vm/conf.py | 5 ----- tests/supervisor/test_qemu_instance.py | 6 +++--- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/aleph/vm/conf.py b/src/aleph/vm/conf.py index c63f2069c..8c4c145d6 100644 --- a/src/aleph/vm/conf.py +++ b/src/aleph/vm/conf.py @@ -492,10 +492,5 @@ def make_db_url(): return f"sqlite+aiosqlite:///{settings.EXECUTION_DATABASE}" -def initialize_settings(): - global settings - settings = Settings() - - # Settings singleton settings = Settings() diff --git a/tests/supervisor/test_qemu_instance.py b/tests/supervisor/test_qemu_instance.py index ed3be5252..da803034b 100644 --- a/tests/supervisor/test_qemu_instance.py +++ b/tests/supervisor/test_qemu_instance.py @@ -7,7 +7,7 @@ import pytest from aleph_message.models import ItemHash -from aleph.vm.conf import initialize_settings, settings +from aleph.vm.conf import settings from aleph.vm.controllers.__main__ import configuration_from_file, execute_persistent_vm from aleph.vm.controllers.qemu import AlephQemuInstance from aleph.vm.hypervisors.qemu.qemuvm import QemuVM @@ -50,10 +50,10 @@ async def test_create_qemu_instance(): Create an instance and check that it start / init / stop properly. """ - initialize_settings() settings.USE_FAKE_INSTANCE_BASE = True settings.FAKE_INSTANCE_MESSAGE = settings.FAKE_INSTANCE_QEMU_MESSAGE settings.FAKE_INSTANCE_BASE = settings.FAKE_QEMU_INSTANCE_BASE + settings.ENABLE_CONFIDENTIAL_COMPUTING = False settings.ALLOW_VM_NETWORKING = False settings.USE_JAILER = False @@ -106,10 +106,10 @@ async def test_create_qemu_instance_online(): Create an instance and check that it start / init / stop properly. """ - initialize_settings() settings.USE_FAKE_INSTANCE_BASE = True settings.FAKE_INSTANCE_MESSAGE = settings.FAKE_INSTANCE_QEMU_MESSAGE settings.FAKE_INSTANCE_BASE = settings.FAKE_QEMU_INSTANCE_BASE + settings.ENABLE_CONFIDENTIAL_COMPUTING = False settings.ALLOW_VM_NETWORKING = True settings.USE_JAILER = False