Skip to content

Commit

Permalink
Support overlake SOC on BareMetal
Browse files Browse the repository at this point in the history
  • Loading branch information
paull committed Sep 18, 2024
1 parent a1358de commit 5bbbc1a
Show file tree
Hide file tree
Showing 5 changed files with 309 additions and 36 deletions.
170 changes: 170 additions & 0 deletions lisa/sut_orchestrator/baremetal/bootconfig.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

from dataclasses import dataclass, field
from typing import Optional, Type

from dataclasses_json import dataclass_json
from marshmallow import validate

from lisa import schema
from lisa.node import quick_connect
from lisa.util import InitializableMixin, LisaException, field_metadata, subclasses
from lisa.util.logger import get_logger

from .schema import BootConfigSchema

OV_CP = "cp"
OV_GP = "gp"

OV_DEV = "dev"
OV_PIPELINE = "pipeline"


class BootConfig(subclasses.BaseClassWithRunbookMixin, InitializableMixin):
def __init__(
self,
runbook: BootConfigSchema,
) -> None:
super().__init__(runbook=runbook)
self.boot_config_runbook: BootConfigSchema = self.runbook
self._log = get_logger("boot_config", self.__class__.__name__)

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

def set_boot_config(self) -> None:
raise NotImplementedError()


@dataclass_json()
@dataclass
class PxeBootSchema(BootConfigSchema):
connection: Optional[schema.RemoteNode] = field(
default=None, metadata=field_metadata(required=True)
)
node_setup: str = field(
default=OV_DEV,
metadata=field_metadata(
validate=validate.OneOf(
[
OV_DEV,
OV_PIPELINE,
]
)
),
)
node_type: str = field(
default=OV_CP,
metadata=field_metadata(
validate=validate.OneOf(
[
OV_CP,
OV_GP,
]
)
),
)
node_number: int = field(
default=1, metadata=field_metadata(validate=validate.Range(min=1, max=20))
)
image_source: str = field(default="", metadata=field_metadata(required=True))
kernel_append: Optional[str] = field(default="")


class PxeBoot(BootConfig):
def __init__(
self,
runbook: PxeBootSchema,
) -> None:
super().__init__(runbook=runbook)
self.pxe_runbook: PxeBootSchema = self.runbook
self._log = get_logger("pxe_boot", self.__class__.__name__)
self._boot_label = "label lisav3"
self._boot_dir = "/var/lib/tftpboot/"
self._boot_config = self._boot_dir + "pxelinux.cfg/"

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

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

def set_boot_config(self) -> None:
boot_type = f"{self.pxe_runbook.node_setup}-{self.pxe_runbook.node_type}"
boot_name = f"{boot_type}{self.pxe_runbook.node_number}"
host_ref = f"host {boot_name}"

self._connect_to_pxe_server()
pxe_boot_config_path = self._get_dhcp_info(boot_name, host_ref)
if not pxe_boot_config_path:
raise LisaException(f"Failed to find DHCP entry for {boot_name}")

boot_image = self.pxe_runbook.image_source.replace(
self._boot_dir,
"",
).strip("/")
boot_entry = f"kernel {boot_image}"

# Delete it if one is there, so to always insert at the top
self.pxe_server.execute(
cmd=f"sed -i '/^{self._boot_label}$/,/^$/d' " f"{pxe_boot_config_path}",
expected_exit_code_failure_message=(
f"Failed to delete previous boot entry on {boot_name}"
),
expected_exit_code=0,
sudo=False,
)

self._log.debug(
"Adding boot entry for LISA at " # type: ignore
f"{pxe_boot_config_path}, "
f"pointing to {boot_entry}"
)

temp2 = self.pxe_runbook.kernel_append
append = f"\\n append {temp2}" if temp2 else ""
label = self._boot_label
self.pxe_server.execute(
cmd=f"sed -i '/^menu.*/a \\\\n{label}\\n {boot_entry}{append}' "
f"{pxe_boot_config_path}",
expected_exit_code_failure_message=(
f"Failed to create new boot entry on {boot_name}"
),
expected_exit_code=0,
sudo=False,
)

