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.
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:
Looking at this issue, I have realized that much more work is need to improve how cleantest handles data.

I came to this revelation after experimenting with pickles and JSON packets. The TL;DR is that I found that I would like much more for cleantest to send out "REST" requests rather than directly uploading into the test environment instances. I am thinking something more along the likes of objects dumping json packets that can be processed by cleantest rather than just raw Python scripts. Needs some careful thought], but I believe I can use a mixture of pickles and base64 encoding to accomplish this.
I decided on a new data model for `cleantest`, rather than uploading raw pickles. I am instead going to use JSON packets. I will still use pickles (it's a good data format; haters gonna hate), but encode them using base64. A packet being sent from the host to the test environment instance will look like the following:

```json
{
    "checksum": "Some sha224 hash",
    "data": "base64 encoded pickle",
    "injectable": "Python script that tells the test instance what to do with the object its just received",
}
```

The packet sent back from the test environment instance will look like the following:

```json
{
    "checksum": "Some sha224 hash",
    "result": "base64 encoded Result object",
}
```

The idea behind this "data model" is to eliminate the dependence on dumping objects directly into `/tmp`.
Looking at this issue, I have realized that much more work is need to improve how cleantest handles data.

I came to this revelation after experimenting with pickles and JSON packets. The TL;DR is that I found that I would like much more for cleantest to send out "REST" requests rather than directly uploading into the test environment instances. I am thinking something more along the likes of objects dumping json packets that can be processed by cleantest rather than just raw Python scripts. Needs some careful thought], but I believe I can use a mixture of pickles and base64 encoding to accomplish this.
I decided on a new data model for `cleantest`, rather than uploading raw pickles. I am instead going to use JSON packets. I will still use pickles (it's a good data format; haters gonna hate), but encode them using base64. A packet being sent from the host to the test environment instance will look like the following:

```json
{
    "checksum": "Some sha224 hash",
    "data": "base64 encoded pickle",
    "injectable": "Python script that tells the test instance what to do with the object its just received",
}
```

The packet sent back from the test environment instance will look like the following:

```json
{
    "checksum": "Some sha224 hash",
    "result": "base64 encoded Result object",
}
```

The idea behind this "data model" is to eliminate the dependence on dumping objects directly into `/tmp`.

Fix patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 0c46bfc..95fa58c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -31,7 +31,7 @@ The [Discussions](#discussions) page is a better place to get help for personal
 * Please **do not** derail or troll issues. Keep the discussion on track and have respect for the other 
 users/contributors of cleantest.
 
-* Please **do not** post comments consisting soley of "+1", ":thumbsup:", or something similar. 
+* Please **do not** post comments consisting solely of "+1", ":thumbsup:", or something similar. 
 Use [GitHub's "reactions" feature](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) 
 instead.
   * The maintainers of cleantest reserve the right to delete comments that violate this rule.


Test patch:
diff --git a/src/cleantest/data/directory.py b/src/cleantest/data/directory.py
index 6f80de6..209065b 100644
--- a/src/cleantest/data/directory.py
+++ b/src/cleantest/data/directory.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright 2022 Jason C. Nucciarone, Canonical Ltd.
+# Copyright 2023 Jason C. Nucciarone
 # See LICENSE file for licensing details.
 
 """Abstractions for uploading and downloading directories from test environments."""
@@ -17,20 +17,14 @@
 class DirectoryError(Exception):
     """Base error for Dir class."""
 
-    ...
-
 
 class DirectoryExistsError(Exception):
     """Raised when a directory already exists on the host."""
 
-    ...
-
 
 class DirectoryNotFoundError(Exception):
     """Raised when a directory is not found on the host."""
 
-    ...
-
 
 def _strip_tar(tar: tarfile.TarFile, n_components: int = 1) -> Iterable[tarfile.TarInfo]:
     """Strip components from a tarfile to a specified level.
@@ -93,10 +87,10 @@ def load(self) -> None:
         if self.src.is_file():
             raise NotADirectoryError(f"{self.src} is a file. Use File class instead.")
 
-        old_dir = os.getcwd()
+        _ = os.getcwd()
         os.chdir(os.sep.join(str(self.src).split(os.sep)[:-1]))
-        archive_path = pathlib.Path(tempfile.gettempdir()).joinpath(self.src.name)
-        with tarfile.open(archive_path, "w:gz") as tar:
-            tar.add(self.src.name)
-        os.chdir(old_dir)
-        self._data = archive_path.read_bytes()
+        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(_)
diff --git a/src/cleantest/data/file.py b/src/cleantest/data/file.py
index 67ccb74..9bdc27d 100644
--- a/src/cleantest/data/file.py
+++ b/src/cleantest/data/file.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright 2022 Jason C. Nucciarone, Canonical Ltd.
+# Copyright 2023 Jason C. Nucciarone
 # See LICENSE file for licensing details.
 
 """Abstractions for uploading and downloading files from test environments."""
@@ -10,6 +10,7 @@
 import tempfile
 import textwrap
 from io import BytesIO
+from typing import Dict
 
 from cleantest.meta import Injectable
 
@@ -17,13 +18,9 @@
 class FileError(Exception):
     """Base error for File class."""
 
-    ...
-
 
 class InjectableModeError(Exception):
-    """Raised when an invalid injection mode has been passed to __injectable__()."""
-
-    ...
+    """Raised when an invalid injection mode has been passed to `_injectable`."""
 
 
 class File(Injectable):
@@ -77,23 +74,23 @@ def load(self) -> None:
         if self.src.is_dir():
             raise FileError(f"{self.src} is a directory. Use Dir class instead.")
 
-        old_dir = os.getcwd()
+        _ = os.getcwd()
         os.chdir(os.sep.join(str(self.src).split(os.sep)[:-1]))
-        archive_path = pathlib.Path(tempfile.gettempdir()).joinpath(self.src.name)
-        with tarfile.open(archive_path, "w:gz") as tar:
-            tar.add(self.src.name)
-        os.chdir(old_dir)
-        self._data = archive_path.read_bytes()
+        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(_)
 
-    def __injectable__(self, path: str, verification_hash: str, mode: str) -> str:
+    def _injectable(self, data: Dict[str, str], **kwargs) -> str:
         """Generate injectable script that will be run inside the test environment.
 
         Args:
-            path (str): Path to pickled object inside the test environment.
-            verification_hash (str): Hash to verify authenticity of pickled object.
-            mode (str):
-                "upload" - Uploading artifact into test environment.
-                "download" - Downloading artifact from the test environment.
+            data (Dict[str, str]): Data that needs to be in injectable script.
+                - checksum (str): SHA224 checksum to verify authenticity of object.
+                - data (str): Base64 encoded object to inject.
+            **kwargs:
+                mode (str): "push" or "pull" object to/from test environment instance.
 
         Raises:
             InjectableModeError: Raised if invalid mode has been passed.
@@ -101,18 +98,21 @@ def __injectable__(self, path: str, verification_hash: str, mode: str) -> str:
         Returns:
             (str): Injectable script.
         """
-        if mode == "upload":
+        _ = kwargs.get("mode", None)
+        if _ not in {"push", "pull"}:
+            InjectableModeError(f"Invalid mode: {_}. Please set mode to either 'push' or 'pull'.")
+        elif _ == "push":
             return textwrap.dedent(
                 f"""
                 #!/usr/bin/env python3
                 
                 from {self.__module__} import {self.__class__.__name__}
                 
-                holder = {self.__class__.__name__}._load("{path}", "{verification_hash}")
-                holder.dump()
+                _ = {self.__class__.__name__}._load("{data['checksum']}", "{data['data']}")
+                _.dump()
                 """
             ).strip("\n")
-        elif mode == "download":
+        else:
             return textwrap.dedent(
                 f"""
                 #!/usr/bin/env python3
@@ -121,12 +121,8 @@ def __injectable__(self, path: str, verification_hash: str, mode: str) -> str:
                 
                 from {self.__module__} import {self.__class__.__name__}
                 
-                holder = {self.__class__.__name__}._load("{path}", "{verification_hash}")
-                holder.load()
-                print(json.dumps(holder._dump()._asdict()), file=sys.stdout)
+                _ = {self.__class__.__name__}._load("{data['checksum']}", "{data['data']}")
+                _.load()
+                print(json.dumps(_._dump()), file=sys.stdout)
                 """
             ).strip("\n")
-        else:
-            InjectableModeError(
-                f"Invalid mode: {mode}. Please set mode to either 'upload' or 'download'."
-            )
diff --git a/src/cleantest/data/pkg/charmlib.py b/src/cleantest/data/pkg/charmlib.py
index 51ffd0a..3581bef 100644
--- a/src/cleantest/data/pkg/charmlib.py
+++ b/src/cleantest/data/pkg/charmlib.py
@@ -10,9 +10,9 @@
 import sys
 import textwrap
 from shutil import which
-from typing import List, Union
+from typing import Dict, List, Union
 
-from cleantest.meta import BasePackage, BasePackageError, InjectableData
+from cleantest.meta import BasePackage, BasePackageError
 from cleantest.meta.mixins import SnapdSupport
 from cleantest.utils import snap
 
@@ -81,14 +81,17 @@ def _handle_charm_lib_install(self) -> None:
                     )
                 )
 
-    def _dump(self) -> InjectableData:
-        """Dump Charmlib package handler object.
+    def _dump(self) -> Dict[str, str]:
+        """Prepare Charmlib object for injection.
 
         Raises:
             FileNotFoundError: Raised if authentication token is not found.
 
         Returns:
-            (InjectableData): Path to dumped object and verification hash.
+            (Dict[str, str]):
+                checksum (str): Checksum to verify authenticity of serialized Charmlib object.
+                data (str): Base64 encoded string containing serialized Charmlib object.
+                injectable (str): Injectable to run inside remote environment.
         """
         if not self.auth_token_path.exists() or not self.auth_token_path.is_file():
             raise FileNotFoundError(f"Could not find authentication token {self.auth_token_path}")
@@ -97,12 +100,13 @@ def _dump(self) -> InjectableData:
 
         return super()._dump()
 
-    def __injectable__(self, path: str, verification_hash: str) -> str:
+    def _injectable(self, data: Dict[str, str], **kwargs) -> str:
         """Generate injectable script that will be used to install charm libraries.
 
         Args:
-            path (str): Path to pickled object inside the test environment.
-            verification_hash: Hash to verify authenticity of pickled object.
+            data (Dict[str, str]): Data that needs to be in injectable script.
+                - checksum (str): SHA224 checksum to verify authenticity of Charmlib object.
+                - data (str): Base64 encoded Charmlib object to inject.
 
         Returns:
             (str): Injectable script.
@@ -113,7 +117,7 @@ def __injectable__(self, path: str, verification_hash: str) -> str:
             
             from {self.__module__} import {self.__class__.__name__}
             
-            holder = {self.__class__.__name__}._load("{path}", "{verification_hash}")
+            holder = {self.__class__.__name__}._load("{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 7906f1b..580a6e3 100644
--- a/src/cleantest/data/pkg/pip.py
+++ b/src/cleantest/data/pkg/pip.py
@@ -8,9 +8,9 @@
 import subprocess
 import textwrap
 from shutil import which
-from typing import List, Union
+from typing import Dict, List, Union
 
-from cleantest.meta import BasePackage, BasePackageError, InjectableData
+from cleantest.meta import BasePackage, BasePackageError
 from cleantest.meta.utils import detect_os_variant
 from cleantest.utils import apt
 
@@ -145,14 +145,17 @@ def _handle_pip_install(self) -> None:
                         )
                     )
 
-    def _dump(self) -> InjectableData:
-        """Dump Pip package handler object.
+    def _dump(self) -> Dict[str, str]:
+        """Prepare Pip object for injection.
 
         Raises:
             FileNotFoundError: Raised if a requirements or constraints file is not found.
 
         Returns:
-            (InjectableData): Path to dumped object and verification hash.
+            (Dict[str, str]):
+                checksum (str): Checksum to verify authenticity of serialized Pip object.
+                data (str): Base64 encoded string containing serialized Pip object.
+                injectable (str): Injectable to run inside remote environment.
         """
         if self.requirements is not None:
             for requirement in self.requirements:
@@ -170,12 +173,13 @@ def _dump(self) -> InjectableData:
 
         return super()._dump()
 
-    def __injectable__(self, path: str, verification_hash: str) -> str:
+    def _injectable(self, data: Dict[str, str], **kwargs) -> str:
         """Generate injectable script that will be used to install packages with pip.
 
         Args:
-            path (str): Path to pickled object inside the test environment.
-            verification_hash: Hash to verify authenticity of pickled object.
+            data (Dict[str, str]): Data that needs to be in injectable script.
+                - checksum (str): SHA224 checksum to verify authenticity of Pip object.
+                - data (str): Base64 encoded Pip object to inject.
 
         Returns:
             (str): Injectable script.
@@ -186,7 +190,7 @@ def __injectable__(self, path: str, verification_hash: str) -> str:
 
             from {self.__module__} import {self.__class__.__name__}
 
-            holder = {self.__class__.__name__}._load("{path}", "{verification_hash}")
+            holder = {self.__class__.__name__}._load("{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 3d5aeb4..c44dd0f 100644
--- a/src/cleantest/data/pkg/snap.py
+++ b/src/cleantest/data/pkg/snap.py
@@ -7,9 +7,9 @@
 import pathlib
 import textwrap
 from enum import Enum
-from typing import List, Union
+from typing import Dict, List, Union
 
-from cleantest.meta import BasePackage, BasePackageError, InjectableData
+from cleantest.meta import BasePackage, BasePackageError
 from cleantest.meta.mixins import SnapdSupport
 from cleantest.utils import snap
 
@@ -213,14 +213,17 @@ def _handle_snap_install(self) -> None:
             for alias in self.aliases:
                 alias.alias()
 
-    def _dump(self) -> InjectableData:
-        """Dump Snap package handler object.
+    def _dump(self) -> Dict[str, str]:
+        """Prepare Snap object for injection.
 
         Raises:
             FileNotFoundError: Raised if local snap package is not found.
 
         Returns:
-            (InjectableData): Path to dumped object and verification hash.
+            (Dict[str, str]):
+                checksum (str): Checksum to verify authenticity of serialized Snap object.
+                data (str): Base64 encoded string containing serialized Snap object.
+                injectable (str): Injectable to run inside remote environment.
         """
         if self.local_snaps is not None:
             for local_snap in self.local_snaps:
@@ -231,12 +234,13 @@ def _dump(self) -> InjectableData:
 
         return super()._dump()
 
-    def __injectable__(self, path: str, verification_hash: str) -> str:
+    def _injectable(self, data: Dict[str, str], **kwargs) -> str:
         """Generate injectable script that will be used to install snap packages.
 
         Args:
-            path (str): Path to pickled object inside the test environment.
-            verification_hash: Hash to verify authenticity of pickled object.
+            data (Dict[str, str]): Data that needs to be in injectable script.
+                - checksum (str): SHA224 checksum to verify authenticity of Snap object.
+                - data (str): Base64 encoded Snap object to inject.
 
         Returns:
             (str): Injectable script.
@@ -247,7 +251,7 @@ def __injectable__(self, path: str, verification_hash: str) -> str:
 
             from {self.__module__} import {self.__class__.__name__}
 
-            holder = {self.__class__.__name__}._load("{path}", "{verification_hash}")
+            holder = {self.__class__.__name__}._load("{data['checksum']}", "{data['data']}")
             holder._run()
             """
         ).strip("\n")
diff --git a/src/cleantest/meta/__init__.py b/src/cleantest/meta/__init__.py
index 52ec68f..9a22728 100644
--- a/src/cleantest/meta/__init__.py
+++ b/src/cleantest/meta/__init__.py
@@ -1,8 +1,8 @@
 #!/usr/bin/env python3
-# Copyright 2022 Jason C. Nucciarone, Canonical Ltd.
+# Copyright 2023 Jason C. Nucciarone
 # See LICENSE file for licensing details.
 
 from .base_handler import BaseEntrypoint, BaseHandler, BaseHandlerError, Result
 from .base_package import BasePackage, BasePackageError
 from .cleantest_info import CleantestInfo
-from .injectable import Injectable, InjectableData
+from .injectable import Injectable
diff --git a/src/cleantest/meta/cleantest_info.py b/src/cleantest/meta/cleantest_info.py
index 483fe12..a4e068d 100644
--- a/src/cleantest/meta/cleantest_info.py
+++ b/src/cleantest/meta/cleantest_info.py
@@ -1,23 +1,24 @@
 #!/usr/bin/env python3
-# Copyright 2022 Jason C. Nucciarone, Canonical Ltd.
+# Copyright 2023 Jason C. Nucciarone
 # See LICENSE file for licensing details.
 
 """Metaclass for retrieving information about cleantest."""
 
+import base64
 import csv
+import hashlib
 import os
 import pathlib
 import tarfile
 import tempfile
-import textwrap
 from concurrent.futures import ProcessPoolExecutor
-from typing import Dict
+from typing import Dict, Iterable, Tuple
 
 import pkg_resources
 
 
 class CleantestInfo:
-    """Metaclass for getting information about cleantest module."""
+    """Metaclass for getting information about the cleantest library."""
 
     def __new__(cls) -> "CleantestInfo":
         if not hasattr(cls, "instance"):
@@ -25,78 +26,104 @@ def __new__(cls) -> "CleantestInfo":
         return cls.instance
 
     @property
-    def src(self) -> Dict[str, bytes]:
-        """Source code of cleantest module.
+    def _src(self) -> Dict[str, bytes]:
+        """Retrieve the source code of cleantest.
 
         Returns:
-            (Dict[str, bytes]): Name and source code of cleantest module.
+            (Dict[str, bytes]): Name and base64 encoded source code of cleantest module.
         """
-        old_dir = os.getcwd()
+        _ = os.getcwd()
         os.chdir(pkg_resources.get_distribution("cleantest").location)
-        tar_path = pathlib.Path(tempfile.gettempdir()).joinpath("cleantest")
-        with tarfile.open(tar_path, "w:gz") as tar:
-            tar.add("cleantest")
-        os.chdir(old_dir)
-
-        return {"cleantest": tar_path.read_bytes()}
+        with tempfile.NamedTemporaryFile() as fin:
+            with tarfile.open(fin.name, "w:gz") as tar:
+                tar.add("cleantest")
+            os.chdir(_)
+            return {"cleantest": pathlib.Path(fin.name).read_bytes()}
 
     @property
-    def dependencies(self) -> Dict[str, bytes]:
-        """Source code for dependencies of cleantest module.
+    def _dependencies(self) -> Iterable[Tuple[str, bytes]]:
+        """Retrieve the source code of cleantest's dependencies.
 
-        Returns:
+        Yields:
             (Dict[str, bytes]): Name and source code of dependencies.
         """
-        result = {}
         with ProcessPoolExecutor(max_workers=os.cpu_count()) as pool:
             pool_results = pool.map(
-                self._get_dependencies,
+                self._dependency_processor,
                 pkg_resources.working_set.resolve(
                     pkg_resources.working_set.by_key["cleantest"].requires()
                 ),
             )
             for res in pool_results:
-                [result.update({key: value}) for key, value in res.items()]
-
-        return result
+                for k, v in res.items():
+                    yield k, v
 
-    def _get_dependencies(self, dependency: pkg_resources.Distribution) -> Dict[str, bytes]:
+    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 source code of dependency.
+            (Dict[str, bytes]): Name and base64 encoded source code of dependency.
         """
         os.chdir(dependency.location)
-        tar_path = pathlib.Path(tempfile.gettempdir()).joinpath(dependency.key)
-        with tarfile.open(tar_path, "w:gz") as tar:
-            with pathlib.Path(
+        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 fin:
-                for row in csv.reader(fin):
+            ).joinpath("RECORD").open(mode="rt") as dist_info_fin:
+                for row in csv.reader(dist_info_fin):
                     tar.add(row[0])
 
-        return {dependency.key: tar_path.read_bytes()}
+            return {dependency.key: pathlib.Path(fin.name).read_bytes()}
 
-    def make_pkg_injectable(self, pkg_path: str) -> str:
-        """Generate injectable script to install cleantest packages inside test environment.
+    def _injectable(self, checksum: str, data: str) -> str:
+        """Generate injectable script to install packages inside the test instance.
 
         Args:
-            pkg_path (str): Path to package inside test environment.
+            checksum (str): Checksum to verify base64 encoded object.
+            data (str): Base64 encode tar archive containing source code for cleantest.
 
         Returns:
             (str): Injectable script.
         """
-        return textwrap.dedent(
-            f"""
-            #!/usr/bin/env python3
-            import site
-            import tarfile
-
-            site.getsitepackages()[0]
-            tarball = tarfile.open("{pkg_path}", "r:gz")
-            tarball.extractall(site.getsitepackages()[0])
-            """
-        ).strip("\n")
+        with tempfile.TemporaryFile(mode="w+t") as fout:
+            fout.writelines(
+                [
+                    "#!/usr/bin/env python3\n",
+                    "import base64\n",
+                    "import hashlib\n",
+                    "import site\n",
+                    "import tarfile\n",
+                    "from io import BytesIO\n",
+                    f"_ = base64.b64decode('{data}')\n",
+                    f"if '{checksum}' != hashlib.sha224(_).hexdigest():\n"
+                    "\traise Exception('Hashes do not match')\n",
+                    "tar = tarfile.open(fileobj=BytesIO(_), mode='r:gz')\n",
+                    "tar.extractall(site.getsitepackages()[0])\n",
+                    "tar.close()\n",
+                ]
+            )
+            fout.seek(0)
+            return fout.read()
+
+    def dump(self) -> Iterable[Tuple[str, Dict[str, str]]]:
+        """Prepare cleantest for injection into test environment instance.
+
+        Yields:
+            (Iterable[Tuple[str, Dict[str, str]]]):
+                name (str): Name of library being injected.
+                checksum (str): Checksum to verify authenticity of archive.
+                data (str): Base64 encoded tarball containing source code.
+                injectable (str): Injectable to run inside remote instance.
+        """
+        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),
+            }
diff --git a/src/cleantest/meta/injectable.py b/src/cleantest/meta/injectable.py
index b1ed981..25e6dfd 100644
--- a/src/cleantest/meta/injectable.py
+++ b/src/cleantest/meta/injectable.py
@@ -1,78 +1,66 @@
 #!/usr/bin/env python3
