Skip to content

Commit

Permalink
BareMetal: add PXE and Windows COM serial feature
Browse files Browse the repository at this point in the history
1. PXE provides more flexible on select serial console and ip power solutions. IP power will come later.
2. Windows Serial Console bases on plink to provide an easy way to access COM port.
  • Loading branch information
squirrelsc committed Dec 31, 2024
1 parent 41b898c commit c274dbb
Show file tree
Hide file tree
Showing 3 changed files with 180 additions and 0 deletions.
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, 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: Path | None) -> 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

0 comments on commit c274dbb

Please sign in to comment.