self._log.debug(
"Added boot entry for LISA at " # type: ignore
f"{pxe_boot_config_path}, "
f"pointing to {boot_entry}"
)

def _connect_to_pxe_server(self) -> None:
assert self.pxe_runbook.connection, "connection is required for pxe_server"
self.pxe_runbook.connection.name = "pxe_server"
self.pxe_server = quick_connect(
self.pxe_runbook.connection, logger_name="pxe_server"
)

def _get_dhcp_info(self, node_name: str, host_ref: str) -> str:
output_dhcp_info = self.pxe_server.execute(
cmd="cat /etc/dhcp/dhcpd.conf",
expected_exit_code_failure_message=(
f"failed to obtain {node_name}'s configuration information\n"
),
expected_exit_code=0,
sudo=False,
).stdout

board_info = output_dhcp_info[output_dhcp_info.index(host_ref) :]
config_info = board_info[board_info.index("{") : board_info.index("}") + 1]
start = config_info.index("ethernet") + len("ethernet ")
end = config_info.index(";")
address = "01-" + config_info[start:end].replace(":", "-").strip()
address_path = self._boot_config + address
return address_path
109 changes: 88 additions & 21 deletions lisa/sut_orchestrator/baremetal/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@

import os
import re
import stat
from pathlib import Path
from typing import Dict, List, Type

import paramiko
from smb.SMBConnection import SMBConnection # type: ignore

from lisa import schema
from lisa.util import ContextMixin, InitializableMixin, subclasses
from lisa.util.logger import get_logger

from .schema import BuildSchema, FileSchema, SMBBuildSchema
from .schema import BuildSchema, FileSchema, SMBBuildSchema, TftpBuildSchema


class Build(subclasses.BaseClassWithRunbookMixin, ContextMixin, InitializableMixin):
Expand All @@ -24,6 +26,28 @@ def __init__(self, runbook: BuildSchema) -> None:
def type_schema(cls) -> Type[schema.TypedSchema]:
return BuildSchema

@classmethod
def find_matched_files(
cls, sources_path: List[Path], files_map: List[FileSchema]
) -> Dict[str, FileSchema]:
all_files = []
match_files: Dict[str, FileSchema] = {}
for source_path in sources_path:
for root, _, files in os.walk(source_path):
for file in files:
all_files.append(os.path.join(root, file))

for file_map in files_map:
file_path = rf"{source_path}\{file_map.source}".replace("\\", "\\\\")
pattern = re.compile(
file_path,
re.I | re.M,
)
for file in all_files:
if pattern.match(file):
match_files[file] = file_map
return match_files

def copy(self, sources_path: List[Path], files_map: List[FileSchema]) -> None:
raise NotImplementedError()

Expand Down Expand Up @@ -55,8 +79,9 @@ def copy(self, sources_path: List[Path], files_map: List[FileSchema]) -> None:
) as conn:
conn.connect(server_name)

for file, file_map in self._find_matched_files(
sources_path, files_map
for file, file_map in Build.find_matched_files(
sources_path,
files_map,
).items():
with open(file, "rb") as f:
if file_map.destination:
Expand All @@ -79,23 +104,65 @@ def copy(self, sources_path: List[Path], files_map: List[FileSchema]) -> None:
)
self._log.debug(f"copy file {file} to {share_name}\\{file_name}")

def _find_matched_files(
self, sources_path: List[Path], files_map: List[FileSchema]
) -> Dict[str, FileSchema]:
all_files = []
match_files: Dict[str, FileSchema] = {}
for source_path in sources_path:
for root, _, files in os.walk(source_path):
for file in files:
all_files.append(os.path.join(root, file))