-# Copyright 2022 Jason C. Nucciarone, Canonical Ltd.
+# Copyright 2023 Jason C. Nucciarone
 # See LICENSE file for licensing details.
 
-"""Metaclass for objects that will be injected into environments."""
+"""Abstract base class for injectable objects."""
 
+import base64
 import hashlib
-import pathlib
 import pickle
-import tempfile
-import uuid
 from abc import ABC, abstractmethod
-from collections import namedtuple
+from typing import Dict
 
 
 class InjectionError(Exception):
     """Base error for classes that inherit from Injectable."""
 
-    ...
-
-
-# Namedtuple returned by the _dump() method that contains
-# the path to the dumped object and its verification hash for future loading.
-InjectableData = namedtuple("InjectableData", ["path", "hash"])
-
 
 class Injectable(ABC):
     """Abstract metaclass that provides core methods needed by all injectable objects."""
 
     @classmethod
-    def _load(cls, data: str, verification_hash: str) -> object:
-        """Alternative constructor to load previously created object.
+    def _load(cls, checksum: str, data: str) -> object:
+        """Alternative constructor to load previously initialized object.
 
         Args:
+            checksum (str): Checksum to verify authenticity of serialized object.
             data (str): Path to file containing serialized object.
-            verification_hash (str): Hash to verify authenticity of serialized object.
 
         Returns:
             (object): Deserialized, verified object.
         """
-        fin = pathlib.Path(data)
-        if type(data) == str and fin.exists() and fin.is_file():
-            if verification_hash != hashlib.sha224(fin.read_bytes()).hexdigest():
-                raise InjectionError("Hashes do not match. Will not load untrusted object.")
+        if type(data) != str:
+            raise InjectionError(f"Cannot load object {data}. {type(data)} != str")
+
+        _ = base64.b64decode(data)
+        if checksum != hashlib.sha224(_).hexdigest():
+            raise InjectionError("Hashes do not match. Will not load untrusted object.")
 
-            cls_data = pickle.loads(fin.read_bytes())
-            posargs = [
-                value for key, value in cls_data.__dict__.items() if not key.startswith("_")
-            ]
-            hiddenargs = {
-                key: value for key, value in cls_data.__dict__.items() if key.startswith("_")
-            }
-            new_cls = cls(*posargs)
-            [setattr(new_cls, key, value) for key, value in hiddenargs.items()]
-            return new_cls
-        else:
-            raise InjectionError(
-                f"Cannot load object {data}. Cannot find pickle file or {type(data)} is not str."
-            )
+        _ = pickle.loads(_)
+        posargs = [value for key, value in _.__dict__.items() if not key.startswith("_")]
+        hiddenargs = {key: value for key, value in _.__dict__.items() if key.startswith("_")}
+        new_cls = cls(*posargs)
+        [setattr(new_cls, key, value) for key, value in hiddenargs.items()]
+        return new_cls
 
-    def _dump(self) -> InjectableData:
-        """Serialize object and generate SHA224 hash for verification.
+    def _dump(self, **kwargs) -> Dict[str, str]:
+        """Prepare object for injection.
 
         Returns:
-            (InjectableData):
-                path (str): Path to file containing serialized object.
-                hash (str): Hash to verify authenticity of serialized object.
+            (Dict[str, str]):
+                checksum (str): Checksum to verify authenticity of serialized object.
+                data (str): Base64 encoded string containing serialized object.
+                injectable (str): Injectable to run inside remote environment.
         """
