diff --git a/lisa/sut_orchestrator/libvirt/ch_platform.py b/lisa/sut_orchestrator/libvirt/ch_platform.py index de2c7e7a0d..cbd033a04b 100644 --- a/lisa/sut_orchestrator/libvirt/ch_platform.py +++ b/lisa/sut_orchestrator/libvirt/ch_platform.py @@ -3,6 +3,7 @@ import os import re +import secrets import xml.etree.ElementTree as ET # noqa: N817 from pathlib import Path from typing import List, Type @@ -11,7 +12,11 @@ from lisa.environment import Environment from lisa.feature import Feature from lisa.node import Node -from lisa.sut_orchestrator.libvirt.context import NodeContext, get_node_context +from lisa.sut_orchestrator.libvirt.context import ( + GuestVmType, + NodeContext, + get_node_context, +) from lisa.sut_orchestrator.libvirt.platform import BaseLibvirtPlatform from lisa.tools import QemuImg from lisa.util.logger import Logger, filter_ansi_escape @@ -57,13 +62,22 @@ def _configure_node( assert isinstance(node_runbook, CloudHypervisorNodeSchema) node_context = get_node_context(node) - if self.host_node.is_remote: - node_context.firmware_source_path = node_runbook.firmware - node_context.firmware_path = os.path.join( - self.vm_disks_dir, os.path.basename(node_runbook.firmware) - ) + if node_runbook.kernel: + if self.host_node.is_remote and not node_runbook.kernel.is_remote_path: + node_context.kernel_source_path = node_runbook.kernel.path + node_context.kernel_path = os.path.join( + self.vm_disks_dir, os.path.basename(node_runbook.kernel.path) + ) + else: + node_context.kernel_path = node_runbook.kernel.path else: - node_context.firmware_path = node_runbook.firmware + if self.host_node.is_remote: + node_context.kernel_source_path = node_runbook.firmware + node_context.kernel_path = os.path.join( + self.vm_disks_dir, os.path.basename(node_runbook.firmware) + ) + else: + node_context.kernel_path = node_runbook.firmware def _create_node( self, @@ -72,10 +86,10 @@ def _create_node( environment: Environment, log: Logger, ) -> None: - if node_context.firmware_source_path: + if node_context.kernel_source_path: self.host_node.shell.copy( - Path(node_context.firmware_source_path), - Path(node_context.firmware_path), + Path(node_context.kernel_source_path), + Path(node_context.kernel_path), ) super()._create_node( @@ -113,9 +127,19 @@ def _create_node_domain_xml( os_type = ET.SubElement(os, "type") os_type.text = "hvm" - os_kernel = ET.SubElement(os, "kernel") - os_kernel.text = node_context.firmware_path + os_kernel.text = node_context.kernel_path + if node_context.guest_vm_type is GuestVmType.ConfidentialVM: + launch_sec = ET.SubElement(domain, "launchSecurity") + launch_sec.attrib["type"] = "sev" + cbitpos = ET.SubElement(launch_sec, "cbitpos") + cbitpos.text = "0" + reducedphysbits = ET.SubElement(launch_sec, "reducedPhysBits") + reducedphysbits.text = "0" + policy = ET.SubElement(launch_sec, "policy") + policy.text = "0" + host_data = ET.SubElement(launch_sec, "host_data") + host_data.text = secrets.token_hex(32) devices = ET.SubElement(domain, "devices") if len(node_context.passthrough_devices) > 0: diff --git a/lisa/sut_orchestrator/libvirt/context.py b/lisa/sut_orchestrator/libvirt/context.py index 6e6793ae52..77893a824d 100644 --- a/lisa/sut_orchestrator/libvirt/context.py +++ b/lisa/sut_orchestrator/libvirt/context.py @@ -1,4 +1,5 @@ from dataclasses import dataclass, field +from enum import Enum from typing import Any, Dict, List, Optional, Tuple import libvirt # type: ignore @@ -43,11 +44,18 @@ class DevicePassthroughContext: managed: str = "" +@dataclass +class GuestVmType(Enum): + Standard = "Standard" + ConfidentialVM = "ConfidentialVM" + + @dataclass class NodeContext: vm_name: str = "" - firmware_source_path: str = "" - firmware_path: str = "" + kernel_source_path: str = "" + kernel_path: str = "" + guest_vm_type: GuestVmType = GuestVmType.Standard cloud_init_file_path: str = "" ignition_file_path: str = "" os_disk_source_file_path: Optional[str] = None diff --git a/lisa/sut_orchestrator/libvirt/features.py b/lisa/sut_orchestrator/libvirt/features.py new file mode 100644 index 0000000000..e5f281b48e --- /dev/null +++ b/lisa/sut_orchestrator/libvirt/features.py @@ -0,0 +1,57 @@ +from dataclasses import dataclass +from typing import Any, Type, cast + +from dataclasses_json import dataclass_json + +from lisa import features, schema, search_space +from lisa.environment import Environment +from lisa.features.security_profile import SecurityProfileType +from lisa.sut_orchestrator.libvirt.context import GuestVmType, get_node_context + + +@dataclass_json() +@dataclass() +class SecurityProfileSettings(features.SecurityProfileSettings): + def __hash__(self) -> int: + return hash(self._get_key()) + + def _get_key(self) -> str: + return f"{self.type}/{self.security_profile}/" + + def _call_requirement_method( + self, method: search_space.RequirementMethod, capability: Any + ) -> Any: + super_value: SecurityProfileSettings = super()._call_requirement_method( + method, capability + ) + value = SecurityProfileSettings() + value.security_profile = super_value.security_profile + return value + + +class SecurityProfile(features.SecurityProfile): + _security_profile_mapping = { + SecurityProfileType.Standard: GuestVmType.Standard, + SecurityProfileType.CVM: GuestVmType.ConfidentialVM, + } + + def _initialize(self, *args: Any, **kwargs: Any) -> None: + super()._initialize(*args, **kwargs) + + @classmethod + def settings_type(cls) -> Type[schema.FeatureSettings]: + return SecurityProfileSettings + + @classmethod + def on_before_deployment(cls, *args: Any, **kwargs: Any) -> None: + environment = cast(Environment, kwargs.get("environment")) + settings = kwargs.get("settings") + if not settings: + return + for node in environment.nodes._list: + assert isinstance(settings, SecurityProfileSettings) + assert isinstance(settings.security_profile, SecurityProfileType) + node_context = get_node_context(node) + node_context.guest_vm_type = cls._security_profile_mapping[ + settings.security_profile + ] diff --git a/lisa/sut_orchestrator/libvirt/platform.py b/lisa/sut_orchestrator/libvirt/platform.py index 887176c359..c8d9f807be 100644 --- a/lisa/sut_orchestrator/libvirt/platform.py +++ b/lisa/sut_orchestrator/libvirt/platform.py @@ -20,7 +20,7 @@ import pycdlib # type: ignore import yaml -from lisa import schema, search_space +from lisa import feature, schema, search_space from lisa.environment import Environment from lisa.feature import Feature from lisa.node import Node, RemoteNode, local_node_connect @@ -42,7 +42,12 @@ Uname, Whoami, ) -from lisa.util import LisaException, constants, get_public_key_data +from lisa.util import ( + LisaException, + NotMeetRequirementException, + constants, + get_public_key_data, +) from lisa.util.logger import Logger, filter_ansi_escape, get_logger from . import libvirt_events_thread @@ -54,6 +59,7 @@ get_environment_context, get_node_context, ) +from .features import SecurityProfile, SecurityProfileSettings from .platform_interface import IBaseLibvirtPlatform from .schema import ( FIRMWARE_TYPE_BIOS, @@ -89,6 +95,7 @@ class BaseLibvirtPlatform(Platform, IBaseLibvirtPlatform): _supported_features: List[Type[Feature]] = [ SerialConsole, StartStop, + SecurityProfile, ] def __init__(self, runbook: schema.Platform) -> None: @@ -193,6 +200,36 @@ def _prepare_environment(self, environment: Environment, log: Logger) -> bool: self._configure_environment(environment, log) + if environment.runbook.nodes_requirement: + for node_space in environment.runbook.nodes_requirement: + new_settings = search_space.SetSpace[schema.FeatureSettings]( + is_allow_set=True + ) + if node_space.features: + for current_settings in node_space.features.items: + # reload to type specified settings + try: + settings_type = feature.get_feature_settings_type_by_name( + current_settings.type, + BaseLibvirtPlatform.supported_features(), + ) + except NotMeetRequirementException as identifier: + raise LisaException( + f"platform doesn't support all features. {identifier}" + ) + new_setting = schema.load_by_type( + settings_type, current_settings + ) + existing_setting = feature.get_feature_settings_by_name( + new_setting.type, new_settings, True + ) + if existing_setting: + new_settings.remove(existing_setting) + new_setting = existing_setting.intersect(new_setting) + + new_settings.add(new_setting) + node_space.features = new_settings + return self._configure_node_capabilities(environment, log) def _deploy_environment(self, environment: Environment, log: Logger) -> None: @@ -312,10 +349,12 @@ def _create_node_capabilities( node_capabilities.network_interface.max_nic_count = 1 node_capabilities.network_interface.nic_count = 1 node_capabilities.gpu_count = 0 + security_profile_setting = SecurityProfileSettings() node_capabilities.features = search_space.SetSpace[schema.FeatureSettings]( is_allow_set=True, items=[ schema.FeatureSettings.create(SerialConsole.name()), + security_profile_setting, ], ) @@ -564,6 +603,31 @@ def _create_nodes( log: Logger, ) -> None: self.host_node.shell.mkdir(Path(self.vm_disks_dir), exist_ok=True) + features_settings: Dict[str, schema.FeatureSettings] = {} + + # collect all the features to handle special deployment logic. If one + # node has this, it needs to run. + nodes_requirement = environment.runbook.nodes_requirement + if nodes_requirement: + for node_space in nodes_requirement: + if not node_space.features: + continue + for feature_setting in node_space.features: + if feature_setting.type not in features_settings: + features_settings[feature_setting.type] = feature_setting + + # change deployment for each feature. + for feature_type, setting in [ + (t, s) + for t in self.supported_features() + for s in features_settings.values() + if t.name() == s.type + ]: + feature_type.on_before_deployment( + environment=environment, + log=log, + settings=setting, + ) for node in environment.nodes.list(): node_context = get_node_context(node) diff --git a/lisa/sut_orchestrator/libvirt/schema.py b/lisa/sut_orchestrator/libvirt/schema.py index 2c85bbebd6..eb086e967b 100644 --- a/lisa/sut_orchestrator/libvirt/schema.py +++ b/lisa/sut_orchestrator/libvirt/schema.py @@ -133,10 +133,20 @@ def __post_init__(self) -> None: self.disk_img_format = DiskImageFormat.QCOW2.value +@dataclass_json() +@dataclass +class KernelSchema: + path: str = "" + is_remote_path: bool = False + + @dataclass_json() @dataclass class CloudHypervisorNodeSchema(BaseLibvirtNodeSchema): + # DEPRECATED: use the 'kernel' field instead. # Local path to the cloud-hypervisor firmware. - # Can be obatained from: + # Can be obtained from: # https://github.com/cloud-hypervisor/rust-hypervisor-firmware firmware: str = "" + # Local or remote path to the cloud-hypervisor kernel. + kernel: Optional[KernelSchema] = None diff --git a/pyproject.toml b/pyproject.toml index 7ccbfedca8..893a079292 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,7 +88,7 @@ flake8 = [ ] isort = [ - "isort ~= 5.12.0", + "isort ~= 5.13.2", ] legacy = [