support OS detection and add some tools

1. update OS to support Ubuntu and CentOS
2. add logic to detect OS.
3. support package installation
4. add gcc, make tools
5. support git, ntttcp install
6. other minor improvements
This commit is contained in:
Chi Song 2020-09-10 15:59:24 +08:00
Родитель 75ce9d66d2
Коммит a698de989e
18 изменённых файлов: 258 добавлений и 71 удалений

Просмотреть файл

@ -37,10 +37,11 @@ class HelloWorld(TestSuite):
# get process output directly.
echo = node.tools[Echo]
result = echo.run("hello world!")
self.log.info(f"stdout of node: '{result.stdout}'")
self.log.info(f"stderr of node: '{result.stderr}'")
self.log.info(f"exitCode of node: '{result.exit_code}'")
hello_world = "hello world!"
result = echo.run(hello_world)
self.assertEquals(hello_world, result.stdout)
self.assertEquals("", result.stderr)
self.assertEqual(0, result.exit_code)
@TestCaseMetadata(
description="""
@ -52,7 +53,7 @@ class HelloWorld(TestSuite):
node = self.environment.default_node
# use it once like this way before use short cut
node.tools[Echo]
self.log.info(f"stdout of node: '{node.tools.echo('bye!')}'")
self.assertEqual("bye!", str(node.tools.echo("bye!")))
def before_suite(self) -> None:
self.log.info("setup my test suite")

Просмотреть файл

@ -16,7 +16,7 @@ from lisa.tools import Lscpu, Ntttcp
class MutipleNodesDemo(TestSuite):
@TestCaseMetadata(
description="""
this test case send and receive data by ntttcp
This test case send and receive data by ntttcp
""",
priority=1,
)
@ -43,5 +43,7 @@ class MutipleNodesDemo(TestSuite):
ntttcp_client = client_node.tools[Ntttcp]
server_process = ntttcp_server.run_async("-P 1 -t 5 -e")
ntttcp_client.run(f"-s {server_node.internal_address} -P 1 -n 1 -t 5 -W 1")
ntttcp_client.run(
f"-s {server_node.internal_address} -P 1 -n 1 -t 5 -W 1", no_info_log=False
)
server_process.wait_result()

Просмотреть файл

@ -4,6 +4,7 @@ from lisa import TestCaseMetadata, TestSuite, TestSuiteMetadata
from lisa.executable import CustomScript, CustomScriptBuilder
from lisa.operating_system import Windows
from lisa.testsuite import simple_requirement
from lisa.util.perf_timer import create_timer
@TestSuiteMetadata(
@ -22,16 +23,28 @@ class WithScript(TestSuite):
@TestCaseMetadata(
description="""
this test case run script on test node.
this test case run script on a linux node, and demostrate
1. how to use customized script on tested node.
1. how to use requirement to limit case excludes an os.
2. use perf_timer to measure performance and output result.
""",
priority=1,
requirement=simple_requirement(unsupported_os=[Windows]),
)
def script(self) -> None:
node = self.environment.default_node
timer1 = create_timer()
script: CustomScript = node.tools[self._echo_script]
result = script.run()
self.log.info(f"result1 stdout: {result}")
# the second time should be faster, without uploading
result = script.run()
self.log.info(f"result2 stdout: {result}")
result1 = script.run()
self.log.info(f"first run finished within {timer1}")
timer2 = create_timer()
result2 = script.run()
self.assertEqual(result1.stdout, result2.stdout)
self.assertGreater(
timer1.elapsed(),
timer2.elapsed(),
"the second time should be faster, without uploading",
)
self.log.info(
f"second run finished within {timer2}, total: {timer1.elapsed_text(False)}"
)

Просмотреть файл

@ -84,6 +84,14 @@ class Tool(ABC, InitializableMixin):
"""
return None
@property
def package_name(self) -> str:
"""
return package name,
it may be different with command or different platform.
"""
return self.command
@classmethod
def create(cls, node: Node) -> Tool:
"""
@ -218,7 +226,7 @@ class Tool(ABC, InitializableMixin):
parameters: str = "",
shell: bool = False,
no_error_log: bool = False,
no_info_log: bool = False,
no_info_log: bool = True,
cwd: Optional[pathlib.PurePath] = None,
) -> ExecutableResult:
return self.run(
@ -445,7 +453,7 @@ class Tools:
tool_log.debug(f"installed in {timer}")
else:
raise LisaException(
"doesn't support install on "
f"doesn't support install {tool.name} on "
f"Node({self._node.index}), "
f"Linux({self._node.is_linux}), "
f"Remote({self._node.is_remote})"

Просмотреть файл

@ -9,6 +9,7 @@ from retry import retry # type: ignore
from lisa.parameter_parser.argparser import parse_args
from lisa.util import constants
from lisa.util.logger import get_logger, set_level, set_log_file
from lisa.util.perf_timer import create_timer
@retry(FileExistsError, tries=10, delay=0) # type: ignore
@ -23,37 +24,41 @@ def create_run_path(root_path: Path) -> Path:
def main() -> None:
runtime_root = Path("runtime").absolute()
total_timer = create_timer()
try:
runtime_root = Path("runtime").absolute()
constants.CACHE_PATH = runtime_root.joinpath("cache")
constants.CACHE_PATH.mkdir(parents=True, exist_ok=True)
# create run root path
runs_path = runtime_root.joinpath("runs")
logic_path = create_run_path(runs_path)
local_path = runs_path.joinpath(logic_path)
local_path.mkdir(parents=True)
constants.CACHE_PATH = runtime_root.joinpath("cache")
constants.CACHE_PATH.mkdir(parents=True, exist_ok=True)
# create run root path
runs_path = runtime_root.joinpath("runs")
logic_path = create_run_path(runs_path)
local_path = runs_path.joinpath(logic_path)
local_path.mkdir(parents=True)
constants.RUN_ID = logic_path.name
constants.RUN_LOCAL_PATH = local_path
constants.RUN_LOGIC_PATH = logic_path
constants.RUN_ID = logic_path.name
constants.RUN_LOCAL_PATH = local_path
constants.RUN_LOGIC_PATH = logic_path
args = parse_args()
args = parse_args()
set_log_file(f"{local_path}/lisa-host.log")
set_log_file(f"{local_path}/lisa-host.log")
log = get_logger()
log.info(f"Python version: {sys.version}")
log.info(f"local time: {datetime.now().astimezone()}")
log.info(f"command line args: {sys.argv}")
log.info(f"run local path: {runtime_root}")
log = get_logger()
log.info(f"Python version: {sys.version}")
log.info(f"local time: {datetime.now().astimezone()}")
log.info(f"command line args: {sys.argv}")
log.info(f"run local path: {runtime_root}")
if args.debug:
log_level = DEBUG
else:
log_level = INFO
set_level(log_level)
if args.debug:
log_level = DEBUG
else:
log_level = INFO
set_level(log_level)
args.func(args)
args.func(args)
finally:
log.info(f"finished in {total_timer}")
if __name__ == "__main__":

Просмотреть файл

@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Iterable, List, Optional, TypeVar, Union, cast
from lisa import schema
from lisa.executable import Tools
from lisa.operating_system import Linux, OperatingSystem, Windows
from lisa.operating_system import OperatingSystem
from lisa.tools import Echo
from lisa.util import (
ContextMixin,
@ -30,6 +30,7 @@ class Node(ContextMixin, InitializableMixin):
capability: schema.NodeSpace,
is_remote: bool = True,
is_default: bool = False,
logger_name: str = "node",
) -> None:
super().__init__()
self.is_default = is_default
@ -44,7 +45,7 @@ class Node(ContextMixin, InitializableMixin):
self.working_path: pathlib.PurePath = pathlib.PurePath()
self._connection_info: Optional[ConnectionInfo] = None
self.log = get_logger("node", str(self.index))
self.log = get_logger(logger_name, str(self.index))
@staticmethod
def create(
@ -52,6 +53,7 @@ class Node(ContextMixin, InitializableMixin):
capability: schema.NodeSpace,
node_type: str = constants.ENVIRONMENTS_NODES_REMOTE,
is_default: bool = False,
logger_name: str = "node",
) -> Node:
if node_type == constants.ENVIRONMENTS_NODES_REMOTE:
is_remote = True
@ -60,7 +62,11 @@ class Node(ContextMixin, InitializableMixin):
else:
raise LisaException(f"unsupported node_type '{node_type}'")
node = Node(
index, capability=capability, is_remote=is_remote, is_default=is_default
index,
capability=capability,
is_remote=is_remote,
is_default=is_default,
logger_name=logger_name,
)
node.log.debug(f"created, type: '{node_type}', isDefault: {is_default}")
return node
@ -92,7 +98,7 @@ class Node(ContextMixin, InitializableMixin):
cmd: str,
shell: bool = False,
no_error_log: bool = False,
no_info_log: bool = False,
no_info_log: bool = True,
cwd: Optional[pathlib.PurePath] = None,
) -> ExecutableResult:
process = self.execute_async(
@ -109,7 +115,7 @@ class Node(ContextMixin, InitializableMixin):
cmd: str,
shell: bool = False,
no_error_log: bool = False,
no_info_log: bool = False,
no_info_log: bool = True,
cwd: Optional[pathlib.PurePath] = None,
) -> Process:
self.initialize()
@ -132,10 +138,7 @@ class Node(ContextMixin, InitializableMixin):
def _initialize(self) -> None:
self.log.debug(f"initializing node {self.name}")
self.shell.initialize()
if self.shell.is_linux:
self.os: OperatingSystem = Linux(self)
else:
self.os = Windows(self)
self.os: OperatingSystem = OperatingSystem.create(self)
# set working path
if self.is_remote:
@ -175,9 +178,7 @@ class Node(ContextMixin, InitializableMixin):
cwd: Optional[pathlib.PurePath] = None,
) -> Process:
cmd_id = str(random.randint(0, 10000))
process = Process(
cmd_id, self.shell, parent_logger=self.log, is_linux=self.is_linux
)
process = Process(cmd_id, self.shell, parent_logger=self.log)
process.start(
cmd,
shell=shell,
@ -257,7 +258,9 @@ class Nodes(NodesDict):
for node in self._list:
node.close()
def from_local(self, node_runbook: schema.LocalNode) -> Node:
def from_local(
self, node_runbook: schema.LocalNode, logger_name: str = "node",
) -> Node:
assert isinstance(
node_runbook, schema.LocalNode
), f"actual: {type(node_runbook)}"
@ -266,12 +269,15 @@ class Nodes(NodesDict):
capability=node_runbook.capability,
node_type=node_runbook.type,
is_default=node_runbook.is_default,
logger_name=logger_name,
)
self._list.append(node)
return node
def from_remote(self, node_runbook: schema.RemoteNode) -> Optional[Node]:
def from_remote(
self, node_runbook: schema.RemoteNode, logger_name: str = "node",
) -> Optional[Node]:
assert isinstance(
node_runbook, schema.RemoteNode
), f"actual: {type(node_runbook)}"
@ -281,6 +287,7 @@ class Nodes(NodesDict):
capability=node_runbook.capability,
node_type=node_runbook.type,
is_default=node_runbook.is_default,
logger_name=logger_name,
)
self._list.append(node)

Просмотреть файл

@ -1,14 +1,54 @@
from typing import TYPE_CHECKING, Any
import re
from functools import partial
from typing import TYPE_CHECKING, Any, List, Optional, Type, Union
from lisa.executable import Tool
from lisa.util import LisaException
from lisa.util.logger import get_logger
if TYPE_CHECKING:
from lisa.node import Node
_get_init_logger = partial(get_logger, name="os")
class OperatingSystem:
__lsb_release_pattern = re.compile(r"^Description:[ \t]+([\w]+)[ ]+")
__os_release_pattern = re.compile(r"^NAME=\"?([\w]+)[^\" ]*\"?", re.M)
def __init__(self, node: Any, is_linux: bool) -> None:
super().__init__()
self._node: Node = node
self._is_linux = is_linux
self._log = get_logger(name="os", parent=self._node.log)
@classmethod
def create(cls, node: Any) -> Any:
typed_node: Node = node
log = _get_init_logger(parent=typed_node.log)
result: Optional[OperatingSystem] = None
if typed_node.shell.is_linux:
lsb_output = typed_node.execute("lsb_release -d")
if lsb_output.stdout:
os_info = cls.__lsb_release_pattern.findall(lsb_output.stdout)
if os_info and os_info[0] == "Ubuntu":
result = Ubuntu(typed_node)
if not result:
os_release_output = typed_node.execute("cat /etc/os-release")
if os_release_output.stdout:
os_info = cls.__os_release_pattern.findall(os_release_output.stdout)
if os_info and os_info[0] == "CentOS":
result = CentOs(typed_node)
if not result:
raise LisaException(
f"unknown linux distro {lsb_output.stdout}\n"
f" {os_release_output.stdout}\n"
f"support it in operating_system"
)
else:
result = Windows(typed_node)
log.debug(f"detected OS: {result.__class__.__name__}")
return result
@property
def is_windows(self) -> bool:
@ -27,3 +67,59 @@ class Windows(OperatingSystem):
class Linux(OperatingSystem):
def __init__(self, node: Any) -> None:
super().__init__(node, is_linux=True)
def _install_packages(self, packages: Union[List[str]]) -> None:
raise NotImplementedError()
def install_packages(
self, packages: Union[str, Tool, Type[Tool], List[Union[str, Tool, Type[Tool]]]]
) -> None:
package_names: List[str] = []
if not isinstance(packages, list):
packages = [packages]
assert isinstance(packages, list), f"actual:{type(packages)}"
for item in packages:
if isinstance(item, str):
package_names.append(item)
elif isinstance(item, Tool):
package_names.append(item.package_name)
else:
assert isinstance(item, type), f"actual:{type(item)}"
# Create a temp object, it doesn't trigger install.
# So they can be installed together.
tool = item.create(self._node)
package_names.append(tool.package_name)
self._install_packages(package_names)
class Ubuntu(Linux):
def __init__(self, node: Any) -> None:
super().__init__(node)
self._first_time: bool = True
def _install_packages(self, packages: Union[List[str]]) -> None:
if self._first_time:
self._first_time = False
self._node.execute("sudo apt-get update")
command = (
f"sudo DEBIAN_FRONTEND=noninteractive "
f"apt-get -y install {' '.join(packages)}"
)
self._node.execute(command)
class CentOs(Linux):
def __init__(self, node: Any) -> None:
super().__init__(node)
self._first_time: bool = True
def _install_packages(self, packages: Union[List[str]]) -> None:
if self._first_time:
self._first_time = False
self._node.execute("sudo yum update")
self._node.execute(
f"sudo DEBIAN_FRONTEND=noninteractive yum install -y {' '.join(packages)}"
)

Просмотреть файл

@ -171,9 +171,9 @@ class LISARunner(Action):
result_count_dict[result.status] = result_count
self._log.info("result summary")
self._log.info(f" TOTAL\t: {len(selected_case_results)}")
self._log.info(f" TOTAL : {len(selected_case_results)}")
for key in TestStatus:
self._log.info(f" {key.name}\t: {result_count_dict.get(key, 0)}")
self._log.info(f" {key.name:<9}: {result_count_dict.get(key, 0)}")
# delete enviroment after run
self.set_status(ActionStatus.SUCCESS)

Просмотреть файл

@ -220,13 +220,14 @@ class TestCaseRuntimeData:
return cloned
class TestSuite(Action, unittest.TestCase, metaclass=ABCMeta):
class TestSuite(unittest.TestCase, Action, metaclass=ABCMeta):
def __init__(
self,
environment: Environment,
case_results: List[TestResult],
metadata: TestSuiteMetadata,
) -> None:
super().__init__()
self.environment = environment
# test cases to run, must be a test method in this class.
self.case_results = case_results

Просмотреть файл

@ -1,7 +1,9 @@
from .echo import Echo
from .gcc import Gcc
from .git import Git
from .lscpu import Lscpu
from .make import Make
from .ntttcp import Ntttcp
from .uname import Uname
__all__ = ["Echo", "Git", "Lscpu", "Ntttcp", "Uname"]
__all__ = ["Echo", "Gcc", "Git", "Lscpu", "Make", "Ntttcp", "Uname"]

19
lisa/tools/gcc.py Normal file
Просмотреть файл

@ -0,0 +1,19 @@
from typing import cast
from lisa.executable import Tool
from lisa.operating_system import Linux
class Gcc(Tool):
@property
def command(self) -> str:
return "gcc"
@property
def can_install(self) -> bool:
return True
def _install(self) -> bool:
linux_os: Linux = cast(Linux, self.node.os)
linux_os.install_packages("gcc")
return self._check_exists()

Просмотреть файл

@ -1,6 +1,8 @@
import pathlib
from typing import cast
from lisa.executable import Tool
from lisa.operating_system import Linux
class Git(Tool):
@ -10,8 +12,12 @@ class Git(Tool):
@property
def can_install(self) -> bool:
# TODO support installation later
return False
return True
def _install(self) -> bool:
linux_os: Linux = cast(Linux, self.node.os)
linux_os.install_packages([self])
return self._check_exists()
def clone(self, url: str, cwd: pathlib.PurePath) -> None:
# git print to stderr for normal info, so set no_error_log to True.

26
lisa/tools/make.py Normal file
Просмотреть файл

@ -0,0 +1,26 @@
from pathlib import PurePath
from typing import cast
from lisa.executable import Tool
from lisa.operating_system import Linux
from lisa.tools import Gcc
class Make(Tool):
repo = "https://github.com/microsoft/ntttcp-for-linux"
@property
def command(self) -> str:
return "make"
@property
def can_install(self) -> bool:
return True
def _install(self) -> bool:
linux_os: Linux = cast(Linux, self.node.os)
linux_os.install_packages([self, Gcc])
return self._check_exists()
def make_and_install(self, cwd: PurePath) -> None:
self.run("&& sudo make install", shell=True, cwd=cwd)

Просмотреть файл

@ -1,7 +1,7 @@
from typing import List, Type
from lisa.executable import Tool
from lisa.tools import Git
from lisa.tools import Git, Make
from lisa.util.process import ExecutableResult
@ -10,7 +10,7 @@ class Ntttcp(Tool):
@property
def dependencies(self) -> List[Type[Tool]]:
return [Git]
return [Git, Make]
@property
def command(self) -> str:
@ -25,8 +25,9 @@ class Ntttcp(Tool):
self.node.shell.mkdir(tool_path, exist_ok=True)
git = self.node.tools[Git]
git.clone(self.repo, tool_path)
make = self.node.tools[Make]
code_path = tool_path.joinpath("ntttcp-for-linux/src")
self.node.execute("make && sudo make install", shell=True, cwd=code_path)
make.make_and_install(cwd=code_path)
return self._check_exists()
def help(self) -> ExecutableResult:

Просмотреть файл

@ -95,7 +95,7 @@ class LogWriter(object):
def flush(self) -> None:
if len(self._buffer) > 0:
self._log.log(self._level, self._buffer.strip("\r\n"))
self._log.lines(self._level, self._buffer.strip("\r\n"))
self._buffer = ""
def close(self) -> None:

Просмотреть файл

@ -18,8 +18,11 @@ class Timer:
self._elapsed = timer() - self.start
return self._elapsed
def elapsed_text(self, stop: bool = True) -> str:
return f"{self.elapsed(stop):.3f} sec"
def __str__(self) -> str:
return f"{self.elapsed():.3f} sec"
return f"{self.elapsed_text()}"
def create_timer() -> Timer:

Просмотреть файл

@ -30,16 +30,12 @@ class ExecutableResult:
class Process:
def __init__(
self,
id_: str,
shell: Shell,
parent_logger: Optional[Logger] = None,
is_linux: bool = True,
self, id_: str, shell: Shell, parent_logger: Optional[Logger] = None,
) -> None:
# the shell can be LocalShell or SshShell
self._shell = shell
self._id_ = id_
self._is_linux = is_linux
self._is_linux = shell.is_linux
self._running: bool = False
self._log = get_logger("cmd", id_, parent=parent_logger)

Просмотреть файл

@ -121,6 +121,7 @@ class SshShell(InitializableMixin):
except Exception as identifier:
raise LisaException(f"connect to server failed: {identifier}")
_, stdout, _ = paramiko_client.exec_command("cmd")
paramiko_client.close()
spur_kwargs = {
"hostname": self._connection_info.address,