-        fout = pathlib.Path(tempfile.gettempdir()).joinpath(f"{uuid.uuid4()}.pkl")
-        data = pickle.dumps(self)
-        fout.write_bytes(data)
-        verification_hash = hashlib.sha224(data).hexdigest()
-        return InjectableData(str(fout), verification_hash)
+        pickle_data = pickle.dumps(self)
+        checksum = hashlib.sha224(pickle_data).hexdigest()
+        data = base64.b64encode(pickle_data).decode()
+        return {
+            "checksum": checksum,
+            "data": data,
+            "injectable": self._injectable({"checksum": checksum, "data": data}, **kwargs),
+        }
 
     @abstractmethod
-    def __injectable__(self) -> str:
-        """Generate script that will be injected into test environment provider."""
-        ...
+    def _injectable(self, data: Dict[str, str], **kwargs) -> str:
+        """Python script to run inside of test environment provider."""
diff --git a/src/cleantest/provider/lxd/lxd_handler.py b/src/cleantest/provider/lxd/lxd_handler.py
index 7cfa0a9..5de26f9 100644
--- a/src/cleantest/provider/lxd/lxd_handler.py
+++ b/src/cleantest/provider/lxd/lxd_handler.py
@@ -1,12 +1,11 @@
 #!/usr/bin/env python3
