Repository: NucciTheBoss/cleantest (language: python)

Issue:
Build out initial infrastructure for creating mini-Beowulf clusters
It's time to take the program behind [hpc-workshop](https://github.com/NucciTheBoss/hpc-workshop) and combine it with cleantest. Rather than having decorators do all of the provisioning for test environment instances, I also want to add a `Boostrap` class. It will work something like this:

```python3
x = Bootstrap("lxd")
x.add_node("slurmd-0", image="ubuntu-jammy-amd64", config=[], provision_script=Union[str, os.PathLike])
x.add_node("slurmctld-0", image="ubuntu-jammy-amd64", config=[], provision_script=Union[str, os.PathLike])

...

@lxd.target("slurmctld-0")
def run_example_job() -> None:
    from cleantest.utils import run
    run("srun muh job")

...   # Do some cool tests

x.destroy()
```

Right now I want to hold off implementing hooks for the "manual" bootstrap and just focus on getting a mini-Beowulf cluster to stabilize for tests. In the future, I could add the option to attach resources or "namespace" hooks so that they only run on the intended target machine, but that is something to save for 0.5.0 or later right now.


Discussion / hints:

Fix patch:
diff --git a/tox.ini b/tox.ini
index e4ddae8..2a7af19 100644
--- a/tox.ini
+++ b/tox.ini
@@ -7,9 +7,9 @@ skip_missing_interpreters = True
 envlist = fmt, lint
 
 [vars]
-src_path = {toxinidir}/src/
-tst_path = {toxinidir}/tests/
-docs_path = {toxinidir}/docs/
+src_path = {toxinidir}/src
+tst_path = {toxinidir}/tests
+docs_path = {toxinidir}/docs
 all_path = {[vars]src_path} {[vars]tst_path} {[vars]docs_path} {toxinidir}/.github {toxinidir}/CODE_OF_CONDUCT.md {toxinidir}/README.md {toxinidir}/CONTRIBUTING.md
 all_code_path = {[vars]src_path} {[vars]tst_path}
 
@@ -46,6 +46,7 @@ commands =
 description = Run cleantest functional tests/
 deps =
     -r {toxinidir}/requirements.txt
+    jinja2
     pytest
     pytest-cov
 commands =
@@ -93,7 +94,8 @@ deps =
     -r {toxinidir}/requirements.txt
     pytest
 commands =
-    pytest -v --tb native --log-cli-level=ERROR {[vars]tst_path}/lxd/local
+    pytest -v --tb native --log-cli-level=INFO \
+        {[vars]tst_path}/functional/lxd/local
 
 [testenv:lxd-local-parallel]
 description = Run functional tests in parallel for a locally-hosted LXD cluster.
@@ -101,16 +103,26 @@ deps =
     -r {toxinidir}/requirements.txt
     pytest
 commands =
-    pytest -v --tb native --log-cli-level=ERROR \
+    pytest -v --tb native --log-cli-level=INFO \
         {[vars]tst_path}/functional/lxd/parallel
 
+[testenv:lxd-archon]
+description = Run functional tests for LXD archon
+deps =
+    -r {toxinidir}/requirements.txt
+    jinja2
+    pytest
+commands =
+    pytest -v --tb native --log-cli-level=INFO \
+        {[vars]tst_path}/functional/lxd/archon
+
 [testenv:snaps]
 description = Run functional tests to test if cleantest supports snap packages
 deps =
     -r {toxinidir}/requirements.txt
     pytest
 commands =
-    pytest -v --tb native --log-cli-level=ERROR \
+    pytest -v --tb native --log-cli-level=INFO \
         {[vars]tst_path}/functional/packages/snap
 
 [testenv:files]
@@ -119,5 +131,5 @@ deps =
     -r {toxinidir}/requirements.txt
     pytest
 commands =
-    pytest -v --tb native --log-cli-level=ERROR \
+    pytest -v --tb native --log-cli-level=INFO \
         {[vars]tst_path}/functional/files


Test patch:
diff --git a/src/cleantest/_meta/__init__.py b/src/cleantest/_meta/__init__.py
deleted file mode 100644
index 54a9b2b..0000000
--- a/src/cleantest/_meta/__init__.py
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/usr/bin/env python3
-# Copyright 2023 Jason C. Nucciarone
-# See LICENSE file for licensing details.
-
-"""Private metaclasses used inside cleantest.
-
-Do not use these modules outside cleantest.
-"""
-
-from .base_configurer import BaseConfigurer
-from .sources import LXDDefaultSources
diff --git a/src/cleantest/_meta/hook_registry.py b/src/cleantest/_meta/hook_registry.py
deleted file mode 100644
index 3c4394d..0000000
--- a/src/cleantest/_meta/hook_registry.py
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env python3
-# Copyright 2023 Jason C. Nucciarone
-# See LICENSE file for licensing details.
-
-"""Utilities for working with hooks in cleantest."""
diff --git a/src/cleantest/_meta/mixins/__init__.py b/src/cleantest/_meta/mixins/__init__.py
deleted file mode 100644
index 49474e7..0000000
--- a/src/cleantest/_meta/mixins/__init__.py
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/usr/bin/env python3
-# Copyright 2023 Jason C. Nucciarone
-# See LICENSE file for licensing details.
-
-"""Private mixins used inside cleantest.
-
-Do not use these modules outside cleantest.
-"""
-
-from .resettable import Resettable
diff --git a/src/cleantest/_meta/sources.py b/src/cleantest/_meta/sources.py
deleted file mode 100644
index 09bccfa..0000000
--- a/src/cleantest/_meta/sources.py
+++ /dev/null
@@ -1,279 +0,0 @@
-#!/usr/bin/env python3
-# Copyright 2023 Jason C. Nucciarone
-# See LICENSE file for licensing details.
-
-"""Default sources for test environment providers."""
-
-from enum import Enum
-from typing import Any, List, Tuple
-
-from cleantest.control.lxd import InstanceSource
-
-
-class EnhancedEnum(Enum):
-    """Enums with extra methods for convenience."""
-
-    @classmethod
-    def items(cls) -> List[Tuple[str, Any]]:
-        """Returns items of an Enum."""
-        return [(c.name, c.value) for c in cls]
-
-
-class LXDDefaultSources(EnhancedEnum):
-    """Default sources for LXD test environment provider."""
-
-    ALMALINUX_8_AMD64 = InstanceSource(
-        alias="almalinux/8/amd64",
-        mode="pull",
-        protocol="simplestreams",
-        server="https://images.linuxcontainers.org",
-        type="image",
-    )
-
-    ALMALINUX_8_ARM64 = InstanceSource(
-        alias="almalinux/8/arm64",
-        mode="pull",
-        protocol="simplestreams",
-        server="https://images.linuxcontainers.org",
-        type="image",
-    )
-
-    ALMALINUX_9_AMD64 = InstanceSource(
-        alias="almalinux/9/amd64",
-        mode="pull",
-        protocol="simplestreams",
-        server="https://images.linuxcontainers.org",
-        type="image",
-    )
-
-    ALMALINUX_9_ARM64 = InstanceSource(
-        alias="almalinux/9/arm64",
-        mode="pull",
-        protocol="simplestreams",
-        server="https://images.linuxcontainers.org",
-        type="image",
-    )
-
-    ARCHLINUX_AMD64 = InstanceSource(
-        alias="archlinux/amd64",
-        mode="pull",
-        protocol="simplestreams",
-        server="https://images.linuxcontainers.org",
-        type="image",
-    )
-
-    ARCHLINUX_ARM64 = InstanceSource(
-        alias="archlinux/arm64",
-        mode="pull",
-        protocol="simplestreams",
-        server="https://images.linuxcontainers.org",
-        type="image",
-    )
-
-    CENTOS_8_STREAM_AMD64 = InstanceSource(
-        alias="centos/8-Stream/amd64",
-        mode="pull",
-        protocol="simplestreams",
-        server="https://images.linuxcontainers.org",
-        type="image",
-    )
-
-    CENTOS_8_STREAM_ARM64 = InstanceSource(
-        alias="centos/8-Stream/arm64",
-        mode="pull",
-        protocol="simplestreams",
-        server="https://images.linuxcontainers.org",
-        type="image",
-    )
-
-    CENTOS_9_STREAM_AMD64 = InstanceSource(
-        alias="centos/9-Stream/amd64",
-        mode="pull",
-        protocol="simplestreams",
-        server="https://images.linuxcontainers.org",
-        type="image",
-    )
-
-    CENTOS_9_STREAM_ARM64 = InstanceSource(
-        alias="centos/9-Stream/arm64",
-        mode="pull",
-        protocol="simplestreams",
-        server="https://images.linuxcontainers.org",
-        type="image",
-    )
-
-    DEBIAN_10_AMD64 = InstanceSource(
-        alias="debian/10/amd64",
-        mode="pull",
-        protocol="simplestreams",
-        server="https://images.linuxcontainers.org",
-        type="image",
-    )
-
-    DEBIAN_10_ARM64 = InstanceSource(
-        alias="debian/10/arm64",
-        mode="pull",
-        protocol="simplestreams",
-        server="https://images.linuxcontainers.org",
-        type="image",
-    )
-
-    DEBIAN_11_AMD64 = InstanceSource(
-        alias="debian/11/amd64",
-        mode="pull",
-        protocol="simplestreams",
-        server="https://images.linuxcontainers.org",
-        type="image",
-    )
-
-    DEBIAN_11_ARM64 = InstanceSource(
-        alias="debian/11/arm64",
-        mode="pull",
-        protocol="simplestreams",
-        server="https://images.linuxcontainers.org",
-        type="image",
-    )
-
-    DEBIAN_12_AMD64 = InstanceSource(
-        alias="debian/12/amd64",
-        mode="pull",
-        protocol="simplestreams",
-        server="https://images.linuxcontainers.org",
-        type="image",
-    )
-
-    DEBIAN_12_ARM64 = InstanceSource(
-        alias="debian/12/arm64",
-        mode="pull",
-        protocol="simplestreams",
-        server="https://images.linuxcontainers.org",
-        type="image",
-    )
-
-    FEDORA_35_AMD64 = InstanceSource(
-        alias="fedora/35/amd64",
-        mode="pull",
-        protocol="simplestreams",
-        server="https://images.linuxcontainers.org",
-        type="image",
-    )
-
-    FEDORA_35_ARM64 = InstanceSource(
-        alias="fedora/35/arm64",
-        mode="pull",
-        protocol="simplestreams",
-        server="https://images.linuxcontainers.org",
-        type="image",
-    )
-
-    FEDORA_36_AMD64 = InstanceSource(
-        alias="fedora/36/amd64",
-        mode="pull",
-        protocol="simplestreams",
-        server="https://images.linuxcontainers.org",
-        type="image",
-    )
-
-    FEDORA_36_ARM64 = InstanceSource(
-        alias="fedora/36/arm64",
-        mode="pull",
-        protocol="simplestreams",
-        server="https://images.linuxcontainers.org",
-        type="image",
-    )
-
-    FEDORA_37_AMD64 = InstanceSource(
-        alias="fedora/37/amd64",
-        mode="pull",
-        protocol="simplestreams",
-        server="https://images.linuxcontainers.org",
-        type="image",
-    )
-
-    FEDORA_37_ARM64 = InstanceSource(
-        alias="fedora/37/arm64",
-        mode="pull",
-        protocol="simplestreams",
-        server="https://images.linuxcontainers.org",
-        type="image",
-    )
-
-    ROCKYLINUX_8_AMD64 = InstanceSource(
-        alias="rockylinux/8/amd64",
-        mode="pull",
-        protocol="simplestreams",
-        server="https://images.linuxcontainers.org",
-        type="image",
-    )
-
-    ROCKYLINUX_8_ARM64 = InstanceSource(
-        alias="rockylinux/8/arm64",
-        mode="pull",
-        protocol="simplestreams",
-        server="https://images.linuxcontainers.org",
-        type="image",
-    )
-
-    ROCKYLINUX_9_AMD64 = InstanceSource(
-        alias="rockylinux/9/amd64",
-        mode="pull",
-        protocol="simplestreams",
-        server="https://images.linuxcontainers.org",
-        type="image",
-    )
-
-    ROCKYLINUX_9_ARM64 = InstanceSource(
-        alias="rockylinux/9/arm64",
-        mode="pull",
-        protocol="simplestreams",
-        server="https://images.linuxcontainers.org",
-        type="image",
-    )
-
-    UBUNTU_JAMMY_AMD64 = InstanceSource(
-        alias="ubuntu/jammy/amd64",
-        mode="pull",
-        protocol="simplestreams",
-        server="https://images.linuxcontainers.org",
-        type="image",
-    )
-
-    UBUNTU_JAMMY_ARM64 = InstanceSource(
-        alias="ubuntu/jammy/arm64",
-        mode="pull",
-        protocol="simplestreams",
-        server="https://images.linuxcontainers.org",
-        type="image",
-    )
-
-    UBUNTU_FOCAL_AMD64 = InstanceSource(
-        alias="ubuntu/focal/amd64",
-        mode="pull",
-        protocol="simplestreams",
-        server="https://images.linuxcontainers.org",
-        type="image",
-    )
-
-    UBUNTU_FOCAL_ARM64 = InstanceSource(
-        alias="ubuntu/focal/arm64",
-        mode="pull",
-        protocol="simplestreams",
-        server="https://images.linuxcontainers.org",
-        type="image",
-    )
-
-    UBUNTU_BIONIC_AMD64 = InstanceSource(
-        alias="ubuntu/18.04/amd64",
-        mode="pull",
-        protocol="simplestreams",
-        server="https://images.linuxcontainers.org",
-        type="image",
-    )
-
-    UBUNTU_BIONIC_ARM64 = InstanceSource(
-        alias="ubuntu/18.04/arm64",
-        mode="pull",
-        protocol="simplestreams",
-        server="https://images.linuxcontainers.org",
-        type="image",
-    )
diff --git a/src/cleantest/control/_configurers/__init__.py b/src/cleantest/control/_configurers/__init__.py
deleted file mode 100644
index 58ef5ff..0000000
--- a/src/cleantest/control/_configurers/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env python3
-# Copyright 2023 Jason C. Nucciarone, Canonical Ltd.
-# See LICENSE file for licensing details.
-
-from .lxd_configurer import LXDConfigurer
diff --git a/src/cleantest/control/_configurers/lxd_configurer.py b/src/cleantest/control/_configurers/lxd_configurer.py
deleted file mode 100644
index 610670b..0000000
--- a/src/cleantest/control/_configurers/lxd_configurer.py
+++ /dev/null
@@ -1,92 +0,0 @@
-#!/usr/bin/env python3
-# Copyright 2023 Jason C. Nucciarone
-# See LICENSE file for licensing details.
-
-"""Information needed by LXD test environment provider."""
-
-import copy
-
-from cleantest._meta import BaseConfigurer, LXDDefaultSources
-from cleantest.control.lxd import InstanceConfig
-
-
-class DuplicateLXDInstanceConfigError(Exception):
-    """Raised if two or more instance configurations share the same name."""
-
-
-class LXDInstanceConfigNotFoundError(Exception):
-    """Raised when LXD instance configuration is not found in the registry."""
-
-
-class LXDConfigurer(BaseConfigurer):
-    """Configurer for LXD test environment provider."""
-
-    _configs = set()
-
-    def __new__(cls) -> "LXDConfigurer":
-        if not hasattr(cls, "_instance"):
-            cls._instance = super(LXDConfigurer, cls).__new__(cls)
-            cls.reset()
-        return cls._instance
-
-    @classmethod
-    def reset(cls) -> None:
-        """Reset LXD test environment provider to default configuration."""
-        cls._configs = set()
-        [
-            cls._configs.add(
-                InstanceConfig(name=name.replace("_", "-").lower(), source=source)
-            )
-            for name, source in LXDDefaultSources.items()
-        ]
-        super().reset()
-
-    def add_instance_config(self, *new_config: InstanceConfig) -> None:
-        """Add a new LXD instance configuration to the registry.
-
-        Args:
-            new_config (Dict[str, Any]):
-                New configurations to add to the instance configuration registry.
-
-        Raises:
-            DuplicateLXDInstanceConfigError:
-                Raised if two or more configs have the same name.
-        """
-        for config in new_config:
-            if config.name not in [c.name for c in self._configs]:
-                self._configs.add(config)
-            raise DuplicateLXDInstanceConfigError(
-                f"Instance configuration with name {config.name} already exists."
-            )
-
-    def remove_instance_config(self, *name: str) -> None:
-        """Remove an instance configuration by name.
-
-        Do nothing if the instance configuration does not exist in the registry.
-
-        Args:
-            name (str): Names of the instance configurations to delete.
-        """
-        for config_name in name:
-            for config in self._configs:
-                if config_name == config.name:
-                    self._configs.remove(config)
-
-    def get_instance_config(self, name: str) -> InstanceConfig:
-        """Return an LXD instance configuration.
-
-        Args:
-            name (str): Name of instance configuration to retrieve.
-
-        Raises:
-            LXDInstanceConfigNotFoundError:
-                Raised if configuration does not exist in registry.
-
-        Returns:
-            (InstanceConfig): Retrieved LXD image configuration.
-        """
-        for config in self._configs:
-            if config.name == name:
-                return copy.deepcopy(config)
-
-        raise LXDInstanceConfigNotFoundError(f"Could not find instance {name}.")
diff --git a/src/cleantest/control/configure.py b/src/cleantest/control/configure.py
index 92642ad..9223df4 100644
--- a/src/cleantest/control/configure.py
+++ b/src/cleantest/control/configure.py
@@ -4,7 +4,7 @@
 
 """Configure the flow of cleantest based on the test environment provider used."""
 
