Skip to content

Commit

Permalink
Added tftp deployment on BareMetal platform
Browse files Browse the repository at this point in the history
  • Loading branch information
paull committed Sep 18, 2024
1 parent a1358de commit 733b52a
Show file tree
Hide file tree
Showing 5 changed files with 346 additions and 78 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 "
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 "
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
112 changes: 91 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,68 @@ 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:
assert self.pxe_runbook.connection, "The build server is not specified"

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)
assert sftp, "sft client is not created"

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,
)
Loading

0 comments on commit 733b52a

Please sign in to comment.