зеркало из https://github.com/microsoft/lisa.git
Merge pull request #959 from LIS/chisong/v3_8010
implement tool installation
This commit is contained in:
Коммит
56718c9a77
|
@ -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()
|
||||
|
|
|
@ -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
|
29
lisa/main.py
29
lisa/main.py
|
@ -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()
|
||||
|
||||
|
|
|
@ -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"]
|
|
@ -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
|
|
@ -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,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
|
||||
|
|
|
@ -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)
|
|
@ -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"
|
||||
|
|
Загрузка…
Ссылка в новой задаче