зеркало из https://github.com/microsoft/lisa.git
implement tool installation
1. support remote file operations with shell 2. add shell to deal with different between local and remote 3. add git and ntttcp tools, ntttcp is a complex tool example 4. support remote file operations with shell 5. support shell execution on command
This commit is contained in:
Родитель
09e060b84b
Коммит
2d62443079
|
@ -0,0 +1,24 @@
|
|||
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):
|
||||
@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
|
||||
|
|
|
@ -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,42 +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
|
||||
|
||||
import spur
|
||||
import spurplus
|
||||
|
||||
from lisa.core.tool import Tool
|
||||
from lisa.tool import Echo, Uname
|
||||
from lisa.util import constants
|
||||
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_info: Optional[ConnectionInfo] = None
|
||||
self.tempFolder: str = ""
|
||||
if self.isRemote:
|
||||
self.shell: Optional[spurplus.SshShell] = None
|
||||
else:
|
||||
self.shell: Optional[spur.LocalShell] = None
|
||||
self.workingPath: pathlib.Path = ""
|
||||
self.shell = Shell()
|
||||
|
||||
self._isInitialized: bool = False
|
||||
self._isLinux: bool = True
|
||||
|
@ -46,11 +41,10 @@ class Node:
|
|||
self.hardwarePlatform: str = ""
|
||||
|
||||
self.tools: Dict[Type[Tool], Tool] = dict()
|
||||
for tool_class in self.builtinTools:
|
||||
self.tools[tool_class] = tool_class(self)
|
||||
|
||||
@staticmethod
|
||||
def createNode(
|
||||
id: str,
|
||||
spec: Optional[Dict[str, object]] = None,
|
||||
node_type: str = constants.ENVIRONMENTS_NODES_REMOTE,
|
||||
isDefault: bool = False,
|
||||
|
@ -61,7 +55,7 @@ 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}"
|
||||
)
|
||||
|
@ -99,27 +93,68 @@ class Node:
|
|||
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:
|
||||
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, T(self))
|
||||
tool = cast(Tool, tool_type(self))
|
||||
if not tool.isInstalled:
|
||||
log.debug(f"{tool_prefix} is not installed")
|
||||
if tool.canInstall:
|
||||
tool.install()
|
||||
if not tool.isInstalled:
|
||||
raise Exception(
|
||||
f"Tool {tool_type.__name__} is not found on node, "
|
||||
f"and cannot be installed or is install failed."
|
||||
)
|
||||
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:
|
||||
|
@ -128,11 +163,11 @@ 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,
|
||||
|
@ -150,33 +185,45 @@ class Node:
|
|||
else:
|
||||
log.info(f"initialized Windows node '{self.name}', ")
|
||||
|
||||
def _execute(self, cmd: str, noErrorLog: bool = False) -> ExecutableResult:
|
||||
cmd_prefix = f"cmd[{str(random.randint(0, 10000))}]"
|
||||
start_timer = timer()
|
||||
log.debug(f"{cmd_prefix}remote({self.isRemote}) '{cmd}'")
|
||||
|
||||
if self.shell is None:
|
||||
# set working path
|
||||
if self.isRemote:
|
||||
assert self.connection_info is not None
|
||||
self.shell = spurplus.connect_with_retries(
|
||||
self.connection_info.address,
|
||||
port=self.connection_info.port,
|
||||
username=self.connection_info.username,
|
||||
password=self.connection_info.password,
|
||||
private_key_file=self.connection_info.privateKeyFile,
|
||||
missing_host_key=spur.ssh.MissingHostKey.accept,
|
||||
)
|
||||
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.shell = spur.LocalShell()
|
||||
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)
|
||||
|
||||
process = Process(cmd_prefix, self.shell)
|
||||
process.start(cmd, noErrorLog=noErrorLog)
|
||||
result = process.waitResult()
|
||||
|
||||
end_timer = timer()
|
||||
log.info(f"{cmd_prefix}executed with {end_timer - start_timer:.3f} sec")
|
||||
return result
|
||||
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.shell and isinstance(self.shell, spurplus.SshShell):
|
||||
self.shell.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,43 +1,93 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import pathlib
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING
|
||||
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()
|
||||
|
||||
@abstractmethod
|
||||
def installed(self) -> bool:
|
||||
@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:
|
||||
pass
|
||||
# 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 = "", noErrorLog: bool = False
|
||||
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, noErrorLog)
|
||||
result: ExecutableResult = self.node.execute(
|
||||
command, useBash, noErrorLog=noErrorLog, noInfoLog=noInfoLog, cwd=cwd
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
|
|
23
lisa/main.py
23
lisa/main.py
|
@ -11,21 +11,24 @@ from lisa.util.logger import init_log, log
|
|||
|
||||
|
||||
@retry(FileExistsError, tries=10, delay=0) # type: ignore
|
||||
def create_run_root_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/runs/{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:
|
||||
local_path = Path("runtime").joinpath("runs").absolute()
|
||||
# create run root path
|
||||
run_root_path = create_run_root_path().absolute()
|
||||
run_root_path.mkdir(parents=True)
|
||||
env.set_env(env.KEY_RUN_ROOT_PATH, str(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()
|
||||
|
||||
|
@ -33,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"run root path: {env.get_env(env.KEY_RUN_ROOT_PATH)}")
|
||||
log.info(f"run local path: {env.get_run_local_path()}")
|
||||
|
||||
if args.debug:
|
||||
log_level = DEBUG
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from .echo import Echo
|
||||
from .ping import Ping
|
||||
from .git import Git
|
||||
from .ntttcp import Ntttcp
|
||||
from .uname import Uname
|
||||
|
||||
__all__ = ["Uname", "Ping", "Echo"]
|
||||
__all__ = ["Uname", "Ntttcp", "Echo", "Git"]
|
||||
|
|
|
@ -4,10 +4,15 @@ from lisa.core.tool import Tool
|
|||
class Echo(Tool):
|
||||
@property
|
||||
def command(self) -> str:
|
||||
return "echo"
|
||||
command = "echo"
|
||||
if not self.node.isLinux:
|
||||
command = "cmd /c echo"
|
||||
return command
|
||||
|
||||
@property
|
||||
def canInstall(self) -> bool:
|
||||
return False
|
||||
|
||||
def installed(self) -> bool:
|
||||
@property
|
||||
def isInstalledInternal(self) -> bool:
|
||||
return True
|
||||
|
|
|
@ -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)
|
|
@ -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,5 +0,0 @@
|
|||
from lisa.core.tool import Tool
|
||||
|
||||
|
||||
class Ping(Tool):
|
||||
pass
|
|
@ -21,10 +21,12 @@ class Uname(Tool):
|
|||
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(
|
||||
|
|
|
@ -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,19 +1,18 @@
|
|||
import os
|
||||
from pathlib import PurePath
|
||||
|
||||
WORKING_PATH = "working"
|
||||
|
||||
KEY_RUN_ROOT_PATH = "RUN_ROOT_PATH"
|
||||
KEY_RUN_LOCAL_PATH = "RUN_LOCAL_PATH"
|
||||
KEY_RUN_PATH = "RUN_PATH"
|
||||
|
||||
__prefix = "LISA_"
|
||||
|
||||
|
||||
def get_run_root_path() -> PurePath:
|
||||
return get_env(KEY_RUN_ROOT_PATH)
|
||||
def get_run_local_path() -> PurePath:
|
||||
return PurePath(get_env(KEY_RUN_LOCAL_PATH))
|
||||
|
||||
|
||||
def get_working_path() -> PurePath:
|
||||
return get_run_root_path().joinpath(WORKING_PATH)
|
||||
def get_run_path() -> PurePath:
|
||||
return PurePath(get_env(KEY_RUN_PATH))
|
||||
|
||||
|
||||
def set_env(name: str, value: str, isSecret: bool = False) -> None:
|
||||
|
|
|
@ -7,3 +7,4 @@ class ExecutableResult:
|
|||
stdout: str
|
||||
stderr: str
|
||||
exitCode: Optional[int]
|
||||
elapsed: float
|
||||
|
|
|
@ -3,7 +3,7 @@ import os
|
|||
import time
|
||||
|
||||
# to prevent circular import, hard code it here.
|
||||
env_key_run_root_path = "LISA_RUN_ROOT_PATH"
|
||||
env_key_run_local_path = "LISA_RUN_LOCAL_PATH"
|
||||
|
||||
|
||||
def log_lines(logLevel: int, content: str, prefix: str = "") -> None:
|
||||
|
@ -21,7 +21,7 @@ def init_log() -> None:
|
|||
format=format,
|
||||
datefmt="%m%d %H:%M:%S",
|
||||
handlers=[
|
||||
logging.FileHandler(f"{os.getenv(env_key_run_root_path)}/lisa-host.log"),
|
||||
logging.FileHandler(f"{os.getenv(env_key_run_local_path)}/lisa-host.log"),
|
||||
logging.StreamHandler(),
|
||||
],
|
||||
)
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import logging
|
||||
import pathlib
|
||||
import shlex
|
||||
import time
|
||||
from timeit import default_timer as timer
|
||||
from typing import TYPE_CHECKING, Dict, Optional, Type, Union
|
||||
from typing import TYPE_CHECKING, Dict, Optional, Type
|
||||
|
||||
import spur
|
||||
from spurplus import SshShell # type: ignore
|
||||
|
||||
from lisa.util.executableResult import ExecutableResult
|
||||
from lisa.util.logger import log
|
||||
from lisa.util.shell import Shell
|
||||
|
||||
if TYPE_CHECKING:
|
||||
BaseExceptionType = Type[BaseException]
|
||||
|
@ -35,35 +36,48 @@ class LogWriter:
|
|||
|
||||
|
||||
class Process:
|
||||
def __init__(
|
||||
self, cmd_prefix: str, shell: Union[SshShell, spur.LocalShell]
|
||||
) -> 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 start(
|
||||
self,
|
||||
command: str,
|
||||
cwd: Optional[str] = None,
|
||||
useBash: bool = False,
|
||||
cwd: Optional[pathlib.Path] = None,
|
||||
new_envs: Optional[Dict[str, str]] = None,
|
||||
noErrorLog: bool = False,
|
||||
noInfoLog: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
command include all parameters also.
|
||||
"""
|
||||
self.stdout_writer = LogWriter(logging.INFO, f"{self.cmd_prefix}stdout: ")
|
||||
stdoutLogLevel = logging.INFO
|
||||
stderrLogLevel = logging.ERROR
|
||||
if noInfoLog:
|
||||
stdoutLogLevel = logging.DEBUG
|
||||
if noErrorLog:
|
||||
logLevel = logging.INFO
|
||||
else:
|
||||
logLevel = logging.ERROR
|
||||
self.stderr_writer = LogWriter(logLevel, f"{self.cmd_prefix}stderr: ")
|
||||
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:
|
||||
self.process = self.shell.spawn(
|
||||
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,
|
||||
|
@ -78,12 +92,15 @@ class Process:
|
|||
except (FileNotFoundError, spur.errors.NoSuchCommandError) as identifier:
|
||||
# FileNotFoundError: not found command on Windows
|
||||
# NoSuchCommandError: not found command on remote Linux
|
||||
self.process = ExecutableResult("", identifier.strerror, 1,)
|
||||
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)
|
||||
|
@ -98,16 +115,19 @@ class Process:
|
|||
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
|
||||
|
||||
log.info(f"{self.cmd_prefix}executed with {result.elapsed:.3f} sec")
|
||||
return result
|
||||
|
||||
def stop(self) -> None:
|
||||
|
|
|
@ -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)
|
Загрузка…
Ссылка в новой задаче