for file_map in files_map:
file_path = rf"{source_path}\{file_map.source}".replace("\\", "\\\\")
pattern = re.compile(
file_path,
re.I | re.M,
class TftpBuild(Build):
def __init__(self, runbook: TftpBuildSchema) -> None:
super().__init__(runbook)
self.pxe_runbook: TftpBuildSchema = self.runbook

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

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

def copy(self, sources_path: List[Path], files_map: List[FileSchema]) -> None:
server_address = self.pxe_runbook.connection.address
server_port = self.pxe_runbook.connection.port
server_username = self.pxe_runbook.connection.username
server_password = self.pxe_runbook.connection.password

try:
transport = paramiko.Transport((server_address, server_port))
transport.connect(
username=server_username,
password=server_password,
)
except Exception as ex:
self._log.exception(
"failed to connect to {server_address}:{server_port}",
exc_info=ex,
)
sftp = paramiko.SFTPClient.from_transport(transport)

for file, file_map in Build.find_matched_files(
sources_path,
files_map,
).items():
if file_map.destination:
attrs = sftp.lstat(file_map.destination)
if attrs.st_mode and stat.S_ISDIR(attrs.st_mode):
file_name = file_map.destination + "/" + file.rsplit("\\")[-1]
else:
file_name = file_map.destination
else:
file_name = file.rsplit("\\")[-1]

try:
sftp.put(file, file_name)
except Exception as ex:
self._log.exception(
"failed to copy file {file} to {file_name}",
exc_info=ex,
)
for file in all_files:
if pattern.match(file):
match_files[file] = file_map
return match_files

try:
sftp.close()
transport.close()
except Exception as ex:
self._log.exception(
"failed to close the connection",
exc_info=ex,
)
24 changes: 19 additions & 5 deletions lisa/sut_orchestrator/baremetal/platform_.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@
# Licensed under the MIT license.

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

from lisa import RemoteNode, feature, schema, search_space
from lisa.environment import Environment
from lisa.platform_ import Platform
from lisa.util import fields_to_dict
from lisa.util.logger import Logger
from lisa.util.shell import try_connect
from lisa.util.subclasses import Factory

from .. import BAREMETAL
from .bootconfig import BootConfig
from .build import Build
from .cluster.cluster import Cluster
from .context import get_build_context, get_node_context
Expand Down Expand Up @@ -52,6 +52,7 @@ def _initialize(self, *args: Any, **kwargs: Any) -> None:
self.key_loader_factory = Factory[KeyLoader](KeyLoader)
self.source_factory = Factory[Source](Source)
self.build_factory = Factory[Build](Build)
self.boot_config_factory = Factory[BootConfig](BootConfig)
# currently only support one cluster
assert self._baremetal_runbook.cluster, "no cluster is specified in the runbook"
self._cluster_runbook = self._baremetal_runbook.cluster[0]
Expand Down Expand Up @@ -104,6 +105,12 @@ def _deploy_environment(self, environment: Environment, log: Logger) -> None:
)
build_context.is_copied = True

if self.cluster.runbook.boot_config:
boot_config = self.boot_config_factory.create_by_runbook(
self.cluster.runbook.boot_config
)
boot_config.set_boot_config()

if self.cluster.runbook.key_loader:
key_loader = self.key_loader_factory.create_by_runbook(
self.cluster.runbook.key_loader
Expand Down Expand Up @@ -167,9 +174,16 @@ def _deploy_environment(self, environment: Environment, log: Logger) -> None:
assert isinstance(node, RemoteNode), f"actual: {type(node)}"
node.name = f"node_{index}"
node.set_connection_info(
**fields_to_dict(
node_context.connection,
["address", "port", "username", "password", "private_key_file"],
address=node_context.connection.address,
public_port=node_context.connection.port,
username=node_context.connection.username,
password=cast(
str,
node_context.connection.password,
),
private_key_file=cast(
str,
node_context.connection.private_key_file,
),
)
try_connect(connection_info)
Expand Down
Loading

0 comments on commit 5bbbc1a

Please sign in to comment.