Repository: NucciTheBoss/cleantest (language: python)

Issue:
Cleantest blows up `/tmp` with files
Cleantest blows up the `/tmp` directory on Linux systems with the various pickles, scriptlets, and tarballs that it needs to upload into the test environment providers. `/tmp` is emptied out after every reboot, but that does not do you much good if you almost never reboot your system.

As such, rather than just getting the name of the temporary directory on the host, instead use a named temporary file created with the `tempfile` module. This should cut down on the amount of space being consumed by cleantest on `/tmp`. Lastly, this will allow for better organization of files within the test environment.


Discussion / hints:

Fix patch:
diff --git a/docs/user-guide/packages/charmlib.md b/docs/user-guide/packages/charmlib.md
index 35280a6..3abaa82 100644
--- a/docs/user-guide/packages/charmlib.md
+++ b/docs/user-guide/packages/charmlib.md
@@ -34,7 +34,7 @@ charmcraft login --export charmhub.secret
 ```
 
 After authenticating with Charmhub (you may need to create an Ubuntu One account), you can now use the example
-test script below: 
+test script below:
 
 ```python
 #!/usr/bin/env python3
@@ -44,8 +44,8 @@ test script below:
 import os
 
 from cleantest import Configure
-from cleantest.hooks import StartEnvHook
-from cleantest.pkg import Charmlib
+from cleantest.control.hooks import StartEnvHook
+from cleantest.data.pkg import Charmlib
 from cleantest.provider import lxd
 
 root = os.path.dirname(os.path.realpath(__file__))
@@ -88,7 +88,7 @@ def install_snapd():
 
     sys.exit(0)
 
-    
+
 class TestCharmlib:
     def test_charmlib(self) -> None:
         results = install_snapd()


Test patch:
diff --git a/src/cleantest/__init__.py b/src/cleantest/__init__.py
index 9a75b22..bf98b47 100644
--- a/src/cleantest/__init__.py
+++ b/src/cleantest/__init__.py
@@ -1,5 +1,3 @@
 #!/usr/bin/env python3
 # Copyright 2022 Canonical Ltd.
 # See LICENSE file for licensing details.
-
-from .control import Configure
diff --git a/src/cleantest/control/__init__.py b/src/cleantest/control/__init__.py
index 12260d7..e50b0c2 100644
--- a/src/cleantest/control/__init__.py
+++ b/src/cleantest/control/__init__.py
@@ -1,5 +1,6 @@
 #!/usr/bin/env python3
-# Copyright 2022 Canonical Ltd.
+# Copyright 2023 Jason C. Nucciarone, Canonical Ltd.
 # See LICENSE file for licensing details.
 
 from .configure import Configure
+from .env import Env
diff --git a/src/cleantest/control/_configurers/__init__.py b/src/cleantest/control/_configurers/__init__.py
new file mode 100644
index 0000000..58ef5ff
--- /dev/null
+++ b/src/cleantest/control/_configurers/__init__.py
@@ -0,0 +1,5 @@
+#!/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/_base_configurer.py b/src/cleantest/control/_configurers/_base_configurer.py
new file mode 100644
index 0000000..73a0bba
--- /dev/null
+++ b/src/cleantest/control/_configurers/_base_configurer.py
@@ -0,0 +1,91 @@
+#!/usr/bin/env python3
+# Copyright 2023 Jason C. Nucciarone, Canonical Ltd.
+# See LICENSE file for licensing details.
+
+"""Base tooling needed by configurer classes."""
+
+import copy
+from collections import deque
+from typing import Deque, Union
+
+from cleantest.control.hooks import StartEnvHook, StopEnvHook
+
+
+class DuplicateHookNameError(Exception):
+    """Raised when more than one hook of the same type have the same name."""
+
+    ...
+
+
+class HookRegistry:
+    """Centrally store hooks for use by test environment providers."""
+
+    metadata = set()
+    startenv = deque()
+    stopenv = deque()
+
+    def __new__(cls) -> "HookRegistry":
+        if not hasattr(cls, "instance"):
+            cls._instance = super(HookRegistry, cls).__new__(cls)
+        return cls._instance
+
+    def lint(self, hook: Union[StartEnvHook, StopEnvHook]) -> None:
+        """Lint hooks to ensure they compliant with set restrictions."""
+        lint = [h for h in self.metadata if hook.name == h[0] and hook.__class__.__name__ == h[1]]
+        if len(lint) > 0:
+            raise DuplicateHookNameError(
+                f"Hook type {hook.__class__.__name__} with name {hook.name} already exists."
+            )
+
+
+class BaseConfigurer:
+    """Base configure mixin for configurers."""
+
+    def register_hook(self, *hooks: Union[StartEnvHook, StopEnvHook]) -> None:
+        """Register hooks in the hook registry.
+
+        Args:
+            *hooks (Union[StartEnvHook, StopEnvHook]): Hooks to register.
+        """
+        dispatch = {
+            StartEnvHook.__name__: HookRegistry().startenv,
+            StopEnvHook.__name__: HookRegistry().stopenv,
+        }
+        for hook in hooks:
+            HookRegistry().lint(hook)
+            dispatch[hook.__class__.__name__].appendleft(hook)
+            HookRegistry().metadata.add((hook.name, hook.__class__.__name__))
+
+    def unregister_hook(self, *hook_names: str) -> None:
+        """Unregister hooks from the hook registry.
+
+        Args:
+            *hook_names (str): Names of hooks to unregister.
+        """
+        dispatch = {
+            StartEnvHook.__name__: HookRegistry().startenv,
+            StopEnvHook.__name__: HookRegistry().stopenv,
+        }
+        for name in hook_names:
+            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)
+
+    @property
+    def startenv_hooks(self) -> Deque[StartEnvHook]:
+        """Retrieve hooks that will run when the test environment starts.
+
+        Returns:
+            (Deque[StartEnvHook]): Deque containing start environment hooks.
+        """
+        return copy.deepcopy(HookRegistry().startenv)
+
+    @property
+    def stopenv_hooks(self) -> Deque[StopEnvHook]:
+        """Retrieve hooks that will run when the test environment stops.
+
+        Returns:
+            (Deque[StopEnvHook]): Deque containing stop environment hooks.
+        """
+        return copy.deepcopy(HookRegistry().stopenv)
diff --git a/src/cleantest/control/_configurers/lxd_configurer.py b/src/cleantest/control/_configurers/lxd_configurer.py
new file mode 100644
index 0000000..ad39336
--- /dev/null
+++ b/src/cleantest/control/_configurers/lxd_configurer.py
@@ -0,0 +1,86 @@
+#!/usr/bin/env python3
+# Copyright 2023 Jason C. Nucciarone, Canonical Ltd.
+# See LICENSE file for licensing details.
+
+"""Information needed by LXD test environment provider."""
+
+from typing import Dict
+
+from .._sources import LXDDefaultSources
+from ..lxd.lxd_config import InstanceConfig
+from ._base_configurer import BaseConfigurer
+
+
+class DuplicateLXDInstanceConfigError(Exception):
+    """Raised if two or more instance configurations share the same name."""
+
+
+class LXDInstanceConfigNotFoundError(Exception):
+    """Raised when the configuration for an LXD instance 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._configs.add(
+                    InstanceConfig(name=name.replace("_", "-").lower(), source=source)
+                )
+                for name, source in LXDDefaultSources.items()
+            ]
+        return cls.instance
+
+    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 config
+
+        raise LXDInstanceConfigNotFoundError(f"Could not find instance {name}.")
diff --git a/src/cleantest/control/_sources.py b/src/cleantest/control/_sources.py
new file mode 100644
index 0000000..645be93
--- /dev/null
+++ b/src/cleantest/control/_sources.py
@@ -0,0 +1,279 @@
+#!/usr/bin/env python3
+# Copyright 2023 Jason C. Nucciarone, Canonical Ltd.
+# See LICENSE file for licensing details.
+
+"""Default sources for test environment providers."""
+
+from enum import Enum
+from typing import Any, List, Tuple
+
+from .lxd.lxd_config 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",
+        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",
+        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",
+        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",
+        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",
+        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",
+        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",
+        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",
+        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",
+        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",
+        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",
+        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",
+        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",
+        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",
+        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",
+        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",
+        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/configure.py b/src/cleantest/control/configure.py
index 4f94a67..5d684ca 100644
--- a/src/cleantest/control/configure.py
+++ b/src/cleantest/control/configure.py
@@ -1,119 +1,30 @@
 #!/usr/bin/env python3
-# Copyright 2022 Jason C. Nucciarone, Canonical Ltd.
+# Copyright 2023 Jason C. Nucciarone, Canonical Ltd.
 # See LICENSE file for licensing details.
 
-"""Manage the state and flow of cleantest."""
+"""Configure the flow of cleantest based on the test environment provider used."""
 
-import copy
-from collections import deque
-from typing import Deque, Union
+from ._configurers import LXDConfigurer
 
-from cleantest.hooks import StartEnvHook, StartTestletHook, StopEnvHook, StopTestletHook
 
+class UnknownConfigurerError(Exception):
+    """Raised when an unknown configurer option is passed to Configure."""
 
-class DuplicateHookNameError(Exception):
-    """Raised when more than one hook in the same classification share the same name."""
 
-    ...
+def Configure(configurer: str = "lxd") -> LXDConfigurer:
+    """Configure cleantest based on the test environment provider being used.
 