-# Copyright 2023 Jason C. Nucciarone, Canonical Ltd.
+# Copyright 2023 Jason C. Nucciarone
 # See LICENSE file for licensing details.
 
 """Handler for LXD test environments."""
 
 import inspect
 import json
-import pathlib
 import re
 import tempfile
 from concurrent.futures import ProcessPoolExecutor
@@ -96,14 +95,13 @@ def _init(self, instance: Any) -> None:
             instance (Any): Instance to initialize.
         """
         meta = CleantestInfo()
-        instance.execute(["mkdir", "-p", "/root/init"])
-        for module, src in {**meta.src, **meta.dependencies}.items():
-            instance.files.put(f"/root/init/{module}.tar.gz", src)
+        instance.execute(["mkdir", "-p", "/root/init/cleantest"])
+        for name, data in meta.dump():
             instance.files.put(
-                f"/root/init/install_{module}",
-                meta.make_pkg_injectable(f"/root/init/{module}.tar.gz"),
+                f"/root/init/cleantest/install_{name}",
+                data["injectable"],
             )
-            instance.execute(["python3", f"/root/init/install_{module}"])
+            instance.execute(["python3", f"/root/init/cleantest/install_{name}"])
 
     def _execute(self, test: str, instance: InstanceMetadata) -> Any:
         """Execute a testlet inside an LXD test environment instance.
