diff --git a/dts/conf.yaml b/dts/conf.yaml
new file mode 100644
index 0000000000..11b5f53a66
--- /dev/null
+++ b/dts/conf.yaml
@@ -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"
diff --git a/dts/framework/config/__init__.py b/dts/framework/config/__init__.py
new file mode 100644
index 0000000000..511e70c9a5
--- /dev/null
+++ b/dts/framework/config/__init__.py
@@ -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
diff --git a/dts/framework/config/conf_yaml_schema.json b/dts/framework/config/conf_yaml_schema.json
new file mode 100644
index 0000000000..8dea50e285
--- /dev/null
+++ b/dts/framework/config/conf_yaml_schema.json
@@ -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
diff --git a/dts/framework/exception.py b/dts/framework/exception.py
index 54f9f189a4..252fa47fc4 100644
--- a/dts/framework/exception.py
+++ b/dts/framework/exception.py
@@ -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}]"
diff --git a/dts/framework/settings.py b/dts/framework/settings.py
new file mode 100644
index 0000000000..ca3cbd71b1
--- /dev/null
+++ b/dts/framework/settings.py
@@ -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