-from ._configurers import LXDConfigurer
+from .lxd._lxd_configurer import LXDConfigurer
 
 
 class UnknownConfigurerError(Exception):
diff --git a/src/cleantest/control/env.py b/src/cleantest/control/env.py
index 1e18e92..66b9003 100644
--- a/src/cleantest/control/env.py
+++ b/src/cleantest/control/env.py
@@ -7,7 +7,7 @@
 import os
 from typing import Any, Dict, Optional
 
-from cleantest._meta.mixins import Resettable
+from cleantest.meta.mixins import Resettable
 
 
 class Env(Resettable):
@@ -25,10 +25,9 @@ def __new__(cls) -> "Env":
             cls._instance = super(Env, cls).__new__(cls)
         return cls._instance
 
-    @classmethod
-    def reset(cls) -> None:
+    def reset(self) -> None:
         """Reset environment information."""
-        cls._env = {}
+        self._env = {}
 
     def add(self, env_mapping: Dict[str, Any]) -> None:
         """Add new values to environment.
diff --git a/src/cleantest/control/lxd/__init__.py b/src/cleantest/control/lxd/__init__.py
index 379b879..d48a974 100644
--- a/src/cleantest/control/lxd/__init__.py
+++ b/src/cleantest/control/lxd/__init__.py
@@ -1,5 +1,7 @@
 #!/usr/bin/env python3
-# Copyright 2023 Jason C. Nucciarone, Canonical Ltd.
+# Copyright 2023 Jason C. Nucciarone
 # See LICENSE file for licensing details.
 
+"""Classes for configuring the LXD test environment provider."""
+
 from .lxd_config import ClientConfig, InstanceConfig, InstanceSource
diff --git a/src/cleantest/control/lxd/_lxd_configurer.py b/src/cleantest/control/lxd/_lxd_configurer.py
new file mode 100644
index 0000000..9696190
--- /dev/null
+++ b/src/cleantest/control/lxd/_lxd_configurer.py
@@ -0,0 +1,133 @@
+#!/usr/bin/env python3
+# Copyright 2023 Jason C. Nucciarone
+# See LICENSE file for licensing details.
+
+"""Configure and control the LXD test environment provider."""
+
+import copy
+from typing import Any, Dict
+
+from cleantest.meta._base_configurer import BaseConfigurer, BaseConfigurerError
+
+from .lxd_config import ClientConfig, InstanceConfig, _DefaultSources
+
+
+class BadClientConfigurationError(BaseConfigurerError):
+    """Raised if given client configuration is bad."""
+
+
+class DuplicateLXDInstanceConfigError(BaseConfigurerError):
+    """Raised if two or more instance configurations share the same name."""
+
+
+class LXDInstanceConfigNotFoundError(BaseConfigurerError):
+    """Raised when LXD instance configuration is not found in the registry."""
+
+
+class LXDConfigurer(BaseConfigurer):
+    """Configurer for LXD test environment provider."""
+
+    __configs = {
+        InstanceConfig(name=name.replace("_", "-").lower(), source=source)
+        for name, source in _DefaultSources.items()
+    }
+    __client_config = ClientConfig()
+
+    def __new__(cls) -> "LXDConfigurer":
+        """Create new LXDConfigurer instance.
+
+        Returns:
+            (LXDConfigurer): New LXDConfigurer instance.
+        """
+        if not hasattr(cls, f"_{cls.__name__}__instance"):
+            cls.__instance = super(LXDConfigurer, cls).__new__(cls)
+        return cls.__instance
+
+    @property
+    def client_config(self) -> ClientConfig:
+        """Get LXD client configuration information.
+
+        Returns:
+            (ClientConfig): Current LXD client configuration.
+        """
+        return self.__client_config
+
+    @client_config.setter
+    def client_config(self, new_conf: Dict[str, Any]) -> None:
+        """Set LXD client configuration.
+
+        Raises:
+            BadClientConfigurationError:
+                Raised if new configuration contains invalid configuration option.
+        """
+        valid_config_options = self.__client_config.keys()
+        for k in new_conf.keys():
+            if k not in valid_config_options:
+                raise BadClientConfigurationError(
+                    (
+                        f"Configuration {k} is not a valid client "
+                        f"configuration option. Valid client configuration options are "
+                        f"{', '.join(k for k in valid_config_options)}."
+                    )
+                )
+        self.__client_config = ClientConfig(**new_conf)
+
+    def reset(self) -> None:
+        """Reset LXD test environment provider to default configuration."""
+        self.__configs = {
+            InstanceConfig(name=name.replace("_", "-").lower(), source=source)
+            for name, source in _DefaultSources.items()
+        }
+        self.__client_config = ClientConfig()
+        super().reset()
+
+    def add_instance_config(self, *new_config: InstanceConfig) -> None:
+        """Add a new LXD instance configuration to the registry.
+
+        Args:
+            new_config (Dict[str, Any]):
+                New configurations to add to the instance configuration registry.
+
+        Raises:
+            DuplicateLXDInstanceConfigError:
+                Raised if two or more configs have the same name.
+        """
+        for config in new_config:
+            if config.name not in [c.name for c in self.__configs]:
+                self.__configs.add(config)
+            else:
+                raise DuplicateLXDInstanceConfigError(
+                    f"Instance configuration with name {config.name} already exists."
+                )
+
+    def remove_instance_config(self, *name: str) -> None:
+        """Remove an instance configuration by name.
+
+        Do nothing if the instance configuration does not exist in the registry.
+
+        Args:
+            name (str): Names of the instance configurations to delete.
+        """
+        for config_name in name:
+            for config in self.__configs:
+                if config_name == config.name:
+                    self.__configs.remove(config)
+
+    def get_instance_config(self, name: str) -> InstanceConfig:
+        """Return an LXD instance configuration.
+
+        Args:
+            name (str): Name of instance configuration to retrieve.
+
+        Raises:
+            LXDInstanceConfigNotFoundError:
+                Raised if configuration does not exist in registry.
+
+        Returns:
+            (InstanceConfig): Retrieved LXD image configuration.
+        """
+        for config in self.__configs:
+            if config.name == name:
+                return copy.deepcopy(config)
+
+        raise LXDInstanceConfigNotFoundError(f"Could not find instance {name}.")
diff --git a/src/cleantest/control/lxd/lxd_config.py b/src/cleantest/control/lxd/lxd_config.py
index bd63abe..078cf54 100644
--- a/src/cleantest/control/lxd/lxd_config.py
+++ b/src/cleantest/control/lxd/lxd_config.py
@@ -1,19 +1,19 @@
 #!/usr/bin/env python3
-# Copyright 2023 Jason C. Nucciarone, Canonical Ltd.
+# Copyright 2023 Jason C. Nucciarone
 # See LICENSE file for licensing details.
 
-"""Classes to assist with LXD instance configuration."""
+"""Dataclasses to assist with LXD provider and instance configuration."""
 
-from typing import Any, Dict, List, Optional, Tuple, Union
+from typing import Dict, List, Optional, Tuple, Union
 
-from cleantest.meta.mixins import DictOps
+from cleantest.meta.mixins import DictLike, EnhancedEnum
 
 
 class BadLXDConfigError(Exception):
     """Raised when the newly entered configuration fails the lint check."""
 
 
-class ClientConfig:
+class ClientConfig(DictLike):
     """Define an LXD client connection.
 
     Args:
@@ -48,6 +48,11 @@ def __init__(
         self.timeout = timeout
         self.project = project
 
+    def __repr__(self) -> str:
+        """String representation of ClientConfig."""
+        attrs = ", ".join(f"{k}={v}" for k, v in self.__dict__.items())
+        return f"{self.__class__.__name__}({attrs})"
+
 
 class InstanceSource:
     """Define an LXD instance source to use for a test environment instance.
@@ -137,8 +142,13 @@ def __init__(
         if source is not None:
             setattr(self, "source", source)
 
+    def __repr__(self) -> str:
+        """String representation of InstanceSource."""
+        attrs = ", ".join(f"{k}={v}" for k, v in self.__dict__.items())
+        return f"{self.__class__.__name__}({attrs})"
+
 
-class InstanceConfig(DictOps):
+class InstanceConfig(DictLike):
     """Define an LXD instance that can be brought up for test environments.
 
     Args:
@@ -199,25 +209,287 @@ def __init__(
         if stateful is not None:
             setattr(self, "stateful", stateful)
 
-        self._lint(self.dict())
+        self.__lint()
 
-    def _lint(self, new_config: Dict[str, Any]) -> None:
+    def __lint(self) -> None:
         """Lint a new LXD instance configuration to ensure that it is valid.
 
-        Args:
-            new_config (Dict[str, Any]): New configuration to lint.
-
         Raises:
-            BadLXDConfigError: Raised if the passed LXD instance configuration is invalid.
+            BadLXDConfigError:
+                Raised if the passed LXD instance configuration is invalid.
         """
         checks = ["name", "server", "alias", "protocol", "type", "mode"]
-        config = self._deconstruct(new_config)
         for i in checks:
-            if i not in config:
+            if i not in self.keys(all_keys=True):
                 raise BadLXDConfigError(
                     (
-                        f"Bad instance configuration: {new_config}. "
+                        f"Bad instance configuration: {self.dict()}. "
                         "Please ensure instance configuration has the "
-                        f"following values set: {checks}."
+                        f"following values set: {', '.join(checks)}."
                     )
                 )
+
+    def __repr__(self) -> str:
+        """String representation of InstanceConfig."""
+        attrs = ", ".join(f"{k}={v}" for k, v in self.__dict__.items())
+        return f"{self.__class__.__name__}({attrs})"
+
+
+class _DefaultSources(EnhancedEnum):
+    """Default sources for LXD test environment provider."""
+
+    ALMALINUX_8_AMD64 = InstanceSource(
+        alias="almalinux/8/amd64",
+        mode="pull",
+        protocol="simplestreams",
+        server="https://images.linuxcontainers.org",
+        type="image",
+    )
+
+    ALMALINUX_8_ARM64 = InstanceSource(
+        alias="almalinux/8/arm64",
+        mode="pull",
+        protocol="simplestreams",
+        server="https://images.linuxcontainers.org",
+        type="image",
+    )
+
+    ALMALINUX_9_AMD64 = InstanceSource(
+        alias="almalinux/9/amd64",
+        mode="pull",
+        protocol="simplestreams",
+        server="https://images.linuxcontainers.org",
+        type="image",
+    )
+
+    ALMALINUX_9_ARM64 = InstanceSource(
+        alias="almalinux/9/arm64",
+        mode="pull",
+        protocol="simplestreams",
+        server="https://images.linuxcontainers.org",
+        type="image",
+    )
+
+    ARCHLINUX_AMD64 = InstanceSource(
+        alias="archlinux/amd64",
+        mode="pull",
+        protocol="simplestreams",
+        server="https://images.linuxcontainers.org",
+        type="image",
+    )
+
+    ARCHLINUX_ARM64 = InstanceSource(
+        alias="archlinux/arm64",
+        mode="pull",
+        protocol="simplestreams",
+        server="https://images.linuxcontainers.org",
+        type="image",
+    )
+
+    CENTOS_8_STREAM_AMD64 = InstanceSource(
+        alias="centos/8-Stream/amd64",
+        mode="pull",
+        protocol="simplestreams",
+        server="https://images.linuxcontainers.org",
+        type="image",
+    )
+
+    CENTOS_8_STREAM_ARM64 = InstanceSource(
+        alias="centos/8-Stream/arm64",
+        mode="pull",
+        protocol="simplestreams",
+        server="https://images.linuxcontainers.org",
+        type="image",
+    )
+
+    CENTOS_9_STREAM_AMD64 = InstanceSource(
+        alias="centos/9-Stream/amd64",
+        mode="pull",
+        protocol="simplestreams",
+        server="https://images.linuxcontainers.org",
+        type="image",
+    )
+
+    CENTOS_9_STREAM_ARM64 = InstanceSource(
+        alias="centos/9-Stream/arm64",
+        mode="pull",
+        protocol="simplestreams",
+        server="https://images.linuxcontainers.org",
+        type="image",
+    )
+
+    DEBIAN_10_AMD64 = InstanceSource(
+        alias="debian/10/amd64",
+        mode="pull",
+        protocol="simplestreams",
+        server="https://images.linuxcontainers.org",
+        type="image",
+    )
+
+    DEBIAN_10_ARM64 = InstanceSource(
+        alias="debian/10/arm64",
+        mode="pull",
+        protocol="simplestreams",
+        server="https://images.linuxcontainers.org",
+        type="image",
+    )
+
+    DEBIAN_11_AMD64 = InstanceSource(
+        alias="debian/11/amd64",
+        mode="pull",
+        protocol="simplestreams",
+        server="https://images.linuxcontainers.org",
+        type="image",
+    )
+
+    DEBIAN_11_ARM64 = InstanceSource(
+        alias="debian/11/arm64",
+        mode="pull",
+        protocol="simplestreams",
+        server="https://images.linuxcontainers.org",
+        type="image",
+    )
+
+    DEBIAN_12_AMD64 = InstanceSource(
+        alias="debian/12/amd64",
+        mode="pull",
+        protocol="simplestreams",
+        server="https://images.linuxcontainers.org",
+        type="image",
+    )
+
+    DEBIAN_12_ARM64 = InstanceSource(
+        alias="debian/12/arm64",
+        mode="pull",
+        protocol="simplestreams",
+        server="https://images.linuxcontainers.org",
+        type="image",
+    )
+
+    FEDORA_35_AMD64 = InstanceSource(
+        alias="fedora/35/amd64",
+        mode="pull",
+        protocol="simplestreams",
+        server="https://images.linuxcontainers.org",
+        type="image",
+    )
+
+    FEDORA_35_ARM64 = InstanceSource(
+        alias="fedora/35/arm64",
+        mode="pull",
+        protocol="simplestreams",
+        server="https://images.linuxcontainers.org",
+        type="image",
+    )
+
+    FEDORA_36_AMD64 = InstanceSource(
+        alias="fedora/36/amd64",
+        mode="pull",
+        protocol="simplestreams",
+        server="https://images.linuxcontainers.org",
+        type="image",
+    )
+
+    FEDORA_36_ARM64 = InstanceSource(
+        alias="fedora/36/arm64",
+        mode="pull",
+        protocol="simplestreams",
+        server="https://images.linuxcontainers.org",
+        type="image",
+    )
+
+    FEDORA_37_AMD64 = InstanceSource(
+        alias="fedora/37/amd64",
+        mode="pull",
+        protocol="simplestreams",
+        server="https://images.linuxcontainers.org",
+        type="image",
+    )
+
+    FEDORA_37_ARM64 = InstanceSource(
+        alias="fedora/37/arm64",
+        mode="pull",
+        protocol="simplestreams",
+        server="https://images.linuxcontainers.org",
+        type="image",
+    )
+
+    ROCKYLINUX_8_AMD64 = InstanceSource(
+        alias="rockylinux/8/amd64",
+        mode="pull",
+        protocol="simplestreams",
+        server="https://images.linuxcontainers.org",
+        type="image",
+    )
+
+    ROCKYLINUX_8_ARM64 = InstanceSource(
+        alias="rockylinux/8/arm64",
+        mode="pull",
+        protocol="simplestreams",
+        server="https://images.linuxcontainers.org",
+        type="image",
+    )
+
+    ROCKYLINUX_9_AMD64 = InstanceSource(
+        alias="rockylinux/9/amd64",
+        mode="pull",
+        protocol="simplestreams",
+        server="https://images.linuxcontainers.org",
+        type="image",
+    )
+
+    ROCKYLINUX_9_ARM64 = InstanceSource(
+        alias="rockylinux/9/arm64",
+        mode="pull",
+        protocol="simplestreams",
+        server="https://images.linuxcontainers.org",
+        type="image",
+    )
+
+    UBUNTU_JAMMY_AMD64 = InstanceSource(
+        alias="ubuntu/jammy/amd64",
+        mode="pull",
+        protocol="simplestreams",
+        server="https://images.linuxcontainers.org",
+        type="image",
+    )
+
+    UBUNTU_JAMMY_ARM64 = InstanceSource(
+        alias="ubuntu/jammy/arm64",
+        mode="pull",
+        protocol="simplestreams",
+        server="https://images.linuxcontainers.org",
+        type="image",
+    )
+
+    UBUNTU_FOCAL_AMD64 = InstanceSource(
+        alias="ubuntu/focal/amd64",
+        mode="pull",
+        protocol="simplestreams",
+        server="https://images.linuxcontainers.org",
+        type="image",
+    )
+
+    UBUNTU_FOCAL_ARM64 = InstanceSource(
+        alias="ubuntu/focal/arm64",
+        mode="pull",
+        protocol="simplestreams",
+        server="https://images.linuxcontainers.org",
+        type="image",
+    )
+
+    UBUNTU_BIONIC_AMD64 = InstanceSource(
+        alias="ubuntu/18.04/amd64",
+        mode="pull",
+        protocol="simplestreams",
+        server="https://images.linuxcontainers.org",
+        type="image",
+    )
+
+    UBUNTU_BIONIC_ARM64 = InstanceSource(
+        alias="ubuntu/18.04/arm64",
+        mode="pull",
+        protocol="simplestreams",
+        server="https://images.linuxcontainers.org",
+        type="image",
+    )
diff --git a/src/cleantest/data/directory.py b/src/cleantest/data/directory.py
index c8fcc93..b6aaa15 100644
--- a/src/cleantest/data/directory.py
+++ b/src/cleantest/data/directory.py
@@ -9,7 +9,7 @@
 import tarfile
 import tempfile
 from io import BytesIO
-from typing import Iterable
+from typing import Iterable, Union
 
 from cleantest.data.file import File
 
@@ -48,33 +48,20 @@ class Dir(File):
     """Represents a directory that can be shared between host and test environment.
 
     Args:
-        src (str): Where to load directory from.
-        dest (str): Where to dump directory to.
+        src (Union[str, os.PathLike]): Where to load directory from.
+        dest (Union[str, os.PathLike]): Where to dump directory to.
         overwrite (bool):
             True - overwrite directory if it already exists when dumping.
             False - raise error if directory already exists when dumping.
     """
 
-    def __init__(self, src: str, dest: str, overwrite: bool = False) -> None:
-        super().__init__(src, dest, overwrite)
-
-    def dump(self) -> None:
-        """Dump directory to specified destination.
-
-        Raises:
-            DirectoryExistsError: Raised if directory already exists and overwrite=False.
-            DirectoryError: Raised if no data has been loaded prior to calling dump().
-        """
-        if self.dest.exists() and self.overwrite is False:
-            raise DirectoryExistsError(
-                f"{self.dest} already exists. Set overwrite = True to overwrite {self.dest}."
-            )
-
-        if self._data is None:
-            DirectoryError("Nothing to write.")
-
-        with tarfile.open(fileobj=BytesIO(self._data), mode="r:gz") as tar:
-            tar.extractall(self.dest, members=_strip_tar(tar))
+    def __init__(
+        self,
+        src: Union[str, os.PathLike],
+        dest: Union[str, os.PathLike],
+        overwrite: bool = False,
+    ) -> None:
+        super().__init__(pathlib.Path(src), dest, overwrite)
 
     def load(self) -> None:
         """Load directory from specified source.
@@ -94,5 +81,23 @@ def load(self) -> None:
         with tempfile.NamedTemporaryFile() as fin:
             with tarfile.open(fin.name, "w:gz") as tar:
                 tar.add(self.src.name)
-            self._data = pathlib.Path(fin.name).read_bytes()
+            self.__data = pathlib.Path(fin.name).read_bytes()
         os.chdir(_)
+
+    def dump(self) -> None:
+        """Dump directory to specified destination.
+
+        Raises:
+            DirectoryExistsError: Raised if directory already exists and overwrite=False.
+            DirectoryError: Raised if no data has been loaded prior to calling dump().
+        """
+        if self.dest.exists() and self.overwrite is False:
+            raise DirectoryExistsError(
+                f"{self.dest} already exists. Set overwrite = True to overwrite {self.dest}."
+            )
+
+        if self.__data is None:
+            DirectoryError("Nothing to write.")
+
+        with tarfile.open(fileobj=BytesIO(self.__data), mode="r:gz") as tar:
+            tar.extractall(self.dest, members=_strip_tar(tar))
diff --git a/src/cleantest/data/file.py b/src/cleantest/data/file.py
index 385fde2..c5dd62d 100644
--- a/src/cleantest/data/file.py
+++ b/src/cleantest/data/file.py
@@ -4,13 +4,15 @@
 
 """Abstractions for uploading and downloading files from test environments."""
 
+import copy
 import os
 import pathlib
+import shutil
 import tarfile
 import tempfile
 import textwrap
-from io import BytesIO
-from typing import Dict
+from io import BytesIO, StringIO
+from typing import Dict, Union
 
 from cleantest.meta import Injectable
 
@@ -27,18 +29,66 @@ class File(Injectable):
     """Represents a file that can be shared between host and test environment.
 
     Args:
-        src (pathlib.Path): Where to load file from.
-        dest (pathlib.Path): Where to dump file to.
+        src (Union[str, pathlib.Path, StringIO, BytesIO]):
+            Where to load file from.
+        dest (Union[str, pathlib.Path]): Where to dump file to.
         overwrite (bool):
             True - overwrite file if it already exists when dumping.
             False - raise error if file already exists when dumping.
     """
 
-    def __init__(self, src: str, dest: str, overwrite: bool = False) -> None:
-        self.src = pathlib.Path(src)
+    def __init__(
+        self,
+        src: Union[str, pathlib.Path, bytes, StringIO, BytesIO],
+        dest: Union[str, pathlib.Path],
+        overwrite: bool = False,
+    ) -> None:
+        self.src = src
         self.dest = pathlib.Path(dest)
         self.overwrite = overwrite
-        self._data = None
+        self.__data = None
+
+    def load(self) -> None:
+        """Load file from specified source.
+
+        Raises:
+            FileNotFoundError: Raised if file is not found.
+            FileError: Raised if source is a directory rather than a file.
+        """
+        with tempfile.TemporaryDirectory() as tmp_dir:
+            _ = pathlib.Path(tmp_dir)
+            with tarfile.open(pathlib.Path(tmp_dir) / "_", "w:gz") as tar:
+                old_pwd = os.getcwd()
+                os.chdir(_)
+                if type(self.src) == str or isinstance(self.src, pathlib.Path):
+                    data = pathlib.Path(self.src)
+                    if data.exists() is False:
+                        raise FileNotFoundError(f"Could not find {self.src}.")
+
+                    if data.is_dir():
+                        raise FileError(
+                            f"{self.src} is a directory. Use Dir class instead."
+                        )
+                    shutil.copy(data, _ / "data")
+                    tar.add("data")
+                elif isinstance(self.src, StringIO) or isinstance(self.src, BytesIO):
+                    data = copy.deepcopy(self.src)
+                    placeholder = pathlib.Path("data")
+                    placeholder.write_text(data.read()) if isinstance(
+                        self.src, StringIO
+                    ) else placeholder.write_bytes(data.read())
+                    tar.add(placeholder)
+                else:
+                    raise FileError(
+                        (
+                            "Expected type str, os.PathLike, StringIO, or BytesIO, "
+                            f"not {type(self.src)}."
+                        )
+                    )
+
+                os.chdir(old_pwd)
+
+            self.__data = pathlib.Path(tar.name).read_bytes()
 
     def dump(self) -> None:
         """Dump directory to specified destination.
@@ -49,40 +99,20 @@ def dump(self) -> None:
         """
         if self.dest.exists() and self.overwrite is False:
             raise FileExistsError(
-                f"{self.dest} already exists. Set overwrite = True to overwrite {self.dest}."
+                (
+                    f"{self.dest} already exists. Set overwrite = True to "
+                    f"overwrite {self.dest}."
+                )
             )
 
-        if self._data is None:
+        if self.__data is None:
             raise FileError("Nothing to write.")
 
-        with tarfile.open(
-            fileobj=BytesIO(self._data), mode="r:gz"
-        ) as tar, tempfile.TemporaryDirectory() as tmp_dir:
+        with tempfile.TemporaryDirectory() as tmp_dir, tarfile.open(
+            fileobj=BytesIO(self.__data), mode="r:gz"
+        ) as tar:
             tar.extractall(tmp_dir)
-            self.dest.write_bytes(
-                pathlib.Path(tmp_dir).joinpath(self.src.name).read_bytes()
-            )
-
-    def load(self) -> None:
-        """Load file from specified source.
-
-        Raises:
-            FileNotFoundError: Raised if file is not found.
-            FileError: Raised if source is a directory rather than a file.
-        """
-        if self.src.exists() is False:
-            raise FileNotFoundError(f"Could not find {self.src}.")
-
-        if self.src.is_dir():
-            raise FileError(f"{self.src} is a directory. Use Dir class instead.")
-
-        _ = os.getcwd()
-        os.chdir(os.sep.join(str(self.src).split(os.sep)[:-1]))
-        with tempfile.NamedTemporaryFile() as fin:
-            with tarfile.open(fin.name, "w:gz") as tar:
-                tar.add(self.src.name)
-            self._data = pathlib.Path(fin.name).read_bytes()
-        os.chdir(_)
+            shutil.copy((pathlib.Path(tmp_dir) / "data"), self.dest)
 
     def _injectable(self, data: Dict[str, str], **kwargs) -> str:
         """Generate injectable script that will be run inside the test environment.
@@ -112,7 +142,7 @@ def _injectable(self, data: Dict[str, str], **kwargs) -> str:
                 
                 from {self.__module__} import {self.__class__.__name__}
                 
-                _ = {self.__class__.__name__}._load("{data['checksum']}", "{data['data']}")
+                _ = {self.__class__.__name__}._loads("{data['checksum']}", "{data['data']}")
                 _.dump()
                 """
             ).strip("\n")
@@ -125,8 +155,15 @@ def _injectable(self, data: Dict[str, str], **kwargs) -> str:
                 
                 from {self.__module__} import {self.__class__.__name__}
                 
-                _ = {self.__class__.__name__}._load("{data['checksum']}", "{data['data']}")
+                _ = {self.__class__.__name__}._loads("{data['checksum']}", "{data['data']}")
                 _.load()
-                print(json.dumps(_._dump()), file=sys.stdout)
+                print(json.dumps(_._dumps()), file=sys.stdout)
                 """
             ).strip("\n")
+
+    def __repr__(self) -> str:
+        """String representation of File."""
+        attrs = ", ".join(
+            f"{k}={v}" for k, v in self.__dict__.items() if not k.startswith("_")
+        )
+        return f"{self.__class__.__name__}({attrs})"
diff --git a/src/cleantest/data/pkg/charmlib.py b/src/cleantest/data/pkg/charmlib.py
index d1f9b5e..9f30aca 100644
--- a/src/cleantest/data/pkg/charmlib.py
+++ b/src/cleantest/data/pkg/charmlib.py
@@ -83,7 +83,7 @@ def _handle_charm_lib_install(self) -> None:
                     )
                 )
 
