Merge pull request #959 from LIS/chisong/v3_8010

implement tool installation
This commit is contained in:
Andy Schwartzmeyer 2020-08-10 15:54:18 -07:00 коммит произвёл GitHub
Родитель 191e2af054 3b8ff9b06d
Коммит 56718c9a77
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
29 изменённых файлов: 661 добавлений и 394 удалений

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

@ -1,7 +1,6 @@
from lisa import CaseMetadata, SuiteMetadata
from lisa.core.testSuite import TestSuite
from lisa.executable import Uname
from lisa.executable.echo import Echo
from lisa.tool import Echo, Uname
from lisa.util.logger import log
@ -34,9 +33,10 @@ class HelloWorld(TestSuite):
if node.isLinux:
uname = node.getTool(Uname)
release, version, hardware = uname.getLinuxInformation()
release, version, hardware, os = uname.getLinuxInformation()
log.info(
f"release: '{release}', version: '{version}', hardware: '{hardware}'"
f"release: '{release}', version: '{version}', "
f"hardware: '{hardware}', os: '{os}'"
)
log.info("It's Linux, try on Windows!")
else:

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

@ -0,0 +1,29 @@
from lisa import CaseMetadata, SuiteMetadata
from lisa.core.testSuite import TestSuite
from lisa.tool import Ntttcp
@SuiteMetadata(
area="demo",
category="simple",
description="""
This test suite run a script
""",
tags=["demo"],
)
class WithScript(TestSuite):
@property
def skipRun(self) -> bool:
node = self.environment.defaultNode
return not node.isLinux
@CaseMetadata(
description="""
this test case run script on test node.
""",
priority=1,
)
def script(self) -> None:
node = self.environment.defaultNode
ntttcp = node.getTool(Ntttcp)
ntttcp.help()

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

@ -19,6 +19,6 @@ class CaseMetadata:
start = timer()
func(*args)
end = timer()
log.info(f"case '{func.__name__}' ended with {end - start:.3f}")
log.info(f"case '{func.__name__}' ended with {end - start:.3f} sec")
return wrapper

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

@ -31,8 +31,11 @@ class SuiteMetadata:
)
def wrapper(
test_class: Type[TestSuite], environment: Environment, cases: List[str]
test_class: Type[TestSuite],
environment: Environment,
cases: List[str],
metadata: SuiteMetadata,
) -> TestSuite:
return test_class(environment, cases)
return test_class(environment, cases, metadata)
return wrapper

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

@ -35,7 +35,8 @@ class Environment(object):
List[Dict[str, object]], spec.get(constants.ENVIRONMENTS_NODES)
)
for node_config in nodes_config:
node = NodeFactory.createNodeFromConfig(node_config)
index = str(len(environment.nodes))
node = NodeFactory.createNodeFromConfig(index, node_config)
if node is not None:
environment.nodes.append(node)
else:

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

@ -1,46 +0,0 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING
from lisa.util.executableResult import ExecutableResult
if TYPE_CHECKING:
from lisa.core.node import Node
class Executable(ABC):
def __init__(self, node: Node) -> None:
self.node: Node = node
self.initialize()
def initialize(self) -> None:
pass
@property
@abstractmethod
def command(self) -> str:
raise NotImplementedError()
@abstractmethod
def canInstall(self) -> bool:
raise NotImplementedError()
@abstractmethod
def installed(self) -> bool:
raise NotImplementedError()
def install(self) -> None:
pass
def run(
self, extraParameters: str = "", noErrorLog: bool = False
) -> ExecutableResult:
command = f"{self.command} {extraParameters}"
result: ExecutableResult = self.node.execute(command, noErrorLog)
return result
class ExecutableException(Exception):
def __init__(self, exe: Executable, message: str):
self.message = f"{exe.command}: {message}"

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