+    Args:
+        configurer (str): Configurer to use. Defaults to "lxd".
 
-class HookRegistry:
-    """Dataclass to store hooks based on hook classification."""
+    Raises:
+       UnknownConfigurerError: Raised if unknown configurer is specified.
 
-    start_env: Deque[StartEnvHook] = deque()
-    stop_env: Deque[StopEnvHook] = deque()
-    start_testlet: Deque[StartTestletHook] = deque()
-    stop_testlet: Deque[StopTestletHook] = deque()
+    Returns:
+        (LXDConfigurer): Configurer for LXD test environment provider.
+    """
+    dispatch = {"lxd": LXDConfigurer}
+    if configurer not in dispatch.keys():
+        raise UnknownConfigurerError(f"{configurer} is not a valid configurer option.")
 
-
-class Configure:
-    """Configure cleantest."""
-
-    _hook_registry = HookRegistry()
-    _metadata = set()
-
-    def __new__(cls) -> "Configure":
-        if not hasattr(cls, "instance"):
-            cls.instance = super(Configure, cls).__new__(cls)
-        return cls.instance
-
-    def register_hook(
-        self, *hooks: Union[StartEnvHook, StopEnvHook, StartTestletHook, StopTestletHook]
-    ) -> None:
-        """Register hooks in the hook registry.
-
-        Args:
-            *hooks (Union[StartEnvHook, StopEnvHook, StartTestletHook, StopTestletHook]):
-                Hooks to register.
-        """
-        dispatch = {
-            StartEnvHook.__name__: self._hook_registry.start_env,
-            StopEnvHook.__name__: self._hook_registry.stop_env,
-            StartTestletHook.__name__: self._hook_registry.start_testlet,
-            StopTestletHook.__name__: self._hook_registry.stop_testlet,
-        }
-        for hook in hooks:
-            lint = [
-                h for h in self._metadata if hook.name == h[0] and hook.__class__.__name__ == h[1]
-            ]
-            if len(lint) > 0:
-                raise DuplicateHookNameError(
-                    f"Hook type {hook.__class__.__name__} with name {hook.name} already exists."
-                )
-            dispatch[hook.__class__.__name__].appendleft(hook)
-            self._metadata.add((hook.name, hook.__class__.__name__))
-
-    def deregister_hook(self, *hooks: str) -> None:
-        """Deregister hooks from the hook registry.
-
-        Args:
-            *hooks (str): Name of hook to deregister.
-        """
-        dispatch = {
-            StartEnvHook.__name__: self._hook_registry.start_env,
-            StopEnvHook.__name__: self._hook_registry.stop_env,
-            StartTestletHook.__name__: self._hook_registry.start_testlet,
-            StopTestletHook.__name__: self._hook_registry.stop_testlet,
-        }
-        for hook_name in hooks:
-            for hook in self._metadata:
-                if hook_name == hook[0]:
-                    dispatch[hook[1]].remove(hook_name)
-
-    def get_start_env_hooks(self) -> Deque[StartEnvHook]:
-        """Get hooks that will run when the test environment starts.
-
-        Returns:
-            (Deque[StartEnvHook]): Deque containing start environment hooks.
-        """
-        return copy.deepcopy(self._hook_registry.start_env)
-
-    def get_stop_env_hooks(self) -> Deque[StopEnvHook]:
-        """Get hooks that will run when the test environment stops.
-
-        Returns:
-            (Deque[StopEnvHook]): Deque containing stop environment hooks.
-        """
-        return copy.deepcopy(self._hook_registry.stop_env)
-
-    def get_start_testlet_hooks(self) -> Deque[StartTestletHook]:
-        """Get hooks that will run before the testlet is started.
-
-        Returns:
-            (Deque[StartTestletHook]): Deque containing start testlet hooks.
-
-        Warnings:
-            This hook may be deleted in a later version of cleantest.
-        """
-        return copy.deepcopy(self._hook_registry.start_testlet)
-
-    def get_stop_testlet_hooks(self) -> Deque[StopTestletHook]:
-        """Get hooks that will run after the testlet has completed.
-
-        Returns:
-            (Deque[StopTestletHook]): Deque containing stop testlet hooks.
-
-        Warnings:
-            This hook may be deleted in a later version of cleantest.
-        """
-        return copy.deepcopy(self._hook_registry.stop_testlet)
+    return dispatch[configurer]()
diff --git a/src/cleantest/provider/data/env_data.py b/src/cleantest/control/env.py
similarity index 67%
rename from src/cleantest/provider/data/env_data.py
rename to src/cleantest/control/env.py
index 73401a9..a880bd8 100644
--- a/src/cleantest/provider/data/env_data.py
+++ b/src/cleantest/control/env.py
@@ -1,21 +1,21 @@
 #!/usr/bin/env python3
-# Copyright 2022 Jason C. Nucciarone, Canonical Ltd.
+# Copyright 2023 Jason C. Nucciarone, Canonical Ltd.
 # See LICENSE file for licensing details.
 
 """Environment variable management for test providers."""
 
 import os
-from typing import Any, Dict, Union
+from typing import Any, Dict, Optional
 
 
-class EnvDataStore:
+class Env:
     """Manage environment data for test environments."""
 
     _env = {}
 
-    def __new__(cls) -> object:
+    def __new__(cls) -> "Env":
         if not hasattr(cls, "instance"):
-            cls.instance = super(EnvDataStore, cls).__new__(cls)
+            cls.instance = super(Env, cls).__new__(cls)
         return cls.instance
 
     def add(self, env_mapping: Dict[str, Any]) -> None:
@@ -26,14 +26,25 @@ def add(self, env_mapping: Dict[str, Any]) -> None:
         """
         self._env.update(env_mapping)
 