-    def _dump(self) -> Dict[str, str]:
+    def _dumps(self) -> Dict[str, str]:
         """Prepare Charmlib object for injection.
 
         Raises:
@@ -102,7 +102,7 @@ def _dump(self) -> Dict[str, str]:
 
         self._auth_token = self.auth_token_path.read_text()
 
-        return super()._dump()
+        return super()._dumps()
 
     def _injectable(self, data: Dict[str, str], **kwargs) -> str:
         """Generate injectable script that will be used to install charm libraries.
@@ -121,7 +121,7 @@ def _injectable(self, data: Dict[str, str], **kwargs) -> str:
             
             from {self.__module__} import {self.__class__.__name__}
             
-            holder = {self.__class__.__name__}._load("{data['checksum']}", "{data['data']}")
+            holder = {self.__class__.__name__}._loads("{data['checksum']}", "{data['data']}")
             holder._run()
             """
         ).strip("\n")
diff --git a/src/cleantest/data/pkg/pip.py b/src/cleantest/data/pkg/pip.py
index 6fed095..3b86899 100644
--- a/src/cleantest/data/pkg/pip.py
+++ b/src/cleantest/data/pkg/pip.py
@@ -160,7 +160,7 @@ def _handle_pip_install(self) -> None:
                         )
                     )
 
-    def _dump(self) -> Dict[str, str]:
+    def _dumps(self) -> Dict[str, str]:
         """Prepare Pip object for injection.
 
         Raises:
@@ -190,7 +190,7 @@ def _dump(self) -> Dict[str, str]:
                     )
                 self._constraints_store.append(fin.read_text())
 
-        return super()._dump()
+        return super()._dumps()
 
     def _injectable(self, data: Dict[str, str], **kwargs) -> str:
         """Generate injectable script that will be used to install packages with pip.
@@ -209,7 +209,7 @@ def _injectable(self, data: Dict[str, str], **kwargs) -> str:
 
             from {self.__module__} import {self.__class__.__name__}
 
-            holder = {self.__class__.__name__}._load("{data['checksum']}", "{data['data']}")
+            holder = {self.__class__.__name__}._loads("{data['checksum']}", "{data['data']}")
             holder._run()
             """
         ).strip("\n")
diff --git a/src/cleantest/data/pkg/snap.py b/src/cleantest/data/pkg/snap.py
index 3eae715..4dfc547 100644
--- a/src/cleantest/data/pkg/snap.py
+++ b/src/cleantest/data/pkg/snap.py
@@ -227,7 +227,7 @@ def _handle_snap_install(self) -> None:
             for alias in self.aliases:
                 alias.alias()
 
-    def _dump(self) -> Dict[str, str]:
+    def _dumps(self) -> Dict[str, str]:
         """Prepare Snap object for injection.
 
         Raises:
@@ -248,7 +248,7 @@ def _dump(self) -> Dict[str, str]:
                     )
                 self._cached_local_snaps.add(snap_path.read_bytes())
 
-        return super()._dump()
+        return super()._dumps()
 
     def _injectable(self, data: Dict[str, str], **kwargs) -> str:
         """Generate injectable script that will be used to install snap packages.
@@ -267,7 +267,7 @@ def _injectable(self, data: Dict[str, str], **kwargs) -> str:
 
             from {self.__module__} import {self.__class__.__name__}
 
-            holder = {self.__class__.__name__}._load("{data['checksum']}", "{data['data']}")
+            holder = {self.__class__.__name__}._loads("{data['checksum']}", "{data['data']}")
             holder._run()
             """
         ).strip("\n")
diff --git a/src/cleantest/meta/__init__.py b/src/cleantest/meta/__init__.py
index b98d5f6..8e11dc5 100644
--- a/src/cleantest/meta/__init__.py
+++ b/src/cleantest/meta/__init__.py
@@ -2,18 +2,8 @@
 # Copyright 2023 Jason C. Nucciarone
 # See LICENSE file for licensing details.
 
-"""Public metaclasses used inside cleantest.
+"""Public metaclasses used inside cleantest."""
 
-You can use these modules for developing third-party plugins.
-"""
-
-from .base_handler import (
-    BaseEntrypoint,
-    BaseEntrypointError,
-    BaseHandler,
-    BaseHandlerError,
-)
 from .base_package import BasePackage, BasePackageError
-from .cleantest_info import CleantestInfo
 from .injectable import Injectable
 from .result import Result
diff --git a/src/cleantest/_meta/base_configurer.py b/src/cleantest/meta/_base_configurer.py
similarity index 65%
rename from src/cleantest/_meta/base_configurer.py
rename to src/cleantest/meta/_base_configurer.py
index 0cee128..e13ca4a 100644
--- a/src/cleantest/_meta/base_configurer.py
+++ b/src/cleantest/meta/_base_configurer.py
@@ -2,38 +2,41 @@
 # Copyright 2023 Jason C. Nucciarone
 # See LICENSE file for licensing details.
 
-"""Base tooling needed by configurer classes."""
+"""Private metaclass that provides tooling needed by configurer classes."""
 
 import copy
 from collections import deque
 from typing import Deque, Union
 
-from cleantest._meta.mixins import Resettable
 from cleantest.control.hooks import StartEnvHook, StopEnvHook
+from cleantest.meta.mixins import Resettable
+
+
+class BaseConfigurerError(Exception):
+    """Base error for all configurer classes."""
 
 
 class DuplicateHookNameError(Exception):
     """Raised when more than one hook of the same type have the same name."""
 
 
-class HookRegistry(Resettable):
+class _HookRegistry(Resettable):
     """Centrally store hooks for use by test environment providers."""
 
     metadata = set()
     startenv = deque()
     stopenv = deque()
 
-    def __new__(cls) -> "HookRegistry":
+    def __new__(cls) -> "_HookRegistry":
         if not hasattr(cls, "_instance"):
-            cls._instance = super(HookRegistry, cls).__new__(cls)
+            cls._instance = super(_HookRegistry, cls).__new__(cls)
         return cls._instance
 
-    @classmethod
-    def reset(cls) -> None:
+    def reset(self) -> None:
         """Reset metadata and hook queues."""
-        cls.metadata = set()
-        cls.startenv = deque()
-        cls.stopenv = deque()
+        self.metadata = set()
+        self.startenv = deque()
+        self.stopenv = deque()
 
     def lint(self, hook: Union[StartEnvHook, StopEnvHook]) -> None:
         """Lint hooks to ensure they compliant with set restrictions."""
@@ -44,17 +47,19 @@ def lint(self, hook: Union[StartEnvHook, StopEnvHook]) -> None:
         ]
         if len(lint) > 0:
             raise DuplicateHookNameError(
-                f"Hook type {hook.__class__.__name__} with name {hook.name} already exists."
+                (
+                    f"Hook type {hook.__class__.__name__} with name "
+                    f"{hook.name} already exists."
+                )
             )
 
 
 class BaseConfigurer(Resettable):
     """Base configure mixin for configurers."""
 
-    @classmethod
-    def reset(cls) -> None:
+    def reset(self) -> None:
         """Reset the hook registry."""
-        HookRegistry().reset()
+        _HookRegistry().reset()
 
     def register_hook(self, *hooks: Union[StartEnvHook, StopEnvHook]) -> None:
         """Register hooks in the hook registry.
@@ -63,13 +68,13 @@ def register_hook(self, *hooks: Union[StartEnvHook, StopEnvHook]) -> None:
             *hooks (Union[StartEnvHook, StopEnvHook]): Hooks to register.
         """
         dispatch = {
-            StartEnvHook.__name__: HookRegistry().startenv,
-            StopEnvHook.__name__: HookRegistry().stopenv,
+            StartEnvHook.__name__: _HookRegistry().startenv,
+            StopEnvHook.__name__: _HookRegistry().stopenv,
         }
         for hook in hooks:
-            HookRegistry().lint(hook)
+            _HookRegistry().lint(hook)
             dispatch[hook.__class__.__name__].appendleft(hook)
-            HookRegistry().metadata.add((hook.name, hook.__class__.__name__))
+            _HookRegistry().metadata.add((hook.name, hook.__class__.__name__))
 
     def unregister_hook(self, *hook_names: str) -> None:
         """Unregister hooks from the hook registry.
@@ -78,18 +83,18 @@ def unregister_hook(self, *hook_names: str) -> None:
             *hook_names (str): Names of hooks to unregister.
         """
         dispatch = {
-            StartEnvHook.__name__: HookRegistry().startenv,
-            StopEnvHook.__name__: HookRegistry().stopenv,
+            StartEnvHook.__name__: _HookRegistry().startenv,
+            StopEnvHook.__name__: _HookRegistry().stopenv,
         }
         for name in hook_names:
-            for hook in HookRegistry().metadata:
+            for hook in _HookRegistry().metadata:
                 if name == hook[0]:
                     [
                         dispatch[hook[1]].remove(i)
                         for i in dispatch[hook[1]]
                         if i.name == name
                     ]
-                    HookRegistry().metadata.remove(hook)
+                    _HookRegistry().metadata.remove(hook)
 
     @property
     def startenv_hooks(self) -> Deque[StartEnvHook]:
@@ -98,7 +103,7 @@ def startenv_hooks(self) -> Deque[StartEnvHook]:
         Returns:
             (Deque[StartEnvHook]): Deque containing start environment hooks.
         """
-        return copy.deepcopy(HookRegistry().startenv)
+        return copy.deepcopy(_HookRegistry().startenv)
 
     @property
     def stopenv_hooks(self) -> Deque[StopEnvHook]:
@@ -107,4 +112,4 @@ def stopenv_hooks(self) -> Deque[StopEnvHook]:
         Returns:
             (Deque[StopEnvHook]): Deque containing stop environment hooks.
         """
-        return copy.deepcopy(HookRegistry().stopenv)
+        return copy.deepcopy(_HookRegistry().stopenv)
diff --git a/src/cleantest/meta/base_handler.py b/src/cleantest/meta/_base_harness.py
similarity index 88%
rename from src/cleantest/meta/base_handler.py
rename to src/cleantest/meta/_base_harness.py
index 71fd43d..d9e6d69 100644
--- a/src/cleantest/meta/base_handler.py
+++ b/src/cleantest/meta/_base_harness.py
@@ -16,8 +16,8 @@ class BaseEntrypointError(Exception):
     """Base error for test run entrypoints."""
 
 
-class BaseHandlerError(Exception):
-    """Base error for test environment handlers."""
+class BaseHarnessError(Exception):
+    """Base error for test environment provider harnesses."""
 
 
 class BaseEntrypoint(ABC):
@@ -31,10 +31,11 @@ def run(self) -> Dict[str, Result]:
         """Run handler for test environment."""
 
 
-class BaseHandler(ABC):
-    """Metaclass for test environment handlers.
+class BaseHarness(ABC):
+    """Base class for test environment provider harnesses.
 
-    Handlers define the tooling stubs needed to run tests inside the test environment.
+    Harnesses wrap around testlets to ensure they are run inside the
+    specified test environment instance.
     """
 
     @abstractmethod
diff --git a/src/cleantest/meta/cleantest_info.py b/src/cleantest/meta/_cleantest_info.py
similarity index 70%
rename from src/cleantest/meta/cleantest_info.py
rename to src/cleantest/meta/_cleantest_info.py
index 24642f7..4433ba5 100644
--- a/src/cleantest/meta/cleantest_info.py
+++ b/src/cleantest/meta/_cleantest_info.py
@@ -16,6 +16,28 @@
 
 import pkg_resources
 
+from cleantest.meta.utils import thread_count
+
+
+def _dependency_processor(dependency: pkg_resources.Distribution) -> Dict[str, bytes]:
+    """Collect source code of cleantest dependency.
+
+    Args:
+        dependency (pkg_resources.Distribution): Dependency of cleantest.
+
+    Returns:
+        (Dict[str, bytes]): Name and base64 encoded source code of dependency.
+    """
+    os.chdir(dependency.location)
+    with tempfile.NamedTemporaryFile() as fin:
+        with tarfile.open(fin.name, "w:gz") as tar, pathlib.Path(
+            f"{dependency.key.replace('-', '_')}-{dependency.version}.dist-info"
+        ).joinpath("RECORD").open(mode="rt") as dist_info_fin:
+            for row in csv.reader(dist_info_fin):
+                tar.add(row[0])
+
+        return {dependency.key: pathlib.Path(fin.name).read_bytes()}
+
 
 class CleantestInfo:
     """Metaclass for getting information about the cleantest library."""
@@ -26,12 +48,12 @@ def __new__(cls) -> "CleantestInfo":
         Returns:
             (CleantestInfo): New object instance.
         """
-        if not hasattr(cls, "instance"):
-            cls.instance = super(CleantestInfo, cls).__new__(cls)
-        return cls.instance
+        if not hasattr(cls, f"_{cls.__name__}__instance"):
+            cls.__instance = super(CleantestInfo, cls).__new__(cls)
+        return cls.__instance
 
     @property
-    def _src(self) -> Dict[str, bytes]:
+    def __src(self) -> Dict[str, bytes]:
         """Retrieve the source code of cleantest.
 
         Returns:
@@ -46,15 +68,15 @@ def _src(self) -> Dict[str, bytes]:
             return {"cleantest": pathlib.Path(fin.name).read_bytes()}
 
     @property
-    def _dependencies(self) -> Iterable[Tuple[str, bytes]]:
+    def __dependencies(self) -> Iterable[Tuple[str, bytes]]:
         """Retrieve the source code of cleantest's dependencies.
 
         Yields:
             (Dict[str, bytes]): Name and source code of dependencies.
         """
