Repository: NucciTheBoss/cleantest (language: python)

Issue:
Add example of testing code on a mini-HPC cluster to documentation
Even though I have not promised comprehensive examples until version `0.5.0` of cleantest, I still think having this document will be important both for FOSDEM attendees and potential users future reference. The document will most likely read the same as other mini/micro-HPC documents/white-papers I have written, but this time it will be on the internet!

This document will be linked as extra material for my talk at FOSDEM. 


Discussion / hints:

Fix patch:
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index 1e4152f..28990c3 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -1,3 +1,6 @@
+[//]: # "Copyright 2023 Jason C. Nucciarone"
+[//]: # "See LICENSE file for licensing details."
+
 # Community
 
 cleantest is about providing an easy way to test software using open-source container and virtual machine hypervisors
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f0cc10d..e13a092 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,3 +1,6 @@
+[//]: # "Copyright 2023 Jason C. Nucciarone"
+[//]: # "See LICENSE file for licensing details."
+
 # Contributing to cleantest
 
 Do you have something that you wish to contribute to cleantest? **If so, here is how you can help!**
diff --git a/README.md b/README.md
index 6acbd7e..76df37a 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,6 @@
+[//]: # "Copyright 2023 Jason C. Nucciarone"
+[//]: # "See LICENSE file for licensing details."
+
 <h3 align="center">cleantest</h3>
 
 <p align="center">
@@ -83,8 +86,8 @@ def do_something():
 
 class TestSuite:
     def test_do_something(self) -> None:
-        result = do_something()
-        assert result.exit_code == 0
+        for name, result in do_something():
+            assert result.exit_code == 0
 ```
 </details>
 
@@ -107,21 +110,28 @@ standards, and how to submit contributions to cleantest.
 
 ## License
 
-Code and documentation copyright &copy; 2022 Jason C. Nucciarone, Canonical Ltd. Please see the 
+Code and documentation copyright &copy; 2023 Jason C. Nucciarone. Please see the 
 [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.html) license for more details.
 
 ## Roadmap
 
 Here are my (NucciTheBoss's) goals to get cleantest to release version 1.0.0:
 
+> We are sort of the way there on fully support multiple Linux distributions.
+> The big hang-up now is just ensuring that the current macros and utilities are
+> compatible with the new distributions and writing integration tests to ensure that
+> the distributions actually work with cleantest.
+> 
+> __Note:__ cleantest can boot these new distributions but support might be slightly flaky.
+
 * ~~Add support for injecting tests into LXD~~
-* Add support for multiple distros:
+* ~~Add support for multiple distros:~~
   * ~~Ubuntu~~
-  * Debian
-  * CentOS
-  * Rocky
-  * Fedora
-  * Arch
+  * ~~Debian~~
+  * ~~CentOS~~
+  * ~~Rocky~~
+  * ~~Fedora~~
+  * ~~Arch~~
 * ~~Enable support for running parallel tests with LXD~~
 * Better test logging capabilities
 * Support for a select few popular packaging formats:
diff --git a/docs/about/license.md b/docs/about/license.md
deleted file mode 100644
index 537fde8..0000000
--- a/docs/about/license.md
+++ /dev/null
@@ -1,170 +0,0 @@
-Apache License
-==============
-
-_Version 2.0, January 2004_  
-_&lt;<http://www.apache.org/licenses/>&gt;_
-
-### Terms and Conditions for use, reproduction, and distribution
-
-#### 1. Definitions
-
-“License” shall mean the terms and conditions for use, reproduction, and
-distribution as defined by Sections 1 through 9 of this document.
-
-“Licensor” shall mean the copyright owner or entity authorized by the copyright
-owner that is granting the License.
-
-“Legal Entity” shall mean the union of the acting entity and all other entities
-that control, are controlled by, or are under common control with that entity.
-For the purposes of this definition, “control” means **(i)** the power, direct or
-indirect, to cause the direction or management of such entity, whether by
-contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
-outstanding shares, or **(iii)** beneficial ownership of such entity.
-
-“You” (or “Your”) shall mean an individual or Legal Entity exercising
-permissions granted by this License.
-
-“Source” form shall mean the preferred form for making modifications, including
-but not limited to software source code, documentation source, and configuration
-files.
-
-“Object” form shall mean any form resulting from mechanical transformation or
-translation of a Source form, including but not limited to compiled object code,
-generated documentation, and conversions to other media types.
-
-“Work” shall mean the work of authorship, whether in Source or Object form, made
-available under the License, as indicated by a copyright notice that is included
-in or attached to the work (an example is provided in the Appendix below).
-
-“Derivative Works” shall mean any work, whether in Source or Object form, that
-is based on (or derived from) the Work and for which the editorial revisions,
-annotations, elaborations, or other modifications represent, as a whole, an
-original work of authorship. For the purposes of this License, Derivative Works
-shall not include works that remain separable from, or merely link (or bind by
-name) to the interfaces of, the Work and Derivative Works thereof.
-
-“Contribution” shall mean any work of authorship, including the original version
-of the Work and any modifications or additions to that Work or Derivative Works
-thereof, that is intentionally submitted to Licensor for inclusion in the Work
-by the copyright owner or by an individual or Legal Entity authorized to submit
-on behalf of the copyright owner. For the purposes of this definition,
-“submitted” means any form of electronic, verbal, or written communication sent
-to the Licensor or its representatives, including but not limited to
-communication on electronic mailing lists, source code control systems, and
-issue tracking systems that are managed by, or on behalf of, the Licensor for
-the purpose of discussing and improving the Work, but excluding communication
-that is conspicuously marked or otherwise designated in writing by the copyright
-owner as “Not a Contribution.”
-
-“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
-of whom a Contribution has been received by Licensor and subsequently
-incorporated within the Work.
-
-#### 2. Grant of Copyright License
-
-Subject to the terms and conditions of this License, each Contributor hereby
-grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
-irrevocable copyright license to reproduce, prepare Derivative Works of,
-publicly display, publicly perform, sublicense, and distribute the Work and such
-Derivative Works in Source or Object form.
-
-#### 3. Grant of Patent License
-
-Subject to the terms and conditions of this License, each Contributor hereby
-grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
-irrevocable (except as stated in this section) patent license to make, have
-made, use, offer to sell, sell, import, and otherwise transfer the Work, where
-such license applies only to those patent claims licensable by such Contributor
-that are necessarily infringed by their Contribution(s) alone or by combination
-of their Contribution(s) with the Work to which such Contribution(s) was
-submitted. If You institute patent litigation against any entity (including a
-cross-claim or counterclaim in a lawsuit) alleging that the Work or a
-Contribution incorporated within the Work constitutes direct or contributory
-patent infringement, then any patent licenses granted to You under this License
-for that Work shall terminate as of the date such litigation is filed.
-
-#### 4. Redistribution
-
-You may reproduce and distribute copies of the Work or Derivative Works thereof
-in any medium, with or without modifications, and in Source or Object form,
-provided that You meet the following conditions:
-
-* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
-this License; and
-* **(b)** You must cause any modified files to carry prominent notices stating that You
-changed the files; and
-* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
-all copyright, patent, trademark, and attribution notices from the Source form
-of the Work, excluding those notices that do not pertain to any part of the
-Derivative Works; and
-* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
-Derivative Works that You distribute must include a readable copy of the
-attribution notices contained within such NOTICE file, excluding those notices
-that do not pertain to any part of the Derivative Works, in at least one of the
-following places: within a NOTICE text file distributed as part of the
-Derivative Works; within the Source form or documentation, if provided along
-with the Derivative Works; or, within a display generated by the Derivative
-Works, if and wherever such third-party notices normally appear. The contents of
-the NOTICE file are for informational purposes only and do not modify the
-License. You may add Your own attribution notices within Derivative Works that
-You distribute, alongside or as an addendum to the NOTICE text from the Work,
-provided that such additional attribution notices cannot be construed as
-modifying the License.
-
-You may add Your own copyright statement to Your modifications and may provide
-additional or different license terms and conditions for use, reproduction, or
-distribution of Your modifications, or for any such Derivative Works as a whole,
-provided Your use, reproduction, and distribution of the Work otherwise complies
-with the conditions stated in this License.
-
-#### 5. Submission of Contributions
-
-Unless You explicitly state otherwise, any Contribution intentionally submitted
-for inclusion in the Work by You to the Licensor shall be under the terms and
-conditions of this License, without any additional terms or conditions.
-Notwithstanding the above, nothing herein shall supersede or modify the terms of
-any separate license agreement you may have executed with Licensor regarding
-such Contributions.
-
-#### 6. Trademarks
-
-This License does not grant permission to use the trade names, trademarks,
-service marks, or product names of the Licensor, except as required for
-reasonable and customary use in describing the origin of the Work and
-reproducing the content of the NOTICE file.
-
-#### 7. Disclaimer of Warranty
-
-Unless required by applicable law or agreed to in writing, Licensor provides the
-Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
-including, without limitation, any warranties or conditions of TITLE,
-NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
-solely responsible for determining the appropriateness of using or
-redistributing the Work and assume any risks associated with Your exercise of
-permissions under this License.
-
-#### 8. Limitation of Liability
-
-In no event and under no legal theory, whether in tort (including negligence),
-contract, or otherwise, unless required by applicable law (such as deliberate
-and grossly negligent acts) or agreed to in writing, shall any Contributor be
-liable to You for damages, including any direct, indirect, special, incidental,
-or consequential damages of any character arising as a result of this License or
-out of the use or inability to use the Work (including but not limited to
-damages for loss of goodwill, work stoppage, computer failure or malfunction, or
-any and all other commercial damages or losses), even if such Contributor has
-been advised of the possibility of such damages.
-
-#### 9. Accepting Warranty or Additional Liability
-
-While redistributing the Work or Derivative Works thereof, You may choose to
-offer, and charge a fee for, acceptance of support, warranty, indemnity, or
-other liability obligations and/or rights consistent with this License. However,
-in accepting such obligations, You may act only on Your own behalf and on Your
-sole responsibility, not on behalf of any other Contributor, and only if You
-agree to indemnify, defend, and hold each Contributor harmless for any liability
-incurred by, or claims asserted against, such Contributor by reason of your
-accepting any such warranty or additional liability.
-
-_END OF TERMS AND CONDITIONS_
\ No newline at end of file
diff --git a/docs/examples/coming-soon.md b/docs/examples/coming-soon.md
deleted file mode 100644
index d6aac3b..0000000
--- a/docs/examples/coming-soon.md
+++ /dev/null
@@ -1,9 +0,0 @@
-# Coming soon
-
-!!! example "This page is currently under construction"
-
-    Hey there from NucciTheBoss! I am in the process of creating some examples to demonstrate some of the cool things
-    that you can do with cleantest such as creating a continuous integration pipeline on GitHub to test packages 
-    or building your own micro-HPC cluster to test that your simulation works. Stay tuned!
-
-    __Examples will be added in cleantest 0.5.0__
\ No newline at end of file
diff --git a/docs/index.md b/docs/index.md
index 98c41d7..08f6661 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,28 +1,39 @@
-# Welcome to cleantest's documentation!
+[//]: # "Copyright 2023 Jason C. Nucciarone"
+[//]: # "See LICENSE file for licensing details."
 
-`cleantest` is a testing framework for developers who need clean testing environments in a hurry.
+# Welcome to the documentation for the cleantest testing framework!
 
-## History
+`cleantest` is a testing framework that brings up clean environments and mini-clusters 
+for developers in a hurry.
 
-The first version of `cleantest` was authored by Jason Nucciarone when he was having trouble finding a safe way to 
-test destructive code locally on his system. He needed to test some code that he wrote to install and manage 
-the Slurm Workload Manager, but he did not want to install the Debian packages directly on his system, and he did not 
-want to go through the environment of setting up a CI/CD pipeline on GitHub. Irked by the idea of potentially needing 
-to run his IDE out of a virtual machine and test his code there, he had an idea: what if there was library that could 
-grab the body of a test, bring up a container, run the test inside that container, and then report back the results as 
-if it never left the current process?
+## Get started with cleantest
 
-With his idea in mind, he set out to scratch his itch, and eventually decided to name the collection of Python 
-decorators he created `cleantest`. Over time, and after lots of feedback from colleagues and friends, `cleantest` is 
-now the library you see before you.
+New user? No problem. Just head on over to the [user guide](./user-guide) 
+to start learning how to use cleantest. Want to learn how to do some amazing
+things with cleantest in the field? Head on over to our [tutorials](./tutorials).
+Want to contribute a bug fix, enhancement proposal, or pull request to cleantest? 
+Read our [contributing guidelines](https://github.com/NucciTheBoss/cleantest/blob/main/CONTRIBUTING.md) 
+and 
+[code of conduct](https://github.com/NucciTheBoss/cleantest/blob/main/CODE_OF_CONDUCT.md).
 
-## What is cleantest?
+## Meet the Developers
 
-The sales pitch for `cleantest` is _a testing framework for developers who need clean testing environments in a hurry_,
-but is more than just that. It aims to be an easy way to test code, whether you are on your laptop or an
-exa-scale high-performance computing cluster. It provides tools to work with popular packaging formats, utilities 
-for pushing or pulling artifacts, and ways to test code across multiple Linux distributions. It also enables you to 
-simulate high-performance computing clusters to test software deployments and installation scripts.
+These are the folks who put in the effort to make cleantest great! Feel free to reach out
+to the development team if your are interested in becoming a contributor to cleantest.
 
-If you are interested in learning more, then head on over to [installation](user-guide/installation.md) page of this 
-documentation!
\ No newline at end of file
+| [![Jason C. Nucciarone](https://s.gravatar.com/avatar/00c5a27b2478f8194fcd3221f8be7463?s=245)](https://github.com/NucciTheBoss/) |
+|:--------------------------------------------------------------------------------------------------------------------------------:|
+|                              [Jason C. Nucciarone (NucciTheBoss)](https://github.com/NucciTheBoss/)                              |
+
+## Licensing information
+
+The cleantest source code and complimentary documentation are licensed under the Apache
+Software License, version 2.0. You may not use cleantest or its documentation except in
+compliance with the license. Unless required by applicable law or agreed to in writing, 
+software distributed under the the Apache Software License, version 2.0 is distributed 
+on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 
+implied. See the Apache Software License, version 2.0, for the specific language 
+governing permissions and limitations under the License.
+
+An electronic copy of cleantest's license can be obtained 
+[here](https://github.com/NucciTheBoss/cleantest/blob/main/LICENSE).
\ No newline at end of file
diff --git a/docs/news/news.md b/docs/news/news.md
new file mode 100644
index 0000000..3e677fb
--- /dev/null
+++ b/docs/news/news.md
@@ -0,0 +1,85 @@
+[//]: # "Copyright 2023 Jason C. Nucciarone"
+[//]: # "See LICENSE file for licensing details."
+
+# Your hotspot for the latest and greatest news about the development of cleantest!
+
+## __0.4.0-rc1 released__ -- February 18th, 2023
+
+__What is new?__
+
+Over the past two months, I have been hard at work adding a lot of new features for 0.4.0
+in my quest to make cleantest a great testing framework. The 0.4.0-rc1 pre-release is so that
+folks can start getting a feel for all the features that I have been adding. Not everything I want
+to have has been added to cleantest yet, but I want to give folks something new to get their hands on
+in the meantime. Here is what is new in 0.4.0-rc1:
+
+* Added a CI/CD pipeline for linting and functional tests via GitHub Actions. No more hip firing here!
+* Fixed mutability issues when running clean tests in sequence. Now singleton state can be rest between runs.
+* Added a `run` utility for executing shell commands inside test environment instances.
+* Added a `systemd` utility for controlling services inside test environment instances.
+* Added a `apt` utility for interfacing with the apt package manager on Ubuntu/Debian-based test environment instances.
+* General quality of life refactors. Removed methods that did not need to be there.
+* New module structure - I tried to further refine imports from version 0.3.0.
+* PRELIMINARY MULTI-DISTRIBUTION SUPPORT!!! You can know launch Rocky, AlmaLinux instances and more, but robust support
+    is not fully there yet (i.e. package macros do not work yet).
+* Fixed issue [#21](https://github.com/NucciTheBoss/cleantest/issues/21).
+* Introduction of _Archon_ and _Harness_ classes. Archon can be used to manually direct the test environment provider
+    and Harness is the new name for the legacy _Provider_ classes. _Harness_ is what wraps around testlets when
+    invoking the provider decorators. _Archon_ can be used to set up more complex cloud deployments such as
+    mini high-performance computing clusters.
+* Enhanced documentation. There is now a News page (what you are reading currently), a reference page
+    (to be completed by 0.6.0), and a community page that routes to GitHub Discussions. I also cleaned up
+    the home page to make it more concise.
+* An actual tutorial! I know I said I would not add them until 0.5.0, but I decided to share a sneak-peak ;)
+
+__What still needs to be done for 0.4.0?__
+
+I still need to do a bit of work before the final 0.4.0 release is ready. Here is what still needs to be done:
+
+* Add a `dnf` utility for interfacing with the dnf package manager.
+* Add a `pacman` utility for interfacing with the pacman package manager.
+* Add a `passwd` utility for creating users and groups on test environment instances.
+* Low-level refactor to improve LXD API socket interaction.
+    See issue [#32](https://github.com/NucciTheBoss/cleantest/issues/32).
+* Add some logging output to show cleantest's progress.
+    See issue [#4](https://github.com/NucciTheBoss/cleantest/issues/4).
+* Fix bug where cleantest will fail if using LXD virtual machines instead of containers.
+    See issue [#12](https://github.com/NucciTheBoss/cleantest/issues/12).
+
+__What comes after 0.4.0?__
+
+I have big plans for cleantest 0.5.0. The focus of the 0.5.0 release will be cloud-interoperability,
+test result reporting, and adding a CLI front-end to cleantest. Here is a sneak-peak of what I am
+planning to do:
+
+* Add Juju as a test environment provider.
+* Add a REPL for interactively running cleantest.
+* Add report generation abilities to cleantest.
+* Add support for "gambols". More on that later ;)
+
+As always, feel free to make a discussion post if you have any questions and I hope you continue to enjoy
+using cleantest!
+
+## __cleantest goes to FOSDEM'2023!__ -- February 6th, 2023
+
+This past weekend I gave my talk __"Developing effective testing 
+pipelines for HPC applications"__ in the HPC, Big Data, and Data Science devroom at FOSDEM
+in Brussels. This was cleantest's first public appearance! In the talk, I showcased the
+new features I have been adding to cleantest such as the _Archon_ (fancy term for director) 
+class for directing test environment providers, the _Harness_ class for encapsulating
+testlets, and general methods to my madness. The talk went off without a hitch except for
+when I got bitten by YouTube autoplay being on.
+
+Overall, I got a lot of great feedback about the current state of cleantest as well as
+establishing connections with lots of amazing folks in the HPC industry. Hopefully I get
+invited again next year! Check out the recording of my talk below if you are interested
+to see what I talked about!
+
+<p align="center">
+  <iframe 
+    width="620" height="375" src="https://www.youtube.com/embed/Ph3-WfaWBmE" 
+    title="cleantest FOSDEM talk" frameborder="0" 
+    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" 
+    allowfullscreen>
+  </iframe>
+</p>
diff --git a/docs/reference/coming-soon.md b/docs/reference/coming-soon.md
new file mode 100644
index 0000000..fa0ff82
--- /dev/null
+++ b/docs/reference/coming-soon.md
@@ -0,0 +1,12 @@
+# Coming soon
+
+!!! example "This page is currently under construction"
+
+    Hey there from NucciTheBoss! I am currently in the process of evaluating tools for generating
+    reference documentation from Python docstrings. Unfortunately, it is not super high on my list
+    of priorities for the time being due to me just needing to get features into cleantest, but I
+    plan on adding some reference documentation later on. If you have any recommendation for autogen
+    tools or want to help me put together reference documentation, do not hesitate to get in touch!
+    I know some of you might need the nitty gritty technical documentation!
+
+    __Reference documentation will be added in cleantest 0.6.0__
\ No newline at end of file
diff --git a/docs/tutorials/using-a-mini-hpc-cluster.md b/docs/tutorials/using-a-mini-hpc-cluster.md
new file mode 100644
index 0000000..7f4d415
--- /dev/null
+++ b/docs/tutorials/using-a-mini-hpc-cluster.md
@@ -0,0 +1,875 @@
+[//]: # "Copyright 2023 Jason C. Nucciarone"
+[//]: # "See LICENSE file for licensing details."
+
+# Using a mini-HPC cluster to test batch jobs
+
+Testing applications intended to run on high-performance computing clusters can be a painful process, 
+especially when you do not have access to the resources you need. Sometimes you just want to check if 
+you have the right compile flag specified, if the script you wrote actually works, or if your 
+application can successfully use multiple cores. One way to test your HPC software is by emulation; 
+instead of wasting precious compute time on your bare-metal cluster, you use a mini-HPC cluster that 
+provides the same functionality as the bare-metal cluster on a smaller scale.
+
+Using cleantest, you can build yourself a mini-HPC cluster anywhere you need it, whether it be on your laptop
+or a continuous integration pipeline runner. In this tutorial, you will learn how to build a mini-HPC cluster
+and submit a test batch jobs to the cluster's resource manager/workload scheduler. Below is a diagram outlining
+the architecture of the mini-HPC cluster that we are going to build with cleantest.
+
+```mermaid
+flowchart LR
+    subgraph identity [Identity]
+        ldap(OpenLDAP)
+    end
+    subgraph filesystem [Shared Filesystem]
+        nfs(NFS)
+    end
+    subgraph cluster [Resource Management & Compute Service]
+        controller(slurmctld)
+        compute0(slurmd)
+        compute1(slurmd)
+        compute2(slurmd)
+    end
+
+    identity --> filesystem
+    identity --> cluster
+    filesystem --> cluster
+    cluster --> filesystem
+    controller --> compute0
+    compute0 --> controller
+    controller --> compute1
+    compute1 --> controller
+    controller --> compute2
+    compute2 --> controller
+```
+
+## Setting up the cleantest environment
+
+### Test dependencies
+
+This tutorial will be using the [LXD test environment provider](../user-guide/test-env-providers.md#lxd) to provide
+the test environment instances that will compose the mini-HPC cluster. If you do not have LXD installed on your
+system, please visit the [Installation guide](../user-guide/installation.md#lxd) for instructions on how to set up
+LXD.
+
+This tutorial will also being using the Jinja templating engine for rendering specific configuration files for the
+services used inside the mini-HPC cluster. You can use the `pip` package manager to install Jinja on your system:
+
+```shell
+python3 -m pip install Jinja2
+```
+
+We will also be using `pytest` to run our "clean tests". `pytest` can be installed using `pip` as well:
+
+```shell
+python3 -m pip install pytest
+```
+
+### Required template files
+
+The following Jinja template files are needed for rendering service configuration files. Please create a `templates`
+directory in your current working directory and copy the templates to the newly created directory.
+
+__sssd.conf.tmpl__
+
+We will be using [sssd](https://ubuntu.com/server/docs/service-sssd) (System Security Services Daemon) to connect
+clients to the mini-HPC cluster's identity service. This Jinja template will be used to render the _sssd.conf_ file
+that will be used by the sssd service to locate the identity service.
+
+```text
+[sssd]
+config_file_version = 2
+domains = mini-hpc.org
+
+[domain/mini-hpc.org]
+id_provider = ldap
+auth_provider = ldap
+ldap_uri = ldap://{{ ldap_server_address }}
+cache_credentials = True
+ldap_search_base = dc=mini-hpc,dc=org
+```
+
+__slurm.conf.tmpl__
+
+We will be using the [SLURM workload manager](https://slurm.schedmd.com/overview.html) to provide resource management,
+workload scheduling, and compute service in the mini-HPC cluster. This Jinja template will be used to configure SLURM
+after the controller and compute nodes have been created.
+
+```text
+SlurmctldHost={{ slurmctld_name }}({{ slurmctld_address }})
+ClusterName=mini-hpc
+
+AuthType=auth/munge
+FirstJobId=65536
+InactiveLimit=120
+JobCompType=jobcomp/filetxt
+JobCompLoc=/var/log/slurm/jobcomp
+ProctrackType=proctrack/linuxproc
+KillWait=30
+MaxJobCount=10000
+MinJobAge=3600
+ReturnToService=0
+SchedulerType=sched/backfill
+SlurmctldLogFile=/var/log/slurm/slurmctld.log
+SlurmdLogFile=/var/log/slurm/slurmd.log
+SlurmctldPort=7002
+SlurmdPort=7003
+SlurmdSpoolDir=/var/spool/slurmd.spool
+StateSaveLocation=/var/spool/slurm.state
+SwitchType=switch/none
+TmpFS=/tmp
+WaitTime=30
+
+# Node Configurations
+NodeName={{ slurmd_0_name }} NodeAddr={{ slurmd_0_address }} CPUs=1 RealMemory=1000 TmpDisk=10000
+NodeName={{ slurmd_1_name }} NodeAddr={{ slurmd_1_address }} CPUs=1 RealMemory=1000 TmpDisk=10000
+NodeName={{ slurmd_2_name }} NodeAddr={{ slurmd_2_address }} CPUs=1 RealMemory=1000 TmpDisk=10000
+
+# Partition Configurations
+PartitionName=all Nodes={{ slurmd_0_name }},{{ slurmd_1_name }},{{ slurmd_2_name }} MaxTime=30 MaxNodes=3 State=UP
+```
+
+### Create the test file
+
+Create the file _test_mini_hpc.py_ in your current working directory; this file is where we will write our test.
+Once you have created _test_mini_hpc.py_, add the following lines to the top of the file:
+
+```python
+#!/usr/bin/env python3
+
+"""Test batch job using mini-HPC cluster created with cleantest."""
+
+import json
+import os
+import pathlib
+from io import StringIO
+
+from jinja2 import Environment, FileSystemLoader
+
+from cleantest.control.hooks import StopEnvHook
+from cleantest.control.lxd import InstanceConfig
+from cleantest.data import File
+from cleantest.provider import lxd, LXDArchon
+
+root = pathlib.Path(os.path.dirname(os.path.realpath(__file__)))
+templates = Environment(loader=FileSystemLoader(root / "templates"))
+
+# Where the testlet will be added.
+...
+
+
+def test_mini_hpc() -> None:
+    """Test batch job inside mini-hpc cluster."""
+    archon = LXDArchon()
+    archon.config.register_hook(
+        StopEnvHook(name="get_result", download=[File("/tmp/result", root / "result")])
+    )
+    placeholder = archon.config.get_instance_config("ubuntu-jammy-amd64").dict()
+    placeholder["name"] = "mini-hpc-sm"
+    archon.config.add_instance_config(
+        InstanceConfig(
+            config={
+                "limits.cpu": "1",
+                "limits.memory": "8GB",
+                "security.privileged": "true",
+                "raw.apparmor": "mount fstype=nfs*, mount fstype=rpc_pipefs,",
+            },
+            **placeholder,
+        )
+    )
+
+    # Where most of the code snippets will be appended 
+    # for creating the mini-HPC cluster.
+    ...
+```
+
+These imports/variable declarations at the beginning of the test file will be used throughout the code snippets in
+the rest of this tutorial.
+
+Inside our test function, we instantiate an instance of `LXDArchon` that will be used to
+control the LXD test environment provider. We also define a stop environment hook that will be used to retrieve
+the result of our test batch job. Lastly, we create a custom test environment instance configuration. This
+configuration will have the necessary privileges set so that we do not eat all of your workstation's resources
+as well as enabling support for NFS mounts inside the test environment instances.
+
+## Configuring the identity service
+
+To start, we will be configuring an identity service for our mini-HPC cluster. We cannot let user `root` be the 
+primary user of our mini-HPC cluster; that would create a disaster. To prevent the mini-HPC cluster from going 
+up into flames, we are going to use [slapd](https://manpages.ubuntu.com/manpages/jammy/en/man8/slapd.8.html)
+(Stand-alone LDAP Daemon) to provide the identity service for our cluster.
+
+### Provision script for `ldap-0`
+
+cleantest has a built-in provisioning mechanism for test environment instances. You can write custom Python scripts
+that use utilities provided by cleantest to set up the test environment instance how you like. Below is the provision
+script that we will use to provision the `ldap-0` instance in our mini-HPC cluster. Save the provision script to the
+file _ldap_provision_script.py_ in your current working directory.
+
+```python
+#!/usr/bin/env python3
+# Copyright 2023 Jason C. Nucciarone
+# See LICENSE file for licensing details.
+
+"""Provision LDAP server nodes."""
+
+import pathlib
+import tempfile
+import textwrap
+
+from cleantest.utils import apt, systemd, run
+
+# Define resources needed to set up LDAP.
+slapd_preseed = textwrap.dedent(
+    """
+    slapd slapd/no_configuration boolean false
+    slapd slapd/domain string mini-hpc.org
+    slapd shared/organization string mini-hpc
+    slapd slapd/password1 password test
+    slapd slapd/password2 password test
+    slapd slapd/purge_database boolean true
+    slapd slapd/move_old_database boolean true
+    """
+).strip("\n")
+default_ldif = textwrap.dedent(
+    """
+    dn: ou=People,dc=mini-hpc,dc=org
+    objectClass: organizationalUnit
+    ou: People
+    
+    dn: ou=Groups,dc=mini-hpc,dc=org
+    objectClass: organizationalUnit
+    ou: Groups
+    
+    dn: uid=nucci,ou=People,dc=mini-hpc,dc=org
+    uid: nucci
+    objectClass: inetOrgPerson
+    objectClass: posixAccount
+    cn: nucci
+    sn: nucci
+    givenName: nucci
+    mail: nucci@example.com
+    userPassword: test
+    uidNumber: 10000
+    gidNumber: 10000
+    loginShell: /bin/bash
+    homeDirectory: /home/nucci
+    
+    dn: cn=nucci,ou=Groups,dc=mini-hpc,dc=org
+    cn: nucci
+    objectClass: posixGroup
+    gidNumber: 10000
+    memberUid: nucci
+    
+    dn: cn=research,ou=Groups,dc=mini-hpc,dc=org
+    cn: research
+    objectClass: posixGroup
+    gidNumber: 10100
+    memberUid: nucci
+    """
+).strip("\n")
+
+# Set up slapd service.
+apt.update()
+apt.install("slapd", "ldap-utils", "debconf-utils")
+with tempfile.NamedTemporaryFile() as preseed, tempfile.NamedTemporaryFile() as ldif:
+    pathlib.Path(preseed.name).write_text(slapd_preseed)
+    pathlib.Path(ldif.name).write_text(default_ldif)
+    results = run(
+        f"debconf-set-selections < {preseed.name}",
+        "dpkg-reconfigure -f noninteractive slapd",
+        (
+            "ldapadd -x -D cn=admin,dc=mini-hpc,dc=org -w "
+            f"test -f {ldif.name} -H ldap:///"
+        ),
+    )
+    for result in results:
+        assert result.exit_code == 0
+
+systemd.restart("slapd")
+```
+
+The provision script works through the following steps:
+
+1. Import the `apt`, `systemd`, and `run` utilities for interfacing with the APT package manager and systemd on the 
+    instance, and executing shell commands respectively.
+2. Define a preseed file that will be used by `debconf-set-selections` to configure the slapd service on the instance.
+3. Define a ldif (LDAP Data Interchange Format) file that will be used to create our test user and group.
+4. Update APT cache and install `slapd`, `ldap-utils`, and `debconf-utils`.
+5. Configure slapd service using `debconf-set-selections` and add test user and group to the LDAP server.
+6. Restart slapd so that new configurations will be set.
+
+### Use cleantest archon to create `ldap-0`
+
+Now that we have our provision script _ldap_provision_script.py_ written, use the following block of code
+to add the instance `ldap-0` to our LXD test environment provider:
+
+```python
+archon.add(
+    "ldap-0",
+    image="mini-hpc-sm",
+    provision_script=root / "ldap_provision_script.py",
+)
+```
+
+The provision script will be injected into `ldap-0` and executed after the instance becomes active.
+
+## Setting up the shared filesystem
+
+We will be using [NFS](https://en.wikipedia.org/wiki/Network_File_System) (Network File System) to provide the
+shared filesystem in the mini-HPC cluster. Many compute instances are generally operating on the same set of data
+across the cluster, so the compute nodes all need access to the same data. Using NFS, we can ensure that each
+compute instance has access to the same file and/or directories. 
+
+### Provision script for `nfs-0`
+
+With the `ldap-0` instance created, create the provision script _nfs_provision_script.py_ in your current working
+directory and add the following code block to the file:
+
+```python
+#!/usr/bin/env python3
+# Copyright 2023 Jason C. Nucciarone
+# See LICENSE file for licensing details.
+
+"""Provision NFS server nodes."""
+
+import pathlib
+import textwrap
+
+from cleantest.utils import apt, systemd, run
+
+# Define resources needed to set up nfs-kernel-server.
+default_exports = textwrap.dedent(
+    """
+    /srv     *(ro,sync,subtree_check)
+    /home    *(rw,sync,no_subtree_check)
+    /data    *(rw,sync,no_subtree_check,no_root_squash)
+    /opt     *(rw,sync,no_subtree_check,no_root_squash)
+    """
+).strip("\n")
+
+# Set up SSSD service.
+apt.update()
+apt.install("nfs-kernel-server", "sssd-ldap")
+for result in run(
+    "mv /root/.init/sssd.conf /etc/sssd/sssd.conf",
+    "chmod 0600 /etc/sssd/sssd.conf",
+    "pam-auth-update --enable mkhomedir",
+):
+    assert result.exit_code == 0
+
+systemd.restart("sssd")
+
+# Set up NFS kernel server.
+for result in run(
+    "mkdir -p /data/nucci",
+    "mkdir -p /home/nucci",
+    "chown -R nucci:nucci /data/nucci",
+    "chown -R nucci:nucci /home/nucci",
+    "chmod 0755 /data",
+    "chmod -R 0750 /data/nucci",
+    "chmod -R 0740 /home/nucci",
+    "ln -s /data/nucci /home/nucci/data",
+):
+    assert result.exit_code == 0
+
+pathlib.Path("/etc/exports").write_text(default_exports)
+for result in run("exportfs -a"):
+    assert result.exit_code == 0
+
+systemd.restart("nfs-kernel-server")
+```
+
+???+ note "Connecting to the LDAP server on `ldap-0` using sssd"
+
+    Notice how in _nfs_provision_script.py_ when are configuring the sssd service, we are first moving a sssd.conf
+    file from `/root/.init/sssd.conf` to `/etc/sssd/sssd.conf`? You may be wondering "how did that file get there?"
+    Worry not! In the [next section](#use-cleantest-archon-to-create-nfs-0), we will be generating that sssd.conf
+    file using a Jinja template and uploading to `nfs-0` as a "provisioning resource."
+
+This script will work through the following the steps to configure the NFS server that will provide the shared
+filesystem for the mini-HPC cluster:
+
+1. Import `apt`, `systemd`, and `run` utilities for interfacing with the APT package manager and systemd on the 
+    instance, and executing shell commands respectively.
+2. Define the _exports_ file that will be used to tell the NFS kernel server which directories to export.
+3. Update the APT cache and install `nfs-kernel-server` and `sssd-ldap` for running the NFS server and connecting to
+    the LDAP server respectively.
+4. Connect to the LDAP server on `ldap-0` using sssd.
+5. Set up the test user's home and data directories.
+6. Share the `/data` and `/home` directories across the network.
+7. Restart the `nfs-kernel-service` so the new _/etc/exports_ file takes effect.
+
+### Use cleantest archon to create `nfs-0`
+
+Now with our _nfs_provision_script.py_ file created, use the following block of code to add the `nfs-0` 
+instance to our LXD test environment provider.
+
+```python
+sssd_conf = StringIO(
+    templates.get_template("sssd.conf.tmpl").render(
+        ldap_server_address=archon.get_public_address("ldap-0")
+    )
+)
+archon.add(
+    "nfs-0",
+    image="mini-hpc-sm",
+    provision_script=root / "nfs_provision_script.py",
+    resources=[File(sssd_conf, "/root/.init/sssd.conf")],
+)
+```
+
+The provision script and generated sssd.conf file will be injected into `nfs-0` after the instance becomes active.
+
+## Starting the SLURM cluster
+
+SLURM is one of the most popular open-source workload managers out there for HPC. SLURM scales very well, so we
+can use it inside the mini-HPC cluster even if SLURM is meant to used with multi-thousand node clusters.
+
+### Provision script for `slurmctld-0`
+
+Now that we have both the `ldap-0` and `nfs-0` instances created, it is time to provision to controller server
+`slurmctld-0` for the mini-HPC cluster. Create the file _slurmctld_provision_script.py_ in your current working
+directory and copy the following code block to it:
+
+```python
+#!/usr/bin/env python3
+# Copyright 2023 Jason C. Nucciarone
+# See LICENSE file for licensing details.
+
+"""Provision slurmctld nodes."""
+
+import pathlib
+import json
+from io import StringIO
+
+from cleantest.utils import apt, systemd, run
+
+# Set up SSSD service.
+apt.update()
+apt.install("slurmctld", "nfs-common", "sssd-ldap")
+for result in run(
+    "mv /root/.init/sssd.conf /etc/sssd/sssd.conf",
+    "chmod 0600 /etc/sssd/sssd.conf",
+    "pam-auth-update --enable mkhomedir",
+):
+    assert result.exit_code == 0
+
+systemd.restart("sssd")
+
+# Set up NFS mount.
+nfs_ip = json.load(StringIO(pathlib.Path("/root/.init/nfs-0").read_text()))
+for result in run(
+    f"mount {nfs_ip['nfs-0']}:/home /home",
+    "mkdir -p /data",
+    f"mount {nfs_ip['nfs-0']}:/data /data",
+):
+    assert result.exit_code == 0
+```
+
+???+ note "Mounting directories shared by `nfs-0` and connecting to LDAP server on `ldap-0`"
+
+    Notice how like when we provisioned the `nfs-0` instance, we used a "provisioning resource" to connect the
+    sssd service to the LDAP server? In the above provisioning script for `slurmctld-0`, we use the same mechanism
+    for mounting the shared directories on `nfs-0`. We will create this `/root/.init/nfs-0` resource in 
+    [the section](#use-cleantest-archon-to-create-slurmctld-0) about creating the `slurmctld-0` instance.
+
+This script will work through the following steps to configure the controller service for the mini-HPC cluster.
+We will be starting the control server manually rather than having the provision script start it for us:
+
+1. Import `apt`, `systemd`, and `run` utilities for interfacing with the APT package manager and systemd on the 
+    instance, and executing shell commands respectively.
+2. Update the APT cache and install `slurmctld`, `nfs-common`, `sssd-ldap` for running the controller server,
+    mounting the shared directories, and connecting to the LDAP server respectively.
+3. Connect to the LDAP server running on `ldap-0` using sssd.
+4. Mount the shared directories exported by the NFS server running on `nfs-0`.
+
+### Provision script for `slurmd-{0,1,2}`
+
+We can also provision the compute nodes alongside the controller server `slurmctld-0`. Create the file
+_slurmd_provision_script.py_ in your current working directory and copy the following code block to it:
+
+```python
+#!/usr/bin/env python3
+# Copyright 2023 Jason C. Nucciarone
+# See LICENSE file for licensing details.
+
+"""Provision slurmd nodes."""
+
+import json
+import pathlib
+from io import StringIO
+
+from cleantest.utils import apt, systemd, run
+
+# Set up SSSD service.
+apt.update()
+apt.install("slurmd", "nfs-common", "sssd-ldap")
+for result in run(
+    "mv /root/.init/sssd.conf /etc/sssd/sssd.conf",
+    "chmod 0600 /etc/sssd/sssd.conf",
+    "pam-auth-update --enable mkhomedir",
+):
+    assert result.exit_code == 0
+
+systemd.restart("sssd")
+
+# Set up NFS mount.
+nfs_ip = json.load(StringIO(pathlib.Path("/root/.init/nfs-0").read_text()))
+for result in run(
+    f"mount {nfs_ip['nfs-0']}:/home /home",
+    "mkdir -p /data",
+    f"mount {nfs_ip['nfs-0']}:/data /data",
+):
+    assert result.exit_code == 0
+
+# Set up munge key.
+for result in run(
+    "mv /root/.init/munge.key /etc/munge/munge.key",
+    "chown munge:munge /etc/munge/munge.key",
+    "chmod 0600 /etc/munge/munge.key",
+):
+    assert result.exit_code == 0
+
+systemd.restart("munge")
+```
+
+???+ note "Setting up MUNGE authentication service"
+
+    Similar to how `nfs-0` and `slurmctld-0` are provisioned, the slurmd instances also use "provisioning resources."
+    However, unlike those instances, the slurmd instances require a MUNGE key from the `slurmctld-0`; MUNGE is the
+    authentication service that the SLURM workload manager uses to verify nodes in SLURM cluster. 
+    [The section](#pull-munge-key-from-slurmctld-0-and-create-slurmd-012) on
+    creating the slurmd instances will show you how to pull resources from other instances.
+
+This script will work through the following steps to configure the compute service for the mini-HPC cluster.
+We will be starting the compute servers manually rather than having the provision script start it for us:
+
+1. Import `apt`, `systemd`, and `run` utilities for interfacing with the APT package manager and systemd on the 
+    instance, and executing shell commands respectively.
+2. Update the APT cache and install `slurmd`, `nfs-common`, `sssd-ldap` for running the compute server,
+    mounting the shared directories, and connecting to the LDAP server respectively.
+3. Connect to the LDAP server running on `ldap-0` using sssd.
+4. Mount the shared directories exported by the NFS server running on `nfs-0`.
+5. Set up MUNGE key pulled from `slurmctld-0` instance.
+
+### Use cleantest archon to create `slurmctld-0`
+
+Now with our _slurmctld_provision_script.py_ file created, use the following block of code to add the `slurmctld-0` 
+instance to our LXD test environment provider.
+
+```python
+nfs_ip = json.dumps({"nfs-0": str(archon.get_public_address("nfs-0"))})
+archon.add(
+    "slurmctld-0",
+    image="mini-hpc-sm",
+    provision_script=root / "slurmctld_provision_script.py",
+    resources=[
+        File(sssd_conf, "/root/.init/sssd.conf"),
+        File(StringIO(nfs_ip), "/root/.init/nfs-0"),
+    ],
+)
+```
+
+The provision script, generated sssd.conf and generated nfs-0 files will be injected into `slurmctld-0` after the
+instance becomes active.
+
+### Pull MUNGE key from `slurmctld-0` and create `slurmd-{0,1,2}`
+
+Using our other provisioning script _slurmd_provision_script.py_, use the following code block to pull the 
+_munge.key_ file from `slurmctld-0` and create the instance `slurmd-0`, `slurmd-1`, and `slurmd-2`.
+
+```python
+archon.pull(
+    "slurmctld-0", data_obj=[File("/etc/munge/munge.key", root / "munge.key")]
+)
+archon.add(
+    ["slurmd-0", "slurmd-1", "slurmd-2"],
+    image="mini-hpc-sm",
+    provision_script=root / "slurmd_provision_script.py",
+    resources=[
+        File(sssd_conf, "/root/.init/sssd.conf"),
+        File(StringIO(nfs_ip), "/root/.init/nfs-0"),
+        File(root / "munge.key", "/root/.init/munge.key"),
+    ],
+)
+```
+
+The provision script, generated sssd.conf file, generated nfs-0 file, and pulled munge.key file will be 
+injected into `slurmd-0`, `slurmd-1`, and `slurmd-2` after the instance becomes active.
+
+### Sync slurm.conf across `slurmctld-0` and `slurmd-{0,1,2}` and start SLURM services
+
+Now that `slurmctld-0`, `slurmd-0`, `slurmd-1`, and `slurmd-2` have been created, we can now use the following code
+block to start the SLURM cluster:
+
+```python
+slurm_node_info = {
+    "slurmctld_name": "slurmctld-0",
+    "slurmctld_address": archon.get_public_address("slurmctld-0"),
+    "slurmd_0_name": "slurmd-0",
+    "slurmd_0_address": archon.get_public_address("slurmd-0"),
+    "slurmd_1_name": "slurmd-1",
+    "slurmd_1_address": archon.get_public_address("slurmd-1"),
+    "slurmd_2_name": "slurmd-2",
+    "slurmd_2_address": archon.get_public_address("slurmd-2"),
+}
+slurm_conf = StringIO(
+    templates.get_template("slurm.conf.tmpl").render(**slurm_node_info)
+)
+for node in ["slurmctld-0", "slurmd-0", "slurmd-1", "slurmd-2"]:
+    archon.push(node, data_obj=[File(slurm_conf, "/etc/slurm/slurm.conf")])
+archon.execute("slurmctld-0", command="systemctl start slurmctld")
+archon.execute(
+    ["slurmd-0", "slurmd-1", "slurmd-2"], command="systemctl start slurmd"
+)
+```
+
+This code block will pull the IPv4 addresses of `slurmctld-0`, `slurmd-0`, `slurmd-1`, and `slurmd-2`,
+generate the slurm.conf from a Jinja template, push slurm.conf into all the slurm instances, and then
+start each slurm service.
+
+## Write a testlet to submit test batch job
+
+Congratulations, we finally have all the code that we need to create the mini-HPC cluster when we go to run our
+test. Now it is time to write the testlet. In the code block below, `@lxd.target("...")` is used to target a
+specific test environment instance rather than creating a unique instance for the testlet:
+
+```python3
+@lxd.target("slurmctld-0")
+def run_job():
+    import os
+    import pathlib
+    import shutil
+    import textwrap
+    from time import sleep
+
+    from cleantest.utils import run
+
+    tmp_dir = pathlib.Path("/tmp")
+    (tmp_dir / "research.submit").write_text(
+        textwrap.dedent(
+            """
+            #!/bin/bash
+            #SBATCH --job-name=research
+            #SBATCH --partition=all
+            #SBATCH --nodes=1
+            #SBATCH --ntasks-per-node=1
+            #SBATCH --cpus-per-task=1
+            #SBATCH --mem=500mb
+            #SBATCH --time=00:00:30
+            #SBATCH --error=research.err
+            #SBATCH --output=research.out
+            
+            echo "I love doing research!"
+            """
+        ).strip("\n")
+    )
+
+    # Set user to test cluster user nucci.
+    os.setuid(10000)
+    os.chdir("/home/nucci")
+    for result in run(
+        f"cp {(tmp_dir / 'research.submit')} .",
+        "sbatch research.submit",
+    ):
+        assert result.exit_code == 0
+    sleep(60)
+    shutil.copy("research.out", (tmp_dir / "result"))
+```
+
+??? warning "A warning about using cleantest with the SLURM workload manager"
+
+    One thing to note about the SLURM workload manager is that it does not let user `root` submit jobs to
+    the scheduler (for obvious reasons). Unfortunately, cleantest currently only allows for testlets to be
+    run as user `root`. To get around this limitation, we can use Python's `os` module to change to our test
+    user, but we cannot change back to user `root` after the switch. You should have your "power-user" operations
+    taken care of before switching to the test user.
+
+
+## Bringing it all together
+
+Your completed test file _test_mini_hpc.py_ should look like the following:
+
+```python
+#!/usr/bin/env python3
+
+"""Test batch job using mini-HPC cluster created with cleantest."""
+
+import json
+import os
+import pathlib
+from io import StringIO
+
+from jinja2 import Environment, FileSystemLoader
+
+from cleantest.control.hooks import StopEnvHook
+from cleantest.control.lxd import InstanceConfig
+from cleantest.data import File
+from cleantest.provider import lxd, LXDArchon
+
+root = pathlib.Path(os.path.dirname(os.path.realpath(__file__)))
+templates = Environment(loader=FileSystemLoader(root / "templates"))
+
+
+@lxd.target("slurmctld-0")
+def run_job():
+    import os
+    import pathlib
+    import shutil
+    import textwrap
+    from time import sleep
+
+    from cleantest.utils import run
+
+    tmp_dir = pathlib.Path("/tmp")
+    (tmp_dir / "research.submit").write_text(
+        textwrap.dedent(
+            """
+            #!/bin/bash
+            #SBATCH --job-name=research
+            #SBATCH --partition=all
+            #SBATCH --nodes=1
+            #SBATCH --ntasks-per-node=1
+            #SBATCH --cpus-per-task=1
+            #SBATCH --mem=500mb
+            #SBATCH --time=00:00:30
+            #SBATCH --error=research.err
+            #SBATCH --output=research.out
+            
+            echo "I love doing research!"
+            """
+        ).strip("\n")
+    )
+
+    # Set user to test cluster user nucci.
+    os.setuid(10000)
+    os.chdir("/home/nucci")
+    for result in run(
+        f"cp {(tmp_dir / 'research.submit')} .",
+        "sbatch research.submit",
+    ):
+        assert result.exit_code == 0
+    sleep(60)
+    shutil.copy("research.out", (tmp_dir / "result"))
+
+
+def test_lxd_archon_local() -> None:
+    """Test LXDArchon against local LXD cluster."""
+    archon = LXDArchon()
+    archon.config.register_hook(
+        StopEnvHook(name="get_result", download=[File("/tmp/result", root / "result")])
+    )
+    placeholder = archon.config.get_instance_config("ubuntu-jammy-amd64").dict()
+    placeholder["name"] = "mini-hpc-sm"
+    archon.config.add_instance_config(
+        InstanceConfig(
+            config={
+                "limits.cpu": "1",
+                "limits.memory": "8GB",
+                "security.privileged": "true",
+                "raw.apparmor": "mount fstype=nfs*, mount fstype=rpc_pipefs,",
+            },
+            **placeholder,
+        )
+    )
+    archon.add(
+        "ldap-0",
+        image="mini-hpc-sm",
+        provision_script=root / "ldap_provision_script.py",
+    )
+    sssd_conf = StringIO(
+        templates.get_template("sssd.conf.tmpl").render(
+            ldap_server_address=archon.get_public_address("ldap-0")
+        )
+    )
+    archon.add(
+        "nfs-0",
+        image="mini-hpc-sm",
+        provision_script=root / "nfs_provision_script.py",
+        resources=[File(sssd_conf, "/root/.init/sssd.conf")],
+    )
+    nfs_ip = json.dumps({"nfs-0": str(archon.get_public_address("nfs-0"))})
+    archon.add(
+        "slurmctld-0",
+        image="mini-hpc-sm",
+        provision_script=root / "slurmctld_provision_script.py",
+        resources=[
+            File(sssd_conf, "/root/.init/sssd.conf"),
+            File(StringIO(nfs_ip), "/root/.init/nfs-0"),
+        ],
+    )
+    archon.pull(
+        "slurmctld-0", data_obj=[File("/etc/munge/munge.key", root / "munge.key")]
+    )
+    archon.add(
+        ["slurmd-0", "slurmd-1", "slurmd-2"],
+        image="mini-hpc-sm",
+        provision_script=root / "slurmd_provision_script.py",
+        resources=[
+            File(sssd_conf, "/root/.init/sssd.conf"),
+            File(StringIO(nfs_ip), "/root/.init/nfs-0"),
+            File(root / "munge.key", "/root/.init/munge.key"),
+        ],
+    )
+    slurm_node_info = {
+        "slurmctld_name": "slurmctld-0",
+        "slurmctld_address": archon.get_public_address("slurmctld-0"),
+        "slurmd_0_name": "slurmd-0",
+        "slurmd_0_address": archon.get_public_address("slurmd-0"),
+        "slurmd_1_name": "slurmd-1",
+        "slurmd_1_address": archon.get_public_address("slurmd-1"),
+        "slurmd_2_name": "slurmd-2",
+        "slurmd_2_address": archon.get_public_address("slurmd-2"),
+    }
+    slurm_conf = StringIO(
+        templates.get_template("slurm.conf.tmpl").render(**slurm_node_info)
+    )
+    for node in ["slurmctld-0", "slurmd-0", "slurmd-1", "slurmd-2"]:
+        archon.push(node, data_obj=[File(slurm_conf, "/etc/slurm/slurm.conf")])
+    archon.execute("slurmctld-0", command="systemctl start slurmctld")
+    archon.execute(
+        ["slurmd-0", "slurmd-1", "slurmd-2"], command="systemctl start slurmd"
+    )
+    for name, result in run_job():
+        assert "I love doing research!" in pathlib.Path(root / "result").read_text()
+    (root / "munge.key").unlink(missing_ok=True)
+    (root / "result").unlink(missing_ok=True)
+    archon.execute(
+        ["slurmctld-0", "slurmd-0", "slurmd-1", "slurmd-2"],
+        command=f"umount /home /data",
+    )
+    archon.destroy()
+```
+
+Do not forget to add the bottom part here that evaluates the result of the testlet! After the evaluation, some
+basic cleanup is also performed so that the mini-HPC cluster does not continue to use your workstation's resources
+after the test has completed. Now use `pytest` to run your "clean test"!
+
+```shell
+pytest test_mini_hpc.py
+```
+
+Now sit back, relax, and wait for your test to complete! You can watch the following video to get an idea
+of what cleantest is doing "under the hood" on your system to run the test:
+
+<p align="center">
+  <iframe 
+    width="620" height="375" src="https://www.youtube.com/embed/UXQ9WHRiBfo" 
+    title="mini-hpc cluster demo video" frameborder="0" 
+    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" 
+    allowfullscreen>
+  </iframe>
+</p>
+
+
+## Where to go from here
+
+This was quite a long tutorial, so make sure to grant yourself a well-earned coffee break! 
+
+If you are interested in learning more about the underpinnings of a mini-HPC cluster, check out my 
+[intro to open-source HPC workshop](https://nuccitheboss.github.io/hpc-workshop/) that I gave at the 2022 Ubuntu 
+Summit in Prague. My workshop will take your through building your own mini-HPC cluster manually, and it takes you
+further than just setting up the infrastructure such as building containers and deploying your own software stack.
+
+If you want to sink your teeth further into cleantest, I suggest reading more of the tutorials or heading over
+onto the [User Guide](../user-guide/installation.md) page to learn about what cleantest is capable of!
diff --git a/docs/user-guide/artifacts/directory.md b/docs/user-guide/artifacts/directory.md
index 730d8bd..ec35c0e 100644
--- a/docs/user-guide/artifacts/directory.md
+++ b/docs/user-guide/artifacts/directory.md
@@ -1,3 +1,6 @@
+[//]: # "Copyright 2023 Jason C. Nucciarone" 
+[//]: # "See LICENSE file for licensing details."
+
 # Working with directories
 
 Directories can be uploaded to and downloaded from test environment instances as artifacts.
@@ -41,43 +44,43 @@ and how `Dir` behaves when accessed by them:
 
 import os
 import pathlib
-import tempfile
+import shutil
 
-from cleantest import Configure
-from cleantest.hooks import StartEnvHook, StopEnvHook
-from cleantest.hooks.data import Dir
+from cleantest.control import Configure
+from cleantest.control.hooks import StartEnvHook, StopEnvHook
+from cleantest.data import Dir
 from cleantest.provider import lxd
 
-root = os.path.dirname(os.path.realpath(__file__))
-config = Configure()
-start_hook = StartEnvHook(
-    name="upload_artifact",
-    upload=[
-        Dir(os.path.join(root, "greetings"), "/root/greetings"),
-    ],
-)
-stop_hook = StopEnvHook(
-    name="download_artifact",
-    download=[
-        Dir("/root/dump", os.path.join(tempfile.gettempdir(), "dump"), overwrite=True),
-    ],
-)
-config.register_hook(start_hook, stop_hook)
-
-
-@lxd(image="jammy-amd64", preserve=True)
+
+@lxd(image="ubuntu-jammy-amd64", preserve=False)
 def work_on_artifacts():
     import os
     import pathlib
     import sys
 
-    print(pathlib.Path("/root/dump").is_dir(), file=sys.stdout)
+    print(pathlib.Path("/root/greetings").is_dir(), file=sys.stdout)
+
     os.mkdir("/root/dump")
     pathlib.Path("/root/dump/dump_1.txt").write_text("Oh I have been dumped again!")
 
 
-class TestUploadDownload:
-    def test_upload_download(self) -> None:
-        work_on_artifacts()
-        assert pathlib.Path(tempfile.gettempdir()).joinpath("dump").is_dir() is True
+def test_upload_download(clean_slate) -> None:
+    root = pathlib.Path(os.path.dirname(os.path.realpath(__file__)))
+    config = Configure("lxd")
+    start_hook = StartEnvHook(
+        name="upload_artifact",
+        upload=[
+            Dir(root / "greetings", "/root/greetings"),
+        ],
+    )
+    stop_hook = StopEnvHook(
+        name="download_artifact",
+        download=[
+            Dir("/root/dump", root / "dump", overwrite=True,),
+        ],
+    )
+    config.register_hook(start_hook, stop_hook)
+    for name, result in work_on_artifacts():
+        assert (root / "dump").is_dir() is True
+    shutil.rmtree(root / "dump")
 ```
\ No newline at end of file
diff --git a/docs/user-guide/artifacts/file.md b/docs/user-guide/artifacts/file.md
index e3c3340..a4bfabc 100644
--- a/docs/user-guide/artifacts/file.md
+++ b/docs/user-guide/artifacts/file.md
@@ -1,3 +1,6 @@
+[//]: # "Copyright 2023 Jason C. Nucciarone"
+[//]: # "See LICENSE file for licensing details."
+
 # Working with files
 
 Files can be uploaded to and downloaded from test environment instances as artifacts.
@@ -41,41 +44,40 @@ and how `File` behaves when accessed by them:
 
 import os
 import pathlib
-import tempfile
 
-from cleantest import Configure
-from cleantest.hooks import StartEnvHook, StopEnvHook
-from cleantest.hooks.data import File
+from cleantest.control import Configure
+from cleantest.control.hooks import StartEnvHook, StopEnvHook
+from cleantest.data import File
 from cleantest.provider import lxd
 
-root = os.path.dirname(os.path.realpath(__file__))
-config = Configure()
-start_hook = StartEnvHook(
-    name="upload_artifact",
-    upload=[
-        File(os.path.join(root, "greeting.txt"), "/root/greeting.txt"),
-    ],
-)
-stop_hook = StopEnvHook(
-    name="download_artifact",
-    download=[
-        File("/root/dump.txt", os.path.join(tempfile.gettempdir(), "dump.txt"), overwrite=True),
-    ],
-)
-config.register_hook(start_hook, stop_hook)
-
-
-@lxd(image="jammy-amd64", preserve=True)
+
+@lxd(image="ubuntu-jammy-amd64", preserve=False)
 def work_on_artifacts():
     import pathlib
     import sys
 
     print(pathlib.Path("/root/greeting.txt").is_file(), file=sys.stdout)
+
     pathlib.Path("/root/dump.txt").write_text("Dumped like a sack of rocks")
 
 
-class TestUploadDownload:
-    def test_upload_download(self) -> None:
-        work_on_artifacts()
-        assert pathlib.Path(tempfile.gettempdir()).joinpath("dump.txt").is_file() is True
+def test_upload_download(clean_slate) -> None:
+    root = pathlib.Path(os.path.dirname(os.path.realpath(__file__)))
+    config = Configure("lxd")
+    start_hook = StartEnvHook(
+        name="upload_artifact",
+        upload=[
+            File(root / "greeting.txt", "/root/greeting.txt"),
+        ],
+    )
+    stop_hook = StopEnvHook(
+        name="download_artifact",
+        download=[
+            File("/root/dump.txt", root / "dump.txt", overwrite=True,),
+        ],
+    )
+    config.register_hook(start_hook, stop_hook)
+    for name, result in work_on_artifacts():
+        assert (root / "dump.txt").is_file() is True
+    (root / "dump.txt").unlink(missing_ok=True)
 ```
diff --git a/docs/user-guide/getting-started.md b/docs/user-guide/getting-started.md
index b15a78a..21fc17c 100644
--- a/docs/user-guide/getting-started.md
+++ b/docs/user-guide/getting-started.md
@@ -1,3 +1,6 @@
+[//]: # "Copyright 2023 Jason C. Nucciarone"
+[//]: # "See LICENSE file for licensing details."
+
 # Getting started with cleantest
 
 Now that you have the cleantest framework installed on your system, let's get you introduced to writing some basic 
@@ -56,8 +59,7 @@ of choice. In our case, we used __pytest__:
 ```python
 class TestSuite:
     def test_do_something(self) -> None:
-        results = do_something()
-        for name, result in results.items()
+        for name, result in do_something():
             assert result.exit_code == 0
 ```
 
@@ -89,8 +91,7 @@ def do_something():
 
 class TestSuite:
     def test_do_something(self) -> None:
-        results = do_something()
-        for name, result in results.items()
+        for name, result in do_something():
             assert result.exit_code == 0
 ```
 
diff --git a/docs/user-guide/hooks.md b/docs/user-guide/hooks.md
index af68a0d..8f01294 100644
--- a/docs/user-guide/hooks.md
+++ b/docs/user-guide/hooks.md
@@ -1,3 +1,6 @@
+[//]: # "Copyright 2023 Jason C. Nucciarone"
+[//]: # "See LICENSE file for licensing details."
+
 # Using hooks
 
 Hooks are used to run actions at various stages of the cleantest lifecycle. They can be used to configure test
@@ -27,47 +30,51 @@ Start environment hooks accept the following arguments:
 """Example usage of StartEnvHook."""
 
 import os
+import pathlib
 
-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__))
-config = Configure()
-start_hook = StartEnvHook(
-    name="test_snaps",
-    packages=[
-        Snap(
-            snaps="pypi-server",
-            connections=[
-                Connection(Plug("pypi-server", "removable-media"), Slot(name="removable-media"))
-            ],
-        ),
-        Snap(local_snaps=os.path.join(root, "hello-world-gtk_0.1_amd64.snap"), dangerous=True),
-    ],
-)
-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
 
     if which("pypi-server") is None:
         sys.exit(1)
-    elif which("hello-world-gtk") is None:
+    elif which("marktext") is None:
         sys.exit(1)
     else:
         sys.exit(0)
 
 
-class TestLocalLXD:
-    def test_snap_package(self) -> None:
-        results = functional_snaps()
-        for name, result in results.items():
-            assert result.exit_code == 0
+def test_snap_package(clean_slate) -> None:
+    root = pathlib.Path(os.path.dirname(os.path.realpath(__file__)))
+    config = Configure("lxd")
+    start_hook = StartEnvHook(
+        name="test_snaps",
+        packages=[
+            Snap(
+                snaps="pypi-server",
+                connections=[
+                    Connection(
+                        Plug("pypi-server", "removable-media"),
+                        Slot(name="removable-media"),
+                    )
+                ],
+            ),
+            Snap(
+                local_snaps=root / "marktext.snap",
+                dangerous=True,
+            ),
+        ],
+    )
+    config.register_hook(start_hook)
+    for name, result in functional_snaps():
+        assert result.exit_code == 0
 ```
 
 ## StopEnvHook
@@ -91,22 +98,13 @@ Stop environment hooks accept the following arguments:
 
 import os
 import pathlib
-import tempfile
+import shutil
 
-from cleantest import Configure
-from cleantest.hooks import StopEnvHook
-from cleantest.hooks.data import Dir, File
+from cleantest.control import Configure
+from cleantest.control.hooks import StopEnvHook
+from cleantest.data import Dir, File
 from cleantest.provider import lxd
 
-config = Configure()
-stop_hook = StopEnvHook(
-    name="download_artifact",
-    download=[
-        File("/root/dump.txt", os.path.join(tempfile.gettempdir(), "dump.txt"), overwrite=True),
-        Dir("/root/dump", os.path.join(tempfile.gettempdir(), "dump"), overwrite=True),
-    ],
-)
-config.register_hook(stop_hook)
 
 @lxd(image="jammy-amd64", preserve=True)
 def work_on_artifacts():
@@ -118,9 +116,20 @@ def work_on_artifacts():
     pathlib.Path("/root/dump/dump_1.txt").write_text("Oh I have been dumped again!")
 
 
-class TestUploadDownload:
-    def test_upload_download(self) -> None:
-        work_on_artifacts()
-        assert pathlib.Path(tempfile.gettempdir()).joinpath("dump.txt").is_file() is True
-        assert pathlib.Path(tempfile.gettempdir()).joinpath("dump").is_dir() is True
+def test_download() -> None:
+    root = pathlib.Path(os.path.dirname(os.path.realpath(__file__)))
+    config = Configure("lxd")
+    stop_hook = StopEnvHook(
+        name="download_artifact",
+        download=[
+            File("/root/dump.txt", root / "dump.txt", overwrite=True,),
+            Dir("/root/dump", root / "dump", overwrite=True,),
+        ],
+    )
+    config.register_hook(stop_hook)
+    for name, result in work_on_artifacts():
+        assert (root / "dump.txt").is_file() is True
+        assert (root / "dump").is_dir() is True
+    (root / "dump.txt").unlink(missing_ok=True)
+    shutil.rmtree(root / "dump")
 ```
\ No newline at end of file
diff --git a/docs/user-guide/installation.md b/docs/user-guide/installation.md
index 2cd0bde..3e21ea0 100644
--- a/docs/user-guide/installation.md
+++ b/docs/user-guide/installation.md
@@ -1,3 +1,6 @@
+[//]: # "Copyright 2023 Jason C. Nucciarone"
+[//]: # "See LICENSE file for licensing details."
+
 # Installation
 
 ## Install cleantest
diff --git a/docs/user-guide/packages/charmlib.md b/docs/user-guide/packages/charmlib.md
index 3abaa82..af4f56c 100644
--- a/docs/user-guide/packages/charmlib.md
+++ b/docs/user-guide/packages/charmlib.md
@@ -1,3 +1,6 @@
+[//]: # "Copyright 2023 Jason C. Nucciarone"
+[//]: # "See LICENSE file for licensing details."
+
 # Working with charm libraries
 
 Charm libraries are special Python modules used in charmed operators deployed by [__Juju__](https://juju.is). They
@@ -42,27 +45,15 @@ test script below:
 """Example usage of Charmlib package metaclass."""
 
 import os
+import pathlib
 
-from cleantest import Configure
+from cleantest.control import Configure
 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__))
-config = Configure()
-start_hook = StartEnvHook(
-    name="charmlib_start_hook",
-    packages=[
-        Charmlib(
-            auth_token_path=os.path.join(root, "charmhub.secret"),
-            charmlibs=["charms.operator_libs_linux.v0.apt"],
-        ),
-    ],
-)
-config.register_hook(start_hook)
-
-
-@lxd(image="jammy-amd64", preserve=False)
+
+@lxd(image="ubuntu-jammy-amd64", preserve=False)
 def install_snapd():
     import sys
 
@@ -89,9 +80,19 @@ def install_snapd():
     sys.exit(0)
 
 
-class TestCharmlib:
-    def test_charmlib(self) -> None:
-        results = install_snapd()
-        for name, result in results.items():
-            assert result.exit_code == 0
+def test_local_lxd(clean_slate) -> None:
+    root = pathlib.Path(os.path.dirname(os.path.realpath(__file__)))
+    config = Configure("lxd")
+    start_hook = StartEnvHook(
+        name="setup_deps",
+        packages=[
+            Charmlib(
+                auth_token_path=root / "charmhub.secret",
+                charmlibs=["charms.operator_libs_linux.v0.apt"],
+            ),
+        ],
+    )
+    config.register_hook(start_hook)
+    for name, result in install_snapd():
+        assert result.exit_code == 0
 ```
\ No newline at end of file
diff --git a/docs/user-guide/packages/pip.md b/docs/user-guide/packages/pip.md
index bd2b50e..f735e49 100644
--- a/docs/user-guide/packages/pip.md
+++ b/docs/user-guide/packages/pip.md
@@ -1,3 +1,6 @@
+[//]: # "Copyright 2023 Jason C. Nucciarone"
+[//]: # "See LICENSE file for licensing details."
+
 # Working with pip packages
 
 Pip is the package installer for the Python programming language. You can use it to install packages from the
@@ -30,23 +33,17 @@ accepts three arguments:
 
 """Example usage of Pip package metaclass."""
 
-from cleantest import Configure
-from cleantest.hooks import StartEnvHook
-from cleantest.pkg import Pip
-from cleantest.provider import lxd
+import os
+import pathlib
 
-config = Configure()
-start_hook = StartEnvHook(
-    name="install_tabulate",
-    packages=[
-        Pip(packages=["tabulate"]),
-    ],
-)
-config.register_hook(start_hook)
+from cleantest.control import Configure
+from cleantest.control.hooks import StartEnvHook
+from cleantest.data.pkg import Pip
+from cleantest.provider import lxd
 
 
-@lxd(image="jammy-amd64", preserve=False)
-def install_tabulate():
+@lxd(image="ubuntu-jammy-amd64", preserve=False)
+def install_snapd():
     import sys
 
     try:
@@ -60,9 +57,16 @@ def install_tabulate():
     sys.exit(0)
 
 
-class TestLocalLXD:
-    def test_local_lxd(self) -> None:
-        results = install_tabulate()
-        for name, result in results.items():
-            assert result.exit_code == 0
+def test_local_lxd(clean_slate) -> None:
+    root = pathlib.Path(os.path.dirname(os.path.realpath(__file__)))
+    config = Configure("lxd")
+    start_hook = StartEnvHook(
+        name="setup_deps",
+        packages=[
+            Pip(requirements=[root / "requirements.txt"]),
+        ],
+    )
+    config.register_hook(start_hook)
+    for name, result in install_snapd():
+        assert result.exit_code == 0
 ```
\ No newline at end of file
diff --git a/docs/user-guide/packages/snaps.md b/docs/user-guide/packages/snaps.md
index 7774b77..c5f7d36 100644
--- a/docs/user-guide/packages/snaps.md
+++ b/docs/user-guide/packages/snaps.md
@@ -1,3 +1,6 @@
+[//]: # "Copyright 2023 Jason C. Nucciarone"
+[//]: # "See LICENSE file for licensing details."
+
 # Working with snap packages
 
 Snap packages are a bundle of an app and its dependencies that work across a variety of Linux distributions
@@ -104,45 +107,49 @@ when the class goes to install the listed snap packages.
 """Example usage of Snap package metaclass with related classes."""
 
 import os
+import pathlib
 
-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__))
-config = Configure()
-start_hook = StartEnvHook(
-    name="test_snaps",
-    packages=[
-        Snap(
-            snaps="pypi-server",
-            connections=[
-                Connection(Plug("pypi-server", "removable-media"), Slot(name="removable-media"))
-            ],
-        ),
-        Snap(local_snaps=os.path.join(root, "hello-world-gtk_0.1_amd64.snap"), dangerous=True),
-    ],
-)
-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
 
     if which("pypi-server") is None:
         sys.exit(1)
-    elif which("hello-world-gtk") is None:
+    elif which("marktext") is None:
         sys.exit(1)
     else:
         sys.exit(0)
 
 
-class TestLocalLXD:
-    def test_snap_package(self) -> None:
-        results = functional_snaps()
-        for name, result in results.items():
-            assert result.exit_code == 0
+def test_snap_package(clean_slate) -> None:
+    root = pathlib.Path(os.path.dirname(os.path.realpath(__file__)))
+    config = Configure("lxd")
+    start_hook = StartEnvHook(
+        name="test_snaps",
+        packages=[
+            Snap(
+                snaps="pypi-server",
+                connections=[
+                    Connection(
+                        Plug("pypi-server", "removable-media"),
+                        Slot(name="removable-media"),
+                    )
+                ],
+            ),
+            Snap(
+                local_snaps=[root / "marktext.snap"],
+                dangerous=True,
+            ),
+        ],
+    )
+    config.register_hook(start_hook)
+    for name, result in functional_snaps():
+        assert result.exit_code == 0
 ```
\ No newline at end of file
diff --git a/docs/user-guide/parallelization.md b/docs/user-guide/parallelization.md
index 21a16dd..3bbbb3a 100644
--- a/docs/user-guide/parallelization.md
+++ b/docs/user-guide/parallelization.md
@@ -1,3 +1,6 @@
+[//]: # "Copyright 2023 Jason C. Nucciarone"
+[//]: # "See LICENSE file for licensing details."
+
 # Running tests in parallel
 
 cleantest has the ability to run test environment instances in parallel to increase the efficiency of test runs.
@@ -47,23 +50,17 @@ you need to use one of Python's many parallel computing libraries, or third-part
 
 """Parallel testing example using LXD as test environment provider."""
 
-import os
-
-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
 
-config = Configure()
-start_hook = StartEnvHook(name="pip_injection", packages=[Pip(packages="tabulate")])
-config.register_hook(start_hook)
-
 
 @lxd(
-    image=["jammy-amd64", "focal-amd64", "bionic-amd64"],
+    image=["ubuntu-jammy-amd64", "ubuntu-focal-amd64", "ubuntu-bionic-amd64"],
     preserve=False,
     parallel=True,
-    num_threads=os.cpu_count(),
+    num_threads=3,
 )
 def install_tabulate():
     import sys
@@ -79,12 +76,13 @@ def install_tabulate():
     sys.exit(0)
 
 
-class TestParallelLXD:
-    def test_parallel_lxd(self) -> None:
-        results = install_tabulate()
-        for name, result in results.items():
-            try:
-                assert result.exit_code == 0
-            except AssertionError:
-                raise Exception(f"{name} failed. Result: {result}")
+def test_parallel_lxd(clean_slate) -> None:
+    config = Configure("lxd")
+    start_hook = StartEnvHook(name="pip_injection", packages=[Pip(packages="tabulate")])
+    config.register_hook(start_hook)
+    for name, result in install_tabulate():
+        try:
+            assert result.exit_code == 0
+        except AssertionError:
+            raise Exception(f"{name} failed. Result: {result}")
 ```
\ No newline at end of file
diff --git a/docs/user-guide/using-diff-linux-distro.md b/docs/user-guide/using-diff-linux-distro.md
index 1a88653..5dc6664 100644
--- a/docs/user-guide/using-diff-linux-distro.md
+++ b/docs/user-guide/using-diff-linux-distro.md
@@ -1,3 +1,6 @@
+[//]: # "Copyright 2023 Jason C. Nucciarone"
+[//]: # "See LICENSE file for licensing details."
+
 # Using different Linux distributions
 
 !!! example "This page is currently under construction"
diff --git a/mkdocs.yml b/mkdocs.yml
index 31e0af1..3510eec 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -1,3 +1,6 @@
+# Copyright 2023 Jason C. Nucciarone
+# See LICENSE file for licensing details.
+
 site_name: cleantest
 site_url: https://nuccitheboss.github.io/cleantest/
 site_description: |
@@ -46,6 +49,8 @@ nav:
         - 'Files': 'user-guide/artifacts/file.md'
     - 'Using different Linux distributions': 'user-guide/using-diff-linux-distro.md'
     - 'Parallel testing': 'user-guide/parallelization.md'
-  - Examples: 'examples/coming-soon.md'
-  - About:
-    - 'License': 'about/license.md'
+  - Tutorials:
+      - 'Using a mini-HPC cluster to test batch jobs': 'tutorials/using-a-mini-hpc-cluster.md'
+  - Reference: 'reference/coming-soon.md'
+  - News: 'news/news.md'
+  - Community: 'https://github.com/NucciTheBoss/cleantest/discussions'
diff --git a/setup.py b/setup.py
index 7b5827d..4029a33 100644
--- a/setup.py
+++ b/setup.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.
 
 import pathlib
@@ -12,7 +12,7 @@
 
 setup(
     name="cleantest",
-    version="0.3.0",
+    version="0.4.0-rc1",
     description="Clean tests for developers in a hurry",
     long_description=long_description,
     long_description_content_type="text/markdown",


Test patch:
diff --git a/docs/user-guide/test-env-providers.md b/docs/user-guide/test-env-providers.md
index 2026ec2..90120d3 100644
--- a/docs/user-guide/test-env-providers.md
+++ b/docs/user-guide/test-env-providers.md
@@ -1,3 +1,6 @@
+[//]: # "Copyright 2023 Jason C. Nucciarone"
+[//]: # "See LICENSE file for licensing details."
+
 # Test environment providers
 
 Test environment providers are the backbone of cleantest; they provide the containers or virtual machines that
@@ -12,15 +15,11 @@ The following is a list of all the supported test environment providers in clean
 The `lxd` decorator handles running testlets inside of containers and/or virtual machines controlled by the LXD
 hypervisor. The decorator accepts the following arguments:
 
-* `name (str)`: Prefix to use in name of test environment instances. Defaults to _"test"_.
-* `image (List[str])`: List of LXD images to use for created test environments. Defaults to _["jammy-amd64"]_.
-* `preserve (bool)`: Preserve test environment after testlet has completed. Defaults to _True_.
-* `env (EnvDataStore)`: Environment to use in test environment. Defaults to base instance of _EnvDataStore_.
-* `data (LXDDataStore)`: Data necessary for LXD hypervisor. Defaults to base instance of _LXDDataStore_.
-* `image_config (List[Dict[str, Any]])`: List of image configurations to add to _LXDDataStore_.
-* `client_config (LXDClientConfig)`: Configuration to use for LXD client. Defaults to _None_.
-* `parallel (bool)`: Run test environments in parallel. Defaults to `False`.
-* `num_threads (int)`: Number of threads to use when running test environment in parallel. Defaults to _None_.
+* `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).
+* `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).
 
 ### Example usage
 
@@ -30,30 +29,15 @@ hypervisor. The decorator accepts the following arguments:
 """Example usage of LXD test environment provider."""
 
 import os
+import pathlib
 
-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__))
-config = Configure()
-start_hook = StartEnvHook(
-    name="my_start_hook",
-    packages=[
-        Charmlib(
-            auth_token_path=os.path.join(root, "charmhub.secret"),
-            charmlibs=["charms.operator_libs_linux.v0.apt"],
-        ),
-        Pip(requirements=os.path.join(root, "requirements.txt")),
-    ],
-)
-config.register_hook(start_hook)
-
-
-# Define the testlets.
-@lxd(image="jammy-amd64", preserve=False)
+
+@lxd(image="ubuntu-jammy-amd64", preserve=False)
 def install_snapd():
     import sys
 
@@ -87,10 +71,21 @@ def install_snapd():
 
     sys.exit(0)
 
-    
-class TestLocalLXD:
-    def test_local_lxd(self) -> None:
-        results = install_snapd()
-        for name, result in results.items():
-            assert result.exit_code == 0
+
+def test_local_lxd(clean_slate) -> None:
+    root = pathlib.Path(os.path.dirname(os.path.realpath(__file__)))
+    config = Configure("lxd")
+    start_hook = StartEnvHook(
+        name="setup_deps",
+        packages=[
+            Charmlib(
+                auth_token_path=root / "charmhub.secret",
+                charmlibs=["charms.operator_libs_linux.v0.apt"],
+            ),
+            Pip(requirements=[root / "requirements.txt"]),
+        ],
+    )
+    config.register_hook(start_hook)
+    for name, result in install_snapd():
+        assert result.exit_code == 0
 ```
diff --git a/src/cleantest/__init__.py b/src/cleantest/__init__.py
index bf98b47..846b83a 100644
--- a/src/cleantest/__init__.py
+++ b/src/cleantest/__init__.py
@@ -1,3 +1,3 @@
 #!/usr/bin/env python3
-# Copyright 2022 Canonical Ltd.
+# Copyright 2023 Jason C. Nucciarone
 # See LICENSE file for licensing details.
diff --git a/src/cleantest/control/__init__.py b/src/cleantest/control/__init__.py
index e50b0c2..33925ca 100644
--- a/src/cleantest/control/__init__.py
+++ b/src/cleantest/control/__init__.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright 2023 Jason C. Nucciarone, Canonical Ltd.
+# Copyright 2023 Jason C. Nucciarone
 # See LICENSE file for licensing details.
 
 from .configure import Configure
diff --git a/src/cleantest/control/hooks/startenv.py b/src/cleantest/control/hooks/startenv.py
index 6aa9d15..5ba4731 100644
--- a/src/cleantest/control/hooks/startenv.py
+++ b/src/cleantest/control/hooks/startenv.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.
 
 """Hook run when test environment first starts."""
diff --git a/src/cleantest/control/hooks/stopenv.py b/src/cleantest/control/hooks/stopenv.py
index 9a2d106..eec578d 100644
--- a/src/cleantest/control/hooks/stopenv.py
+++ b/src/cleantest/control/hooks/stopenv.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright 2022 Canonical Ltd.
+# Copyright 2023 Jason C. Nucciarone
 # See LICENSE file for licensing details.
 
 """Hook run before test environment stops."""
diff --git a/src/cleantest/data/__init__.py b/src/cleantest/data/__init__.py
index d20cb62..1088143 100644
--- a/src/cleantest/data/__init__.py
+++ b/src/cleantest/data/__init__.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright 2023 Jason C. Nucciarone, Canonical Ltd.
+# Copyright 2023 Jason C. Nucciarone
 # See LICENSE file for licensing details.
 
 from .directory import Dir
diff --git a/src/cleantest/data/pkg/charmlib.py b/src/cleantest/data/pkg/charmlib.py
index 9f30aca..da56cc5 100644
--- a/src/cleantest/data/pkg/charmlib.py
+++ b/src/cleantest/data/pkg/charmlib.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright 2023 Jason C. Nucciarone, Canonical Ltd.
+# Copyright 2023 Jason C. Nucciarone
 # See LICENSE file for licensing details.
 
 """Manager for installing charm libraries inside remote processes."""
diff --git a/src/cleantest/data/pkg/pip.py b/src/cleantest/data/pkg/pip.py
index 3b86899..7568813 100644
--- a/src/cleantest/data/pkg/pip.py
+++ b/src/cleantest/data/pkg/pip.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright 2023 Jason C. Nucciarone, Canonical Ltd.
+# Copyright 2023 Jason C. Nucciarone
 # See LICENSE file for licensing details.
 
 """Manager for installing pip packages inside remote processes."""
diff --git a/src/cleantest/data/pkg/snap.py b/src/cleantest/data/pkg/snap.py
index 4dfc547..a396200 100644
--- a/src/cleantest/data/pkg/snap.py
+++ b/src/cleantest/data/pkg/snap.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright 2023 Jason C. Nucciarone, Canonical Ltd.
+# Copyright 2023 Jason C. Nucciarone
 # See LICENSE file for licensing details.
 
 """Manager for installing snap packages inside remote processes."""
diff --git a/src/cleantest/meta/base_package.py b/src/cleantest/meta/base_package.py
index bae534c..2043dcb 100644
--- a/src/cleantest/meta/base_package.py
+++ b/src/cleantest/meta/base_package.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.
 
 """Metaclass for objects that handle installing packages inside test environments."""
@@ -12,8 +12,6 @@
 class BasePackageError(Exception):
     """Base error for package handlers."""
 
-    ...
-
 
 class BasePackage(Injectable):
     """Metaclass for package handlers.
@@ -24,9 +22,7 @@ class BasePackage(Injectable):
     @abstractmethod
     def _run(self) -> None:
         """Run installer for package."""
-        ...
 
     @abstractmethod
     def _setup(self) -> None:
         """Perform setup needed inside test environment to run installer."""
-        ...
diff --git a/src/cleantest/meta/utils/detect_os.py b/src/cleantest/meta/utils/detect_os.py
index cd5d441..9562e9a 100644
--- a/src/cleantest/meta/utils/detect_os.py
+++ b/src/cleantest/meta/utils/detect_os.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.
 
 """Detect operating system of test environment."""
@@ -10,8 +10,6 @@
 class UnknownOSError(Exception):
     """Raised when an unknown operating system is detected."""
 
-    ...
-
 
 def detect_os_variant() -> str:
     """Detect the operating system of a test environment.
diff --git a/src/cleantest/utils/apt.py b/src/cleantest/utils/apt.py
index 0e4f425..5c83807 100644
--- a/src/cleantest/utils/apt.py
+++ b/src/cleantest/utils/apt.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright 2023 Jason C. Nucciarone, Canonical Ltd.
+# Copyright 2023 Jason C. Nucciarone
 # See LICENSE file for licensing details.
 
 """Handler for installing and managing Debian packages inside test environment instances."""
diff --git a/src/cleantest/utils/snap.py b/src/cleantest/utils/snap.py
index a1311b5..8ff0f44 100644
--- a/src/cleantest/utils/snap.py
+++ b/src/cleantest/utils/snap.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright 2023 Jason C. Nucciarone, Canonical Ltd.
+# Copyright 2023 Jason C. Nucciarone
 # See LICENSE file for licensing details.
 
 """Handler for installing and managing snap packages inside a remote instance."""
diff --git a/tests/functional/files/test_upload_download.py b/tests/functional/files/test_upload_download.py
index 1edc8e0..61f0723 100644
--- a/tests/functional/files/test_upload_download.py
+++ b/tests/functional/files/test_upload_download.py
@@ -6,7 +6,7 @@
 
 import os
 import pathlib
-import tempfile
+import shutil
 
 from cleantest.control import Configure
 from cleantest.control.hooks import StartEnvHook, StopEnvHook
@@ -29,13 +29,13 @@ def work_on_artifacts():
 
 
 def test_upload_download(clean_slate) -> None:
-    root = os.path.dirname(os.path.realpath(__file__))
+    root = pathlib.Path(os.path.dirname(os.path.realpath(__file__)))
     config = Configure("lxd")
     start_hook = StartEnvHook(
         name="upload_artifact",
         upload=[
-            File(os.path.join(root, "greeting.txt"), "/root/greeting.txt"),
-            Dir(os.path.join(root, "greetings"), "/root/greetings"),
+            File(root / "greeting.txt", "/root/greeting.txt"),
+            Dir(root / "greetings", "/root/greetings"),
         ],
     )
     stop_hook = StopEnvHook(
@@ -43,19 +43,19 @@ def test_upload_download(clean_slate) -> None:
         download=[
             File(
                 "/root/dump.txt",
-                os.path.join(tempfile.gettempdir(), "dump.txt"),
+                root / "dump.txt",
                 overwrite=True,
             ),
             Dir(
                 "/root/dump",
-                os.path.join(tempfile.gettempdir(), "dump"),
+                root / "dump",
                 overwrite=True,
             ),
         ],
     )
     config.register_hook(start_hook, stop_hook)
     for name, result in work_on_artifacts():
-        assert (
-            pathlib.Path(tempfile.gettempdir()).joinpath("dump.txt").is_file() is True
-        )
-        assert pathlib.Path(tempfile.gettempdir()).joinpath("dump").is_dir() is True
+        assert (root / "dump.txt").is_file() is True
+        assert (root / "dump").is_dir() is True
+    (root / "dump.txt").unlink(missing_ok=True)
+    shutil.rmtree(root / "dump")
diff --git a/tests/functional/lxd/archon/test_archon.py b/tests/functional/lxd/archon/test_archon.py
index 80c6a40..425cbd9 100644
--- a/tests/functional/lxd/archon/test_archon.py
+++ b/tests/functional/lxd/archon/test_archon.py
@@ -68,8 +68,8 @@ def test_lxd_archon_local() -> None:
     archon.config.register_hook(
         StopEnvHook(name="get_result", download=[File("/tmp/result", root / "result")])
     )
-    _ = archon.config.get_instance_config("ubuntu-jammy-amd64").dict()
-    _["name"] = "mini-hpc-sm"
+    placeholder = archon.config.get_instance_config("ubuntu-jammy-amd64").dict()
+    placeholder["name"] = "mini-hpc-sm"
     archon.config.add_instance_config(
         InstanceConfig(
             config={
@@ -78,7 +78,7 @@ def test_lxd_archon_local() -> None:
                 "security.privileged": "true",
                 "raw.apparmor": "mount fstype=nfs*, mount fstype=rpc_pipefs,",
             },
-            **_,
+            **placeholder,
         )
     )
     archon.add(
diff --git a/tests/functional/lxd/local/test_lxd_local.py b/tests/functional/lxd/local/test_lxd_local.py
index e6c2e16..5158055 100644
--- a/tests/functional/lxd/local/test_lxd_local.py
+++ b/tests/functional/lxd/local/test_lxd_local.py
@@ -5,6 +5,7 @@
 """Test LXD environment capabilities using local LXD cluster."""
 
 import os
+import pathlib
 
 from cleantest.control import Configure
 from cleantest.control.hooks import StartEnvHook
@@ -48,16 +49,16 @@ def install_snapd():
 
 
 def test_local_lxd(clean_slate) -> None:
-    root = os.path.dirname(os.path.realpath(__file__))
+    root = pathlib.Path(os.path.dirname(os.path.realpath(__file__)))
     config = Configure("lxd")
     start_hook = StartEnvHook(
         name="setup_deps",
         packages=[
             Charmlib(
-                auth_token_path=os.path.join(root, "charmhub.secret"),
+                auth_token_path=root / "charmhub.secret",
                 charmlibs=["charms.operator_libs_linux.v0.apt"],
             ),
-            Pip(requirements=os.path.join(root, "requirements.txt")),
+            Pip(requirements=[root / "requirements.txt"]),
         ],
     )
     config.register_hook(start_hook)
diff --git a/tests/functional/lxd/parallel/test_lxd_parallel.py b/tests/functional/lxd/parallel/test_lxd_parallel.py
index 8421b6d..7dad1c6 100644
--- a/tests/functional/lxd/parallel/test_lxd_parallel.py
+++ b/tests/functional/lxd/parallel/test_lxd_parallel.py
@@ -14,7 +14,7 @@
     image=["ubuntu-jammy-amd64", "ubuntu-focal-amd64", "ubuntu-bionic-amd64"],
     preserve=False,
     parallel=True,
-    num_threads=2,
+    num_threads=3,
 )
 def install_tabulate():
     import sys
diff --git a/tests/functional/packages/snap/test_snap.py b/tests/functional/packages/snap/test_snap.py
index cb0f898..2a24e6e 100644
--- a/tests/functional/packages/snap/test_snap.py
+++ b/tests/functional/packages/snap/test_snap.py
@@ -5,6 +5,7 @@
 """Test that cleantest can successfully install and operate snap packages."""
 
 import os
+import pathlib
 
 from cleantest.control import Configure
 from cleantest.control.hooks import StartEnvHook
@@ -26,7 +27,7 @@ def functional_snaps():
 
 
 def test_snap_package(clean_slate) -> None:
-    root = os.path.dirname(os.path.realpath(__file__))
+    root = pathlib.Path(os.path.dirname(os.path.realpath(__file__)))
     config = Configure("lxd")
     start_hook = StartEnvHook(
         name="test_snaps",
@@ -41,7 +42,7 @@ def test_snap_package(clean_slate) -> None:
                 ],
             ),
             Snap(
-                local_snaps=os.path.join(root, "marktext.snap"),
+                local_snaps=[root / "marktext.snap"],
                 dangerous=True,
             ),
         ],