<snip> 
> +# 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"),
> +        )
> +
Out of curiosity, what is the reason for having a static "from_dict" method
rather than just a regular constructor function that takes a dict as
parameter?

@dataclass(...) is a class annotation that transforms the thing it annotates into a dataclass. This means it creates the constructor for you based on the property type annotations. If you create your own constructor, you need a constructor that can either take a single dictionary or all of the parameters like a normal constructor. Making it a static method also means that each class can manage how it should be constructed from a dictionary. Some of the other classes will transform lists or perform other assertions. It also makes it easier to have specialized types. For instance, a NICConfiguration class would have to handle all of the possible device arguments that could be passed to any PMD driver if things were passed as parameters. 
 
> +
> +@dataclass(slots=True, frozen=True)
> +class ExecutionConfiguration:
> +    system_under_test: NodeConfiguration
> +
Minor comment: seems strange having only a single member variable in this
class, effectively duplicating the class above.

More is intended to go here. For instance, what tests to run, configuration for virtual machines, the traffic generator node.
 
<snip>
> +    @staticmethod
> +    def from_dict(d: dict, node_map: dict) -> "ExecutionConfiguration":
from reading the code it appears that node_map is a dict of
NodeConfiguration objects, right? Might be worth adding that to the
definition for clarity, and also the specific type of the dict "d" (if it
has one)
> +        sut_name = d["system_under_test"]
> +        assert sut_name in node_map, f"Unknown SUT {sut_name} in execution {d}"
> +
> +        return ExecutionConfiguration(
> +            system_under_test=node_map[sut_name],
> +        )
> +
> +
> +@dataclass(slots=True, frozen=True)
> +class Configuration:
> +    executions: list[ExecutionConfiguration]
> +
> +    @staticmethod
> +    def from_dict(d: dict) -> "Configuration":
> +        nodes: list[NodeConfiguration] = list(
> +            map(NodeConfiguration.from_dict, d["nodes"])
So "d" is a dict of dicts?

d is a dictionary which matches the json schema for the class. In the case of the Configuration class, it is a dictionary matching the entire json schema.
 
> +        )
> +        assert len(nodes) > 0, "There must be a node to test"
> +
> +        node_map = {node.name: node for node in nodes}
> +        assert len(nodes) == len(node_map), "Duplicate node names are not allowed"
> +
> +        executions: list[ExecutionConfiguration] = list(
> +            map(
> +                ExecutionConfiguration.from_dict, d["executions"], [node_map for _ in d]
> +            )
> +        )
> +
> +        return Configuration(executions=executions)
> +
> +
> +def load_config() -> Configuration:
> +    """
> +    Loads the configuration file and the configuration file schema,
> +    validates the configuration file, and creates a configuration object.
> +    """
> +    with open(SETTINGS.config_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
> +
> +
> +CONFIGURATION = load_config()
<snip>