-        with ProcessPoolExecutor(max_workers=os.cpu_count()) as pool:
+        with ProcessPoolExecutor(max_workers=thread_count()) as pool:
             pool_results = pool.map(
-                self._dependency_processor,
+                _dependency_processor,
                 pkg_resources.working_set.resolve(
                     pkg_resources.working_set.by_key["cleantest"].requires()
                 ),
@@ -63,28 +85,7 @@ def _dependencies(self) -> Iterable[Tuple[str, bytes]]:
                 for k, v in res.items():
                     yield k, v
 
-    def _dependency_processor(
-        self, dependency: pkg_resources.Distribution
-    ) -> Dict[str, bytes]:
-        """Collect source code of cleantest dependency.
-
-        Args:
-            dependency (pkg_resources.Distribution): Dependency of cleantest.
-
-        Returns:
-            (Dict[str, bytes]): Name and base64 encoded source code of dependency.
-        """
-        os.chdir(dependency.location)
-        with tempfile.NamedTemporaryFile() as fin:
-            with tarfile.open(fin.name, "w:gz") as tar, pathlib.Path(
-                f"{dependency.key.replace('-', '_')}-{dependency.version}.dist-info"
-            ).joinpath("RECORD").open(mode="rt") as dist_info_fin:
-                for row in csv.reader(dist_info_fin):
-                    tar.add(row[0])
-
-            return {dependency.key: pathlib.Path(fin.name).read_bytes()}
-
-    def _injectable(self, checksum: str, data: str) -> str:
+    def __injectable(self, checksum: str, data: str) -> str:
         """Generate injectable script to install packages inside the test instance.
 
         Args:
@@ -114,7 +115,7 @@ def _injectable(self, checksum: str, data: str) -> str:
             fout.seek(0)
             return fout.read()
 
-    def dump(self) -> Iterable[Tuple[str, Dict[str, str]]]:
+    def dumps(self) -> Iterable[Tuple[str, Dict[str, str]]]:
         """Prepare cleantest for injection into test environment instance.
 
         Yields:
@@ -124,13 +125,13 @@ def dump(self) -> Iterable[Tuple[str, Dict[str, str]]]:
                 data (str): Base64 encoded tarball containing source code.
                 injectable (str): Injectable to run inside remote instance.
         """
-        packages = self._src
-        packages.update(dict(self._dependencies))
+        packages = self.__src
+        packages.update(dict(self.__dependencies))
         for k, v in packages.items():
             checksum = hashlib.sha224(v).hexdigest()
             data = base64.b64encode(v).decode()
             yield k, {
                 "checksum": checksum,
                 "data": data,
-                "injectable": self._injectable(checksum, data),
+                "injectable": self.__injectable(checksum, data),
             }
diff --git a/src/cleantest/meta/injectable.py b/src/cleantest/meta/injectable.py
index 8b5a813..8ef4602 100644
--- a/src/cleantest/meta/injectable.py
+++ b/src/cleantest/meta/injectable.py
@@ -19,7 +19,7 @@ class Injectable(ABC):
     """Abstract metaclass that provides core methods needed by all injectable objects."""
 
     @classmethod
-    def _load(cls, checksum: str, data: str) -> object:
+    def _loads(cls, checksum: str, data: str) -> object:
         """Alternative constructor to load previously initialized object.
 
         Args:
@@ -47,7 +47,7 @@ def _load(cls, checksum: str, data: str) -> object:
         [setattr(new_cls, key, value) for key, value in hiddenargs.items()]
         return new_cls
 
-    def _dump(self, **kwargs) -> Dict[str, str]:
+    def _dumps(self, **kwargs) -> Dict[str, str]:
         """Prepare object for injection.
 
         Returns:
diff --git a/src/cleantest/meta/mixins/__init__.py b/src/cleantest/meta/mixins/__init__.py
index 7bc5cfe..0147b5c 100644
--- a/src/cleantest/meta/mixins/__init__.py
+++ b/src/cleantest/meta/mixins/__init__.py
@@ -1,6 +1,10 @@
 #!/usr/bin/env python3
-# Copyright 2023 Jason C. Nucciarone, Canonical Ltd.
+# Copyright 2023 Jason C. Nucciarone
 # See LICENSE file for licensing details.
 
-from .dict_ops import DictOps
+"""Public mixins used inside cleantest."""
+
+from .dict_like import DictLike
+from .enhanced_enum import EnhancedEnum
+from .resettable import Resettable
 from .snapd_support import SnapdSupport
diff --git a/src/cleantest/meta/mixins/dict_like.py b/src/cleantest/meta/mixins/dict_like.py
new file mode 100644
index 0000000..8186040
--- /dev/null
+++ b/src/cleantest/meta/mixins/dict_like.py
@@ -0,0 +1,130 @@
+#!/usr/bin/env python3
+# Copyright 2023 Jason C. Nucciarone
+# See LICENSE file for licensing details.
+
+"""Mixin for objects that need to emulate a dictionary."""
+
+from typing import Any, Dict, Iterable, Tuple
+
+
+class DictLike:
+    """Mixin for objects that need to emulate a dictionary."""
+
+    def dict(self) -> Dict:
+        """Return class as a unidirectional dictionary.
+
+        Returns:
+            (Dict): Class as a dictionary.
+        """
+        return self.__process_dict(self.__dict__)
+
+    def keys(self, all_keys: bool = False) -> Iterable[Any]:
+        """Get dictionary keys.
+
+        Args:
+            all_keys (bool):
+                If False, only get top-level dictionary keys.
+                If True, get all keys in the multi-node dictionary.
+                (Default: False).
+
+        Returns:
+            (Iterable[Any]): Iterable containing dictionary keys.
+        """
+        if not all_keys:
+            return iter(k for k in self.__dict__.keys())
+        else:
+            return self.__process_keys(self.dict())
+
+    def values(self, all_values: bool = False) -> Iterable[Any]:
+        """Get dictionary values.
+
+        Args:
+            all_values (bool):
+                If False, only get top-level dictionary values.
+                If True, get all values in the multi-node dictionary.
+                (Default: False).
+
+        Returns:
+            (Iterable[Any]): Iterable containing dictionary values.
+        """
+        if not all_values:
+            return iter(v for v in self.__dict__.values())
+        else:
+            return self.__process_values(self.dict())
+
+    def items(self, all_items: bool = False) -> Iterable[Tuple[Any, Any]]:
+        """Get dictionary items.
+
+        Args:
+            all_items (bool):
+                If False, only get top-level dictionary items.
+                If True, get all items in the multi-node dictionary.
+                (Default: False).
+
+        Returns:
+            (Iterable[Tuple[Any, Any]]): Iterable containing dictionary items.
+        """
+        if not all_items:
+            return iter(
+                (k, v) for k, v in zip(self.__dict__.keys(), self.__dict__.values())
+            )
+        else:
+            return iter(
+                (k, v) for k, v in zip(self.keys(all_items), self.values(all_items))
+            )
+
+    def __process_dict(self, input_dict: Dict[Any, Any]) -> Dict[Any, Any]:
+        """Process dictionary containing object with __dict__ attribute recursively.
+
+        Args:
+            input_dict (Dict): Dictionary containing object with __dict__ attribute.
+
+        Returns:
+            (Dict): Dictionary with containing objects converted to dictionaries.
+        """
+        result = {}
+        for key, value in input_dict.items():
+            if hasattr(value, "__dict__"):
+                result.update({key: self.__process_dict(value.__dict__)})
+            else:
+                result.update({key: value})
+
+        return result
+
+    def __process_keys(self, input_dict: Dict[Any, Any]) -> Iterable[Any]:
+        """Get all keys in a multi-node dictionary recursively.
+
+        Args:
+            input_dict (Dict[Any, Any]):
+                Dictionary to get all keys from.
+
+        Returns:
+            (Iterable[Any]): All dictionary keys.
+        """
+        result = []
+        for k, v in input_dict.items():
+            if type(v) == dict:
+                result.extend(self.__process_keys(v))
+            else:
+                result.append(k)
+
+        return iter(result)
+
+    def __process_values(self, input_dict: Dict[Any, Any]) -> Iterable[Any]:
+        """Get all values in a multi-node dictionary recursively.
+
+        Args:
+            input_dict (Dict[Any, Any]):
+                Dictionary to get all values from.
+
+        Returns:
+            (Iterable[Any]): All dictionary values.
+        """
+        result = []
+        for k, v in input_dict.items():
+            if type(v) == dict:
+                result.extend(self.__process_values(v))
+            else:
+                result.append(v)
+
+        return iter(result)
diff --git a/src/cleantest/meta/mixins/dict_ops.py b/src/cleantest/meta/mixins/dict_ops.py
deleted file mode 100644
index a2af633..0000000
--- a/src/cleantest/meta/mixins/dict_ops.py
+++ /dev/null
@@ -1,55 +0,0 @@
-#!/usr/bin/env python3
-# Copyright 2023 Jason C. Nucciarone, Canonical Ltd.
-# See LICENSE file for licensing details.
-
-"""Common operations needed by classes that aim to emulate dictionaries."""
-
-from typing import Any, Dict, List
-
-
-class DictOps:
-    """Mixin that provides dictionary methods needed by data providers."""
-
-    def dict(self) -> Dict:
-        """Return class as a unidirectional dictionary.
-
-        Returns:
-            (Dict): Class as a dictionary.
-        """
-        return self._filterdict(self.__dict__)
-
-    def _filterdict(self, input_dict: Dict) -> Dict:
-        """Recursively filter dictionary containing classes with __dict__ attribute.
-
-        Args:
-            input_dict (Dict): Dictionary containing classes with __dict__ attribute.
-
-        Returns:
-            (Dict): Dictionary with containing parsed __dict__ attributes.
-        """
-        result = {}
-        for key, value in input_dict.items():
-            if hasattr(value, "__dict__"):
-                result.update({key: self._filterdict(value.__dict__)})
-            else:
-                result.update({key: value})
-
-        return result
-
-    def _deconstruct(self, d: Dict[str, Any]) -> List[str]:
-        """Recursively deconstruct a dictionary to get all of its keys.
-
-        Args:
-            d (Dict[str, Any]): Dictionary to deconstruct.
-
-        Returns:
-            (List[str]): Keys of deconstructed dictionary.
-        """
-        config = []
-        for k in d.keys():
-            if isinstance(d[k], dict):
-                config.extend(self._deconstruct(d[k]))
-            else:
-                config.append(k)
-
-        return config
diff --git a/src/cleantest/meta/mixins/enhanced_enum.py b/src/cleantest/meta/mixins/enhanced_enum.py
new file mode 100644
index 0000000..000c9bf
--- /dev/null
+++ b/src/cleantest/meta/mixins/enhanced_enum.py
@@ -0,0 +1,17 @@
+#!/usr/bin/env python3
+# Copyright 2023 Jason C. Nucciarone
+# See LICENSE file for licensing details.
+
+"""Mixin for enhancing Enum objects."""
+
+from enum import Enum
+from typing import Any, List, Tuple
+
+
+class EnhancedEnum(Enum):
+    """Mixin for giving enums extra methods for convenience."""
+
+    @classmethod
+    def items(cls) -> List[Tuple[str, Any]]:
+        """Returns items of an Enum."""
+        return [(c.name, c.value) for c in cls]
diff --git a/src/cleantest/_meta/mixins/resettable.py b/src/cleantest/meta/mixins/resettable.py
similarity index 89%
rename from src/cleantest/_meta/mixins/resettable.py
rename to src/cleantest/meta/mixins/resettable.py
index dc443b9..97244db 100644
--- a/src/cleantest/_meta/mixins/resettable.py
+++ b/src/cleantest/meta/mixins/resettable.py
@@ -10,7 +10,6 @@
 class Resettable(ABC):
     """Abstract mixin for Singletons that need to implement reset behavior."""
 
-    @classmethod
     @abstractmethod
-    def reset(cls) -> None:
+    def reset(self) -> None:
         """Reset the current Singleton to its default state."""
diff --git a/src/cleantest/meta/utils/__init__.py b/src/cleantest/meta/utils/__init__.py
index f2b24ef..6678d64 100644
--- a/src/cleantest/meta/utils/__init__.py
+++ b/src/cleantest/meta/utils/__init__.py
@@ -2,4 +2,10 @@
 # Copyright 2023 Jason C. Nucciarone
 # See LICENSE file for licensing details.
 
+"""Public utilities used inside cleantest.
+
+You can use these utilities for developing third-party plugins.
+"""
+
 from .detect_os import detect_os_variant
+from .thread_count import thread_count
diff --git a/src/cleantest/meta/utils/thread_count.py b/src/cleantest/meta/utils/thread_count.py
new file mode 100644
index 0000000..4a27c65
--- /dev/null
+++ b/src/cleantest/meta/utils/thread_count.py
@@ -0,0 +1,17 @@
+#!/usr/bin/env python3
+# Copyright 2023 Jason C. Nucciarone
+# See LICENSE file for licensing details.
+
+"""Detect thread count on host system."""
+
+import os
+
+
+def thread_count() -> int:
+    """Get number of allowable threads on host system.
+
+    Returns:
+        (int): Number of allowable threads (Default: os.cpu_count())
+    """
+    env_var = os.getenv("CLEANTEST_NUM_THREADS")
+    return env_var if env_var is not None and type(env_var) == int else os.cpu_count()
diff --git a/src/cleantest/provider/__init__.py b/src/cleantest/provider/__init__.py
index 6019e22..fd0bcb9 100644
--- a/src/cleantest/provider/__init__.py
+++ b/src/cleantest/provider/__init__.py
@@ -1,5 +1,8 @@
 #!/usr/bin/env python3
-# Copyright 2023 Jason C. Nucciarone, Canonical Ltd.
+# Copyright 2023 Jason C. Nucciarone
 # See LICENSE file for licensing details.
 
+"""Test environment providers that can be used by cleantest."""
+
 from .lxd.lxd import lxd
+from .lxd.lxd_archon import LXDArchon
diff --git a/src/cleantest/provider/lxd/__init__.py b/src/cleantest/provider/lxd/__init__.py
index ecc7ad5..846b83a 100644
--- a/src/cleantest/provider/lxd/__init__.py
+++ b/src/cleantest/provider/lxd/__init__.py
@@ -1,3 +1,3 @@
 #!/usr/bin/env python3
-# Copyright 2023 Jason C. Nucciarone, Canonical Ltd.
+# Copyright 2023 Jason C. Nucciarone
 # See LICENSE file for licensing details.
diff --git a/src/cleantest/provider/lxd/lxd_handler.py b/src/cleantest/provider/lxd/_lxd_harness.py
similarity index 91%
rename from src/cleantest/provider/lxd/lxd_handler.py
rename to src/cleantest/provider/lxd/_lxd_harness.py
index e93bb6b..b2b14e3 100644
--- a/src/cleantest/provider/lxd/lxd_handler.py
+++ b/src/cleantest/provider/lxd/_lxd_harness.py
@@ -13,15 +13,13 @@
 
 from pylxd import Client
 
-from cleantest.meta import (
+from cleantest.meta import BasePackage, Injectable, Result
+from cleantest.meta._base_harness import (
     BaseEntrypoint,
     BaseEntrypointError,
-    BaseHandler,
-    BasePackage,
-    CleantestInfo,
-    Injectable,
-    Result,
+    BaseHarness,
 )
+from cleantest.meta._cleantest_info import CleantestInfo
 
 
 class LXDEntrypointError(BaseEntrypointError):
@@ -56,7 +54,7 @@ def __repr__(self) -> str:
         )
 
 
-class LXDHandler(BaseHandler):
+class LXDHarness(BaseHarness):
     """Mixin for controlling the LXD hypervisor via its unix socket."""
 
     @property
@@ -66,17 +64,7 @@ def _client(self) -> Client:
         Returns:
             (Client): Connection to LXD API socket.
         """
-        if self._lxd_client_config is None:
-            return Client(project="default")
-        else:
-            return Client(
-                endpoint=self._lxd_client_config.endpoint,
-                version=self._lxd_client_config.version,
-                cert=self._lxd_client_config.cert,
-                verify=self._lxd_client_config.verify,
-                timeout=self._lxd_client_config.timeout,
-                project=self._lxd_client_config.project,
-            )
+        return Client(**self._lxd_config.client_config.dict())
 
     @property
     def _instance_metadata(self) -> List[InstanceMetadata]:
@@ -110,14 +98,14 @@ def _init(self, instance: InstanceMetadata) -> None:
             instance (InstanceMetadata): Instance to initialize.
         """
         if instance.exists is False:
-            config = self._lxd_provider_config.get_instance_config(instance.image)
+            config = self._lxd_config.get_instance_config(instance.image)
             config.name = instance.name
             self._client.instances.create(config.dict(), wait=True)
             instance = self._client.instances.get(instance.name)
             instance.start(wait=True)
             meta = CleantestInfo()
             instance.execute(["mkdir", "-p", "/root/init/cleantest"])
-            for name, data in meta.dump():
+            for name, data in meta.dumps():
                 instance.files.put(
                     f"/root/init/cleantest/install_{name}",
                     data["injectable"],
@@ -161,7 +149,7 @@ def _handle_start_env_hooks(self, instance: InstanceMetadata) -> None:
         Args:
             instance (InstanceMetadata): Instance to run start env hooks in.
         """
-        start_env_hooks = self._lxd_provider_config.startenv_hooks
+        start_env_hooks = self._lxd_config.startenv_hooks
         while start_env_hooks:
             hook = start_env_hooks.pop()
             instance = self._client.instances.get(instance.name)
@@ -178,7 +166,7 @@ def _handle_stop_env_hooks(self, instance: InstanceMetadata) -> None:
         Args:
             instance (InstanceMetadata): Instance to run stop env hooks in.
         """
-        stop_env_hooks = self._lxd_provider_config.stopenv_hooks
+        stop_env_hooks = self._lxd_config.stopenv_hooks
         while stop_env_hooks:
             hook = stop_env_hooks.pop()
             instance = self._client.instances.get(instance.name)
@@ -195,7 +183,7 @@ def _handle_package_install(self, instance: Any, pkg: BasePackage) -> None:
         """
         dispatch = {"charmlib": lambda x: self._env.add(json.loads(x))}
 
-        dump_data = pkg._dump()
+        dump_data = pkg._dumps()
         instance.execute(["mkdir", "-p", "/root/init/pkg"])
         instance.files.put("/root/init/pkg/install", dump_data["injectable"])
         result = instance.execute(["python3", "/root/init/pkg/install"])
@@ -211,7 +199,7 @@ def _handle_artifact_upload(self, instance: Any, artifact: Injectable) -> None:
             artifact (Injectable): Artifact to upload.
         """
         artifact.load()
-        dump_data = artifact._dump(mode="push")
+        dump_data = artifact._dumps(mode="push")
         instance.execute(["mkdir", "-p", "/root/init/data"])
         instance.files.put("/root/init/data/dump", dump_data["injectable"])
         instance.execute(["python3", "/root/init/data/dump"])
@@ -223,7 +211,7 @@ def _handle_artifact_download(self, instance: Any, artifact: Injectable) -> None
             instance (Any): Instance to download artifact from.
             artifact (Injectable): Artifact to download.
         """
-        dump_data = artifact._dump(mode="pull")
+        dump_data = artifact._dumps(mode="pull")
         instance.execute(["mkdir", "-p", "/root/post/data"])
         instance.files.put(
             "/root/post/data/load",
@@ -233,11 +221,11 @@ def _handle_artifact_download(self, instance: Any, artifact: Injectable) -> None
             instance.execute(["python3", "/root/post/data/load"]).stdout
         )
         with tempfile.NamedTemporaryFile():
-            holder = artifact.__class__._load(result["checksum"], result["data"])
+            holder = artifact.__class__._loads(result["checksum"], result["data"])
             holder.dump()
 
 
-class LXDProviderEntrypoint(BaseEntrypoint, LXDHandler):
+class LXDProviderEntrypoint(BaseEntrypoint, LXDHarness):
     """Entrypoint to running testlets with LXD test environment provider.
 
     Args:
@@ -365,9 +353,12 @@ def _run_target(self, instance: InstanceMetadata) -> Tuple[str, Result]:
                 Result of test run inside of pre-existing
                 LXD test environment instance.
         """
-        return instance.name, self._execute(
+        self._handle_start_env_hooks(instance)
+        result = self._execute(
             self._make_testlet(
-                self._func, self._func_name, [re.compile(r"^@lxd\(([^)]+)\)")]
+                self._func, self._func_name, [re.compile(r"^@lxd\.target\(([^)]+)\)")]
             ),
             instance,
         )
+        self._handle_stop_env_hooks(instance)
+        return instance.name, result
diff --git a/src/cleantest/provider/lxd/lxd.py b/src/cleantest/provider/lxd/lxd.py
index 332bad7..65c3fcd 100644
--- a/src/cleantest/provider/lxd/lxd.py
+++ b/src/cleantest/provider/lxd/lxd.py
@@ -5,14 +5,13 @@
 """LXD test environment provider functions and utilities."""
 
 import functools
-import os
 from typing import Callable, Iterable, List, Optional, Tuple, Union
 
 from cleantest.control import Configure, Env
-from cleantest.control.lxd import ClientConfig
 from cleantest.meta import Result
+from cleantest.meta.utils import thread_count
 
-from .lxd_handler import LXDProviderEntrypoint
+from ._lxd_harness import LXDProviderEntrypoint
 
 
 class lxd:  # noqa N801
@@ -23,7 +22,6 @@ class lxd:  # noqa N801
         image (List[str]):
             LXD image to use for test environment (Default: ["ubuntu-jammy-amd64"]).
         preserve (bool): Preserve test environment after test has completed (Default: True).
-        lxd_client_config (ClientConfig): Configuration to use for LXD client (Default: None).
         parallel (bool): Run test environment instances in parallel (Default: False).
         num_threads (int): Number of threads to use when running
             test environment instances in parallel (Default: None).
@@ -33,26 +31,19 @@ def __init__(
         self,
         name: str = "test",
         image: Union[str, List[str]] = ["ubuntu-jammy-amd64"],
-        lxd_client_config: Optional[ClientConfig] = None,
         preserve: bool = True,
         parallel: bool = False,
         num_threads: Optional[int] = None,
     ) -> None:
         self._name = name
         self._image = [image] if type(image) == str else image
-        self._lxd_client_config = lxd_client_config
         self._preserve = preserve
         self._env = Env()
         self._parallel = parallel
-        self._lxd_provider_config = Configure("lxd")
+        self._lxd_config = Configure("lxd")
 
         if (type(num_threads) != int or num_threads < 1) and self._parallel is True:
-            env_var = os.getenv("CLEANTEST_NUM_THREADS")
-            self._num_threads = (
-                env_var
-                if env_var is not None and type(env_var) == int
-                else os.cpu_count()
-            )
+            self._num_threads = thread_count()
         elif type(num_threads) == int and self._parallel is True:
             self._num_threads = num_threads
 
@@ -76,7 +67,6 @@ def wrapper(*args, **kwargs) -> Iterable[Tuple[str, Result]]:
     def target(
         cls,
         *instances: str,
-        lxd_client_config: Optional[ClientConfig] = None,
         parallel: bool = False,
         num_threads: Optional[int] = None,
     ) -> Callable:
@@ -84,8 +74,6 @@ def target(
 
         Args:
             *instances (str): Test environment instance name.
-            lxd_client_config (ClientConfig):
-                Configuration to use for LXD client (Default: None).
             parallel (bool):
                 Run test environment instances in parallel (Default: False).
             num_threads (int): Number of threads to use when running
@@ -98,7 +86,24 @@ def target(
         def decorator(func: Callable) -> Callable:
             @functools.wraps(func)
             def wrapper(*args, **kwargs) -> Iterable[Tuple[str, Result]]:
-                ...
+                if (type(num_threads) != int or num_threads < 1) and parallel is True:
+                    _num_threads = num_threads
+                elif type(num_threads) == int and parallel is True:
+                    _num_threads = num_threads
+                else:
+                    _num_threads = None
+                _ = {
+                    "_target_instances": [*instances],
+                    "_env": Env(),
+                    "_lxd_config": Configure("lxd"),
+                    "_num_threads": _num_threads,
+                }
+                handler = (
+                    LXDProviderEntrypoint(strategy="parallel_target", func=func, **_)
+                    if parallel is True
+                    else LXDProviderEntrypoint(strategy="serial_target", func=func, **_)
+                )
+                return handler.run()
 
             return wrapper
 
diff --git a/src/cleantest/provider/lxd/lxd_archon.py b/src/cleantest/provider/lxd/lxd_archon.py
new file mode 100644
index 0000000..4b0b233
--- /dev/null
+++ b/src/cleantest/provider/lxd/lxd_archon.py
@@ -0,0 +1,435 @@
+#!/usr/bin/env python3
+# Copyright 2023 Jason C. Nucciarone
+# See LICENSE file for licensing details.
+
+"""Direct the LXD test environment provider and instances."""
+
+import csv
+import json
+import logging
+import os
+import pathlib
+import shlex
+import shutil
+from collections import namedtuple
+from concurrent.futures import ProcessPoolExecutor
+from io import StringIO
+from ipaddress import IPv4Address, IPv6Address
+from typing import Dict, List, Optional, Tuple, Union
+
+from pylxd import Client
+
+from cleantest.control.lxd._lxd_configurer import LXDConfigurer
+from cleantest.data import Dir, File
+from cleantest.meta import Result
+from cleantest.meta._cleantest_info import CleantestInfo
+from cleantest.meta.utils import thread_count
+from cleantest.utils import run
+
+logger = logging.getLogger(__name__)
+
+# Metaclass to encapsulate work information sent to _add threads.
+_AddWorkOrder = namedtuple(
+    "_AddWorkOrder",
+    ["name", "image", "provision_script", "resources"],
+    defaults=[None, None, None, None],
+)
+
+# Metaclass to encapsulate work information sent to _execute threads.
+_ExecuteWorkOrder = namedtuple(
+    "_ExecuteWorkOrder",
+    ["target", "command"],
+    defaults=[None, None],
+)
+
+
+class LXDArchonError(Exception):
+    """Raise when LXDArchon encounters an error."""
+
+
+class LXDArchon:
+    """Direct the LXD test environment provider via the LXD API socket."""
+
+    __instances = set()
+
+    def __new__(cls) -> "LXDArchon":
+        """Create new LXDArchon instance.
+
+        Returns:
+            (LXDArchon): New LXDArchon instance.
+        """
+        if not hasattr(cls, f"_{cls.__name__}__instance"):
+            cls.__instance = super(LXDArchon, cls).__new__(cls)
+        return cls.__instance
+
+    @property
+    def __client(self) -> Client:
+        """Establish connection to LXD API socket.
+
+        Returns:
+            (Client): Connection to LXD API socket.
+        """
+        return Client(**self.config.client_config.dict())
+
+    @property
+    def config(self) -> LXDConfigurer:
+        """Get LXDConfigurer instance containing configuration information.
+
+        Returns:
+            (LXDConfigurer): LXDConfigurer instance.
+        """
+        return LXDConfigurer()
+
+    def exists(self, target: str) -> bool:
+        """Check that specified test environment instance exists.
+
+        Args:
+            target (str): Instance to determine existence of.
+
+        Returns:
+            (bool): True if instance exists; False if otherwise.
+        """
+        return self.__client.instances.exists(target)
+
+    def add(
+        self,
+        name: Union[str, List[str]],
+        image: str,
+        provision_script: Optional[Union[str, os.PathLike]] = None,
+        resources: Optional[List[Union[File, Dir]]] = None,
+    ) -> None:
+        """Add a new test environment instance to the LXD cluster.
+
+        Args:
+            name (Union[str, List[str]]):
+                Name of new test environment instance to add to the cluster.
+            image (str):
+                Image to use for new test environment instance.
+            provision_script (Optional[Union[bytes, os.PathLike, str]]):
+                Provisioning script to run after new instance starts.
+            resources (Optional[List[Union[File, Dir]]]):
+                Resources to upload into the test environment instance after
+                new instance starts. You should only upload resources that are
+                required for provisioning the test environment instance.
+        """
+        names = [name] if type(name) == str else name
+        resources = resources if resources is not None else []
+        with ProcessPoolExecutor(max_workers=thread_count()) as pool:
+            for result in pool.map(
+                self._add,
+                (
+                    _AddWorkOrder(name, image, provision_script, resources)
+                    for name in names
+                ),
+            ):
+                self.__instances.add(result)
+
+    def _add(self, work_order: _AddWorkOrder) -> str:
+        """Sub-function for setting up new test environment instance.
+
+        Args:
+            work_order (_AddWorkOrder):
+                Information needed to add a new test environment instance.
+        """
+        _ = self.config.get_instance_config(work_order.image)
+        _.name = work_order.name
+        self.__client.instances.create(_.dict(), wait=True)
+        instance = self.__client.instances.get(work_order.name)
+        # TODO: Need to modify the start function so that it does not
+        #   progress until a LXD VM has been assigned a public address.
+        instance.start(wait=True)
+
+        logger.info("Before injection")
+        instance.execute(shlex.split("mkdir -p /root/.init/cleantest"))
+        for name, data in CleantestInfo().dumps():
+            instance.files.put(
+                f"/root/.init/cleantest/install_{name}",
+                data["injectable"],
+            )
+            instance.execute(
+                shlex.split(f"python3 /root/.init/cleantest/install_{name}")
+            )
+        logger.info("After injection")
+        for resource in work_order.resources:
+            self.push(
+                work_order.name, data_obj=resource, username="root", groupname="root"
+            )
+
+        if work_order.provision_script is not None:
+            logger.info(f"Executing provision script {work_order.provision_script}")
+            self.push(
+                work_order.name,
+                data_obj=[File(work_order.provision_script, "/root/.init/provision")],
+            )
+            instance.execute(shlex.split("python3 /root/.init/provision"))
+
+        return work_order.name
+
+    def execute(self, target: Union[str, List[str]], command: str) -> Dict[str, Result]:
+        """Execute commands on test environment instances.
+
+        Args:
+            target (Union[str, List[str]]):
+                Test environment instances to execute commands inside of.
+            command (str):
+                Command to execute.
+        """
+        _ = {}
+        targets = [target] if type(target) == str else target
+        [self.exists(target) for target in targets]
+        with ProcessPoolExecutor(max_workers=thread_count()) as pool:
+            for name, result in pool.map(
+                self._execute,
+                (_ExecuteWorkOrder(target, command) for target in targets),
+            ):
+                assert result.exit_code == 0
+                _.update({name: result})
+
+        return _
+
+    def _execute(self, work_order: _ExecuteWorkOrder) -> Tuple[str, Result]:
+        """Sub-function for executing commands on test environment instances.
+
+        Args:
+            work_order (_ExecuteWorkOrder):
+                Information needed to execute command on a test environment instance.
+
+        Returns:
+            (Tuple[str, Result]): Name of target instance and result of execution.
+        """
+        instance = self.__client.instances.get(work_order.target)
+        _ = instance.execute(shlex.split(work_order.command))
+        return work_order.target, Result(_.exit_code, _.stdout, _.stderr)
+
+    def pull(  # noqa C901
+        self,
+        target: str,
+        src: Optional[Union[str, os.PathLike]] = None,
+        dest: Optional[Union[str, os.PathLike]] = None,
+        data_obj: Optional[List[Union[Dir, File]]] = None,
+        uid: Optional[int] = None,
+        username: Optional[str] = None,
+        gid: Optional[int] = None,
+        groupname: Optional[str] = None,
+        mode: Optional[oct] = None,
+        overwrite: bool = False,
+    ) -> None:
+        """Pull object from test environment instance.
+
+        Args:
+            target (str):
+                Instance to pull object from.
+            src (Optional[Union[str, os.PathLike]]):
+                File path of object to pull from test environment instance.
+                (Default: None).
+            dest (Optional[Union[str, os.PathLike]]):
+                Destination to pull object to. (Default: None).
+            data_obj (Optional[List[Union[Dir, File]]]):
+                Cleantest data object(s) to pull. (Default: None).
+            uid (Optional[int]):
+                uid to set as owner of object on host. (Default: None).
+            username (Optional[str]):
+                User to set as owner of object on host. (Default: None).
+            gid (Optional[int]):
+                gid to set as group of object on host. (Default: None).
+            groupname (Optional[str]):
+                Group name to set as group of object on host. (Default: None).
+            mode (Optional[oct]):
+                Mode to set on object on host. (Default: None).
+            overwrite (bool):
+                Overwrite object if it already exists on host. (Default: False).
+
+        Raises:
+            LXDArchonError: Raised if error is encountered when pulling object.
+        """
+        _objects = (
+            [data_obj]
+            if isinstance(data_obj, File) or isinstance(data_obj, Dir)
+            else data_obj
+            if data_obj is not None
+            else []
+        )
+
+        if not self.exists(target):
+            raise LXDArchonError(
+                f"Instance {target} does not exist. Cannot pull object."
+            )
+        if src is None and dest is None and data_obj is None:
+            raise LXDArchonError(f"Nothing to pull from {target}.")
+        if (src is not None and dest is None) or (src is None and dest is not None):
+            raise LXDArchonError(
+                f"src or dest not specified. (src: {src}, dest: {dest}),"
+            )
+        if uid is not None and username is not None:
+            raise LXDArchonError("Please specify only uid or username, not both.")
+        if gid is not None and groupname is not None:
+            raise LXDArchonError("Please specify only gid or groupname, not both.")
+
+        instance = self.__client.instances.get(target)
+        instance.execute(shlex.split("mkdir -p /root/.pull"))
+
+        if src is not None and dest is not None:
+            if instance.execute(shlex.split(f"test -f  {src}")).exit_code == 0:
+                _ = File(src, dest, overwrite=overwrite)
+            elif instance.execute(shlex.split(f"test -d {src}")).exit_code == 0:
+                _ = Dir(src, dest, overwrite=overwrite)
+            else:
+                raise LXDArchonError(f"{src} is not a file or directory.")
+
+            _objects.append(_)
+
+        for _ in _objects:
+            instance.files.put("/root/.pull/load", _._dumps(mode="pull")["injectable"])
+            result = json.loads(
+                instance.execute(shlex.split("python3 /root/.pull/load")).stdout
+            )
+            placeholder = _.__class__._loads(result["checksum"], result["data"])
+            placeholder.dump()
+            if (
+                uid is not None
+                or username is not None
+                or gid is not None
+                or groupname is not None
+            ):
+                if placeholder.dest.is_dir():
+                    for d in placeholder.dest.iterdir():
+                        shutil.chown(d, user=uid or username, group=gid or groupname)
+                else:
+                    shutil.chown(
+                        placeholder.dest, user=uid or username, group=gid or groupname
+                    )
+            if mode is not None:
+                if placeholder.dest.is_dir():
+                    for d in placeholder.dest.iterdir():
+                        d.chmod(mode)
+                else:
+                    placeholder.chmod(mode)
+
+    def push(  # noqa C901
+        self,
+        target: str,
+        src: Optional[Union[str, os.PathLike]] = None,
+        dest: Optional[Union[str, os.PathLike]] = None,
+        data_obj: Optional[List[Union[Dir, File]]] = None,
+        uid: Optional[int] = None,
+        username: Optional[str] = None,
+        gid: Optional[int] = None,
+        groupname: Optional[str] = None,
+        mode: Optional[oct] = None,
+        overwrite: bool = False,
+    ) -> None:
+        """Push object into test environment instance.
+
+        Args:
+            target (str):
+                Instance to push object to.
+            src (Optional[Union[str, os.PathLike]]):
+                File path of object to push. (Default: None).
+            dest (Optional[Union[str, os.PathLike]]):
+                Destination to push object to. (Default: None).
+            data_obj (Optional[List[Union[Dir, File]]]):
+                Cleantest data object(s) to push. (Default: None).
+            uid (Optional[int]):
+                uid to set as owner of object inside test environment instance.
+                (Default: None).
+            username (Optional[str]):
+                User to set as owner of object inside test environment instance.
+                (Default: None).
+            gid (Optional[int]):
+                gid to set as group of object inside test environment instance.
+                (Default: None).
+            groupname (Optional[str]):
+                Group name to set as group of object inside test environment instance.
+                (Default: None).
+            mode (Optional[oct]):
+                Mode to set on object inside test environment instance.
+                (Default: None).
+            overwrite (bool):
+                Overwrite object if it already exists in the test environment instance.
+                (Default: False).
+
+        Raises:
+            LXDArchonError: Raised if error is encountered when pushing object.
+        """
+        _objects = (
+            [data_obj]
+            if isinstance(data_obj, File) or isinstance(data_obj, Dir)
+            else data_obj
+            if data_obj is not None
+            else []
+        )
+
+        if not self.exists(target):
+            raise LXDArchonError(
+                f"Instance {target} does not exist. Cannot push object."
+            )
+        if src is None and dest is None and data_obj is None:
+            raise LXDArchonError(f"Nothing to push to {target}.")
+        if (src is not None and dest is None) or (src is None and dest is not None):
+            raise LXDArchonError(
+                f"src or dest not specified. (src: {src}, dest: {dest}),"
+            )
+        if uid is not None and username is not None:
+            raise LXDArchonError("Please specify only uid or username, not both.")
+        if gid is not None and groupname is not None:
+            raise LXDArchonError("Please specify only gid or groupname, not both.")
+
+        instance = self.__client.instances.get(target)
+        instance.execute(shlex.split("mkdir -p /root/.push"))
+        if src is not None and dest is not None:
+            if pathlib.Path(src).is_file():
+                _ = File(src, dest, overwrite=overwrite)
+            elif pathlib.Path(src).is_dir():
+                _ = Dir(src, dest, overwrite=overwrite)
+            else:
+                raise FileNotFoundError(f"Cannot locate src {src} on file system.")
+
+            _objects.append(_)
+
+        for _ in _objects:
+            _.load()
+            instance.files.put("/root/.push/dump", _._dumps(mode="push")["injectable"])
+            instance.execute(shlex.split("python3 /root/.push/dump"))
+            if (
+                uid is not None
+                or username is not None
+                or gid is not None
+                or groupname is not None
+            ):
+                instance.execute(
+                    shlex.split(
+                        f"chown -R {uid or username}:{gid or groupname} {str(_.dest)}"
+                    )
+                )
+            if mode is not None:
+                instance.execute(shlex.split(f"chmod -R {mode} {str(_.dest)}"))
+
+    def destroy(self) -> None:
+        """Destroy all test environment instances in LXD cluster."""
+        logger.info(f"Destroying instances {self.__instances}")
+        for i in self.__instances:
+            instance = self.__client.instances.get(i)
+            instance.stop(wait=True)
+            instance.delete(wait=True)
+
+    def get_public_address(
+        self, target: str, ipv6: bool = False
+    ) -> Optional[Union[IPv4Address, IPv6Address]]:
+        """Get the public address of a test environment instance.
+
+        Args:
+            target (str): Name of test environment instance.
+            ipv6 (bool): Get the public IPv6 address rather than IPv4 address.
+                (Default: False).
+
+        Returns:
+            (Union[IPv4Address, IPv6Address]):
+                Public address of test environment instance.
+                None if the instance does not have a public address.
+        """
+        _ = csv.reader(StringIO(next(run("lxc list -c n46 -f csv")).stdout))
+        for row in _:
+            if row[0] == target and ipv6:
+                return IPv6Address(row[2].split(" ")[0]) if row[2] != "" else None
+            elif row[0] == target:
+                return IPv4Address(row[1].split(" ")[0]) if row[1] != "" else None
diff --git a/src/cleantest/utils/__init__.py b/src/cleantest/utils/__init__.py
index 3bacad3..966d529 100644
--- a/src/cleantest/utils/__init__.py
+++ b/src/cleantest/utils/__init__.py
@@ -2,6 +2,9 @@
 # Copyright 2023 Jason C. Nucciarone
 # See LICENSE file for licensing details.
 
-from . import apt_handler as apt
-from . import snap_handler as snap
+"""Utilities for testlets and cleantest."""
+
+from . import apt
+from . import snap
+from . import systemd
 from .run import run
diff --git a/src/cleantest/utils/apt_handler.py b/src/cleantest/utils/apt.py
similarity index 100%
rename from src/cleantest/utils/apt_handler.py
rename to src/cleantest/utils/apt.py
diff --git a/src/cleantest/utils/run.py b/src/cleantest/utils/run.py
index 6bc30d1..95f4153 100644
--- a/src/cleantest/utils/run.py
+++ b/src/cleantest/utils/run.py
@@ -1,9 +1,10 @@
 #!/usr/bin/env python3
-# Copyright 2023 Jason C. Nucciarone, Canonical Ltd.
+# Copyright 2023 Jason C. Nucciarone
 # See LICENSE file for licensing details.
 
 """Run commands inside test environment instances."""
 
+import shlex
 import subprocess
 from typing import Any, Dict, Iterable, Optional
 
@@ -35,7 +36,7 @@ def run(
     for command in commands:
         try:
             res = subprocess.run(
-                command.split(" "),
+                shlex.split(command),
                 env=(env if env else None),
                 cwd=(cwd if cwd else None),
                 stdout=subprocess.PIPE,
diff --git a/src/cleantest/utils/snap_handler.py b/src/cleantest/utils/snap.py
similarity index 100%
rename from src/cleantest/utils/snap_handler.py
rename to src/cleantest/utils/snap.py
diff --git a/tests/functional/files/test_upload_download.py b/tests/functional/files/test_upload_download.py
index 1637bce..1edc8e0 100644
--- a/tests/functional/files/test_upload_download.py
+++ b/tests/functional/files/test_upload_download.py
@@ -21,7 +21,7 @@ def work_on_artifacts():
     import sys
 
     print(pathlib.Path("/root/greeting.txt").is_file(), file=sys.stdout)
-    print(pathlib.Path("/root/dump").is_dir(), file=sys.stdout)
+    print(pathlib.Path("/root/greetings").is_dir(), file=sys.stdout)
 
     pathlib.Path("/root/dump.txt").write_text("Dumped like a sack of rocks")
     os.mkdir("/root/dump")
diff --git a/tests/functional/lxd/archon/ldap_provision_script.py b/tests/functional/lxd/archon/ldap_provision_script.py
new file mode 100644
index 0000000..a6ac903
--- /dev/null
+++ b/tests/functional/lxd/archon/ldap_provision_script.py
@@ -0,0 +1,80 @@
+#!/usr/bin/env python3
+# Copyright 2023 Jason C. Nucciarone
+# See LICENSE file for licensing details.
+
+"""Provision LDAP server nodes."""
+
+import pathlib
+import tempfile
+import textwrap
+
+from cleantest.utils import apt, systemd, run
+
+# Define resources needed to set up LDAP.
+slapd_preseed = textwrap.dedent(
+    """
+    slapd slapd/no_configuration boolean false
+    slapd slapd/domain string mini-hpc.org
+    slapd shared/organization string mini-hpc
+    slapd slapd/password1 password test
+    slapd slapd/password2 password test
+    slapd slapd/purge_database boolean true
+    slapd slapd/move_old_database boolean true
+    """
+).strip("\n")
+default_ldif = textwrap.dedent(
+    """
+    dn: ou=People,dc=mini-hpc,dc=org
+    objectClass: organizationalUnit
+    ou: People
+    
+    dn: ou=Groups,dc=mini-hpc,dc=org
+    objectClass: organizationalUnit
+    ou: Groups
+    
+    dn: uid=nucci,ou=People,dc=mini-hpc,dc=org
+    uid: nucci
+    objectClass: inetOrgPerson
+    objectClass: posixAccount
+    cn: nucci
+    sn: nucci
+    givenName: nucci
+    mail: nucci@example.com
+    userPassword: test
+    uidNumber: 10000
+    gidNumber: 10000
+    loginShell: /bin/bash
+    homeDirectory: /home/nucci
+    
+    dn: cn=nucci,ou=Groups,dc=mini-hpc,dc=org
+    cn: nucci
+    objectClass: posixGroup
+    gidNumber: 10000
+    memberUid: nucci
+    
+    dn: cn=research,ou=Groups,dc=mini-hpc,dc=org
+    cn: research
+    objectClass: posixGroup
+    gidNumber: 10100
+    memberUid: nucci
+    """
+).strip("\n")
+
+# Set up slapd service.
+apt.update()
+apt.install("slapd", "ldap-utils", "debconf-utils")
+with tempfile.NamedTemporaryFile() as preseed, tempfile.NamedTemporaryFile() as ldif:
+    pathlib.Path(preseed.name).write_text(slapd_preseed)
+    pathlib.Path(ldif.name).write_text(default_ldif)
+    results = run(
+        f"debconf-set-selections < {preseed.name}",
+        "dpkg-reconfigure -f noninteractive slapd",
+        (
+            "ldapadd -x -D cn=admin,dc=mini-hpc,dc=org -w "
+            f"test -f {ldif.name} -H ldap:///"
+        ),
+    )
+    for result in results:
+        assert result.exit_code == 0
+
+systemd.restart("slapd")
diff --git a/tests/functional/lxd/archon/nfs_provision_script.py b/tests/functional/lxd/archon/nfs_provision_script.py
new file mode 100644
index 0000000..a35e66e
--- /dev/null
+++ b/tests/functional/lxd/archon/nfs_provision_script.py
@@ -0,0 +1,51 @@
+#!/usr/bin/env python3
+# Copyright 2023 Jason C. Nucciarone
+# See LICENSE file for licensing details.
+
+"""Provision NFS server nodes."""
+
+import pathlib
+import textwrap
+
+from cleantest.utils import apt, systemd, run
+
+# Define resources needed to set up nfs-kernel-server.
+default_exports = textwrap.dedent(
+    """
+    /srv     *(ro,sync,subtree_check)
+    /home    *(rw,sync,no_subtree_check)
+    /data    *(rw,sync,no_subtree_check,no_root_squash)
+    /opt     *(rw,sync,no_subtree_check,no_root_squash)
+    """
+).strip("\n")
+
+# Set up SSSD service.
+apt.update()
+apt.install("nfs-kernel-server", "sssd-ldap")
+for result in run(
+    "mv /root/.init/sssd.conf /etc/sssd/sssd.conf",
+    "chmod 0600 /etc/sssd/sssd.conf",
+    "pam-auth-update --enable mkhomedir",
+):
+    assert result.exit_code == 0
+
+systemd.restart("sssd")
+
+# Set up NFS kernel server.
+for result in run(
+    "mkdir -p /data/nucci",
+    "mkdir -p /home/nucci",
+    "chown -R nucci:nucci /data/nucci",
+    "chown -R nucci:nucci /home/nucci",
+    "chmod 0755 /data",
+    "chmod -R 0750 /data/nucci",
+    "chmod -R 0740 /home/nucci",
+    "ln -s /data/nucci /home/nucci/data",
+):
+    assert result.exit_code == 0
+
+pathlib.Path("/etc/exports").write_text(default_exports)
+for result in run("exportfs -a"):
+    assert result.exit_code == 0
+
+systemd.restart("nfs-kernel-server")
diff --git a/tests/functional/lxd/archon/slurmctld_provision_script.py b/tests/functional/lxd/archon/slurmctld_provision_script.py
new file mode 100644
index 0000000..2e5c482
--- /dev/null
+++ b/tests/functional/lxd/archon/slurmctld_provision_script.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python3
+# Copyright 2023 Jason C. Nucciarone
+# See LICENSE file for licensing details.
+
+"""Provision slurmctld nodes."""
+
+import pathlib
+import json
+from io import StringIO
+
+from cleantest.utils import apt, systemd, run
+
+# Set up SSSD service.
+apt.update()
+apt.install("slurmctld", "nfs-common", "sssd-ldap")
+for result in run(
+    "mv /root/.init/sssd.conf /etc/sssd/sssd.conf",
+    "chmod 0600 /etc/sssd/sssd.conf",
+    "pam-auth-update --enable mkhomedir",
+):
+    assert result.exit_code == 0
+
+systemd.restart("sssd")
+
+# Set up NFS mount.
+nfs_ip = json.load(StringIO(pathlib.Path("/root/.init/nfs-0").read_text()))
+for result in run(
+    f"mount {nfs_ip['nfs-0']}:/home /home",
+    "mkdir -p /data",
+    f"mount {nfs_ip['nfs-0']}:/data /data",
+):
+    assert result.exit_code == 0
diff --git a/tests/functional/lxd/archon/slurmd_provision_script.py b/tests/functional/lxd/archon/slurmd_provision_script.py
new file mode 100644
index 0000000..9c0cc12
--- /dev/null
+++ b/tests/functional/lxd/archon/slurmd_provision_script.py
@@ -0,0 +1,42 @@
+#!/usr/bin/env python3
+# Copyright 2023 Jason C. Nucciarone
+# See LICENSE file for licensing details.
+
+"""Provision slurmd nodes."""
+
+import json
+import pathlib
+from io import StringIO
+
+from cleantest.utils import apt, systemd, run
+
+# Set up SSSD service.
+apt.update()
+apt.install("slurmd", "nfs-common", "sssd-ldap")
+for result in run(
+    "mv /root/.init/sssd.conf /etc/sssd/sssd.conf",
+    "chmod 0600 /etc/sssd/sssd.conf",
+    "pam-auth-update --enable mkhomedir",
+):
+    assert result.exit_code == 0
+
+systemd.restart("sssd")
+
+# Set up NFS mount.
+nfs_ip = json.load(StringIO(pathlib.Path("/root/.init/nfs-0").read_text()))
+for result in run(
+    f"mount {nfs_ip['nfs-0']}:/home /home",
+    "mkdir -p /data",
+    f"mount {nfs_ip['nfs-0']}:/data /data",
+):
+    assert result.exit_code == 0
+
+# Set up munge key.
+for result in run(
+    "mv /root/.init/munge.key /etc/munge/munge.key",
+    "chown munge:munge /etc/munge/munge.key",
+    "chmod 0600 /etc/munge/munge.key",
+):
+    assert result.exit_code == 0
+
+systemd.restart("munge")
diff --git a/tests/functional/lxd/archon/templates/slurm.conf.tmpl b/tests/functional/lxd/archon/templates/slurm.conf.tmpl
new file mode 100644
index 0000000..3d804e1
--- /dev/null
+++ b/tests/functional/lxd/archon/templates/slurm.conf.tmpl
@@ -0,0 +1,31 @@
+SlurmctldHost={{ slurmctld_name }}({{ slurmctld_address }})
+ClusterName=mini-hpc
+
+AuthType=auth/munge
+FirstJobId=65536
+InactiveLimit=120
+JobCompType=jobcomp/filetxt
+JobCompLoc=/var/log/slurm/jobcomp
+ProctrackType=proctrack/linuxproc
+KillWait=30
+MaxJobCount=10000
+MinJobAge=3600
+ReturnToService=0
+SchedulerType=sched/backfill
+SlurmctldLogFile=/var/log/slurm/slurmctld.log
+SlurmdLogFile=/var/log/slurm/slurmd.log
+SlurmctldPort=7002
+SlurmdPort=7003
+SlurmdSpoolDir=/var/spool/slurmd.spool
+StateSaveLocation=/var/spool/slurm.state
+SwitchType=switch/none
+TmpFS=/tmp
+WaitTime=30
+
+# Node Configurations
+NodeName={{ slurmd_0_name }} NodeAddr={{ slurmd_0_address }} CPUs=1 RealMemory=1000 TmpDisk=10000
+NodeName={{ slurmd_1_name }} NodeAddr={{ slurmd_1_address }} CPUs=1 RealMemory=1000 TmpDisk=10000
+NodeName={{ slurmd_2_name }} NodeAddr={{ slurmd_2_address }} CPUs=1 RealMemory=1000 TmpDisk=10000
+
+# Partition Configurations
+PartitionName=all Nodes={{ slurmd_0_name }},{{ slurmd_1_name }},{{ slurmd_2_name }} MaxTime=30 MaxNodes=3 State=UP
\ No newline at end of file
diff --git a/tests/functional/lxd/archon/templates/sssd.conf.tmpl b/tests/functional/lxd/archon/templates/sssd.conf.tmpl
new file mode 100644
index 0000000..b9b6ea9
--- /dev/null
+++ b/tests/functional/lxd/archon/templates/sssd.conf.tmpl
@@ -0,0 +1,10 @@
+[sssd]
+config_file_version = 2
+domains = mini-hpc.org
+
+[domain/mini-hpc.org]
+id_provider = ldap
+auth_provider = ldap
+ldap_uri = ldap://{{ ldap_server_address }}
+cache_credentials = True
+ldap_search_base = dc=mini-hpc,dc=org
diff --git a/tests/functional/lxd/archon/test_archon.py b/tests/functional/lxd/archon/test_archon.py
new file mode 100644
index 0000000..80c6a40
--- /dev/null
+++ b/tests/functional/lxd/archon/test_archon.py
@@ -0,0 +1,150 @@
+#!/usr/bin/env python3
+# Copyright 2023 Jason C. Nucciarone
+# See LICENSE file for licensing details.
+
+"""Test LXDArchon functionality."""
+
+import json
+import os
+import pathlib
+from io import StringIO
+
+from jinja2 import Environment, FileSystemLoader
+
+from cleantest.control.hooks import StopEnvHook
+from cleantest.control.lxd import InstanceConfig
+from cleantest.data import File
+from cleantest.provider import lxd, LXDArchon
+
+root = pathlib.Path(os.path.dirname(os.path.realpath(__file__)))
+templates = Environment(loader=FileSystemLoader(root / "templates"))
+
+
+@lxd.target("slurmctld-0")
+def run_job():
+    import os
+    import pathlib
+    import shutil
+    import textwrap
+    from time import sleep
+
+    from cleantest.utils import run
+
+    tmp_dir = pathlib.Path("/tmp")
+    (tmp_dir / "research.submit").write_text(
+        textwrap.dedent(
+            """
+            #!/bin/bash
+            #SBATCH --job-name=research
+            #SBATCH --partition=all
+            #SBATCH --nodes=1
+            #SBATCH --ntasks-per-node=1
+            #SBATCH --cpus-per-task=1
+            #SBATCH --mem=500mb
+            #SBATCH --time=00:00:30
+            #SBATCH --error=research.err
+            #SBATCH --output=research.out
+            
+            echo "I love doing research!"
+            """
+        ).strip("\n")
+    )
+
+    # Set user to test cluster user nucci.
+    os.setuid(10000)
+    os.chdir("/home/nucci")
+    for result in run(
+        f"cp {(tmp_dir / 'research.submit')} .",
+        "sbatch research.submit",
+    ):
+        assert result.exit_code == 0
+    sleep(60)
+    shutil.copy("research.out", (tmp_dir / "result"))
+
+
+def test_lxd_archon_local() -> None:
+    """Test LXDArchon against local LXD cluster."""
+    archon = LXDArchon()
+    archon.config.register_hook(
+        StopEnvHook(name="get_result", download=[File("/tmp/result", root / "result")])
+    )
+    _ = archon.config.get_instance_config("ubuntu-jammy-amd64").dict()
+    _["name"] = "mini-hpc-sm"
+    archon.config.add_instance_config(
+        InstanceConfig(
+            config={
+                "limits.cpu": "1",
+                "limits.memory": "8GB",
+                "security.privileged": "true",
+                "raw.apparmor": "mount fstype=nfs*, mount fstype=rpc_pipefs,",
+            },
+            **_,
+        )
+    )
+    archon.add(
+        "ldap-0",
+        image="mini-hpc-sm",
+        provision_script=root / "ldap_provision_script.py",
+    )
+    sssd_conf = StringIO(
+        templates.get_template("sssd.conf.tmpl").render(
+            ldap_server_address=archon.get_public_address("ldap-0")
+        )
+    )
+    archon.add(
+        "nfs-0",
+        image="mini-hpc-sm",
+        provision_script=root / "nfs_provision_script.py",
+        resources=[File(sssd_conf, "/root/.init/sssd.conf")],
+    )
+    nfs_ip = json.dumps({"nfs-0": str(archon.get_public_address("nfs-0"))})
+    archon.add(
+        "slurmctld-0",
+        image="mini-hpc-sm",
+        provision_script=root / "slurmctld_provision_script.py",
+        resources=[
+            File(sssd_conf, "/root/.init/sssd.conf"),
+            File(StringIO(nfs_ip), "/root/.init/nfs-0"),
+        ],
+    )
+    archon.pull(
+        "slurmctld-0", data_obj=[File("/etc/munge/munge.key", root / "munge.key")]
+    )
+    archon.add(
+        ["slurmd-0", "slurmd-1", "slurmd-2"],
+        image="mini-hpc-sm",
+        provision_script=root / "slurmd_provision_script.py",
+        resources=[
+            File(sssd_conf, "/root/.init/sssd.conf"),
+            File(StringIO(nfs_ip), "/root/.init/nfs-0"),
+            File(root / "munge.key", "/root/.init/munge.key"),
+        ],
+    )
+    slurm_node_info = {
+        "slurmctld_name": "slurmctld-0",
+        "slurmctld_address": archon.get_public_address("slurmctld-0"),
+        "slurmd_0_name": "slurmd-0",
+        "slurmd_0_address": archon.get_public_address("slurmd-0"),
+        "slurmd_1_name": "slurmd-1",
+        "slurmd_1_address": archon.get_public_address("slurmd-1"),
+        "slurmd_2_name": "slurmd-2",
+        "slurmd_2_address": archon.get_public_address("slurmd-2"),
+    }
+    slurm_conf = StringIO(
+        templates.get_template("slurm.conf.tmpl").render(**slurm_node_info)
+    )
+    for node in ["slurmctld-0", "slurmd-0", "slurmd-1", "slurmd-2"]:
+        archon.push(node, data_obj=[File(slurm_conf, "/etc/slurm/slurm.conf")])
+    archon.execute("slurmctld-0", command="systemctl start slurmctld")
+    archon.execute(
+        ["slurmd-0", "slurmd-1", "slurmd-2"], command="systemctl start slurmd"
+    )
+    for name, result in run_job():
+        assert "I love doing research!" in pathlib.Path(root / "result").read_text()
+    (root / "munge.key").unlink(missing_ok=True)
+    (root / "result").unlink(missing_ok=True)
+    archon.execute(
+        ["slurmctld-0", "slurmd-0", "slurmd-1", "slurmd-2"],
+        command=f"umount /home /data",
+    )
+    archon.destroy()