@@ -196,13 +194,8 @@ def _handle_package_install(self, instance: Any, pkg: BasePackage) -> None:
         dispatch = {"charmlib": lambda x: self._env.add(json.loads(x))}
 
         dump_data = pkg._dump()
-        data_path = pathlib.Path(dump_data.path)
         instance.execute(["mkdir", "-p", "/root/init/pkg"])
-        instance.files.put(f"/root/init/pkg/{data_path.name}", data_path.read_bytes())
-        instance.files.put(
-            "/root/init/pkg/install",
-            pkg.__injectable__(f"/root/init/pkg/{data_path.name}", dump_data.hash),
-        )
+        instance.files.put("/root/init/pkg/install", dump_data["injectable"])
         result = instance.execute(["python3", "/root/init/pkg/install"])
 
         if pkg.__class__.__name__.lower() in dispatch:
@@ -216,16 +209,9 @@ def _handle_artifact_upload(self, instance: Any, artifact: Injectable) -> None:
             artifact (Injectable): Artifact to upload.
         """
         artifact.load()
-        dump_data = artifact._dump()
-        data_path = pathlib.Path(dump_data.path)
+        dump_data = artifact._dump(mode="push")
         instance.execute(["mkdir", "-p", "/root/init/data"])
-        instance.files.put(f"/root/init/data/{data_path.name}", data_path.read_bytes())
-        instance.files.put(
-            "/root/init/data/dump",
-            artifact.__injectable__(
-                f"/root/init/data/{data_path.name}", dump_data.hash, mode="upload"
-            ),
-        )
+        instance.files.put("/root/init/data/dump", dump_data["injectable"])
         instance.execute(["python3", "/root/init/data/dump"])
 
     def _handle_artifact_download(self, instance: Any, artifact: Injectable) -> None:
@@ -235,22 +221,15 @@ 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()
-        data_path = pathlib.Path(dump_data.path)
+        dump_data = artifact._dump(mode="pull")
         instance.execute(["mkdir", "-p", "/root/post/data"])
-        instance.files.put(f"/root/post/data/{data_path.name}", data_path.read_bytes())
         instance.files.put(
             "/root/post/data/load",
-            artifact.__injectable__(
-                f"/root/post/data/{data_path.name}", dump_data.hash, mode="download"
-            ),
+            dump_data["injectable"],
         )
         result = json.loads(instance.execute(["python3", "/root/post/data/load"]).stdout)
-        data = instance.files.get(result["path"])
         with tempfile.NamedTemporaryFile() as fout:
-            handler = pathlib.Path(fout.name)
-            handler.write_bytes(data)
-            holder = artifact.__class__._load(str(handler), result["hash"])
+            holder = artifact.__class__._load(result["checksum"], result["data"])
             holder.dump()
 
 
diff --git a/src/cleantest/utils/apt_handler.py b/src/cleantest/utils/apt_handler.py
index 035665e..d3ade1d 100644
--- a/src/cleantest/utils/apt_handler.py
+++ b/src/cleantest/utils/apt_handler.py
@@ -6,12 +6,14 @@
 
 # Note: Support for third-party repositories is not implemented yet.
 
+import os
 import pathlib
 import re
 import subprocess
 from shutil import which
-from typing import Any, Iterable, List, Optional, Union
+from typing import Iterable, List, Optional, Union
 
+from cleantest.meta import Result
 from cleantest.meta.utils import detect_os_variant
 
 
@@ -36,22 +38,33 @@ def _is_apt_available() -> None:
 
 def _apt(
     command: str, packages: Union[str, List[str]], optargs: Optional[List[str]] = None
-) -> Union[subprocess.CompletedProcess[Any], subprocess.CompletedProcess[str]]:
+) -> Result:
     """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.