-    def get(self, env_var: str) -> Union[Any, None]:
+    def remove(self, env_var: str) -> None:
+        """Remove an environment variable from store.
+
+        Does nothing if environment variable does not exist in store.
+
+        Args:
+            env_var (str): Environment variable to Remove from the store.
+        """
+        if env_var in self._env.keys():
+            del self._env[env_var]
+
+    def get(self, env_var: str) -> Optional[Any]:
         """Retrieve environment variable from store.
 
         Args:
             env_var (str): Environment variable to retrieve.
 
         Returns:
-            (Union[Any, None]): Environment variable value.
+            (Optional[Any]): Environment variable value.
                 Returns None if variable is not in store.
         """
         try:
diff --git a/src/cleantest/hooks/__init__.py b/src/cleantest/control/hooks/__init__.py
similarity index 53%
rename from src/cleantest/hooks/__init__.py
rename to src/cleantest/control/hooks/__init__.py
index f25b449..ff53a6d 100644
--- a/src/cleantest/hooks/__init__.py
+++ b/src/cleantest/control/hooks/__init__.py
@@ -1,8 +1,6 @@
 #!/usr/bin/env python3
-# Copyright 2022 Canonical Ltd.
+# Copyright 2023 Jason C. Nucciarone, Canonical Ltd.
 # See LICENSE file for licensing details.
 
 from .startenv import StartEnvHook
-from .starttestlet import StartTestletHook
 from .stopenv import StopEnvHook
-from .stoptestlet import StopTestletHook
diff --git a/src/cleantest/hooks/startenv.py b/src/cleantest/control/hooks/startenv.py
similarity index 100%
rename from src/cleantest/hooks/startenv.py
rename to src/cleantest/control/hooks/startenv.py
diff --git a/src/cleantest/hooks/stopenv.py b/src/cleantest/control/hooks/stopenv.py
similarity index 100%
rename from src/cleantest/hooks/stopenv.py
rename to src/cleantest/control/hooks/stopenv.py
diff --git a/src/cleantest/control/lxd/__init__.py b/src/cleantest/control/lxd/__init__.py
new file mode 100644
index 0000000..379b879
--- /dev/null
+++ b/src/cleantest/control/lxd/__init__.py
@@ -0,0 +1,5 @@
+#!/usr/bin/env python3
+# Copyright 2023 Jason C. Nucciarone, Canonical Ltd.
+# See LICENSE file for licensing details.
+
+from .lxd_config import ClientConfig, InstanceConfig, InstanceSource
diff --git a/src/cleantest/control/lxd/lxd_config.py b/src/cleantest/control/lxd/lxd_config.py
new file mode 100644
index 0000000..4909120
--- /dev/null
+++ b/src/cleantest/control/lxd/lxd_config.py
@@ -0,0 +1,222 @@
+#!/usr/bin/env python3
+# Copyright 2023 Jason C. Nucciarone, Canonical Ltd.
+# See LICENSE file for licensing details.
+
+"""Classes to assist with LXD instance configuration."""
+
+from typing import Any, Dict, List, Optional, Tuple, Union
+
+from cleantest.meta.mixins import DictOps
+
+
+class BadLXDConfigError(Exception):
+    """Raised when the newly entered configuration fails the lint check."""
+
+
+class ClientConfig:
+    def __init__(
+        self,
+        endpoint: Optional[str] = None,
+        version: str = "1.0",
+        cert: Optional[Tuple[str, str]] = None,
+        verify: bool = True,
+        timeout: Optional[Union[float, Tuple[float, float]]] = None,
+        project: Optional[str] = None,
+    ) -> None:
+        """Define an LXD client connection.
+
+        Args:
+            endpoint (Optional[str]): Endpoint can be an HTTP endpoint or
+                a path to a unix socket (Default: None).
+            version (str): API version string to use with LXD
+            cert (Optional[Tuple[str, str]]): A tuple of (cert, key) to use with
+                the HTTP socket for client authentication (Default: "1.0").
+            verify (bool): Either a boolean, in which case it controls
+                whether we verify the server's TLS certificate, or a string, in
+                which case it must be a path to a CA bundle to use.
+                (Default: True).
+            timeout (Optional[Union[float, Tuple[float, float]]]):
+                How long to wait for the server to send data before giving up, as a float, or a
+                (connect timeout, read timeout) tuple.
+            project (Optional[str]): Name of the LXD project to interact with (Default: None).
+        """
+        self.endpoint = endpoint
+        self.version = version
+        self.cert = cert
+        self.verify = verify
+        self.timeout = timeout
+        self.project = project
+
+
+class InstanceSource:
+    """Define an LXD instance source to use for a test environment instance.
+
+    Args:
+        alias (str): Alias of source.
+        mode (str): Mode for accessing source.
+        protocol (str): Protocol to use when pulling source.
+        server (str): Server to get source from.
+        type (str): Type of source.
+        allow_inconsistent (bool):
+            Whether to ignore errors when copying. e.g. for volatile files. (Default: None).
+        base_image (str):
+            Base image fingerprint for faster migration. (Default: None).
+        certificate (str):
+            Certificate for remote images or migration. (Default: None).
+        fingerprint (str): Fingerprint of image source. (Default: None).
+        instance_only (bool):
+            Whether the copy should skip the snapshots for copy. (Default: None).
+        live (bool): Whether this is a live migration. (Default: None).
+        operation (str):
+            Remote operation URL for migration. (Default: None).
+        project (str):
+            Source project name for copy and local image. (Default: None).
+        properties (Dict[str, str]): Image filters for image source. (Default: None).
+        refresh (bool):
+            Whether this is refreshing an existing instance for migration and copy.
+            (Default: None).
+        secret (str):
+            Remote server secret for remote private images. (Default: None).
+        secrets (Dict[str, str]):
+            Map of migration websockets for migration. (Default: None).
+        source (str):
+            Existing instance name or snapshot for copy. (Default: None)
+    """
+
+    def __init__(
+        self,
+        alias: str,
+        mode: str,
+        protocol: str,
+        server: str,
+        type: str,
+        allow_inconsistent: bool = None,
+        base_image: str = None,
+        certificate: str = None,
+        fingerprint: str = None,
+        instance_only: bool = None,
+        live: bool = None,
+        operation: str = None,
+        project: str = None,
+        properties: Dict[str, str] = None,
+        refresh: bool = None,
+        secret: str = None,
+        secrets: Dict[str, str] = None,
+        source: str = None,
+    ) -> None:
+        self.alias = alias
+        self.type = type
+        self.mode = mode
+        self.server = server
+        self.protocol = protocol
+        if allow_inconsistent is not None:
+            setattr(self, "allow_inconsistent", allow_inconsistent)
+        if base_image is not None:
+            setattr(self, "base_image", base_image)
+        if certificate is not None:
+            setattr(self, "certificate", certificate)
+        if fingerprint is not None:
+            setattr(self, "fingerprint", fingerprint)
+        if instance_only is not None:
+            setattr(self, "instance_only", instance_only)
+        if live is not None:
+            setattr(self, "live", live)
+        if operation is not None:
+            setattr(self, "operation", operation)
+        if project is not None:
+            setattr(self, "project", project)
+        if properties is not None:
+            setattr(self, "properties", properties)
+        if refresh is not None:
+            setattr(self, "refresh", refresh)
+        if secret is not None:
+            setattr(self, "secret", secret)
+        if secrets is not None:
+            setattr(self, "secrets", secrets)
+        if source is not None:
+            setattr(self, "source", source)
+
+
+class InstanceConfig(DictOps):
+    """Define an LXD instance that can be brought up for test environments.
+
+    Args:
+        name (str): Name to use for container or virtual machine.
+        source (InstanceSource): Source for LXD instance and where to get it from.
+        architecture (str): Architecture name.
+        config (Dict[str, str]): Instance configuration.
+        description (str): Description of instance.
+        devices (Dict[str, Dict[str, str]]): Devices to use with instance.
+        ephemeral (bool):
+            Whether the instance is ephemeral; deleted on shutdown. (Default: False).
+        instance_type (str):
+            Cloud instance type (AWS, GCP, Azure, ...) to emulate with limits. (Default: None).
+        profiles (List[str]):
+            List of profiles apply to the instance. (Default: None).
+        restore (str):
+            If set, instance will be restored to the provided snapshot name. (Default: None).
+        stateful (bool):
+            Whether the instance currently has saved state on the host. (Default: False).
+        type (str):
+            Type of instance. i.e. "container" or "vm". (Default: "container").
+    """
+
+    def __init__(
+        self,
+        name: str,
+        source: InstanceSource,
+        architecture: str = None,
+        config: Dict[str, str] = None,
+        description: str = None,
+        devices: Dict[str, Dict[str, str]] = None,
+        ephemeral: bool = None,
+        instance_type: str = None,
+        profiles: List[str] = None,
+        restore: str = None,
+        stateful: bool = None,
+        type: str = "container",
+    ) -> None:
+        self.name = name
+        self.source = source
+        self.type = type
+        if architecture is not None:
+            setattr(self, "architecture", architecture)
+        if config is not None:
+            setattr(self, "config", config)
+        if description is not None:
+            setattr(self, "description", description)
+        if devices is not None:
+            setattr(self, "devices", devices)
+        if ephemeral is not None:
+            setattr(self, "ephemeral", ephemeral)
+        if instance_type is not None:
+            setattr(self, "instance_type", instance_type)
+        if profiles is not None:
+            setattr(self, "profiles", profiles)
+        if restore is not None:
+            setattr(self, "restore", restore)
+        if stateful is not None:
+            setattr(self, "stateful", stateful)
+
+        self._lint(self.dict())
+
+    def _lint(self, new_config: Dict[str, Any]) -> 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.
+        """
+        checks = ["name", "server", "alias", "protocol", "type", "mode"]
+        config = self._deconstruct(new_config)
+        for i in checks:
+            if i not in config:
+                raise BadLXDConfigError(
+                    (
+                        f"Bad instance configuration: {new_config}. "
+                        "Please ensure instance configuration has the "
+                        f"following values set: {checks}."
+                    )
+                )
diff --git a/src/cleantest/hooks/data/__init__.py b/src/cleantest/data/__init__.py
similarity index 68%
rename from src/cleantest/hooks/data/__init__.py
rename to src/cleantest/data/__init__.py
index 91ed1fb..d20cb62 100644
--- a/src/cleantest/hooks/data/__init__.py
+++ b/src/cleantest/data/__init__.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright 2022 Jason C. Nucciarone, Canonical Ltd.
+# Copyright 2023 Jason C. Nucciarone, Canonical Ltd.
 # See LICENSE file for licensing details.
 
 from .directory import Dir
diff --git a/src/cleantest/hooks/data/directory.py b/src/cleantest/data/directory.py
similarity index 98%
rename from src/cleantest/hooks/data/directory.py
rename to src/cleantest/data/directory.py
index 6c0503f..6f80de6 100644
--- a/src/cleantest/hooks/data/directory.py
+++ b/src/cleantest/data/directory.py
@@ -11,7 +11,7 @@
 from io import BytesIO
 from typing import Iterable
 
-from .file import File
+from cleantest.data.file import File
 
 
 class DirectoryError(Exception):
diff --git a/src/cleantest/hooks/data/file.py b/src/cleantest/data/file.py
similarity index 100%
rename from src/cleantest/hooks/data/file.py
rename to src/cleantest/data/file.py
diff --git a/src/cleantest/pkg/__init__.py b/src/cleantest/data/pkg/__init__.py
similarity index 77%
rename from src/cleantest/pkg/__init__.py
rename to src/cleantest/data/pkg/__init__.py
index de25b6d..c470732 100644
--- a/src/cleantest/pkg/__init__.py
+++ b/src/cleantest/data/pkg/__init__.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright 2022 Jason C. Nucciarone, Canonical Ltd.
+# Copyright 2023 Jason C. Nucciarone, Canonical Ltd.
 # See LICENSE file for licensing details.
 
 from .charmlib import Charmlib
diff --git a/src/cleantest/pkg/charmlib.py b/src/cleantest/data/pkg/charmlib.py
similarity index 96%
rename from src/cleantest/pkg/charmlib.py
rename to src/cleantest/data/pkg/charmlib.py
index 401c71d..51ffd0a 100644
--- a/src/cleantest/pkg/charmlib.py
+++ b/src/cleantest/data/pkg/charmlib.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright 2022 Jason C. Nucciarone, Canonical Ltd.
+# Copyright 2023 Jason C. Nucciarone, Canonical Ltd.
 # See LICENSE file for licensing details.
 
 """Manager for installing charm libraries inside remote processes."""
@@ -13,16 +13,13 @@
 from typing import List, Union
 
 from cleantest.meta import BasePackage, BasePackageError, InjectableData
-from cleantest.pkg.handler import snap
-
-from ._mixins import SnapdSupport
+from cleantest.meta.mixins import SnapdSupport
+from cleantest.utils import snap
 
 
 class CharmlibPackageError(BasePackageError):
     """Base error for Charmlib package handler."""
 
-    ...
-
 
 class Charmlib(BasePackage, SnapdSupport):
     """Represents `charmcraft fetch-lib`. See: https://juju.is/docs/sdk/charm-libraries.
