new file mode 100644
@@ -0,0 +1,7 @@
+executions:
+ - system_under_test: "SUT 1"
+nodes:
+ - name: "SUT 1"
+ hostname: "SUT IP Address or hostname"
+ user: root
+ password: "leave blank to use SSH keys"
new file mode 100644
@@ -0,0 +1,116 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2010-2021 Intel Corporation
+# Copyright(c) 2022 University of New Hampshire
+#
+
+"""
+Generic port and topology nodes configuration file load function
+"""
+import json
+import os.path
+import pathlib
+from dataclasses import dataclass
+from enum import Enum, auto, unique
+from typing import Any, Optional
+
+import warlock
+import yaml
+
+from framework.settings import get_config_file_path
+
+
+class StrEnum(Enum):
+ @staticmethod
+ def _generate_next_value_(
+ name: str, start: int, count: int, last_values: object
+ ) -> str:
+ return name
+
+
+@unique
+class NodeType(StrEnum):
+ physical = auto()
+ virtual = auto()
+
+
+# Slots enables some optimizations, by pre-allocating space for the defined
+# attributes in the underlying data structure.
+#
+# Frozen makes the object immutable. This enables further optimizations,
+# and makes it thread safe should we every want to move in that direction.
+@dataclass(slots=True, frozen=True)
+class NodeConfiguration:
+ name: str
+ hostname: str
+ user: str
+ password: Optional[str]
+
+ @staticmethod
+ def from_dict(d: dict) -> "NodeConfiguration":
+ return NodeConfiguration(
+ name=d["name"],
+ hostname=d["hostname"],
+ user=d["user"],
+ password=d.get("password"),
+ )
+
+
+@dataclass(slots=True, frozen=True)
+class ExecutionConfiguration:
+ system_under_test: str
+
+ @staticmethod
+ def from_dict(d: dict) -> "ExecutionConfiguration":
+ return ExecutionConfiguration(
+ system_under_test=d["system_under_test"],
+ )
+
+
+@dataclass(slots=True, frozen=True)
+class Configuration:
+ executions: list[ExecutionConfiguration]
+ nodes: list[NodeConfiguration]
+
+ @staticmethod
+ def from_dict(d: dict) -> "Configuration":
+ executions: list[ExecutionConfiguration] = list(
+ map(ExecutionConfiguration.from_dict, d["executions"])
+ )
+ nodes: list[NodeConfiguration] = list(
+ map(NodeConfiguration.from_dict, d["nodes"])
+ )
+ assert len(nodes) > 0, "There must be a node to test"
+
+ for i, n1 in enumerate(nodes):
+ for j, n2 in enumerate(nodes):
+ if i != j:
+ assert n1.name == n2.name, "Duplicate node names are not allowed"
+
+ node_names = {node.name for node in nodes}
+ for execution in executions:
+ assert (
+ execution.system_under_test in node_names
+ ), f"Unknown SUT {execution.system_under_test} in execution"
+
+ return Configuration(executions=executions, nodes=nodes)
+
+
+def load_config(conf_file_path: str) -> Configuration:
+ """
+ Loads the configuration file and the configuration file schema,
+ validates the configuration file, and creates a configuration object.
+ """
+ conf_file_path: str = get_config_file_path(conf_file_path)
+ with open(conf_file_path, "r") as f:
+ config_data = yaml.safe_load(f)
+ schema_path = os.path.join(
+ pathlib.Path(__file__).parent.resolve(), "conf_yaml_schema.json"
+ )
+
+ with open(schema_path, "r") as f:
+ schema = json.load(f)
+ config: dict[str, Any] = warlock.model_factory(schema, name="_Config")(
+ config_data
+ )
+ config_obj: Configuration = Configuration.from_dict(dict(config))
+ return config_obj
new file mode 100644
@@ -0,0 +1,73 @@
+{
+ "$schema": "https://json-schema.org/draft-07/schema",
+ "title": "DPDK DTS Config Schema",
+ "definitions": {
+ "node_name": {
+ "type": "string",
+ "description": "A unique identifier for a node"
+ },
+ "node_role": {
+ "type": "string",
+ "description": "The role a node plays in DTS",
+ "enum": [
+ "system_under_test",
+ "traffic_generator"
+ ]
+ }
+ },
+ "type": "object",
+ "properties": {
+ "nodes": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "A unique identifier for this node"
+ },
+ "hostname": {
+ "type": "string",
+ "description": "A hostname from which the node running DTS can access this node. This can also be an IP address."
+ },
+ "user": {
+ "type": "string",
+ "description": "The user to access this node with."
+ },
+ "password": {
+ "type": "string",
+ "description": "The password to use on this node. SSH keys are preferred."
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "name",
+ "hostname",
+ "user"
+ ]
+ },
+ "minimum": 1
+ },
+ "executions": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "system_under_test": {
+ "$ref": "#/definitions/node_name"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "system_under_test"
+ ]
+ },
+ "minimum": 1
+ }
+ },
+ "required": [
+ "executions",
+ "nodes"
+ ],
+ "additionalProperties": false
+}
\ No newline at end of file
@@ -58,3 +58,18 @@ def __init__(self, host: str):
def __str__(self) -> str:
return f"SSH session with {self.host} has died"
+
+
+class ConfigParseException(Exception):
+
+ """
+ Configuration file parse failure exception.
+ """
+
+ config: str
+
+ def __init__(self, conf_file: str):
+ self.config = conf_file
+
+ def __str__(self) -> str:
+ return f"Failed to parse config file [{self.config}]"
new file mode 100644
@@ -0,0 +1,40 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2010-2021 Intel Corporation
+# Copyright(c) 2022 PANTHEON.tech s.r.o.
+# Copyright(c) 2022 University of New Hampshire
+#
+
+import os
+import re
+
+DEFAULT_CONFIG_FILE_PATH: str = "./conf.yaml"
+
+# DTS global environment variables
+DTS_ENV_PAT: str = r"DTS_*"
+DTS_CFG_FILE: str = "DTS_CFG_FILE"
+
+
+def load_global_setting(key: str) -> str:
+ """
+ Load DTS global setting
+ """
+ if re.match(DTS_ENV_PAT, key):
+ env_key = key
+ else:
+ env_key = "DTS_" + key
+
+ return os.environ.get(env_key, "")
+
+
+def get_config_file_path(conf_file_path: str) -> str:
+ """
+ The root path of framework configs.
+ """
+ if conf_file_path == DEFAULT_CONFIG_FILE_PATH:
+ # if the user didn't specify a path on cmdline, they could've specified
+ # it in the env variable
+ conf_file_path = load_global_setting(DTS_CFG_FILE)
+ if conf_file_path == "":
+ conf_file_path = DEFAULT_CONFIG_FILE_PATH
+
+ return conf_file_path