+
+    Raises:
+        AptHandlerError: Raised if executed command fails.
+
+    Returns:
+        (Result): Captured exit code, stdout, and stderr.
     """
     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
+        process = subprocess.run(
+            _cmd,
+            env={"DEBIAN_FRONTEND": "noninteractive", "PATH": os.getenv("PATH")},
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+            universal_newlines=True,
         )
-        return res
+        return Result(process.returncode, process.stdout, process.stderr)
+
     except subprocess.CalledProcessError:
         raise AptHandlerError(
             f"Could not perform command {_cmd} on the following packages: {', '.join(packages)}"
diff --git a/src/cleantest/utils/run.py b/src/cleantest/utils/run.py
index bae2675..f49fbea 100644
--- a/src/cleantest/utils/run.py
+++ b/src/cleantest/utils/run.py
@@ -38,9 +38,10 @@ def run(
                 command.split(" "),
                 env=(env if env else None),
                 cwd=(cwd if cwd else None),
+                stdout=subprocess.PIPE,
+                stderr=subprocess.PIPE,
+                universal_newlines=True,
                 check=True,
-                capture_output=True,
-                text=True,
             )
         except subprocess.CalledProcessError as e:
             raise CommandExecutionError(f"Failed to execute command {command}. Reason: {e}.")
diff --git a/tests/files/test_upload_download.py b/tests/files/test_upload_download.py
index 2a23d3b..39fc7d2 100644
--- a/tests/files/test_upload_download.py
+++ b/tests/files/test_upload_download.py
@@ -8,9 +8,9 @@
 import pathlib
 import tempfile
 
-from cleantest import Configure
-from cleantest.hooks import StartEnvHook, StopEnvHook
-from cleantest.hooks.data import Dir, File
+from cleantest.control import Configure
+from cleantest.control.hooks import StartEnvHook, StopEnvHook
+from cleantest.data import Dir, File
 from cleantest.provider import lxd
 
 root = os.path.dirname(os.path.realpath(__file__))
@@ -32,7 +32,7 @@
 config.register_hook(start_hook, stop_hook)
 
 
-@lxd(image="jammy-amd64", preserve=True)
+@lxd(image="ubuntu-jammy-amd64", preserve=True)
 def work_on_artifacts():
     import os
     import pathlib
diff --git a/tests/lxd/parallel/test_lxd_parallel.py b/tests/lxd/parallel/test_lxd_parallel.py
index 59f7402..8ba856f 100644
--- a/tests/lxd/parallel/test_lxd_parallel.py
+++ b/tests/lxd/parallel/test_lxd_parallel.py
@@ -4,9 +4,9 @@
 
 """Test parallel testing capabilities of LXD provider using local LXD cluster."""
 
-from cleantest import Configure
-from cleantest.hooks import StartEnvHook
-from cleantest.pkg import Pip
+from cleantest.control import Configure
+from cleantest.control.hooks import StartEnvHook
+from cleantest.data.pkg import Pip
 from cleantest.provider import lxd
 
 cleantest_config = Configure()
@@ -15,7 +15,7 @@
 
 
 @lxd(
-    image=["jammy-amd64", "focal-amd64", "bionic-amd64"],
+    image=["ubuntu-jammy-amd64", "ubuntu-focal-amd64", "ubuntu-bionic-amd64"],
     preserve=False,
     parallel=True,
     num_threads=2,
diff --git a/tests/packages/snap/test_snap.py b/tests/packages/snap/test_snap.py
index 48eb778..56c2a0c 100644
--- a/tests/packages/snap/test_snap.py
+++ b/tests/packages/snap/test_snap.py
@@ -6,9 +6,9 @@
 
 import os
 
-from cleantest import Configure
-from cleantest.hooks import StartEnvHook
-from cleantest.pkg import Connection, Plug, Slot, Snap
+from cleantest.control import Configure
+from cleantest.control.hooks import StartEnvHook
+from cleantest.data.pkg import Connection, Plug, Slot, Snap
 from cleantest.provider import lxd
 
 root = os.path.dirname(os.path.realpath(__file__))
@@ -28,7 +28,7 @@
 config.register_hook(start_hook)
 
 
-@lxd(image="jammy-amd64", preserve=False)
+@lxd(image="ubuntu-jammy-amd64", preserve=False)
 def functional_snaps():
     import sys
     from shutil import which