diff --git a/src/cleantest/pkg/pip.py b/src/cleantest/data/pkg/pip.py
similarity index 92%
rename from src/cleantest/pkg/pip.py
rename to src/cleantest/data/pkg/pip.py
index bafbcf9..7906f1b 100644
--- a/src/cleantest/pkg/pip.py
+++ b/src/cleantest/data/pkg/pip.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright 2022 Jason C. Nucciarone, Canonical Ltd.
+# Copyright 2023 Jason C. Nucciarone, Canonical Ltd.
 # See LICENSE file for licensing details.
 
 """Manager for installing pip packages inside remote processes."""
@@ -11,14 +11,13 @@
 from typing import List, Union
 
 from cleantest.meta import BasePackage, BasePackageError, InjectableData
-from cleantest.utils import detect_os_variant
+from cleantest.meta.utils import detect_os_variant
+from cleantest.utils import apt
 
 
 class PipPackageError(BasePackageError):
     """Base error for Pip package handler."""
 
-    ...
-
 
 class Pip(BasePackage):
     """Represents `pip install`.
@@ -77,15 +76,7 @@ def _setup(self) -> None:
 
         if which("pip") is None:
             if os_variant == "ubuntu":
-                cmd = ["apt", "install", "-y", "python3-pip"]
-                try:
-                    subprocess.run(
-                        cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True
-                    )
-                except subprocess.CalledProcessError:
-                    raise PipPackageError(
-                        f"Failed to install pip using the following command: {' '.join(cmd)}"
-                    )
+                apt.install("python3-pip")
             else:
                 raise NotImplementedError(
                     f"Support for {os_variant.capitalize()} not available yet."
diff --git a/src/cleantest/pkg/snap.py b/src/cleantest/data/pkg/snap.py
similarity index 98%
rename from src/cleantest/pkg/snap.py
rename to src/cleantest/data/pkg/snap.py
index 82ab6dd..3d5aeb4 100644
--- a/src/cleantest/pkg/snap.py
+++ b/src/cleantest/data/pkg/snap.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright 2022 Jason C. Nucciarone, Canonical Ltd.
+# Copyright 2023 Jason C. Nucciarone, Canonical Ltd.
 # See LICENSE file for licensing details.
 
 """Manager for installing snap packages inside remote processes."""
@@ -10,16 +10,13 @@
 from typing import List, Union
 
 from cleantest.meta import BasePackage, BasePackageError, InjectableData
-from cleantest.pkg.handler import snap
-
-from ._mixins import SnapdSupport
+from cleantest.meta.mixins import SnapdSupport
+from cleantest.utils import snap
 
 
 class SnapPackageError(BasePackageError):
     """Base error for Snap package handler."""
 
-    ...
-
 
 class Confinement(Enum):
     """Confinement modes for snap packages."""
diff --git a/src/cleantest/hooks/starttestlet.py b/src/cleantest/hooks/starttestlet.py
deleted file mode 100644
index c661a5a..0000000
--- a/src/cleantest/hooks/starttestlet.py
+++ /dev/null
@@ -1,12 +0,0 @@
-#!/usr/bin/env python3
-# Copyright 2022 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-"""Hook run before testlet starts but after StartEnv hook."""
-
-
-class StartTestletHook:
-    """Not implemented yet as I do not know if hooks are the move."""
-
-    def __init__(self):
-        raise NotImplementedError("Hook not implemented yet.")
diff --git a/src/cleantest/hooks/stoptestlet.py b/src/cleantest/hooks/stoptestlet.py
deleted file mode 100644
index f69c3c4..0000000
--- a/src/cleantest/hooks/stoptestlet.py
+++ /dev/null
@@ -1,12 +0,0 @@
-#!/usr/bin/env python3
-# Copyright 2022 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-"""Hook run after testlet finishes but before StopEnv hook."""
-
-
-class StopTestletHook:
-    """Not implemented yet as I do not know if hooks are the move."""
-
-    def __init__(self):
-        raise NotImplementedError("Hook not implemented yet.")
diff --git a/src/cleantest/pkg/_mixins/__init__.py b/src/cleantest/meta/mixins/__init__.py
similarity index 56%
rename from src/cleantest/pkg/_mixins/__init__.py
rename to src/cleantest/meta/mixins/__init__.py
index e58aad9..7bc5cfe 100644
--- a/src/cleantest/pkg/_mixins/__init__.py
+++ b/src/cleantest/meta/mixins/__init__.py
@@ -1,5 +1,6 @@
 #!/usr/bin/env python3
-# Copyright 2022 Jason C. Nucciarone, Canonical Ltd.
+# Copyright 2023 Jason C. Nucciarone, Canonical Ltd.
 # See LICENSE file for licensing details.
 
+from .dict_ops import DictOps
 from .snapd_support import SnapdSupport
diff --git a/src/cleantest/provider/data/_mixins/dict_ops.py b/src/cleantest/meta/mixins/dict_ops.py
similarity index 63%
rename from src/cleantest/provider/data/_mixins/dict_ops.py
rename to src/cleantest/meta/mixins/dict_ops.py
index 6f95b12..a2af633 100644
--- a/src/cleantest/provider/data/_mixins/dict_ops.py
+++ b/src/cleantest/meta/mixins/dict_ops.py
@@ -1,10 +1,10 @@
 #!/usr/bin/env python3
-# Copyright 2022 Jason C. Nucciarone, Canonical Ltd.
+# 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 Dict
+from typing import Any, Dict, List
 
 
 class DictOps:
@@ -35,3 +35,21 @@ def _filterdict(self, input_dict: Dict) -> Dict:
                 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/pkg/_mixins/snapd_support.py b/src/cleantest/meta/mixins/snapd_support.py
similarity index 65%
rename from src/cleantest/pkg/_mixins/snapd_support.py
rename to src/cleantest/meta/mixins/snapd_support.py
index 030404f..e781e3e 100644
--- a/src/cleantest/pkg/_mixins/snapd_support.py
+++ b/src/cleantest/meta/mixins/snapd_support.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright 2022 Jason C. Nucciarone, Canonical Ltd.
+# Copyright 2023 Jason C. Nucciarone, Canonical Ltd.
 # See LICENSE file for licensing details.
 
 """Common operations needed by classes to support snapd and snap."""
@@ -7,7 +7,8 @@
 import subprocess
 from shutil import which
 
-from cleantest.utils import detect_os_variant
+from cleantest.meta.utils import detect_os_variant
+from cleantest.utils import apt
 
 
 class SnapdSupportError(Exception):
@@ -35,15 +36,7 @@ def _install_snapd() -> None:
 
         if which("snap") is None:
             if os_variant == "ubuntu":
-                cmd = ["apt", "install", "-y", "snapd"]
-                try:
-                    subprocess.run(
-                        cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True
-                    )
-                except subprocess.CalledProcessError:
-                    raise SnapdSupportError(
-                        f"Failed to install snapd using the following command: {' '.join(cmd)}."
-                    )
+                apt.install("snapd")
             else:
                 raise NotImplementedError(
                     f"Support for {os_variant.capitalize()} not available yet."
diff --git a/src/cleantest/meta/utils/__init__.py b/src/cleantest/meta/utils/__init__.py
new file mode 100644
index 0000000..f2b24ef
--- /dev/null
+++ b/src/cleantest/meta/utils/__init__.py
@@ -0,0 +1,5 @@
+#!/usr/bin/env python3
+# Copyright 2023 Jason C. Nucciarone
+# See LICENSE file for licensing details.
+
+from .detect_os import detect_os_variant
diff --git a/src/cleantest/utils/detect_os.py b/src/cleantest/meta/utils/detect_os.py
similarity index 100%
rename from src/cleantest/utils/detect_os.py
rename to src/cleantest/meta/utils/detect_os.py
diff --git a/src/cleantest/pkg/handler/__init__.py b/src/cleantest/pkg/handler/__init__.py
deleted file mode 100644
index 754913c..0000000
--- a/src/cleantest/pkg/handler/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env python3
-# Copyright 2022 Jason C. Nucciarone, Canonical Ltd.
-# See LICENSE file for licensing details.
-
-from . import snap_handler as snap
diff --git a/src/cleantest/provider/__init__.py b/src/cleantest/provider/__init__.py
index 951e8c0..6019e22 100644
--- a/src/cleantest/provider/__init__.py
+++ b/src/cleantest/provider/__init__.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright 2022 Jason C. Nucciarone, Canonical Ltd.
+# Copyright 2023 Jason C. Nucciarone, Canonical Ltd.
 # See LICENSE file for licensing details.
 
-from .lxd import lxd
+from .lxd.lxd import lxd
diff --git a/src/cleantest/provider/_handler/__init__.py b/src/cleantest/provider/_handler/__init__.py
deleted file mode 100644
index 9c229ac..0000000
--- a/src/cleantest/provider/_handler/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env python3
-# Copyright 2022 Jason C. Nucciarone, Canonical Ltd.
-# See LICENSE file for licensing details.
-
-from .lxd_handler import LXDProvider
diff --git a/src/cleantest/provider/data/__init__.py b/src/cleantest/provider/data/__init__.py
deleted file mode 100644
index e6958e0..0000000
--- a/src/cleantest/provider/data/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/usr/bin/env python3
-# Copyright 2022 Canonical Ltd.
-# See LICENSE file for licensing details.
-
-from .env_data import EnvDataStore
-from .lxd_data import LXDDataStore
diff --git a/src/cleantest/provider/data/_mixins/__init__.py b/src/cleantest/provider/data/_mixins/__init__.py
deleted file mode 100644
index 8e1f712..0000000
--- a/src/cleantest/provider/data/_mixins/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env python3
-# Copyright 2022 Jason C. Nucciarone, Canonical Ltd.
-# See LICENSE file for licensing details.
-
-from .dict_ops import DictOps
diff --git a/src/cleantest/provider/data/lxd_data.py b/src/cleantest/provider/data/lxd_data.py
deleted file mode 100644
index 6ed83f5..0000000
--- a/src/cleantest/provider/data/lxd_data.py
+++ /dev/null
@@ -1,181 +0,0 @@
-#!/usr/bin/env python3
-# Copyright 2022 Jason C. Nucciarone, Canonical Ltd.
-# See LICENSE file for licensing details.
-
-"""Information needed by LXD test environment provider."""
-
-from typing import Any, Dict, List, Optional, Union
-
-from ._mixins import DictOps
-
-
-class BadLXDConfigError(Exception):
-    """Raised when the newly entered configuration fails the lint check."""
-
-    ...
-
-
-class LXDConfigNotFoundError(Exception):
-    """Raised when the requested configuration for an LXD instance is not found in the registry."""
-
-    ...
-
-
-class LXDSource:
-    """Metaclass to define an LXD image to use for the test environment.
-
-    Args:
-        type (str): Type of artifact to pull.
-        mode (str): Mode in which to access the artifact.
-        server (str): Server to get the artifact from.
-        protocol (str): Protocol to use when pulling/pushing the artifact.
-        alias (str): Alias of the artifact.
-    """
-
-    def __init__(self, type: str, mode: str, server: str, protocol: str, alias: str) -> None:
-        self.type = type
-        self.mode = mode
-        self.server = server
-        self.protocol = protocol
-        self.alias = alias
-
-
-class LXDConfig(DictOps):
-    """Metaclass to define an LXD container or virtual machine to bring up.
-
-    Args:
-        name (str): Name to use for container or virtual machine.
-        source (LXDSource): Where to get the LXD image from.
-        project (str): Project to make LXD container or virtual machine member of.
-    """
-
-    def __init__(self, name: str, source: LXDSource, project: Optional[str] = None) -> None:
-        self.name = name
-        self.source = source
-        self.project = project
-
-
-class Defaults:
-    """Define default images that can be used with test environment provider.
-
-    Notes:
-        jammy_amd64: Ubuntu 22.04 LTS amd64
-        focal_amd64: Ubuntu 20.04 LTS amd64
-        bionic_amd64: Ubuntu 18.04 LTS amd64
-    """
-
-    jammy_amd64: Dict[str, Any] = {
-        "name": "jammy-amd64",
-        "source": {
-            "type": "image",
-            "mode": "pull",
-            "server": "https://images.linuxcontainers.org",
-            "protocol": "simplestreams",
-            "alias": "ubuntu/jammy",
-        },
-        "project": "default",
-    }
-    focal_amd64: Dict[str, Any] = {
-        "name": "focal-amd64",
-        "source": {
-            "type": "image",
-            "mode": "pull",
-            "server": "https://images.linuxcontainers.org",
-            "protocol": "simplestreams",
-            "alias": "ubuntu/focal",
-        },
-        "project": "default",
-    }
-    bionic_amd64: Dict[str, Any] = {
-        "name": "bionic-amd64",
-        "source": {
-            "type": "image",
-            "mode": "pull",
-            "server": "https://images.linuxcontainers.org",
-            "protocol": "simplestreams",
-            "alias": "ubuntu/18.04",
-        },
-        "project": "default",
-    }
-
-
-class LXDDataStore:
-    def __init__(self) -> None:
-        self._defaults = Defaults()
-        self._config_registry = []
-        self.add_config(self._defaults.jammy_amd64)
-        self.add_config(self._defaults.focal_amd64)
-        self.add_config(self._defaults.bionic_amd64)
-
-    def get_config(self, config_name: str) -> LXDConfig:
-        """Get the configuration of an LXD image.
-
-        Args:
-            config_name (str): Name of configuration to get.
-
-        Raises:
-            LXDConfigNotFoundError: Raised if configuration does not exist in registry.
-
-        Returns:
-            (LXDConfig): Retrieved LXD image configuration.
-        """
-        for c in self._config_registry:
-            if c.name == config_name:
-                return c
-
-        raise LXDConfigNotFoundError(config_name)
-
-    def add_config(self, new_config: Dict[str, Any]) -> None:
-        """Add a new LXD image configuration to the registry.
-
-        Args:
-            new_config (Dict[str, Any]): New configuration to add to the registry.
-        """
-        self._lint_config(new_config)
-        source = new_config.get("source")
-        self._config_registry.append(
-            LXDConfig(
-                name=new_config.get("name"),
-                source=LXDSource(
-                    type=source.get("type"),
-                    mode=source.get("mode"),
-                    server=source.get("server"),
-                    protocol=source.get("protocol"),
-                    alias=source.get("alias"),
-                ),
-                project=new_config.get("project", None),
-            )
-        )
-
-    def _lint_config(self, new_config: Dict[str, Any]) -> None:
-        """Lint a new LXD image configuration to ensure that it is valid.
-
-        Args:
-            new_config (Dict[str, Any]): New configuration to lint.
-
-        Raises:
-            BadLXDConfigError: Raised if the passed LXD image configuration is invalid.
-        """
-        checks = ["name", "server", "alias", "protocol", "type", "mode"]
-        config = self._deconstruct(new_config)
-        for i in checks:
-            if i not in config:
-                raise BadLXDConfigError(new_config)
-
-    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/provider/lxd.py b/src/cleantest/provider/lxd.py
deleted file mode 100644
index 8b1d5a2..0000000
--- a/src/cleantest/provider/lxd.py
+++ /dev/null
@@ -1,89 +0,0 @@
-#!/usr/bin/env python3
-# Copyright 2022 Jason C. Nucciarone, Canonical Ltd.
-# See LICENSE file for licensing details.
-
-"""LXD test environment provider functions and utilities."""
-
-import os
-from collections import namedtuple
-from typing import Any, Callable, Dict, List, Union
-
-from cleantest.control.configure import Configure
-from cleantest.meta import Result
-from cleantest.provider.data import EnvDataStore, LXDDataStore
-
-from ._handler import LXDProvider
-
-# Configure LXD client configuration if hypervisor is not on localhost.
-LXDClientConfig = namedtuple(
-    "LXDClientConfig", ["endpoint", "version", "cert", "verify", "timeout", "project"]
-)
-
-
-class lxd:
-    """LXD test environment provider.
-
-    Args:
-        name (str): Name for test environment (Default: "test").
-        image (List[str]): LXD image to use for test environment (Default: ["jammy-amd64"]).
-        preserve (bool): Preserve test environment after test has completed (Default: True).
-        env (EnvDataStore): Environment to use in test environment (Default: EnvDataStore).
-        data (LXDDataStore): Data necessary for LXD (Default: LXDDataStore).
-        image_config (List[Dict[str, Any]]): Configuration to use for LXD image (Default: None).
-        client_config (LXDClientConfig): Configuration to use for LXD client (Default: None).
-        parallel (bool): Run test environments in parallel (Default: False).
-        num_threads (int): Number of threads to use when running
-            test environments in parallel (Default: None).
-    """
-
-    def __init__(
-        self,
-        name: str = "test",
-        image: Union[str, List[str]] = ["jammy-amd64"],
-        preserve: bool = True,
-        env: EnvDataStore = EnvDataStore(),
-        data: LXDDataStore = LXDDataStore(),
-        image_config: Union[Dict[str, Any], List[Dict[str, Any]]] = None,
-        client_config: LXDClientConfig = None,
-        parallel: bool = False,
-        num_threads: int = None,
-    ) -> None:
-        self._name = name
-        self._preserve = preserve
-        self._env = env
-        self._data = data
-        self._parallel = parallel
-        self._client_config = client_config
-        self._clean_config = Configure()
-
-        if type(image) == str:
-            self._image = [image]
-        else:
-            self._image = image
-
-        if type(image_config) == dict:
-            self._data.add_config(image_config)
-        elif type(image_config) == list:
-            for c in image_config:
-                self._data.add_config(c)
-
-        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()
-            )
-        elif type(num_threads) == int and self._parallel is True:
-            self._num_threads = num_threads
-
-    def __call__(self, func: Callable) -> Callable:
-        """Callable for lxd decorator."""
-
-        def wrapper(*args, **kwargs) -> Dict[str, Result]:
-            handler = (
-                LXDProvider.parallel(self, func)
-                if self._parallel is True
-                else LXDProvider.serial(self, func)
-            )
-            return handler.run()
-
-        return wrapper
diff --git a/src/cleantest/provider/lxd/__init__.py b/src/cleantest/provider/lxd/__init__.py
new file mode 100644
index 0000000..ecc7ad5
--- /dev/null
+++ b/src/cleantest/provider/lxd/__init__.py
@@ -0,0 +1,3 @@
+#!/usr/bin/env python3
+# Copyright 2023 Jason C. Nucciarone, Canonical Ltd.
+# See LICENSE file for licensing details.
diff --git a/src/cleantest/provider/lxd/lxd.py b/src/cleantest/provider/lxd/lxd.py
new file mode 100644
index 0000000..6fa5362
--- /dev/null
+++ b/src/cleantest/provider/lxd/lxd.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python3
+# Copyright 2023 Jason C. Nucciarone, Canonical Ltd.
+# See LICENSE file for licensing details.
+
+"""LXD test environment provider functions and utilities."""
+
+import os
+from typing import Callable, Dict, List, Optional, Union
+
+from cleantest.control.configure import Configure
+from cleantest.control.env import Env
+from cleantest.control.lxd.lxd_config import ClientConfig
+from cleantest.meta import Result
+
+from .lxd_handler import LXDProvider
+
+
+class lxd:
+    """LXD test environment provider.
+
+    Args:
+        name (str): Name for test environment (Default: "test").
+        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).
+    """
+
+    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")
+
+        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()
+            )
+        elif type(num_threads) == int and self._parallel is True:
+            self._num_threads = num_threads
+
+    def __call__(self, func: Callable) -> Callable:
+        """Callable for lxd decorator."""
+
+        def wrapper(*args, **kwargs) -> Dict[str, Result]:
+            handler = (
+                LXDProvider.parallel(self, func)
+                if self._parallel is True
+                else LXDProvider.serial(self, func)
+            )
+            return handler.run()
+
+        return wrapper
diff --git a/src/cleantest/provider/_handler/lxd_handler.py b/src/cleantest/provider/lxd/lxd_handler.py
similarity index 94%
rename from src/cleantest/provider/_handler/lxd_handler.py
rename to src/cleantest/provider/lxd/lxd_handler.py
index 0d67b33..7cfa0a9 100644
--- a/src/cleantest/provider/_handler/lxd_handler.py
+++ b/src/cleantest/provider/lxd/lxd_handler.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright 2022 Jason C. Nucciarone, Canonical Ltd.
+# Copyright 2023 Jason C. Nucciarone, Canonical Ltd.
 # See LICENSE file for licensing details.
 
 """Handler for LXD test environments."""
@@ -51,16 +51,16 @@ def _client(self) -> Client:
         Returns:
             (Client): Connection to LXD API socket.
         """
