@@ -1,9 +1,16 @@
# SPDX-License-Identifier: BSD-3-Clause
-# Copyright 2022 The DPDK contributors
+# Copyright 2022-2023 The DPDK contributors
executions:
- - system_under_test: "SUT 1"
+ - build_targets:
+ - arch: x86_64
+ os: linux
+ cpu: native
+ compiler: gcc
+ compiler_wrapper: ccache
+ system_under_test: "SUT 1"
nodes:
- name: "SUT 1"
hostname: sut1.change.me.localhost
user: root
+ os: linux
@@ -1,15 +1,17 @@
# SPDX-License-Identifier: BSD-3-Clause
# Copyright(c) 2010-2021 Intel Corporation
-# Copyright(c) 2022 University of New Hampshire
+# Copyright(c) 2022-2023 University of New Hampshire
+# Copyright(c) 2023 PANTHEON.tech s.r.o.
"""
-Generic port and topology nodes configuration file load function
+Yaml config parsing methods
"""
import json
import os.path
import pathlib
from dataclasses import dataclass
+from enum import Enum, auto, unique
from typing import Any
import warlock # type: ignore
@@ -18,6 +20,47 @@
from framework.settings import SETTINGS
+class StrEnum(Enum):
+ @staticmethod
+ def _generate_next_value_(
+ name: str, start: int, count: int, last_values: object
+ ) -> str:
+ return name
+
+
+@unique
+class Architecture(StrEnum):
+ i686 = auto()
+ x86_64 = auto()
+ x86_32 = auto()
+ arm64 = auto()
+ ppc64le = auto()
+
+
+@unique
+class OS(StrEnum):
+ linux = auto()
+ freebsd = auto()
+ windows = auto()
+
+
+@unique
+class CPUType(StrEnum):
+ native = auto()
+ armv8a = auto()
+ dpaa2 = auto()
+ thunderx = auto()
+ xgene1 = auto()
+
+
+@unique
+class Compiler(StrEnum):
+ gcc = auto()
+ clang = auto()
+ icc = auto()
+ msvc = auto()
+
+
# Slots enables some optimizations, by pre-allocating space for the defined
# attributes in the underlying data structure.
#
@@ -29,6 +72,7 @@ class NodeConfiguration:
hostname: str
user: str
password: str | None
+ os: OS
@staticmethod
def from_dict(d: dict) -> "NodeConfiguration":
@@ -37,19 +81,44 @@ def from_dict(d: dict) -> "NodeConfiguration":
hostname=d["hostname"],
user=d["user"],
password=d.get("password"),
+ os=OS(d["os"]),
+ )
+
+
+@dataclass(slots=True, frozen=True)
+class BuildTargetConfiguration:
+ arch: Architecture
+ os: OS
+ cpu: CPUType
+ compiler: Compiler
+ name: str
+
+ @staticmethod
+ def from_dict(d: dict) -> "BuildTargetConfiguration":
+ return BuildTargetConfiguration(
+ arch=Architecture(d["arch"]),
+ os=OS(d["os"]),
+ cpu=CPUType(d["cpu"]),
+ compiler=Compiler(d["compiler"]),
+ name=f"{d['arch']}-{d['os']}-{d['cpu']}-{d['compiler']}",
)
@dataclass(slots=True, frozen=True)
class ExecutionConfiguration:
+ build_targets: list[BuildTargetConfiguration]
system_under_test: NodeConfiguration
@staticmethod
def from_dict(d: dict, node_map: dict) -> "ExecutionConfiguration":
+ build_targets: list[BuildTargetConfiguration] = list(
+ map(BuildTargetConfiguration.from_dict, d["build_targets"])
+ )
sut_name = d["system_under_test"]
assert sut_name in node_map, f"Unknown SUT {sut_name} in execution {d}"
return ExecutionConfiguration(
+ build_targets=build_targets,
system_under_test=node_map[sut_name],
)
@@ -5,6 +5,68 @@
"node_name": {
"type": "string",
"description": "A unique identifier for a node"
+ },
+ "OS": {
+ "type": "string",
+ "enum": [
+ "linux"
+ ]
+ },
+ "cpu": {
+ "type": "string",
+ "description": "Native should be the default on x86",
+ "enum": [
+ "native",
+ "armv8a",
+ "dpaa2",
+ "thunderx",
+ "xgene1"
+ ]
+ },
+ "compiler": {
+ "type": "string",
+ "enum": [
+ "gcc",
+ "clang",
+ "icc",
+ "mscv"
+ ]
+ },
+ "build_target": {
+ "type": "object",
+ "description": "Targets supported by DTS",
+ "properties": {
+ "arch": {
+ "type": "string",
+ "enum": [
+ "ALL",
+ "x86_64",
+ "arm64",
+ "ppc64le",
+ "other"
+ ]
+ },
+ "os": {
+ "$ref": "#/definitions/OS"
+ },
+ "cpu": {
+ "$ref": "#/definitions/cpu"
+ },
+ "compiler": {
+ "$ref": "#/definitions/compiler"
+ },
+ "compiler_wrapper": {
+ "type": "string",
+ "description": "This will be added before compiler to the CC variable when building DPDK. Optional."
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "arch",
+ "os",
+ "cpu",
+ "compiler"
+ ]
}
},
"type": "object",
@@ -29,13 +91,17 @@
"password": {
"type": "string",
"description": "The password to use on this node. Use only as a last resort. SSH keys are STRONGLY preferred."
+ },
+ "os": {
+ "$ref": "#/definitions/OS"
}
},
"additionalProperties": false,
"required": [
"name",
"hostname",
- "user"
+ "user",
+ "os"
]
},
"minimum": 1
@@ -45,12 +111,20 @@
"items": {
"type": "object",
"properties": {
+ "build_targets": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/build_target"
+ },
+ "minimum": 1
+ },
"system_under_test": {
"$ref": "#/definitions/node_name"
}
},
"additionalProperties": false,
"required": [
+ "build_targets",
"system_under_test"
]
},
@@ -1,67 +1,157 @@
# SPDX-License-Identifier: BSD-3-Clause
# Copyright(c) 2010-2019 Intel Corporation
-# Copyright(c) 2022 PANTHEON.tech s.r.o.
-# Copyright(c) 2022 University of New Hampshire
+# Copyright(c) 2022-2023 PANTHEON.tech s.r.o.
+# Copyright(c) 2022-2023 University of New Hampshire
import sys
-import traceback
-from collections.abc import Iterable
-from framework.testbed_model.node import Node
-
-from .config import CONFIGURATION
+from .config import CONFIGURATION, BuildTargetConfiguration, ExecutionConfiguration
+from .exception import DTSError, ErrorSeverity
from .logger import DTSLOG, getLogger
+from .testbed_model import SutNode
from .utils import check_dts_python_version
-dts_logger: DTSLOG | None = None
+dts_logger: DTSLOG = getLogger("dts_runner")
+errors = []
def run_all() -> None:
"""
- Main process of DTS, it will run all test suites in the config file.
+ The main process of DTS. Runs all build targets in all executions from the main
+ config file.
"""
-
global dts_logger
+ global errors
# check the python version of the server that run dts
check_dts_python_version()
- dts_logger = getLogger("dts")
-
- nodes = {}
- # This try/finally block means "Run the try block, if there is an exception,
- # run the finally block before passing it upward. If there is not an exception,
- # run the finally block after the try block is finished." This helps avoid the
- # problem of python's interpreter exit context, which essentially prevents you
- # from making certain system calls. This makes cleaning up resources difficult,
- # since most of the resources in DTS are network-based, which is restricted.
+ nodes: dict[str, SutNode] = {}
try:
# for all Execution sections
for execution in CONFIGURATION.executions:
- sut_config = execution.system_under_test
- if sut_config.name not in nodes:
- node = Node(sut_config)
- nodes[sut_config.name] = node
- node.send_command("echo Hello World")
+ sut_node = None
+ if execution.system_under_test.name in nodes:
+ # a Node with the same name already exists
+ sut_node = nodes[execution.system_under_test.name]
+ else:
+ # the SUT has not been initialized yet
+ try:
+ sut_node = SutNode(execution.system_under_test)
+ except Exception as e:
+ dts_logger.exception(
+ f"Connection to node {execution.system_under_test} failed."
+ )
+ errors.append(e)
+ else:
+ nodes[sut_node.name] = sut_node
+
+ if sut_node:
+ _run_execution(sut_node, execution)
+
+ except Exception as e:
+ dts_logger.exception("An unexpected error has occurred.")
+ errors.append(e)
+ raise
+
+ finally:
+ try:
+ for node in nodes.values():
+ node.close()
+ except Exception as e:
+ dts_logger.exception("Final cleanup of nodes failed.")
+ errors.append(e)
+ # we need to put the sys.exit call outside the finally clause to make sure
+ # that unexpected exceptions will propagate
+ # in that case, the error that should be reported is the uncaught exception as
+ # that is a severe error originating from the framework
+ # at that point, we'll only have partial results which could be impacted by the
+ # error causing the uncaught exception, making them uninterpretable
+ _exit_dts()
+
+
+def _run_execution(sut_node: SutNode, execution: ExecutionConfiguration) -> None:
+ """
+ Run the given execution. This involves running the execution setup as well as
+ running all build targets in the given execution.
+ """
+ dts_logger.info(f"Running execution with SUT '{execution.system_under_test.name}'.")
+
+ try:
+ sut_node.set_up_execution(execution)
except Exception as e:
- # sys.exit() doesn't produce a stack trace, need to print it explicitly
- traceback.print_exc()
- raise e
+ dts_logger.exception("Execution setup failed.")
+ errors.append(e)
+
+ else:
+ for build_target in execution.build_targets:
+ _run_build_target(sut_node, build_target, execution)
finally:
- quit_execution(nodes.values())
+ try:
+ sut_node.tear_down_execution()
+ except Exception as e:
+ dts_logger.exception("Execution teardown failed.")
+ errors.append(e)
-def quit_execution(sut_nodes: Iterable[Node]) -> None:
+def _run_build_target(
+ sut_node: SutNode,
+ build_target: BuildTargetConfiguration,
+ execution: ExecutionConfiguration,
+) -> None:
"""
- Close session to SUT and TG before quit.
- Return exit status when failure occurred.
+ Run the given build target.
"""
- for sut_node in sut_nodes:
- # close all session
- sut_node.node_exit()
+ dts_logger.info(f"Running build target '{build_target.name}'.")
+
+ try:
+ sut_node.set_up_build_target(build_target)
+ except Exception as e:
+ dts_logger.exception("Build target setup failed.")
+ errors.append(e)
+
+ else:
+ _run_suites(sut_node, execution)
+
+ finally:
+ try:
+ sut_node.tear_down_build_target()
+ except Exception as e:
+ dts_logger.exception("Build target teardown failed.")
+ errors.append(e)
+
+
+def _run_suites(
+ sut_node: SutNode,
+ execution: ExecutionConfiguration,
+) -> None:
+ """
+ Use the given build_target to run execution's test suites
+ with possibly only a subset of test cases.
+ If no subset is specified, run all test cases.
+ """
+
+
+def _exit_dts() -> None:
+ """
+ Process all errors and exit with the proper exit code.
+ """
+ if errors and dts_logger:
+ dts_logger.debug("Summary of errors:")
+ for error in errors:
+ dts_logger.debug(repr(error))
+
+ return_code = ErrorSeverity.NO_ERR
+ for error in errors:
+ error_return_code = ErrorSeverity.GENERIC_ERR
+ if isinstance(error, DTSError):
+ error_return_code = error.severity
+
+ if error_return_code > return_code:
+ return_code = error_return_code
- if dts_logger is not None:
+ if dts_logger:
dts_logger.info("DTS execution has ended.")
- sys.exit(0)
+ sys.exit(return_code)
@@ -1,20 +1,46 @@
# SPDX-License-Identifier: BSD-3-Clause
# Copyright(c) 2010-2014 Intel Corporation
-# Copyright(c) 2022 PANTHEON.tech s.r.o.
-# Copyright(c) 2022 University of New Hampshire
+# Copyright(c) 2022-2023 PANTHEON.tech s.r.o.
+# Copyright(c) 2022-2023 University of New Hampshire
"""
User-defined exceptions used across the framework.
"""
+from enum import IntEnum, unique
+from typing import ClassVar
-class SSHTimeoutError(Exception):
+
+@unique
+class ErrorSeverity(IntEnum):
+ """
+ The severity of errors that occur during DTS execution.
+ All exceptions are caught and the most severe error is used as return code.
+ """
+
+ NO_ERR = 0
+ GENERIC_ERR = 1
+ CONFIG_ERR = 2
+ SSH_ERR = 3
+
+
+class DTSError(Exception):
+ """
+ The base exception from which all DTS exceptions are derived.
+ Stores error severity.
+ """
+
+ severity: ClassVar[ErrorSeverity] = ErrorSeverity.GENERIC_ERR
+
+
+class SSHTimeoutError(DTSError):
"""
Command execution timeout.
"""
command: str
output: str
+ severity: ClassVar[ErrorSeverity] = ErrorSeverity.SSH_ERR
def __init__(self, command: str, output: str):
self.command = command
@@ -27,12 +53,13 @@ def get_output(self) -> str:
return self.output
-class SSHConnectionError(Exception):
+class SSHConnectionError(DTSError):
"""
SSH connection error.
"""
host: str
+ severity: ClassVar[ErrorSeverity] = ErrorSeverity.SSH_ERR
def __init__(self, host: str):
self.host = host
@@ -41,16 +68,25 @@ def __str__(self) -> str:
return f"Error trying to connect with {self.host}"
-class SSHSessionDeadError(Exception):
+class SSHSessionDeadError(DTSError):
"""
SSH session is not alive.
It can no longer be used.
"""
host: str
+ severity: ClassVar[ErrorSeverity] = ErrorSeverity.SSH_ERR
def __init__(self, host: str):
self.host = host
def __str__(self) -> str:
return f"SSH session with {self.host} has died"
+
+
+class ConfigurationError(DTSError):
+ """
+ Raised when an invalid configuration is encountered.
+ """
+
+ severity: ClassVar[ErrorSeverity] = ErrorSeverity.CONFIG_ERR
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: BSD-3-Clause
# Copyright(c) 2010-2014 Intel Corporation
-# Copyright(c) 2022 PANTHEON.tech s.r.o.
-# Copyright(c) 2022 University of New Hampshire
+# Copyright(c) 2022-2023 PANTHEON.tech s.r.o.
+# Copyright(c) 2022-2023 University of New Hampshire
"""
DTS logger module with several log level. DTS framework and TestSuite logs
@@ -33,17 +33,17 @@ class DTSLOG(logging.LoggerAdapter):
DTS log class for framework and testsuite.
"""
- logger: logging.Logger
+ _logger: logging.Logger
node: str
sh: logging.StreamHandler
fh: logging.FileHandler
verbose_fh: logging.FileHandler
def __init__(self, logger: logging.Logger, node: str = "suite"):
- self.logger = logger
+ self._logger = logger
# 1 means log everything, this will be used by file handlers if their level
# is not set
- self.logger.setLevel(1)
+ self._logger.setLevel(1)
self.node = node
@@ -55,9 +55,13 @@ def __init__(self, logger: logging.Logger, node: str = "suite"):
if SETTINGS.verbose is True:
sh.setLevel(logging.DEBUG)
- self.logger.addHandler(sh)
+ self._logger.addHandler(sh)
self.sh = sh
+ # prepare the output folder
+ if not os.path.exists(SETTINGS.output_dir):
+ os.mkdir(SETTINGS.output_dir)
+
logging_path_prefix = os.path.join(SETTINGS.output_dir, node)
fh = logging.FileHandler(f"{logging_path_prefix}.log")
@@ -68,7 +72,7 @@ def __init__(self, logger: logging.Logger, node: str = "suite"):
)
)
- self.logger.addHandler(fh)
+ self._logger.addHandler(fh)
self.fh = fh
# This outputs EVERYTHING, intended for post-mortem debugging
@@ -82,10 +86,10 @@ def __init__(self, logger: logging.Logger, node: str = "suite"):
)
)
- self.logger.addHandler(verbose_fh)
+ self._logger.addHandler(verbose_fh)
self.verbose_fh = verbose_fh
- super(DTSLOG, self).__init__(self.logger, dict(node=self.node))
+ super(DTSLOG, self).__init__(self._logger, dict(node=self.node))
def logger_exit(self) -> None:
"""
@@ -93,7 +97,7 @@ def logger_exit(self) -> None:
"""
for handler in (self.sh, self.fh, self.verbose_fh):
handler.flush()
- self.logger.removeHandler(handler)
+ self._logger.removeHandler(handler)
def getLogger(name: str, node: str = "suite") -> DTSLOG:
@@ -1,14 +1,30 @@
# SPDX-License-Identifier: BSD-3-Clause
-# Copyright(c) 2022 PANTHEON.tech s.r.o.
+# Copyright(c) 2023 PANTHEON.tech s.r.o.
-from framework.config import NodeConfiguration
+"""
+The package provides modules for managing remote connections to a remote host (node),
+differentiated by OS.
+The package provides a factory function, create_session, that returns the appropriate
+remote connection based on the passed configuration. The differences are in the
+underlying transport protocol (e.g. SSH) and remote OS (e.g. Linux).
+"""
+
+# pylama:ignore=W0611
+
+from framework.config import OS, NodeConfiguration
+from framework.exception import ConfigurationError
from framework.logger import DTSLOG
-from .remote_session import RemoteSession
-from .ssh_session import SSHSession
+from .linux_session import LinuxSession
+from .os_session import OSSession
+from .remote import RemoteSession, SSHSession
-def create_remote_session(
+def create_session(
node_config: NodeConfiguration, name: str, logger: DTSLOG
-) -> RemoteSession:
- return SSHSession(node_config, name, logger)
+) -> OSSession:
+ match node_config.os:
+ case OS.linux:
+ return LinuxSession(node_config, name, logger)
+ case _:
+ raise ConfigurationError(f"Unsupported OS {node_config.os}")
new file mode 100644
@@ -0,0 +1,11 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2023 PANTHEON.tech s.r.o.
+# Copyright(c) 2023 University of New Hampshire
+
+from .posix_session import PosixSession
+
+
+class LinuxSession(PosixSession):
+ """
+ The implementation of non-Posix compliant parts of Linux remote sessions.
+ """
new file mode 100644
@@ -0,0 +1,46 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2023 PANTHEON.tech s.r.o.
+# Copyright(c) 2023 University of New Hampshire
+
+from abc import ABC
+
+from framework.config import NodeConfiguration
+from framework.logger import DTSLOG
+
+from .remote import RemoteSession, create_remote_session
+
+
+class OSSession(ABC):
+ """
+ The OS classes create a DTS node remote session and implement OS specific
+ behavior. There a few control methods implemented by the base class, the rest need
+ to be implemented by derived classes.
+ """
+
+ _config: NodeConfiguration
+ name: str
+ _logger: DTSLOG
+ remote_session: RemoteSession
+
+ def __init__(
+ self,
+ node_config: NodeConfiguration,
+ name: str,
+ logger: DTSLOG,
+ ):
+ self._config = node_config
+ self.name = name
+ self._logger = logger
+ self.remote_session = create_remote_session(node_config, name, logger)
+
+ def close(self, force: bool = False) -> None:
+ """
+ Close the remote session.
+ """
+ self.remote_session.close(force)
+
+ def is_alive(self) -> bool:
+ """
+ Check whether the remote session is still responding.
+ """
+ return self.remote_session.is_alive()
new file mode 100644
@@ -0,0 +1,12 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2023 PANTHEON.tech s.r.o.
+# Copyright(c) 2023 University of New Hampshire
+
+from .os_session import OSSession
+
+
+class PosixSession(OSSession):
+ """
+ An intermediary class implementing the Posix compliant parts of
+ Linux and other OS remote sessions.
+ """
new file mode 100644
@@ -0,0 +1,16 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2023 PANTHEON.tech s.r.o.
+
+# pylama:ignore=W0611
+
+from framework.config import NodeConfiguration
+from framework.logger import DTSLOG
+
+from .remote_session import RemoteSession
+from .ssh_session import SSHSession
+
+
+def create_remote_session(
+ node_config: NodeConfiguration, name: str, logger: DTSLOG
+) -> RemoteSession:
+ return SSHSession(node_config, name, logger)
similarity index 61%
rename from dts/framework/remote_session/remote_session.py
rename to dts/framework/remote_session/remote/remote_session.py
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: BSD-3-Clause
# Copyright(c) 2010-2014 Intel Corporation
-# Copyright(c) 2022 PANTHEON.tech s.r.o.
-# Copyright(c) 2022 University of New Hampshire
+# Copyright(c) 2022-2023 PANTHEON.tech s.r.o.
+# Copyright(c) 2022-2023 University of New Hampshire
import dataclasses
from abc import ABC, abstractmethod
@@ -19,14 +19,23 @@ class HistoryRecord:
class RemoteSession(ABC):
+ """
+ The base class for defining which methods must be implemented in order to connect
+ to a remote host (node) and maintain a remote session. The derived classes are
+ supposed to implement/use some underlying transport protocol (e.g. SSH) to
+ implement the methods. On top of that, it provides some basic services common to
+ all derived classes, such as keeping history and logging what's being executed
+ on the remote node.
+ """
+
name: str
hostname: str
ip: str
port: int | None
username: str
password: str
- logger: DTSLOG
history: list[HistoryRecord]
+ _logger: DTSLOG
_node_config: NodeConfiguration
def __init__(
@@ -46,31 +55,34 @@ def __init__(
self.port = int(port)
self.username = node_config.user
self.password = node_config.password or ""
- self.logger = logger
self.history = []
- self.logger.info(f"Connecting to {self.username}@{self.hostname}.")
+ self._logger = logger
+ self._logger.info(f"Connecting to {self.username}@{self.hostname}.")
self._connect()
- self.logger.info(f"Connection to {self.username}@{self.hostname} successful.")
+ self._logger.info(f"Connection to {self.username}@{self.hostname} successful.")
@abstractmethod
def _connect(self) -> None:
"""
Create connection to assigned node.
"""
- pass
def send_command(self, command: str, timeout: float = SETTINGS.timeout) -> str:
- self.logger.info(f"Sending: {command}")
+ """
+ Send a command and return the output.
+ """
+ self._logger.info(f"Sending: {command}")
out = self._send_command(command, timeout)
- self.logger.debug(f"Received from {command}: {out}")
+ self._logger.debug(f"Received from {command}: {out}")
self._history_add(command=command, output=out)
return out
@abstractmethod
def _send_command(self, command: str, timeout: float) -> str:
"""
- Send a command and return the output.
+ Use the underlying protocol to execute the command and return the output
+ of the command.
"""
def _history_add(self, command: str, output: str) -> None:
@@ -79,17 +91,20 @@ def _history_add(self, command: str, output: str) -> None:
)
def close(self, force: bool = False) -> None:
- self.logger.logger_exit()
+ """
+ Close the remote session and free all used resources.
+ """
+ self._logger.logger_exit()
self._close(force)
@abstractmethod
def _close(self, force: bool = False) -> None:
"""
- Close the remote session, freeing all used resources.
+ Execute protocol specific steps needed to close the session properly.
"""
@abstractmethod
def is_alive(self) -> bool:
"""
- Check whether the session is still responding.
+ Check whether the remote session is still responding.
"""
similarity index 91%
rename from dts/framework/remote_session/ssh_session.py
rename to dts/framework/remote_session/remote/ssh_session.py
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: BSD-3-Clause
# Copyright(c) 2010-2014 Intel Corporation
-# Copyright(c) 2022 PANTHEON.tech s.r.o.
-# Copyright(c) 2022 University of New Hampshire
+# Copyright(c) 2022-2023 PANTHEON.tech s.r.o.
+# Copyright(c) 2022-2023 University of New Hampshire
import time
@@ -17,7 +17,7 @@
class SSHSession(RemoteSession):
"""
- Module for creating Pexpect SSH sessions to a node.
+ Module for creating Pexpect SSH remote sessions.
"""
session: pxssh.pxssh
@@ -56,9 +56,9 @@ def _connect(self) -> None:
)
break
except Exception as e:
- self.logger.warning(e)
+ self._logger.warning(e)
time.sleep(2)
- self.logger.info(
+ self._logger.info(
f"Retrying connection: retry number {retry_attempt + 1}."
)
else:
@@ -67,13 +67,13 @@ def _connect(self) -> None:
self.send_expect("stty -echo", "#")
self.send_expect("stty columns 1000", "#")
except Exception as e:
- self.logger.error(RED(str(e)))
+ self._logger.error(RED(str(e)))
if getattr(self, "port", None):
suggestion = (
f"\nSuggestion: Check if the firewall on {self.hostname} is "
f"stopped.\n"
)
- self.logger.info(GREEN(suggestion))
+ self._logger.info(GREEN(suggestion))
raise SSHConnectionError(self.hostname)
@@ -87,8 +87,8 @@ def send_expect(
try:
retval = int(ret_status)
if retval:
- self.logger.error(f"Command: {command} failure!")
- self.logger.error(ret)
+ self._logger.error(f"Command: {command} failure!")
+ self._logger.error(ret)
return retval
else:
return ret
@@ -97,7 +97,7 @@ def send_expect(
else:
return ret
except Exception as e:
- self.logger.error(
+ self._logger.error(
f"Exception happened in [{command}] and output is "
f"[{self._get_output()}]"
)
@@ -1,6 +1,6 @@
# SPDX-License-Identifier: BSD-3-Clause
# Copyright(c) 2010-2021 Intel Corporation
-# Copyright(c) 2022 PANTHEON.tech s.r.o.
+# Copyright(c) 2022-2023 PANTHEON.tech s.r.o.
# Copyright(c) 2022 University of New Hampshire
import argparse
@@ -23,7 +23,7 @@ def __init__(
default: str = None,
type: Callable[[str], _T | argparse.FileType | None] = None,
choices: Iterable[_T] | None = None,
- required: bool = True,
+ required: bool = False,
help: str | None = None,
metavar: str | tuple[str, ...] | None = None,
) -> None:
@@ -63,13 +63,17 @@ class _Settings:
def _get_parser() -> argparse.ArgumentParser:
- parser = argparse.ArgumentParser(description="DPDK test framework.")
+ parser = argparse.ArgumentParser(
+ description="Run DPDK test suites. All options may be specified with "
+ "the environment variables provided in brackets. "
+ "Command line arguments have higher priority.",
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
+ )
parser.add_argument(
"--config-file",
action=_env_arg("DTS_CFG_FILE"),
default="conf.yaml",
- required=False,
help="[DTS_CFG_FILE] configuration file that describes the test cases, SUTs "
"and targets.",
)
@@ -79,7 +83,6 @@ def _get_parser() -> argparse.ArgumentParser:
"--output",
action=_env_arg("DTS_OUTPUT_DIR"),
default="output",
- required=False,
help="[DTS_OUTPUT_DIR] Output directory where dts logs and results are saved.",
)
@@ -88,7 +91,6 @@ def _get_parser() -> argparse.ArgumentParser:
"--timeout",
action=_env_arg("DTS_TIMEOUT"),
default=15,
- required=False,
help="[DTS_TIMEOUT] The default timeout for all DTS operations except for "
"compiling DPDK.",
)
@@ -98,7 +100,6 @@ def _get_parser() -> argparse.ArgumentParser:
"--verbose",
action=_env_arg("DTS_VERBOSE"),
default="N",
- required=False,
help="[DTS_VERBOSE] Set to 'Y' to enable verbose output, logging all messages "
"to the console.",
)
@@ -1,7 +1,13 @@
# SPDX-License-Identifier: BSD-3-Clause
-# Copyright(c) 2022 University of New Hampshire
+# Copyright(c) 2022-2023 University of New Hampshire
+# Copyright(c) 2023 PANTHEON.tech s.r.o.
"""
-This module contains the classes used to model the physical traffic generator,
+This package contains the classes used to model the physical traffic generator,
system under test and any other components that need to be interacted with.
"""
+
+# pylama:ignore=W0611
+
+from .node import Node
+from .sut_node import SutNode
@@ -1,62 +1,118 @@
# SPDX-License-Identifier: BSD-3-Clause
# Copyright(c) 2010-2014 Intel Corporation
-# Copyright(c) 2022 PANTHEON.tech s.r.o.
-# Copyright(c) 2022 University of New Hampshire
+# Copyright(c) 2022-2023 PANTHEON.tech s.r.o.
+# Copyright(c) 2022-2023 University of New Hampshire
"""
A node is a generic host that DTS connects to and manages.
"""
-from framework.config import NodeConfiguration
+from framework.config import (
+ BuildTargetConfiguration,
+ ExecutionConfiguration,
+ NodeConfiguration,
+)
from framework.logger import DTSLOG, getLogger
-from framework.remote_session import RemoteSession, create_remote_session
-from framework.settings import SETTINGS
+from framework.remote_session import OSSession, create_session
class Node(object):
"""
- Basic module for node management. This module implements methods that
+ Basic class for node management. This class implements methods that
manage a node, such as information gathering (of CPU/PCI/NIC) and
environment setup.
"""
+ main_session: OSSession
+ config: NodeConfiguration
name: str
- main_session: RemoteSession
- logger: DTSLOG
- _config: NodeConfiguration
- _other_sessions: list[RemoteSession]
+ _logger: DTSLOG
+ _other_sessions: list[OSSession]
def __init__(self, node_config: NodeConfiguration):
- self._config = node_config
+ self.config = node_config
+ self.name = node_config.name
+ self._logger = getLogger(self.name)
+ self.main_session = create_session(self.config, self.name, self._logger)
+
self._other_sessions = []
- self.name = node_config.name
- self.logger = getLogger(self.name)
- self.logger.info(f"Created node: {self.name}")
- self.main_session = create_remote_session(self._config, self.name, self.logger)
+ self._logger.info(f"Created node: {self.name}")
- def send_command(self, cmds: str, timeout: float = SETTINGS.timeout) -> str:
+ def set_up_execution(self, execution_config: ExecutionConfiguration) -> None:
"""
- Send commands to node and return string before timeout.
+ Perform the execution setup that will be done for each execution
+ this node is part of.
"""
+ self._set_up_execution(execution_config)
- return self.main_session.send_command(cmds, timeout)
+ def _set_up_execution(self, execution_config: ExecutionConfiguration) -> None:
+ """
+ This method exists to be optionally overwritten by derived classes and
+ is not decorated so that the derived class doesn't have to use the decorator.
+ """
- def create_session(self, name: str) -> RemoteSession:
- connection = create_remote_session(
- self._config,
+ def tear_down_execution(self) -> None:
+ """
+ Perform the execution teardown that will be done after each execution
+ this node is part of concludes.
+ """
+ self._tear_down_execution()
+
+ def _tear_down_execution(self) -> None:
+ """
+ This method exists to be optionally overwritten by derived classes and
+ is not decorated so that the derived class doesn't have to use the decorator.
+ """
+
+ def set_up_build_target(
+ self, build_target_config: BuildTargetConfiguration
+ ) -> None:
+ """
+ Perform the build target setup that will be done for each build target
+ tested on this node.
+ """
+ self._set_up_build_target(build_target_config)
+
+ def _set_up_build_target(
+ self, build_target_config: BuildTargetConfiguration
+ ) -> None:
+ """
+ This method exists to be optionally overwritten by derived classes and
+ is not decorated so that the derived class doesn't have to use the decorator.
+ """
+
+ def tear_down_build_target(self) -> None:
+ """
+ Perform the build target teardown that will be done after each build target
+ tested on this node.
+ """
+ self._tear_down_build_target()
+
+ def _tear_down_build_target(self) -> None:
+ """
+ This method exists to be optionally overwritten by derived classes and
+ is not decorated so that the derived class doesn't have to use the decorator.
+ """
+
+ def create_session(self, name: str) -> OSSession:
+ """
+ Create and return a new OSSession tailored to the remote OS.
+ """
+ connection = create_session(
+ self.config,
name,
getLogger(name, node=self.name),
)
self._other_sessions.append(connection)
return connection
- def node_exit(self) -> None:
+ def close(self) -> None:
"""
- Recover all resource before node exit
+ Close all connections and free other resources.
"""
if self.main_session:
self.main_session.close()
for session in self._other_sessions:
session.close()
- self.logger.logger_exit()
+ self._logger.logger_exit()
new file mode 100644
@@ -0,0 +1,13 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2010-2014 Intel Corporation
+# Copyright(c) 2023 PANTHEON.tech s.r.o.
+
+from .node import Node
+
+
+class SutNode(Node):
+ """
+ A class for managing connections to the System under Test, providing
+ methods that retrieve the necessary information about the node (such as
+ CPU, memory and NIC details) and configuration capabilities.
+ """