@ -1,34 +1,37 @@
from __future__ import annotations
import pathlib
import random
from timeit import default_timer as timer
from typing import Dict, Optional, Type, TypeVar, cast
from lisa.core.executable import Executable
from lisa.core.sshConnection import SshConnection
from lisa.executable import Echo, Uname
from lisa.util import constants
from lisa.core.tool import Tool
from lisa.tool import Echo, Uname
from lisa.util import constants, env
from lisa.util.connectionInfo import ConnectionInfo
from lisa.util.executableResult import ExecutableResult
from lisa.util.logger import log
from lisa.util.process import Process
from lisa.util.shell import Shell
T = TypeVar("T")
class Node:
builtinTools = [Uname, Echo]
def __init__(
self,
id: str,
isRemote: bool = True,
spec: Optional[Dict[str, object]] = None,
isDefault: bool = False,
) -> None:
self.name: Optional[str] = None
self.id = id
self.name: str = ""
self.isDefault = isDefault
self.isRemote = isRemote
self.spec = spec
self.connection: Optional[SshConnection] = None
self.connection_info: Optional[ConnectionInfo] = None
self.workingPath: pathlib.Path = ""
self.shell = Shell()
self._isInitialized: bool = False
self._isLinux: bool = True
@ -36,13 +39,13 @@ class Node:
self.kernelRelease: str = ""
self.kernelVersion: str = ""
self.hardwarePlatform: str = ""
self.os: str = ""
self.tools: Dict[Type[Executable], Executable] = dict()
for tool_class in self.builtinTools:
self.tools[tool_class] = tool_class(self)
self.tools: Dict[Type[Tool], Tool] = dict()
@staticmethod
def createNode(
id: str,
spec: Optional[Dict[str, object]] = None,
node_type: str = constants.ENVIRONMENTS_NODES_REMOTE,
isDefault: bool = False,
@ -53,28 +56,106 @@ class Node:
isRemote = False
else:
raise Exception(f"unsupported node_type '{node_type}'")
node = Node(spec=spec, isRemote=isRemote, isDefault=isDefault)
node = Node(id, spec=spec, isRemote=isRemote, isDefault=isDefault)
log.debug(
f"created node '{node_type}', isDefault: {isDefault}, isRemote: {isRemote}"
)
return node
def setConnectionInfo(self, **kwargs: str) -> None:
if self.connection is not None:
def setConnectionInfo(
self,
address: str = "",
port: int = 22,
publicAddress: str = "",
publicPort: int = 22,
username: str = "root",
password: str = "",
privateKeyFile: str = "",
) -> None:
if self.connection_info is not None:
raise Exception(
"node is set connection information already, cannot set again"
)
self.connection = SshConnection(**kwargs)
if not address and not publicAddress:
raise Exception("at least one of address and publicAddress need to be set")
elif not address:
address = publicAddress
elif not publicAddress:
publicAddress = address
if not port and not publicPort:
raise Exception("at least one of port and publicPort need to be set")
elif not port:
port = publicPort
elif not publicPort:
publicPort = port
self.connection_info = ConnectionInfo(
publicAddress, publicPort, username, password, privateKeyFile,
)
self.shell.setConnectionInfo(self.connection_info)
self.internalAddress = address
self.internalPort = port
def getToolPath(self, tool: Optional[Tool] = None) -> pathlib.Path:
assert self.workingPath
if tool:
tool_name = tool.__class__.__name__.lower()
tool_path = self.workingPath.joinpath(constants.PATH_TOOL, tool_name)
else:
tool_path = self.workingPath.joinpath(constants.PATH_TOOL)
return tool_path
def getTool(self, tool_type: Type[T]) -> T:
tool = cast(T, self.tools.get(tool_type))
if tool is None:
raise Exception(f"Tool {tool_type.__name__} is not found on node")
tool_prefix = f"tool '{tool_type.__name__}'"
log.debug(f"{tool_prefix} is initializing")
# the Tool is not installed on current node, try to install it.
tool = cast(Tool, tool_type(self))
if not tool.isInstalled:
log.debug(f"{tool_prefix} is not installed")
if tool.canInstall:
log.debug(f"{tool_prefix} installing")
success = tool.install()
if not success:
raise Exception(f"{tool_prefix} install failed")
else:
raise Exception(
f"{tool_prefix} doesn't support install on Node({self.id}), "
f"Linux({self.isLinux}), Remote({self.isRemote})"
)
else:
log.debug(f"{tool_prefix} is installed already")
return tool
def execute(self, cmd: str, noErrorLog: bool = False) -> ExecutableResult:
def execute(
self,
cmd: str,
useBash: bool = False,
noErrorLog: bool = False,
noInfoLog: bool = False,
cwd: pathlib.Path = None,
) -> ExecutableResult:
self._initialize()
return self._execute(cmd, noErrorLog)
process = self._execute(
cmd, useBash=useBash, noErrorLog=noErrorLog, noInfoLog=noInfoLog, cwd=cwd
)
return process.waitResult()
def executeAsync(
self,
cmd: str,
useBash: bool = False,
noErrorLog: bool = False,
noInfoLog: bool = False,
cwd: pathlib.Path = None,
) -> Process:
self._initialize()
return self._execute(
cmd, useBash=useBash, noErrorLog=noErrorLog, noInfoLog=noInfoLog, cwd=cwd
)
@property
def isLinux(self) -> bool:
@ -83,17 +164,18 @@ class Node:
def _initialize(self) -> None:
if not self._isInitialized:
# prevent loop calls, put it at top
# prevent loop calls, set _isInitialized to True first
self._isInitialized = True
log.debug(f"initializing node {self.name}")
self.shell.initialize()
uname = self.getTool(Uname)
(
self.kernelRelease,
self.kernelVersion,
self.hardwarePlatform,
self.os,
) = uname.getLinuxInformation(noErrorLog=True)
if not self.kernelRelease:
if (not self.kernelRelease) or ("Linux" not in self.os):
self._isLinux = False
if self._isLinux:
log.info(
@ -105,25 +187,45 @@ class Node:
else:
log.info(f"initialized Windows node '{self.name}', ")
def _execute(self, cmd: str, noErrorLog: bool = False) -> ExecutableResult:
cmd_id = str(random.randint(0, 10000))
start_timer = timer()
log.debug(f"cmd[{cmd_id}] remote({self.isRemote}) {cmd}")
if self.isRemote:
# remote
if self.connection is None:
raise Exception(f"cmd[{cmd_id}] remote node has no connection info")
result: ExecutableResult = self.connection.execute(cmd, cmd_id=cmd_id)
else:
# local
process = Process()
with process:
process.start(cmd, cmd_id=cmd_id, noErrorLog=noErrorLog)
result = process.waitResult()
end_timer = timer()
log.info(f"cmd[{cmd_id}] executed with {end_timer - start_timer:.3f} sec")
return result
# set working path
if self.isRemote:
assert self.shell
assert self.connection_info
if self.isLinux:
remote_root_path = pathlib.Path("$HOME")
else:
remote_root_path = pathlib.Path("%TEMP%")
working_path = remote_root_path.joinpath(
constants.PATH_REMOTE_ROOT, env.get_run_path()
).as_posix()
# expand environment variables in path
echo = self.getTool(Echo)
result = echo.run(working_path, useBash=True)
# PurePath is more reasonable here, but spurplus doesn't support it.
self.workingPath = pathlib.Path(result.stdout)
else:
self.workingPath = pathlib.Path(env.get_run_local_path())
log.debug(f"working path is: {self.workingPath.as_posix()}")
self.shell.mkdir(self.workingPath, parents=True, exist_ok=True)
def _execute(
self,
cmd: str,
useBash: bool = False,
noErrorLog: bool = False,
noInfoLog: bool = False,
cwd: pathlib.Path = None,
) -> Process:
cmd_prefix = f"cmd[{str(random.randint(0, 10000))}]"
log.debug(f"{cmd_prefix}remote({self.isRemote}) '{cmd}'")
process = Process(cmd_prefix, self.shell, self.isLinux)
process.start(
cmd, useBash=useBash, noErrorLog=noErrorLog, noInfoLog=noInfoLog, cwd=cwd
)
return process
def close(self) -> None:
if self.connection is not None:
self.connection.close()
self.shell.close()

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

@ -7,7 +7,7 @@ from .node import Node
class NodeFactory:
@staticmethod
def createNodeFromConfig(config: Dict[str, object]) -> Optional[Node]:
def createNodeFromConfig(id: str, config: Dict[str, object]) -> Optional[Node]:
node_type = config.get(constants.TYPE)
node = None
if node_type is None:
@ -16,8 +16,8 @@ class NodeFactory:
constants.ENVIRONMENTS_NODES_LOCAL,
constants.ENVIRONMENTS_NODES_REMOTE,
]:
is_default = NodeFactory._isDefault(config)
node = Node.createNode(node_type=node_type, isDefault=is_default)
is_default = cast(bool, config.get(constants.IS_DEFAULT, False))
node = Node.createNode(id, node_type=node_type, isDefault=is_default)
if node.isRemote:
fields = [
constants.ENVIRONMENTS_NODES_REMOTE_ADDRESS,
@ -39,13 +39,8 @@ class NodeFactory:
def createNodeFromSpec(
spec: Dict[str, object], node_type: str = constants.ENVIRONMENTS_NODES_REMOTE
) -> Node:
is_default = NodeFactory._isDefault(spec)
node = Node.createNode(spec=spec, node_type=node_type, isDefault=is_default)
is_default = cast(bool, spec.get(constants.IS_DEFAULT, False))
node = Node.createNode(
"spec", spec=spec, node_type=node_type, isDefault=is_default
)
return node
@staticmethod
def _isDefault(config: Dict[str, object]) -> bool:
default = cast(bool, config.get(constants.IS_DEFAULT))
if default is not True:
default = False
return default

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

@ -1,119 +0,0 @@
import logging
from typing import Optional
import paramiko # type: ignore
from lisa.util.connectionInfo import ConnectionInfo
from lisa.util.executableResult import ExecutableResult
from lisa.util.logger import log_lines
class SshConnection:
def __init__(
self,
address: str = "",
port: int = 22,
publicAddress: str = "",
publicPort: int = 22,
username: str = "root",
password: str = "",
privateKeyFile: str = "",
) -> None:
self.address = address
self.port = port
self.publicAddress = publicAddress
self.publicPort = publicPort
self.username = username
self.password = password
self.privateKeyFile = privateKeyFile
if not self.address and not self.publicAddress:
raise Exception("at least one of address and publicAddress need to be set")
elif not self.address:
self.address = self.publicAddress
elif not self.publicAddress:
self.publicAddress = self.address
if not self.port and not self.publicPort:
raise Exception("at least one of port and publicPort need to be set")
elif not self.port:
self.port = self.publicPort
elif not self.publicPort:
self.publicPort = self.port
self._connectionInfo = ConnectionInfo(
self.address, self.port, self.username, self.password, self.privateKeyFile
)
self._publicConnectionInfo = ConnectionInfo(
self.publicAddress,
self.publicPort,
self.username,
self.password,
self.privateKeyFile,
)
self._connection: Optional[paramiko.SSHClient] = None
self._publicConnection: Optional[paramiko.SSHClient] = None
self._isConnected: bool = False
self._isPublicConnected: bool = False
@property
def connectionInfo(self) -> ConnectionInfo:
return self._connectionInfo
@property
def publicConnectionInfo(self) -> ConnectionInfo:
return self._publicConnectionInfo
def execute(
self, cmd: str, noErrorLog: bool = False, cmd_id: str = ""
) -> ExecutableResult:
client = self.connect()
_, stdout_file, stderr_file = client.exec_command(cmd)
exit_code: int = stdout_file.channel.recv_exit_status()
stdout: str = stdout_file.read().decode("utf-8").strip()
log_lines(logging.INFO, stdout, prefix=f"cmd[{cmd_id}]stdout: ")
stderr: str = stderr_file.read().decode("utf-8").strip()
if noErrorLog:
log_level = logging.INFO
else:
log_level = logging.ERROR
# fix, cannot print them together
log_lines(log_level, stderr, prefix=f"cmd[{cmd_id}]stderr: ")
result = ExecutableResult(stdout, stderr, exit_code)
return result
def connect(self, isPublic: bool = True) -> paramiko.SSHClient:
if isPublic:
connection = self._publicConnection
connectionInfo = self.publicConnectionInfo
else:
connection = self._connection
connectionInfo = self.connectionInfo
if connection is None:
connection = paramiko.SSHClient()
connection.set_missing_host_key_policy(paramiko.client.AutoAddPolicy)
connection.connect(
connectionInfo.address,
port=connectionInfo.port,
username=connectionInfo.username,
password=connectionInfo.password,
key_filename=connectionInfo.privateKeyFile,
look_for_keys=False,
)
if isPublic:
self._publicConnection = connection
else:
self._connection = connection
return connection
def close(self) -> None:
if self._connection is not None:
self._connection.close()
self._connection = None
if self._publicConnection is not None:
self._publicConnection.close()
self._publicConnection = None

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

@ -8,15 +8,24 @@ from lisa.core.actionStatus import ActionStatus
from lisa.util.logger import log
if TYPE_CHECKING:
from lisa import SuiteMetadata
from .environment import Environment
class TestSuite(Action, metaclass=ABCMeta):
def __init__(self, environment: Environment, cases: List[str]) -> None:
def __init__(
self, environment: Environment, cases: List[str], suiteMetadata: SuiteMetadata
) -> None:
self.environment = environment
self.cases = cases
self.suiteMetadata = suiteMetadata
self.shouldStop = False
@property
def skipRun(self) -> bool:
return False
def beforeSuite(self) -> None:
pass
@ -33,6 +42,9 @@ class TestSuite(Action, metaclass=ABCMeta):
return "TestSuite"
async def start(self) -> None:
if self.skipRun:
log.info(f"suite[{self.suiteMetadata.name}] skipped on this run")
return
self.beforeSuite()
for test_case in self.cases:
self.beforeCase()

96
lisa/core/tool.py Normal file
Просмотреть файл

@ -0,0 +1,96 @@
from __future__ import annotations
import pathlib
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, List, Optional, TypeVar
from lisa.util.executableResult import ExecutableResult
from lisa.util.process import Process
if TYPE_CHECKING:
from lisa.core.node import Node
T = TypeVar("T")
class Tool(ABC):
def __init__(self, node: Node) -> None:
self.node: Node = node
self._isInstalled: Optional[bool] = None
self.initialize()
def initialize(self) -> None:
pass
@property
def dependentedTools(self) -> List[T]:
"""
declare all dependencies here
they can be batch check and installed.
"""
return []
@property
@abstractmethod
def command(self) -> str:
raise NotImplementedError()
@property
@abstractmethod
def canInstall(self) -> bool:
raise NotImplementedError()
@property
def isInstalledInternal(self) -> bool:
result = self.node.execute(f'bash -c "command -v {self.command}"')
self._isInstalled = result.exitCode == 0
return self._isInstalled
@property
def isInstalled(self) -> bool:
# the check may need extra cost, so cache it's result.
if self._isInstalled is None:
self._isInstalled = self.isInstalledInternal
return self._isInstalled
def installInternal(self) -> bool:
raise NotImplementedError()
def install(self) -> bool:
# check dependencies
for dependency in self.dependentedTools:
self.node.getTool(dependency)
result = self.installInternal()
return result
def runAsync(
self,
extraParameters: str = "",
useBash: bool = False,
noErrorLog: bool = False,
cwd: pathlib.Path = None,
) -> Process:
command = f"{self.command} {extraParameters}"
result: ExecutableResult = self.node.execute(
command, useBash, noErrorLog=noErrorLog, cwd=cwd
)
return result
def run(
self,
extraParameters: str = "",
useBash: bool = False,
noErrorLog: bool = False,
noInfoLog: bool = False,
cwd: pathlib.Path = None,
) -> ExecutableResult:
command = f"{self.command} {extraParameters}"
result: ExecutableResult = self.node.execute(
command, useBash, noErrorLog=noErrorLog, noInfoLog=noInfoLog, cwd=cwd
)
return result
class ExecutableException(Exception):
def __init__(self, exe: Tool, message: str):
self.message = f"{exe.command}: {message}"

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

@ -1,5 +0,0 @@
from .echo import Echo
from .ping import Ping
from .uname import Uname
__all__ = ["Uname", "Ping", "Echo"]

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

@ -1,13 +0,0 @@
from lisa.core.executable import Executable
class Echo(Executable):
@property
def command(self) -> str:
return "echo"
def canInstall(self) -> bool:
return False
def installed(self) -> bool:
return True

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

@ -1,5 +0,0 @@
from lisa.core.executable import Executable
class Ping(Executable):
pass

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

@ -1,4 +1,3 @@
import os
import sys
from datetime import datetime
from logging import DEBUG, INFO
@ -12,21 +11,24 @@ from lisa.util.logger import init_log, log
@retry(FileExistsError, tries=10, delay=0) # type: ignore
def create_result_path() -> Path:
def create_run_path(root_path: Path) -> Path:
date = datetime.utcnow().strftime("%Y%m%d")
time = datetime.utcnow().strftime("%H%M%S-%f")[:-3]
current_path = f"runtime/results/{date}/{date}-{time}"
path_obj = Path(current_path)
if path_obj.exists():
raise FileExistsError(f"{current_path} exists, and not found an unique path.")
return path_obj
run_path = Path(f"{date}/{date}-{time}")
local_path = root_path.joinpath(run_path)
if local_path.exists():
raise FileExistsError(f"{local_path} exists, and not found an unique path.")
return run_path
def main() -> None:
# create result path
result_path = create_result_path().absolute()
result_path.mkdir(parents=True)
env.set_env(env.RESULT_PATH, str(result_path))
local_path = Path("runtime").joinpath("runs").absolute()
# create run root path
run_path = create_run_path(local_path)
local_path = local_path.joinpath(run_path)
local_path.mkdir(parents=True)
env.set_env(env.KEY_RUN_LOCAL_PATH, str(local_path))
env.set_env(env.KEY_RUN_PATH, str(run_path))
args = parse_args()
@ -34,7 +36,7 @@ def main() -> None:
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"result path: {env.get_env(env.RESULT_PATH)}")
log.info(f"run local path: {env.get_run_local_path()}")
if args.debug:
log_level = DEBUG
@ -53,5 +55,4 @@ if __name__ == "__main__":
log.exception(exception)
exitCode = -1
finally:
# force all threads end.
os._exit(exitCode)
sys.exit(exitCode)

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

@ -37,7 +37,7 @@ class LISARunner(TestRunner):
for suite in suites.values():
test_object: TestSuite = suite.test_class(
environment, list(suite.cases.keys())
environment, list(suite.cases.keys()), suite
)
await test_object.start()

6
lisa/tool/__init__.py Normal file
Просмотреть файл

@ -0,0 +1,6 @@
from .echo import Echo
from .git import Git
from .ntttcp import Ntttcp
from .uname import Uname
__all__ = ["Uname", "Ntttcp", "Echo", "Git"]

18
lisa/tool/echo.py Normal file
Просмотреть файл

@ -0,0 +1,18 @@
from lisa.core.tool import Tool
class Echo(Tool):
@property
def command(self) -> str:
command = "echo"
if not self.node.isLinux:
command = "cmd /c echo"
return command
@property
def canInstall(self) -> bool:
return False
@property
def isInstalledInternal(self) -> bool:
return True

15
lisa/tool/git.py Normal file
Просмотреть файл

@ -0,0 +1,15 @@
from lisa.core.tool import Tool
class Git(Tool):
@property
def command(self) -> str:
return "git"
@property
def canInstall(self) -> bool:
# TODO support installation later
return False
def clone(self, url: str, cwd: str) -> None:
self.run(f"clone {url}", cwd=cwd)

40
lisa/tool/ntttcp.py Normal file
Просмотреть файл

@ -0,0 +1,40 @@
from typing import List, TypeVar
from lisa.core.tool import Tool
from lisa.tool import Git
from lisa.util.executableResult import ExecutableResult
T = TypeVar("T")
class Ntttcp(Tool):
repo = "https://github.com/microsoft/ntttcp-for-linux"
@property
def dependentedTools(self) -> List[T]:
return [Git]
@property
def command(self) -> str:
return "ntttcp"
@property
def canInstall(self) -> bool:
can_install = False
if self.node.isLinux:
can_install = True
return can_install
def install(self) -> bool:
tool_path = self.node.getToolPath(self)
self.node.shell.mkdir(tool_path)
git = self.node.getTool(Git)
git.clone(self.repo, tool_path)
code_path = tool_path.joinpath("ntttcp-for-linux/src")
self.node.execute(
f"cd {code_path.as_posix()} && make && sudo make install", useBash=True
)
return self.isInstalledInternal
def help(self) -> ExecutableResult:
return self.run("-h")

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

@ -1,13 +1,14 @@
import re
from typing import Tuple
from lisa.core.executable import Executable
from lisa.core.tool import Tool
class Uname(Executable):
class Uname(Tool):
def initialize(self) -> None:
self.key_info_pattern = re.compile(
r"(?P<release>[^ ]*?) (?P<version>[\w\W]*) (?P<platform>[\w_]+?)$"
r"(?P<release>[^ ]*?) (?P<version>[\w\W]*) (?P<platform>[\w\W]+?) "
r"(?P<os>[\w\W]+?)$"
)
# uname's result suppose not be changed frequently,
# so cache it for performance.
@ -16,15 +17,18 @@ class Uname(Executable):
self.kernelRelease: str = ""
self.kernelVersion: str = ""
self.hardwarePlatform: str = ""
self.os: str = ""
@property
def command(self) -> str:
return "uname"
@property
def canInstall(self) -> bool:
return False
def installed(self) -> bool:
@property
def isInstalledInternal(self) -> bool:
return True
def getLinuxInformation(
@ -38,7 +42,7 @@ class Uname(Executable):
"""
if (not self.hasResult) or force:
cmd_result = self.run("-vri", noErrorLog=noErrorLog)
cmd_result = self.run("-vrio", noErrorLog=noErrorLog)
if cmd_result.exitCode != 0:
self.isLinux = False
@ -49,9 +53,11 @@ class Uname(Executable):
self.kernelRelease = match_result.group("release")
self.kernelVersion = match_result.group("version")
self.hardwarePlatform = match_result.group("platform")
self.os = match_result.group("os")
self.hasResult = True
return (
self.kernelRelease,
self.kernelVersion,
self.hardwarePlatform,
self.os,
)

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

@ -3,11 +3,16 @@ CONFIG_CONFIG = "config"
CONFIG_PLATFORM = "platform"
CONFIG_ENVIRONMENT_FACTORY = "environmentFactory"
# path related
PATH_REMOTE_ROOT = "lisa_working"
PATH_TOOL = "tool"
# list types
LIST = "list"
LIST_CASE = "case"
# common
NODE = "node"
NAME = "name"
VALUE = "value"
TYPE = "type"

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

@ -1,10 +1,20 @@
import os
from pathlib import PurePath
RESULT_PATH = "RESULT_PATH"
KEY_RUN_LOCAL_PATH = "RUN_LOCAL_PATH"
KEY_RUN_PATH = "RUN_PATH"
__prefix = "LISA_"
def get_run_local_path() -> PurePath:
return PurePath(get_env(KEY_RUN_LOCAL_PATH))
def get_run_path() -> PurePath:
return PurePath(get_env(KEY_RUN_PATH))
def set_env(name: str, value: str, isSecret: bool = False) -> None:
name = f"{__prefix}{name}"
os.environ[name] = value

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

@ -7,3 +7,4 @@ class ExecutableResult:
stdout: str
stderr: str
exitCode: Optional[int]
elapsed: float

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

@ -3,13 +3,13 @@ import os
import time
# to prevent circular import, hard code it here.
env_result_path = "LISA_RESULT_PATH"
env_key_run_local_path = "LISA_RUN_LOCAL_PATH"
def log_lines(logLevel: int, content: str, prefix: str = "") -> None:
for line in content.splitlines(False):
if prefix:
log.log(logLevel, f"{prefix} {line}")
log.log(logLevel, f"{prefix}{line}")
else:
log.log(logLevel, line)
@ -21,7 +21,7 @@ def init_log() -> None:
format=format,
datefmt="%m%d %H:%M:%S",
handlers=[
logging.FileHandler(f"{os.getenv(env_result_path)}/lisa-host.log"),
logging.FileHandler(f"{os.getenv(env_key_run_local_path)}/lisa-host.log"),
logging.StreamHandler(),
],
)

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

@ -1,16 +1,15 @@
import logging
import os
import subprocess
import pathlib
import shlex
import time
from threading import Thread
from timeit import default_timer as timer
from types import TracebackType
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, cast
from typing import TYPE_CHECKING, Dict, Optional, Type
import psutil # type: ignore
import spur
from lisa.util.executableResult import ExecutableResult
from lisa.util.logger import log, log_lines
from lisa.util.logger import log
from lisa.util.shell import Shell
if TYPE_CHECKING:
BaseExceptionType = Type[BaseException]
@ -18,100 +17,90 @@ else:
BaseExceptionType = bool
class LogPipe(Thread):
def __init__(self, level: int, cmd_id: str = ""):
"""Setup the object with a logger and a loglevel
and start the thread
"""
Thread.__init__(self)
self.output: str = ""
self.daemon = False
class LogWriter:
def __init__(self, level: int, cmd_prefix: str = ""):
self.level = level
self.fdRead, self.fdWrite = os.pipe()
self.pipeReader = os.fdopen(self.fdRead)
self.isReadCompleted = False
self.isClosed = False
self.cmd_id = cmd_id
self.start()
self.cmd_prefix = cmd_prefix
self.buffer: str = ""
def fileno(self) -> int:
"""Return the write file descriptor of the pipe
"""
return self.fdWrite
def write(self, message: str):
if message == "\n":
log.log(self.level, f"{self. cmd_prefix}{self.buffer}")
self.buffer = ""
else:
self.buffer = "".join([self.buffer, message])
def run(self) -> None:
"""Run the thread, logging everything.
"""
output = self.pipeReader.read()
self.output = "".join([self.output, output])
log_lines(self.level, output, prefix=f"cmd[{self.cmd_id}]")
self.pipeReader.close()
self.isReadCompleted = True
def close(self) -> None:
"""Close the write end of the pipe.
"""
if not self.isClosed:
os.close(self.fdWrite)
self.isClosed = True
def close(self):
if len(self.buffer) > 0:
log.log(self.level, f"{self.cmd_prefix}{self.buffer}")
class Process:
def __init__(self) -> None:
self.process: Optional[subprocess.Popen[Any]] = None
self.exitCode: Optional[int] = None
self.log_pipe: Optional[LogPipe] = None
def __init__(self, cmd_prefix: str, shell: Shell, isLinux: bool = True) -> None:
# the shell can be LocalShell or SshShell
self.shell = shell
self.cmd_prefix = cmd_prefix
self.isLinux = isLinux
self._running: bool = False
def __enter__(self) -> None:
pass
def __exit__(
self,
exc_type: Optional[BaseExceptionType],
exc_value: Optional[BaseException],
traceback: Optional[TracebackType],
) -> None:
self.close()
def start(
self,
command: str,
cwd: Optional[str] = None,
useBash: bool = False,
cwd: Optional[pathlib.Path] = None,
new_envs: Optional[Dict[str, str]] = None,
cmd_id: str = "",
noErrorLog: bool = False,
noInfoLog: bool = False,
) -> None:
"""
command include all parameters also.
"""
dictEnv = dict(os.environ.copy())
if new_envs is not None:
for key, value in new_envs.items():
dictEnv[key] = value
self.stdout_pipe = LogPipe(logging.INFO, cmd_id)
stdoutLogLevel = logging.INFO
stderrLogLevel = logging.ERROR
if noInfoLog:
stdoutLogLevel = logging.DEBUG
if noErrorLog:
logLevel = logging.INFO
else:
logLevel = logging.ERROR
self.stderr_pipe = LogPipe(logLevel, cmd_id)
self.process = subprocess.Popen(
command,
shell=True,
stdout=cast(int, self.stdout_pipe),
stderr=cast(int, self.stderr_pipe),
cwd=cwd,
env=cast(Optional[Dict[str, str]], dictEnv),
)
self._running = True
if self.process is not None:
log.debug(f"process {self.process.pid} started")
stderrLogLevel = stdoutLogLevel
self.stdout_writer = LogWriter(stdoutLogLevel, f"{self.cmd_prefix}stdout: ")
self.stderr_writer = LogWriter(stderrLogLevel, f"{self.cmd_prefix}stderr: ")
if useBash:
if self.isLinux:
command = f'bash -c "{command}"'
else:
command = f'cmd /c "{command}"'
split_command = shlex.split(command)
log.debug(f"split command: {split_command}")
try:
real_shell = self.shell.innerShell
assert real_shell
self._start_timer = timer()
self.process = real_shell.spawn(
command=split_command,
stdout=self.stdout_writer,
stderr=self.stderr_writer,
cwd=cwd,
update_env=new_envs,
allow_error=True,
store_pid=True,
encoding="utf-8",
)
self._running = True
log.debug(f"{self.cmd_prefix}started")
except (FileNotFoundError, spur.errors.NoSuchCommandError) as identifier:
# FileNotFoundError: not found command on Windows
# NoSuchCommandError: not found command on remote Linux
self._end_timer = timer()
self.process = ExecutableResult(
"", identifier.strerror, 1, self._end_timer - self._start_timer
)
log.debug(f"{self.cmd_prefix} not found command")
def waitResult(self, timeout: float = 600) -> ExecutableResult:
budget_time = timeout
# wait for all content read
while self.isRunning() and budget_time >= 0:
start = timer()
time.sleep(0.01)
@ -120,48 +109,32 @@ class Process:
if budget_time < 0:
if self.process is not None:
log.warn(f"process {self.process.pid} timeout in {timeout} sec")
log.warn(f"{self.cmd_prefix}timeout in {timeout} sec, and killed")
self.stop()
# close to get pipe complete
self.close()
if not isinstance(self.process, ExecutableResult):
assert self.process
proces_result = self.process.wait_for_result()
self._end_timer = timer()
self.stdout_writer.close()
self.stderr_writer.close()
result = ExecutableResult(
proces_result.output.strip(),
proces_result.stderr_output.strip(),
proces_result.return_code,
self._end_timer - self._start_timer,
)
else:
result = self.process
# wait all content flushed
while (
not self.stdout_pipe.isReadCompleted or not self.stderr_pipe.isReadCompleted
):
time.sleep(0.01)
return ExecutableResult(
self.stdout_pipe.output.strip(),
self.stderr_pipe.output.strip(),
self.exitCode,
)
log.info(f"{self.cmd_prefix}executed with {result.elapsed:.3f} sec")
return result
def stop(self) -> None:
if self.process is not None:
children = cast(
List[psutil.Process], psutil.Process(self.process.pid).children(True)
)
for child in children:
child.terminate()
self.process.terminate()
log.debug(f"process {self.process.pid} stopped")
def close(self) -> None:
if self.stdout_pipe is not None:
self.stdout_pipe.close()
if self.stderr_pipe is not None:
self.stderr_pipe.close()
if self.process and not isinstance(self.process, ExecutableResult):
self.process.send_signal(9)
def isRunning(self) -> bool:
self.exitCode = self.getExitCode()
if self.exitCode is not None and self.process is not None:
if self._running:
log.debug(f"process {self.process.pid} exited: {self.exitCode}")
self._running = False
if self._running:
self._running = self.process.is_running()
return self._running
def getExitCode(self) -> Optional[int]:
if self.process is not None:
self.exitCode = self.process.poll()
return self.exitCode

56
lisa/util/shell.py Normal file
Просмотреть файл

@ -0,0 +1,56 @@
from pathlib import Path
from typing import Optional, Union
import spur
import spurplus
from lisa.util.connectionInfo import ConnectionInfo
class Shell:
"""
this class wraps local and remote file operations with similar behavior.
"""
def __init__(self):
self.isRemote = False
self.innerShell: Optional[Union[spurplus.SshShell, spur.LocalShell]] = None
self._isInitialized = False
def setConnectionInfo(self, connectionInfo: ConnectionInfo) -> None:
self.connectionInfo = connectionInfo
self.isRemote = True
def initialize(self):
if not self._isInitialized:
self._isInitialized = True
if self.isRemote:
assert self.connectionInfo
self.innerShell = spurplus.connect_with_retries(
self.connectionInfo.address,
port=self.connectionInfo.port,
username=self.connectionInfo.username,
password=self.connectionInfo.password,
private_key_file=self.connectionInfo.privateKeyFile,
missing_host_key=spur.ssh.MissingHostKey.accept,
)
else:
self.innerShell = spur.LocalShell()
def close(self):
if self.innerShell and isinstance(self.innerShell, spurplus.SshShell):
self.innerShell.close()
def mkdir(
self,
path: Path,
mode: int = 0o777,
parents: bool = True,
exist_ok: bool = False,
):
self.initialize()
if self.isRemote:
assert self.innerShell
self.innerShell.mkdir(path, mode=mode, parents=parents, exist_ok=exist_ok)
else:
path.mkdir(mode=mode, parents=parents, exist_ok=exist_ok)

89
poetry.lock сгенерированный
Просмотреть файл

@ -6,6 +6,20 @@ optional = false
python-versions = "*"
version = "1.4.4"
[[package]]
category = "main"
description = "Annotate AST trees with source code positions"
name = "asttokens"
optional = false
python-versions = "*"
version = "2.0.4"
[package.dependencies]
six = "*"
[package.extras]
test = ["astroid", "pytest"]
[[package]]
category = "dev"
description = "Classes Without Boilerplate"
@ -158,6 +172,20 @@ version = ">=4.3.5,<5"
[package.extras]
test = ["pytest (>=4.0.2,<6)"]
[[package]]
category = "main"
description = "Provide design-by-contract with informative violation messages."
name = "icontract"
optional = false
python-versions = "*"
version = "2.3.4"
[package.dependencies]
asttokens = ">=2,<3"
[package.extras]
dev = ["mypy (0.750)", "pylint (2.3.1)", "yapf (0.20.2)", "tox (>=3.0.0)", "pydocstyle (>=2.1.1,<3)", "coverage (>=4.5.1,<5)", "docutils (>=0.14,<1)", "pygments (>=2.2.0,<3)", "dpcontracts (0.6.0)", "tabulate (>=0.8.7,<1)", "py-cpuinfo (>=5.0.0,<6)"]
[[package]]
category = "dev"
description = "A Python utility / library to sort Python imports."
@ -466,6 +494,46 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
version = "1.15.0"
[[package]]
category = "main"
description = "Run commands and manipulate files locally or over SSH using the same interface"
name = "spur"
optional = false
python-versions = "*"
version = "0.3.20"
[package.dependencies]
paramiko = ">=1.13.1,<3"
[[package]]
category = "main"
description = "Manage remote machines and file operations over SSH."
name = "spurplus"
optional = false
python-versions = "*"
version = "2.3.3"
[package.dependencies]
icontract = ">=2.0.1,<3"
spur = "0.3.20"
temppathlib = ">=1.0.3,<2"
typing_extensions = ">=3.6.2.1"
[package.extras]
dev = ["mypy (0.620)", "pylint (1.8.2)", "yapf (0.20.2)", "tox (>=3.0.0)", "coverage (>=4.5.1,<5)", "pydocstyle (>=2.1.1,<3)"]
[[package]]
category = "main"
description = "Wraps tempfile to give you pathlib.Path."
name = "temppathlib"
optional = false
python-versions = "*"
version = "1.0.3"
[package.extras]
dev = ["mypy (0.570)", "pylint (1.8.2)", "yapf (0.20.2)", "tox (>=3.0.0)"]
test = ["tox (>=3.0.0)"]
[[package]]
category = "dev"
description = "A collection of helpers and mock objects for unit tests and doc tests."
@ -496,7 +564,7 @@ python-versions = "*"
version = "1.4.1"
[[package]]
category = "dev"
category = "main"
description = "Backported and Experimental Type Hints for Python 3.5+"
name = "typing-extensions"
optional = false
@ -513,7 +581,7 @@ python-versions = "*"
version = "1.35"
[metadata]
content-hash = "59cde0dfa40d3e9d1494ed53142ff22b698a890bb024defd051d4fd1145caa0e"
content-hash = "47bdcdb43ab99e6e4c725e65fd612356d1f74c86032b19d610aa27503d88f560"
lock-version = "1.0"
python-versions = "^3.8"
@ -522,6 +590,10 @@ appdirs = [
{file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
{file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
]
asttokens = [
{file = "asttokens-2.0.4-py2.py3-none-any.whl", hash = "sha256:766d3352908730efb20b95ae22db0f1cb1bedb67c6071fcffb5c236ea673f2f7"},
{file = "asttokens-2.0.4.tar.gz", hash = "sha256:a42e57e28f2ac1c85ed9b1f84109401427e5c63c04f61d15b8842b027eec5128"},
]
attrs = [
{file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"},
{file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"},
@ -624,6 +696,9 @@ flake8-isort = [
{file = "flake8-isort-3.0.1.tar.gz", hash = "sha256:5d976da513cc390232ad5a9bb54aee8a092466a15f442d91dfc525834bee727a"},
{file = "flake8_isort-3.0.1-py2.py3-none-any.whl", hash = "sha256:df1dd6dd73f6a8b128c9c783356627231783cccc82c13c6dc343d1a5a491699b"},
]
icontract = [
{file = "icontract-2.3.4.tar.gz", hash = "sha256:5e45f7fcf957375163d63ef9b34a0413a15024f35029fd4b1f2ec21d8463879f"},
]
isort = [
{file = "isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"},
{file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"},
@ -788,6 +863,16 @@ six = [
{file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
{file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
]
spur = [
{file = "spur-0.3.20-py2.py3-none-any.whl", hash = "sha256:f9446f418f419c046f49543ca55b35eff2e96b13cf7e0cf8605d657e5fa530ee"},
{file = "spur-0.3.20.tar.gz", hash = "sha256:f359e0573c0e4aaf8427494d6e67bc2bfac4f0c719e47f05e4750921b5804760"},
]
spurplus = [
{file = "spurplus-2.3.3.tar.gz", hash = "sha256:71c734a0827a68235d5b610c3570c3abc0c6be56708a340ba7bebc0a6eb77e92"},
]
temppathlib = [
{file = "temppathlib-1.0.3.tar.gz", hash = "sha256:58eaea9190639591f5005289e128b3b822eb5a3341d538ffdb7e67a73526421a"},
]
testfixtures = [
{file = "testfixtures-6.14.1-py2.py3-none-any.whl", hash = "sha256:30566e24a1b34e4d3f8c13abf62557d01eeb4480bcb8f1745467bfb0d415a7d9"},
{file = "testfixtures-6.14.1.tar.gz", hash = "sha256:58d2b3146d93bc5ddb0cd24e0ccacb13e29bdb61e5c81235c58f7b8ee4470366"},

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

@ -13,6 +13,7 @@ regex = "^2020.7.14"
retry = "^0.9.2"
paramiko = "^2.7.1"
singleton-decorator = "^1.0.0"
spurplus = "^2.3.3"
[tool.poetry.dev-dependencies]
black = "^19.10b0"