-        if self._client_config is None:
+        if self._lxd_client_config is None:
             return Client(project="default")
         else:
             return Client(
-                endpoint=self._client_config.endpoint,
-                version=self._client_config.version,
-                cert=self._client_config.cert,
-                verify=self._client_config.verify,
-                timeout=self._client_config.timeout,
-                project=self._client_config.project,
+                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,
             )
 
     @property
@@ -79,7 +79,7 @@ def _build(self, instance: InstanceMetadata) -> None:
             instance (InstanceMetadata): Instance to build.
         """
         if instance.exists is False:
-            config = self._data.get_config(instance.image)
+            config = self._lxd_provider_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)
@@ -161,7 +161,7 @@ def _handle_start_env_hooks(self, instance: InstanceMetadata) -> None:
         Args:
             instance (InstanceMetadata): Instance to run start env hooks in.
         """
-        start_env_hooks = self._clean_config.get_start_env_hooks()
+        start_env_hooks = self._lxd_provider_config.startenv_hooks
         while start_env_hooks:
             hook = start_env_hooks.pop()
             instance = self._client.instances.get(instance.name)
@@ -178,7 +178,7 @@ def _handle_stop_env_hooks(self, instance: InstanceMetadata) -> None:
         Args:
             instance (InstanceMetadata): Instance to run stop env hooks in.
         """
-        stop_env_hooks = self._clean_config.get_stop_env_hooks()
+        stop_env_hooks = self._lxd_provider_config.stopenv_hooks
         while stop_env_hooks:
             hook = stop_env_hooks.pop()
             instance = self._client.instances.get(instance.name)
@@ -358,7 +358,7 @@ def serial(lxd, func: Callable) -> "Serial":
 
     @staticmethod
     def parallel(lxd, func: Callable) -> "Parallel":
-        """Return entrypint for running tests in parallel using LXD.
+        """Return entrypoint for running tests in parallel using LXD.
 
         Args:
             lxd (lxd): LXD decorator.
diff --git a/src/cleantest/utils/__init__.py b/src/cleantest/utils/__init__.py
index 3dbedc6..3bacad3 100644
--- a/src/cleantest/utils/__init__.py
+++ b/src/cleantest/utils/__init__.py
@@ -1,5 +1,7 @@
 #!/usr/bin/env python3
-# Copyright 2022 Canonical Ltd.
+# Copyright 2023 Jason C. Nucciarone
 # See LICENSE file for licensing details.
 
-from .detect_os import detect_os_variant
+from . import apt_handler as apt
+from . import snap_handler as snap
+from .run import run
diff --git a/src/cleantest/utils/apt_handler.py b/src/cleantest/utils/apt_handler.py
new file mode 100644
index 0000000..035665e
--- /dev/null
+++ b/src/cleantest/utils/apt_handler.py
@@ -0,0 +1,156 @@
+#!/usr/bin/env python3
+# Copyright 2023 Jason C. Nucciarone, Canonical Ltd.
+# See LICENSE file for licensing details.
+
+"""Handler for installing and managing Debian packages inside test environment instances."""
+
+# Note: Support for third-party repositories is not implemented yet.
+
+import pathlib
+import re
+import subprocess
+from shutil import which
+from typing import Any, Iterable, List, Optional, Union
+
+from cleantest.meta.utils import detect_os_variant
+
+
+class AptHandlerError(Exception):
+    """Raised when apt handler encounters any errors."""
+
+
+def _is_apt_available() -> None:
+    """Check if apt is available on test environment instance.
+
+    Raises:
+        AptHandlerError: Raised if apt is not available.
+    """
+    if which("apt") is None:
+        raise AptHandlerError(
+            (
+                f"apt is not supported on {detect_os_variant()}. "
+                f"`from cleantest.utils import run` can be used instead."
+            )
+        )
+
+
+def _apt(
+    command: str, packages: Union[str, List[str]], optargs: Optional[List[str]] = None
+) -> Union[subprocess.CompletedProcess[Any], subprocess.CompletedProcess[str]]:
+    """Wrap package management commands from Debian/Ubuntu commands.
+
+    Args:
+        command (str): Command to execute.
+        packages (Union[str, List[str]]): Names of packages to operate on.
+        optargs (Optional[List[str]]): Optional arguments to pass to apt.
+    """
+    packages = [packages] if type(packages) == str else packages
+    optargs = optargs if optargs is not None else []
+    _cmd = ["apt", "-y", *optargs, command, *packages]
+    try:
+        res = subprocess.run(
+            _cmd, env={"DEBIAN_FRONTEND": "noninteractive"}, capture_output=True, text=True
+        )
+        return res
+    except subprocess.CalledProcessError:
+        raise AptHandlerError(
+            f"Could not perform command {_cmd} on the following packages: {', '.join(packages)}"
+        )
+
+
+def update() -> None:
+    """Update apt cache on the test environment instance.
+
+    Raises:
+        AptHandlerError: Raised if unable to update apt cache.
+    """
+    _is_apt_available()
+    try:
+        subprocess.check_call(
+            ["apt", "-y", "update"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
+        )
+    except subprocess.CalledProcessError as e:
+        raise AptHandlerError(f"Failed to update apt cache. Reason: {e}.")
+
+
+def install(*packages: str, version: Optional[str] = None) -> None:
+    """Install Debian package on test environment instance.
+
+    Args:
+        *packages (str): Names of packages to install.
+        version (Optional[str]): Version of package to install.
+            Only set if installing one package (Default: None).
+
+    Raises:
+        AptHandlerError: Raised if an error is encountered when installing packages.
+    """
+    _is_apt_available()
+    if len(packages) == 0:
+        raise AptHandlerError("No package names passed.")
+    if len(packages) > 1 and version:
+        raise AptHandlerError("Version should not be set if packages to install > 1.")
+
+    # Note: Use --force-confold to keep old configuration file rather than generating a new one.
+    if version:
+        _apt(
+            "install",
+            f"{packages[0]}={version}",
+            optargs=["--option=Dpkg::Options::=--force-confold"],
+        )
+    else:
+        _apt("install", [*packages], optargs=["--option=Dpkg::Options::=--force-confold"])
+
+
+def install_local(*package_paths: str) -> None:
+    """Install local Debian package on test environment instance."""
+    _is_apt_available()
+    if len(package_paths) == 0:
+        raise AptHandlerError("No file paths to packages passed.")
+    for path in package_paths:
+        if not pathlib.Path(path).exists():
+            raise FileNotFoundError(f"Could not locate deb archive {path}.")
+        _apt("install", path, optargs=["--option=Dpkg::Options::=--force-confold", "-f"])
+
+
+def remove(*packages: str) -> None:
+    """Remove package from test environment instance.
+
+    Args:
+        *packages (str): Names of packages to remove.
+
+    Raises:
+        AptHandlerError: Raised if an error is encountered when removing packages.
+    """
+    _is_apt_available()
+    if len(packages) == 0:
+        raise AptHandlerError("No package names passed.")
+    _apt("remove", [*packages])
+
+
+def purge(*packages: str) -> None:
+    """Purge package from test environment instance.
+
+    Args:
+        *packages (str): Names of packages to purge.
+    """
+    _is_apt_available()
+    if len(*packages) == 0:
+        raise AptHandlerError("No package names passed.")
+    _apt("purge", [*packages])
+
+
+def installed(*packages: str) -> Iterable[bool]:
+    """Check if packages are installed inside test environment instance.
+
+    Args:
+        *packages (str): Names of packages to check if they are installed.
+
+    Yields:
+        (Iterable[bool]): Status of package. True: Installed - False: Not Installed.
+    """
+    _is_apt_available()
+    if len(*packages) == 0:
+        raise AptHandlerError("No package names passed.")
+    check_installed = re.compile(r"\[installed]")
+    for package in packages:
+        yield True if check_installed.match(_apt("list", package).stdout) else False
diff --git a/src/cleantest/utils/run.py b/src/cleantest/utils/run.py
new file mode 100644
index 0000000..bae2675
--- /dev/null
+++ b/src/cleantest/utils/run.py
@@ -0,0 +1,48 @@
+#!/usr/bin/env python3
+# Copyright 2023 Jason C. Nucciarone, Canonical Ltd.
+# See LICENSE file for licensing details.
+
+"""Run commands inside test environment instances."""
+
+import subprocess
+from typing import Any, Dict, Iterable, Optional
+
+from cleantest.meta import Result
+
+
+class CommandExecutionError(Exception):
+    """Raised if run fails to execute a command."""
+
+
+def run(
+    *commands: str,
+    env: Optional[Dict[str, Any]] = None,
+    cwd: Optional[str] = None,
+) -> Iterable[Result]:
+    """Run a command inside a test environment instance.
+
+    Args:
+        *commands (str): Commands to execute.
+        env (Optional[Dict[str, Any]]): Environment to pass to commands (Default: None).
+        cwd (Optional[str]): Directory to execute commands inside (Default: None).
+
+    Yields:
+        (Iterable[Result]): Captured results of executed commands.
+    """
+    if len(commands) == 0:
+        raise CommandExecutionError("No commands passed.")
+
+    for command in commands:
+        try:
+            res = subprocess.run(
+                command.split(" "),
+                env=(env if env else None),
+                cwd=(cwd if cwd else None),
+                check=True,
+                capture_output=True,
+                text=True,
+            )
+        except subprocess.CalledProcessError as e:
+            raise CommandExecutionError(f"Failed to execute command {command}. Reason: {e}.")
+
+        yield Result(res.returncode, res.stdout, res.stderr)
diff --git a/src/cleantest/pkg/handler/snap_handler.py b/src/cleantest/utils/snap_handler.py
similarity index 99%
rename from src/cleantest/pkg/handler/snap_handler.py
rename to src/cleantest/utils/snap_handler.py
index 786076d..fd362de 100644
--- a/src/cleantest/pkg/handler/snap_handler.py
+++ b/src/cleantest/utils/snap_handler.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright 2022 Jason C. Nucciarone, Canonical Ltd.
+# Copyright 2023 Jason C. Nucciarone, Canonical Ltd.
 # See LICENSE file for licensing details.
 
 """Handler for installing and managing snap packages inside a remote instance."""
@@ -27,8 +27,6 @@
 class SnapHandlerError(Exception):
     """Raised when snap handler encounters any errors."""
 
-    ...
-
 
 class SnapAPIError(Exception):
     """Raised when an HTTP API error occurs talking to the Snapd server."""
diff --git a/tests/lxd/local/test_lxd_local.py b/tests/lxd/local/test_lxd_local.py
index a131b90..78fb7c2 100644
--- a/tests/lxd/local/test_lxd_local.py
+++ b/tests/lxd/local/test_lxd_local.py
@@ -6,16 +6,16 @@
 
 import os
 
-from cleantest import Configure
-from cleantest.hooks import StartEnvHook
-from cleantest.pkg import Charmlib, Pip
+from cleantest.control import Configure
+from cleantest.control.hooks import StartEnvHook
+from cleantest.data.pkg import Charmlib, Pip
 from cleantest.provider import lxd
 
 # Define the hooks and register them.
 root = os.path.dirname(os.path.realpath(__file__))
-cleanconfig = Configure()
-startenvhook = StartEnvHook(
-    name="my_start_hook",
+lxd_provider_config = Configure("lxd")
+startenv_hook = StartEnvHook(
+    name="setup_deps",
     packages=[
         Charmlib(
             auth_token_path=os.path.join(root, "charmhub.secret"),
@@ -24,11 +24,11 @@
         Pip(requirements=os.path.join(root, "requirements.txt")),
     ],
 )
-cleanconfig.register_hook(startenvhook)
+lxd_provider_config.register_hook(startenv_hook)
 
 
 # Define the testlets.
-@lxd(image="jammy-amd64", preserve=False)
+@lxd(image="ubuntu-jammy-amd64", preserve=False)
 def install_snapd():
     import sys