Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BareMetal: add PXE cluster and Windows COM Serial #3576

Merged
merged 3 commits into from
Jan 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lisa/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from lisa.environment import Environment
from lisa.executable import CustomScript, CustomScriptBuilder
from lisa.feature import Feature
from lisa.node import Node, RemoteNode
from lisa.testsuite import (
TestCaseMetadata,
Expand Down Expand Up @@ -36,6 +37,7 @@
"CustomScript",
"CustomScriptBuilder",
"Environment",
"Feature",
"LisaException",
"Logger",
"Node",
Expand Down
1 change: 1 addition & 0 deletions lisa/mixin_modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import lisa.sut_orchestrator.baremetal.build # noqa: F401
import lisa.sut_orchestrator.baremetal.cluster.cluster # noqa: F401
import lisa.sut_orchestrator.baremetal.cluster.idrac # noqa: F401
import lisa.sut_orchestrator.baremetal.cluster.pxe # noqa: F401
import lisa.sut_orchestrator.baremetal.cluster.rackmanager # noqa: F401
import lisa.sut_orchestrator.baremetal.ip_getter # noqa: F401
import lisa.sut_orchestrator.baremetal.platform_ # noqa: F401
Expand Down
141 changes: 141 additions & 0 deletions lisa/sut_orchestrator/baremetal/cluster/pxe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

from pathlib import Path
from typing import Any, Optional, Type, cast

from lisa import features
from lisa.environment import Environment
from lisa.features.serial_console import SerialConsole
from lisa.node import Node, RemoteNode, quick_connect
from lisa.platform_ import Platform
from lisa.schema import FeatureSettings
from lisa.util import LisaException
from lisa.util.logger import Logger

from .. import schema
from ..context import get_node_context
from .cluster import Cluster


class RemoteComSerialConsole(features.SerialConsole):
def __init__(
self, settings: FeatureSettings, node: Node, platform: Platform
) -> None:
super().__init__(settings, node, platform)

def close(self) -> None:
self._process.kill()
self._log.debug("serial console is closed.")

def write(self, data: str) -> None:
self._process.input(f"{data}\n")

def _get_console_log(self, saved_path: Optional[Path]) -> bytes:
return self._process.log_buffer.getvalue().encode("utf-8")

def _initialize(self, *args: Any, **kwargs: Any) -> None:
super()._initialize(*args, **kwargs)

context = get_node_context(self._node)
pxe_cluster = cast(schema.PxeCluster, context.cluster)
assert pxe_cluster.serial_console, "serial_console is not defined"

connection = pxe_cluster.serial_console.get_extended_runbook(
schema.RemoteComSerialConsoleServer
).connection
assert connection, "connection is required for windows remote com"
serial_node = quick_connect(
connection, logger_name="serial", parent_logger=self._log
)

serial_console = pxe_cluster.serial_console.get_extended_runbook(
schema.RemoteComSerialConsoleServer
)

self._plink_path = serial_node.get_pure_path("plink")
if serial_console.plink_path:
# Use remote pure path, because the remote OS may be different with
# LISA running OS.
self._plink_path = serial_console.plink_path / self._plink_path

self._serial_node = cast(RemoteNode, serial_node)
# connect to serial console server from beginning to collect all log.
self._connect()

def _connect(self) -> None:
context = get_node_context(self._node)

client_runbook = cast(schema.PxeClient, context.client)
serial_client_runbook = client_runbook.serial_console
assert serial_client_runbook, "serial_console is not defined"
serial_port = serial_client_runbook.port

pxe_cluster = cast(schema.PxeCluster, context.cluster)
assert pxe_cluster.serial_console, "serial_console is not defined"
server_runbook = pxe_cluster.serial_console.get_extended_runbook(
schema.RemoteComSerialConsoleServer
)

self._log.debug(f"connecting to serial console: {serial_port}")
# Note: the leading whitespace " COM1", which is before the com port, is
# required to avoid plink bug. If there is no leading whitespace, plink
# will fail to open com, because the name is recognized like "\.\\" not
# "\\.\COM1".
process = self._serial_node.execute_async(
f'{self._plink_path} -serial " {serial_port}" '
f"-sercfg {server_runbook.bps},8,n,1,N"
)

found_error = process.wait_output(
"Unable to open connection", timeout=1, error_on_missing=False, interval=0.1
)
if found_error:
process.kill()
raise LisaException(f"failed to connect serial console: {serial_port}")

# entering to make sure connection is established, avoid it's too fast
# to send content
process.input("\n")
process.wait_output("\n", timeout=1, interval=0.1)

self._process = process
self._log.debug("connected to serial console: {serial_port}")


class Pxe(Cluster):
def __init__(self, runbook: schema.ClusterSchema, **kwargs: Any) -> None:
super().__init__(runbook, **kwargs)
self.runbook: schema.PxeCluster = self.runbook

@classmethod
def type_name(cls) -> str:
return "pxe"

@classmethod
def type_schema(cls) -> Type[schema.PxeCluster]:
return schema.PxeCluster

def get_serial_console(self) -> Type[SerialConsole]:
assert self.runbook.serial_console, "serial_console is not defined"
if self.runbook.serial_console.type == "remote_com":
return RemoteComSerialConsole
else:
raise NotImplementedError(
f"serial console type {self.runbook.serial_console.type} "
f"is not supported."
)

def deploy(self, environment: Environment) -> Any:
# connect to serial console
for node in environment.nodes.list():
# start serial console to save all log
_ = node.features[features.SerialConsole]

def delete(self, environment: Environment, log: Logger) -> None:
for node in environment.nodes.list():
serial_console = node.features[features.SerialConsole]
serial_console.close()

def cleanup(self) -> None:
super().cleanup()
38 changes: 38 additions & 0 deletions lisa/sut_orchestrator/baremetal/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,44 @@ class RackManagerSchema(ClusterSchema):
client: List[RackManagerClientSchema] = field(default_factory=list)


@dataclass_json()
@dataclass
class SerialConsoleServer(schema.TypedSchema, schema.ExtendableSchemaMixin):
# for internal most used default value
bps: int = 115200


@dataclass_json()
@dataclass
class RemoteComSerialConsoleServer(SerialConsoleServer):
type: str = "remote_com"
connection: Optional[schema.RemoteNode] = field(
default=None, metadata=field_metadata(required=True)
)
plink_path: str = ""


@dataclass_json()
@dataclass
class SerialConsoleClient(schema.TypedSchema, schema.ExtendableSchemaMixin):
port: str = field(default="", metadata=field_metadata(required=True))
type: str = "com"


@dataclass_json()
@dataclass
class PxeClient(ClientSchema):
serial_console: Optional[SerialConsoleClient] = field(default=None)


@dataclass_json()
@dataclass
class PxeCluster(ClusterSchema):
type: str = "pxe"
serial_console: Optional[SerialConsoleServer] = field(default=None)
client: List[PxeClient] = field(default_factory=list)


@dataclass_json()
@dataclass
class BareMetalPlatformSchema:
Expand Down
37 changes: 20 additions & 17 deletions lisa/sut_orchestrator/hyperv/get_assignable_devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
class HypervAssignableDevices:
PKEY_DEVICE_TYPE = "{3AB22E31-8264-4b4e-9AF5-A8D2D8E33E62} 1"
PKEY_BASE_CLASS = "{3AB22E31-8264-4b4e-9AF5-A8D2D8E33E62} 3"
PKEY_REQUIRES_RESERVED_MEMORY_REGION = "{3AB22E31-8264-4b4e-9AF5-A8D2D8E33E62} 34" # noqa E501
PKEY_ACS_COMPATIBLE_UP_HIERARCHY = "{3AB22E31-8264-4b4e-9AF5-A8D2D8E33E62} 31" # noqa E501
PKEY_REQUIRES_RESERVED_MEMORY_REGION = "{3AB22E31-8264-4b4e-9AF5-A8D2D8E33E62} 34"
PKEY_ACS_COMPATIBLE_UP_HIERARCHY = "{3AB22E31-8264-4b4e-9AF5-A8D2D8E33E62} 31"
PROP_DEVICE_TYPE_PCI_EXPRESS_ENDPOINT = "2"
PROP_DEVICE_TYPE_PCI_EXPRESS_LEGACY_ENDPOINT = "3"
PROP_DEVICE_TYPE_PCI_EXPRESS_ROOT_COMPLEX_INTEGRATED_ENDPOINT = "4"
Expand All @@ -28,9 +28,9 @@ def __init__(self, host_node: Node, log: Logger):
self.host_node = host_node
self.log = log
self.pwsh = self.host_node.tools[PowerShell]
self.pnp_allocated_resources: List[Dict[str, str]] = (
self.__load_pnp_allocated_resources()
)
self.pnp_allocated_resources: List[
Dict[str, str]
] = self.__load_pnp_allocated_resources()

def get_assignable_devices(
self,
Expand Down Expand Up @@ -84,10 +84,12 @@ def __get_devices_by_vendor_device_id(
if not res:
raise LisaException("Can not extract DeviceId/Description")

devices.append({
"device_id": res["device_id"].strip(),
"friendly_name": res["desc"].strip(),
})
devices.append(
{
"device_id": res["device_id"].strip(),
"friendly_name": res["desc"].strip(),
}
)
return devices

def __get_pnp_device_property(self, device_id: str, property_name: str) -> str:
Expand Down Expand Up @@ -169,7 +171,7 @@ def __load_pnp_allocated_resources(self) -> List[Dict[str, str]]:
pnp_allocated_resources = stdout.strip().split("\r\n\r\n")
result: List[Dict[str, str]] = []
# Regular expression to match the key-value pairs
pattern = re.compile(r'(?P<key>\S+)\s*:\s*(?P<value>.*?)(?=\n\S|\Z)', re.DOTALL)
pattern = re.compile(r"(?P<key>\S+)\s*:\s*(?P<value>.*?)(?=\n\S|\Z)", re.DOTALL)

for rec in pnp_allocated_resources:
extract_val = {}
Expand Down Expand Up @@ -247,8 +249,7 @@ def __get_dda_properties(self, device_id: str) -> Optional[DeviceAddressSchema]:
return None

dev_type = self.__get_pnp_device_property(
device_id=device_id,
property_name=self.PKEY_DEVICE_TYPE
device_id=device_id, property_name=self.PKEY_DEVICE_TYPE
)
dev_type = dev_type.strip()
if dev_type == self.PROP_DEVICE_TYPE_PCI_EXPRESS_ENDPOINT:
Expand Down Expand Up @@ -277,8 +278,7 @@ def __get_dda_properties(self, device_id: str) -> Optional[DeviceAddressSchema]:
)
else:
self.log.debug(
"Old-style PCI device, switch port, etc. "
"Not assignable."
"Old-style PCI device, switch port, etc. " "Not assignable."
)
return None

Expand Down Expand Up @@ -311,12 +311,14 @@ def __get_dda_properties(self, device_id: str) -> Optional[DeviceAddressSchema]:
return None

irq_assignements = [
i for i in self.pnp_allocated_resources
i
for i in self.pnp_allocated_resources
if i["Dependent"].find(device_id.replace("\\", "\\\\")) >= 0
]
if irq_assignements:
msi_assignments = [
i for i in self.pnp_allocated_resources
i
for i in self.pnp_allocated_resources
if i["Antecedent"].find("IRQNumber=42949") >= 0
]
if not msi_assignments:
Expand All @@ -330,7 +332,8 @@ def __get_dda_properties(self, device_id: str) -> Optional[DeviceAddressSchema]:
self.log.debug("It has no interrupts at all -- assignment can work.")

mmio_assignments = [
i for i in self.pnp_allocated_resources
i
for i in self.pnp_allocated_resources
if i["Dependent"].find(device_id.replace("\\", "\\\\")) >= 0
and i["__RELPATH"].find("Win32_DeviceMemoryAddres") >= 0
]
Expand Down
Loading