diff --git a/lisa/features/__init__.py b/lisa/features/__init__.py index f860c7a298..7e07b0dbf8 100644 --- a/lisa/features/__init__.py +++ b/lisa/features/__init__.py @@ -1,7 +1,8 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from .gpu import Gpu from .serial_console import SerialConsole from .startstop import StartStop -__all__ = ["SerialConsole", "StartStop"] +__all__ = ["Gpu", "SerialConsole", "StartStop"] diff --git a/lisa/features/gpu.py b/lisa/features/gpu.py new file mode 100644 index 0000000000..c3e228be34 --- /dev/null +++ b/lisa/features/gpu.py @@ -0,0 +1,133 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import re +from enum import Enum +from typing import Any, cast + +from lisa.feature import Feature +from lisa.operating_system import Linux, Redhat, Ubuntu +from lisa.tools import Uname, Wget +from lisa.util import LisaException + +FEATURE_NAME_GPU = "Gpu" + +ComputeSDK = Enum( + "ComputeSDK", + [ + # GRID Driver + "GRID", + # CUDA Driver + "CUDA", + ], +) + + +class Gpu(Feature): + def __init__(self, node: Any, platform: Any) -> None: + super().__init__(node, platform) + self._log = self._node.log + + @classmethod + def name(cls) -> str: + return FEATURE_NAME_GPU + + def _install_grid_driver(self, version: str) -> None: + self._log.info("Starting GRID driver installation") + + def _install_cuda_driver(self, version: str = "10.1.105-1") -> None: + self._log.info("Starting CUDA driver installation") + cuda_repo = "" + linux_os: Linux = cast(Linux, self._node.os) + + # CUDA driver installation for redhat distros + if isinstance(self._node.os, Redhat): + cuda_repo_pkg = f"cuda-repo-rhel7-{version}.x86_64.rpm" + cuda_repo = ( + "http://developer.download.nvidia.com/" + f"compute/cuda/repos/rhel7/x86_64/{cuda_repo_pkg}" + ) + linux_os = Redhat(self._node) + + # CUDA driver installation for Ubuntu distros + elif isinstance(self._node.os, Ubuntu): + release_version = self._node.os._os_version.release + release = re.sub("[^0-9]+", "", release_version) + cuda_repo_pkg = f"cuda-repo-ubuntu{release}_{version}_amd64.deb" + cuda_repo = ( + "http://developer.download.nvidia.com/compute/" + f"cuda/repos/ubuntu{release}/x86_64/{cuda_repo_pkg}" + ) + linux_os = Ubuntu(self._node) + + else: + raise LisaException( + f"Distro {self._node.os.__class__.__name__}" + "not supported to install CUDA driver." + ) + + wget_tool = self._node.tools[Wget] + # download the cuda driver at /tmp/ + wget_tool.get(cuda_repo, "/tmp/", cuda_repo_pkg) + # install the cuda driver rpm + install_result = linux_os.install_packages( + f"/tmp/{cuda_repo_pkg}", signed=False + ) + if install_result.exit_code != 0: + raise LisaException( + f"Failed to install {cuda_repo_pkg}. stdout: {install_result.stdout}" + ) + else: + self._log.info("Sucessfully installed cuda-drivers") + + def install_gpu_dep(self) -> None: + uname_tool = self._node.tools[Uname] + uname_ver = uname_tool.get_linux_information().uname_version + + # install dependency libraries for redhat and CentOS + if isinstance(self._node.os, Redhat): + # install the kernel-devel and kernel-header packages + package_name = f"kernel-devel-{uname_ver} kernel-headers-{uname_ver}" + install_result = self._node.os.install_packages(package_name) + if install_result.exit_code != 0: + raise LisaException( + f"Failed to install {package_name}." + f" stdout: {install_result.stdout}" + ) + # mesa-libEGL install/update is require to avoid a conflict between + # libraries - bugzilla.redhat 1584740 + package_name = "mesa-libGL mesa-libEGL libglvnd-devel" + install_result = self._node.os.install_packages(package_name) + if install_result.exit_code != 0: + raise LisaException( + f"Failed to install {package_name}." + f" stdout: {install_result.stdout}" + ) + # install dkms + package_name = "dkms" + install_result = self._node.os.install_packages(package_name, signed=False) + if install_result.exit_code != 0: + raise LisaException( + f"Failed to install {package_name}. stdout: {install_result.stdout}" + ) + + # install dependency libraraies for Ubuntu + elif isinstance(self._node.os, Ubuntu): + package_name = ( + f"build-essential libelf-dev linux-tools-{uname_ver}" + f" linux-cloud-tools-{uname_ver} python libglvnd-dev ubuntu-desktop" + ) + install_result = self._node.os.install_packages(package_name) + if install_result.exit_code != 0: + raise LisaException( + f"Failed to install {package_name}." + f" stdout: {install_result.stdout}" + ) + + def install_compute_sdk(self, driver: ComputeSDK, version: str) -> None: + if driver == ComputeSDK.GRID: + self._install_grid_driver(version) + elif driver == ComputeSDK.CUDA: + self._install_cuda_driver(version) + else: + raise LisaException("No valid driver SDK name provided to install.") diff --git a/lisa/operating_system.py b/lisa/operating_system.py index b2bde79aed..288a304823 100644 --- a/lisa/operating_system.py +++ b/lisa/operating_system.py @@ -2,12 +2,23 @@ # Licensed under the MIT license. import re +from dataclasses import dataclass from functools import partial -from typing import TYPE_CHECKING, Any, Iterable, List, Optional, Pattern, Type, Union +from typing import ( + TYPE_CHECKING, + Any, + Iterable, + List, + Optional, + Pattern, + Type, + Union, +) from lisa.executable import Tool from lisa.util import BaseClassMixin, LisaException, get_matched_str from lisa.util.logger import get_logger +from lisa.util.process import ExecutableResult from lisa.util.subclasses import Factory if TYPE_CHECKING: @@ -17,6 +28,18 @@ _get_init_logger = partial(get_logger, name="os") +@dataclass +class OsVersion: + vendor: str + release: str = "" + codename: str = "" + package: str = "" + update: str = "" + + def __str__(self) -> str: + return self.vendor + + class OperatingSystem: __lsb_release_pattern = re.compile(r"^Description:[ \t]+([\w]+)[ ]+$", re.M) __os_release_pattern_name = re.compile( @@ -36,6 +59,7 @@ def __init__(self, node: Any, is_posix: bool) -> None: self._node: Node = node self._is_posix = is_posix self._log = get_logger(name="os", parent=self._node.log) + self._os_version: OsVersion = OsVersion("") @classmethod def create(cls, node: Any) -> Any: @@ -93,12 +117,20 @@ def is_windows(self) -> bool: def is_posix(self) -> bool: return self._is_posix + @property + def os_version(self) -> OsVersion: + if not self._os_version: + self._os_version = self._get_os_version() + + return self._os_version + @classmethod def _get_detect_string(cls, node: Any) -> Iterable[str]: typed_node: Node = node cmd_result = typed_node.execute(cmd="lsb_release -d", no_error_log=True) yield get_matched_str(cmd_result.stdout, cls.__lsb_release_pattern) + # It covers distros like ClearLinux too cmd_result = typed_node.execute(cmd="cat /etc/os-release", no_error_log=True) yield get_matched_str(cmd_result.stdout, cls.__os_release_pattern_name) yield get_matched_str(cmd_result.stdout, cls.__os_release_pattern_id) @@ -131,16 +163,35 @@ def _get_detect_string(cls, node: Any) -> Iterable[str]: cmd_result = typed_node.execute(cmd="cat /etc/SuSE-release", no_error_log=True) yield get_matched_str(cmd_result.stdout, cls.__suse_release_pattern) + def _get_os_version(self) -> OsVersion: + return self._os_version + class Windows(OperatingSystem): def __init__(self, node: Any) -> None: super().__init__(node, is_posix=False) + self._get_os_version + + def _get_os_version(self) -> OsVersion: + self._os_version.vendor = "Microsoft Corporation" + cmd_result = self._node.execute( + cmd="systeminfo | findstr /B /C:'OS Version'", + no_error_log=True, + ) + if cmd_result.exit_code == 0 and cmd_result.stdout != "": + for row in cmd_result.stdout.split("\n"): + if ": " in row: + key, value = row.split(": ") + if "OS Version" in key: + self._os_version.release = value.strip() + return self._os_version class Posix(OperatingSystem, BaseClassMixin): def __init__(self, node: Any) -> None: super().__init__(node, is_posix=True) self._first_time_installation: bool = True + self._get_os_version @classmethod def type_name(cls) -> str: @@ -150,7 +201,9 @@ def type_name(cls) -> str: def name_pattern(cls) -> Pattern[str]: return re.compile(f"^{cls.type_name()}$") - def _install_packages(self, packages: Union[List[str]]) -> None: + def _install_packages( + self, packages: Union[List[str]], signed: bool = True + ) -> ExecutableResult: raise NotImplementedError() def _initialize_package_installation(self) -> None: @@ -158,8 +211,10 @@ def _initialize_package_installation(self) -> None: pass def install_packages( - self, packages: Union[str, Tool, Type[Tool], List[Union[str, Tool, Type[Tool]]]] - ) -> None: + self, + packages: Union[str, Tool, Type[Tool], List[Union[str, Tool, Type[Tool]]]], + signed: bool = True, + ) -> ExecutableResult: package_names: List[str] = [] if not isinstance(packages, list): packages = [packages] @@ -179,7 +234,36 @@ def install_packages( if self._first_time_installation: self._first_time_installation = False self._initialize_package_installation() - self._install_packages(package_names) + + return self._install_packages(package_names, signed) + + # TODO: Implement update_packages + def update_packages( + self, packages: Union[str, Tool, Type[Tool], List[Union[str, Tool, Type[Tool]]]] + ) -> None: + pass + + # TODO: Implement query_packages + def query_packages( + self, packages: Union[str, Tool, Type[Tool], List[Union[str, Tool, Type[Tool]]]] + ) -> List[str]: + pass + + def _get_os_version(self) -> OsVersion: + cmd_result = self._node.execute(cmd="cat /etc/os-release", no_error_log=True) + if cmd_result.exit_code == 0 and cmd_result.stdout != "": + for row in cmd_result.stdout.split("\n"): + if "=" in row: + key, value = row.split("=") + if "NAME" in key.upper(): + self._os_version.vendor = value.strip() + elif "VERSION_ID" in key.upper(): + self._os_version.release = value.strip() + elif "VERSION" in key.upper(): + self._os_version.codename = re.search( + r"\(([^)]+)", value.strip() + ) + return self._os_version class BSD(Posix): @@ -198,12 +282,35 @@ def name_pattern(cls) -> Pattern[str]: def _initialize_package_installation(self) -> None: self._node.execute("apt-get update", sudo=True) - def _install_packages(self, packages: Union[List[str]]) -> None: + def _install_packages( + self, packages: Union[List[str]], signed: bool = True + ) -> ExecutableResult: command = ( f"DEBIAN_FRONTEND=noninteractive " f"apt-get -y install {' '.join(packages)}" ) - self._node.execute(command, sudo=True) + if not signed: + command = command.__add__(" --allow-unauthenticated") + + return self._node.execute(command, sudo=True) + + def _get_os_version(self) -> OsVersion: + cmd_result = self._node.execute(cmd="lsb_release -as", no_error_log=True) + if cmd_result.exit_code == 0 and cmd_result.stdout != "": + for row in cmd_result.stdout.split("\n"): + if ": " in row: + key, value = row.split(": ") + if "Distributor ID" in key: + self._os_version.vendor = value.strip() + elif "Release" in key: + self._os_version.release = value.strip() + elif "Codename" in key: + self._os_version.codename = value.strip() + + if self._os_version.vendor in ["Debian", "Ubuntu", "LinuxMint"]: + self._os_version.package = "deb" + + return self._os_version class Ubuntu(Debian): @@ -211,6 +318,10 @@ class Ubuntu(Debian): def name_pattern(cls) -> Pattern[str]: return re.compile("^Ubuntu|ubuntu$") + def _get_os_version(self) -> OsVersion: + self._os_version.vendor = "Ubuntu" + return self._os_version + class FreeBSD(BSD): ... @@ -225,11 +336,32 @@ class Fedora(Linux): def name_pattern(cls) -> Pattern[str]: return re.compile("^Fedora|fedora$") - def _install_packages(self, packages: Union[List[str]]) -> None: - self._node.execute( - f"dnf install -y {' '.join(packages)}", - sudo=True, + def _install_packages( + self, packages: Union[List[str]], signed: bool = True + ) -> ExecutableResult: + command = f"dnf install -y {' '.join(packages)}" + if not signed: + command.__add__(" --nogpgcheck") + return self._node.execute(command, sudo=True) + + def _get_os_version(self) -> OsVersion: + cmd_result = self._node.execute( + cmd="cat /etc/fedora-release", no_error_log=True ) + if cmd_result.exit_code == 0 and cmd_result.stdout != "": + result = cmd_result.stdout + for vendor in ["Fedora", "CentOS", "Red Hat", "XenServer"]: + if vendor in result: + self._os_version.vendor = vendor + if re.search(r"\brelease\b", result, re.IGNORECASE): + self._os_version.release = re.split( + "release", result, flags=re.IGNORECASE + )[1].split()[0] + check_code = re.search(r"\(([^)]+)", result) + if check_code is not None: + self._os_version.codename = check_code.group(1) + break + return self._os_version class Redhat(Fedora): @@ -261,8 +393,32 @@ def _initialize_package_installation(self) -> None: timeout=3600, ) - def _install_packages(self, packages: Union[List[str]]) -> None: - self._node.execute(f"yum install -y {' '.join(packages)}", sudo=True) + def _install_packages( + self, packages: Union[List[str]], signed: bool = True + ) -> ExecutableResult: + command = f"yum install -y {' '.join(packages)}" + if not signed: + command.__add__(" --nogpgcheck") + return self._node.execute(command, sudo=True) + + def _get_os_version(self) -> OsVersion: + cmd_result = self._node.execute( + cmd="cat /etc/redhat-release", no_error_log=True + ) + if cmd_result.exit_code == 0 and cmd_result.stdout != "": + result = cmd_result.stdout + for vendor in ["Red Hat", "CentOS", "Fedora", "XenServer"]: + if vendor in result: + self._os_version.vendor = vendor + if re.search(r"\brelease\b", result, re.IGNORECASE): + self._os_version.release = re.split( + "release", result, flags=re.IGNORECASE + )[1].split()[0] + check_code = re.search(r"\(([^)]+)", result) + if check_code is not None: + self._os_version.codename = check_code.group(1) + break + return self._os_version class CentOs(Redhat): @@ -289,9 +445,17 @@ def name_pattern(cls) -> Pattern[str]: def _initialize_package_installation(self) -> None: self._node.execute("zypper --non-interactive --gpg-auto-import-keys update") - def _install_packages(self, packages: Union[List[str]]) -> None: - command = f"zypper --non-interactive in {' '.join(packages)}" - self._node.execute(command, sudo=True) + def _install_packages( + self, packages: Union[List[str]], signed: bool = True + ) -> ExecutableResult: + command = f"zypper --non-interactive in {' '.join(packages)}" + if not signed: + command.__add__(" --no-gpg-checks") + return self._node.execute(command, sudo=True) + + def _get_os_version(self) -> OsVersion: + os_version = OsVersion("SUSE") + return os_version class NixOS(Linux): diff --git a/lisa/sut_orchestrator/azure/features.py b/lisa/sut_orchestrator/azure/features.py index 64620c5bd4..aaf51cbd15 100644 --- a/lisa/sut_orchestrator/azure/features.py +++ b/lisa/sut_orchestrator/azure/features.py @@ -8,6 +8,8 @@ from lisa import features from lisa.node import Node +from lisa.operating_system import CentOs, Redhat, Ubuntu +from lisa.util import SkippedException from .common import get_compute_client, get_node_context, wait_operation @@ -67,3 +69,14 @@ def _get_console_log(self, saved_path: Optional[Path]) -> bytes: log_response = requests.get(diagnostic_data.serial_console_log_blob_uri) return log_response.content + + +class Gpu(AzureFeatureMixin, features.Gpu): + def _initialize(self, *args: Any, **kwargs: Any) -> None: + super()._initialize(*args, **kwargs) + self._initialize_information(self._node) + + def _is_supported(self) -> None: + supported_distro = (CentOs, Redhat, Ubuntu) + if not isinstance(self._node.os, supported_distro): + raise SkippedException(f"GPU is not supported with distro {self._node.os}") diff --git a/lisa/sut_orchestrator/azure/platform_.py b/lisa/sut_orchestrator/azure/platform_.py index ee9581db5c..4a47a89ff5 100644 --- a/lisa/sut_orchestrator/azure/platform_.py +++ b/lisa/sut_orchestrator/azure/platform_.py @@ -1181,6 +1181,10 @@ def _resource_sku_to_capability( [features.StartStop.name(), features.SerialConsole.name()] ) + # update features list if gpu feature is supported + if node_space.gpu_count: + node_space.features.update(features.Gpu.name()) + return node_space def _get_eligible_vm_sizes( diff --git a/lisa/tools/make.py b/lisa/tools/make.py index b41283a647..3dcf548440 100644 --- a/lisa/tools/make.py +++ b/lisa/tools/make.py @@ -11,8 +11,6 @@ class Make(Tool): - repo = "https://github.com/microsoft/ntttcp-for-linux" - @property def command(self) -> str: return "make" diff --git a/lisa/tools/rpm.py b/lisa/tools/rpm.py new file mode 100644 index 0000000000..19939988ad --- /dev/null +++ b/lisa/tools/rpm.py @@ -0,0 +1,84 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from typing import List, cast + +from lisa.executable import Tool +from lisa.operating_system import Posix +from lisa.util import LisaException + + +class Rpm(Tool): + @property + def command(self) -> str: + return "rpm" + + @property + def can_install(self) -> bool: + return True + + def _install(self) -> bool: + posix_os: Posix = cast(Posix, self.node.os) + posix_os.install_packages("rpm") + return self._check_exists() + + def install_package( + self, package_file: str, force_run: bool = False, no_error_log: bool = False + ) -> None: + self.initialize() + run_command = f"-iv {package_file}" + cmd_result = self.run( + run_command, + force_run=force_run, + no_error_log=no_error_log, + no_info_log=True, + ) + if cmd_result.exit_code != 0 or package_file not in self.query_package( + package_file, True + ): + raise LisaException( + f"could not install package with '{run_command}', it may caused by" + f" missing file. stdout: {cmd_result.stdout}" + ) + + def upgrade_package( + self, package_file: str, force_run: bool = False, no_error_log: bool = False + ) -> None: + self.initialize() + run_command = f"-Uv {package_file}" + cmd_result = self.run( + run_command, + force_run=force_run, + no_error_log=no_error_log, + no_info_log=True, + ) + if cmd_result.exit_code != 0 or package_file not in self.query_package( + package_file, True + ): + raise LisaException( + f"could not install package with '{run_command}', it may caused by" + f" missing file. stdout: {cmd_result.stdout}" + ) + + def query_package( + self, + package_file: str = "", + force_run: bool = False, + no_error_log: bool = False, + ) -> List[str]: + self.initialize() + if package_file: + run_command = f"-qa | grep {package_file}" + else: + run_command = "-qa" + cmd_result = self.run( + run_command, + force_run=force_run, + no_error_log=no_error_log, + no_info_log=True, + ) + package_list = [] + if cmd_result.exit_code == 0: + for row in cmd_result.stdout.split("\n"): + package_list.append(row) + return package_list