feat: Adds playbook client

This commit is contained in:
2024-12-13 11:27:43 -05:00
parent b557a60fa3
commit d1b3379815
42 changed files with 214 additions and 1115 deletions

1
.gitignore vendored
View File

@@ -9,3 +9,4 @@ DerivedData/
.nvim/*
.swiftpm/*
./hpa.toml
./Version.*

View File

@@ -18,6 +18,13 @@
"identifier" : "CliClientTests",
"name" : "CliClientTests"
}
},
{
"target" : {
"containerPath" : "container:",
"identifier" : "ConfigurationClientTests",
"name" : "ConfigurationClientTests"
}
}
],
"version" : 1

View File

@@ -10,7 +10,8 @@ let package = Package(
.library(name: "CliClient", targets: ["CliClient"]),
.library(name: "CodersClient", targets: ["CodersClient"]),
.library(name: "ConfigurationClient", targets: ["ConfigurationClient"]),
.library(name: "FileClient", targets: ["FileClient"])
.library(name: "FileClient", targets: ["FileClient"]),
.library(name: "PlaybookClient", targets: ["PlaybookClient"])
],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"),
@@ -41,13 +42,10 @@ let package = Package(
dependencies: [
"CodersClient",
"ConfigurationClient",
"PlaybookClient",
.product(name: "Dependencies", package: "swift-dependencies"),
.product(name: "DependenciesMacros", package: "swift-dependencies"),
.product(name: "ShellClient", package: "swift-shell-client")
],
resources: [
.copy("Resources/ansible-hpa-playbook"),
.copy("Resources/Brewfile")
]
),
.testTarget(
@@ -98,6 +96,20 @@ let package = Package(
.product(name: "Dependencies", package: "swift-dependencies"),
.product(name: "DependenciesMacros", package: "swift-dependencies")
]
),
.target(
name: "PlaybookClient",
dependencies: [
"ConfigurationClient",
"FileClient",
.product(name: "Dependencies", package: "swift-dependencies"),
.product(name: "DependenciesMacros", package: "swift-dependencies"),
.product(name: "ShellClient", package: "swift-shell-client")
]
),
.testTarget(
name: "PlaybookClientTests",
dependencies: ["PlaybookClient"]
)
]
)

View File

@@ -3,6 +3,7 @@ import Dependencies
import DependenciesMacros
import FileClient
import Foundation
import PlaybookClient
import ShellClient
public extension CliClient {
@@ -28,13 +29,13 @@ public extension CliClient {
shell: String? = nil,
extraArgs: [String]? = nil
) async throws {
guard let url = Bundle.module.url(forResource: "Brewfile", withExtension: nil) else {
throw CliClientError.brewfileNotFound
}
@Dependency(\.playbookClient) var playbookClient
@Dependency(\.configurationClient) var configurationClient
var arguments = [
"brew", "bundle",
"--file", url.cleanFilePath
]
"brew", "install"
] + Constants.brewPackages
if let extraArgs {
arguments.append(contentsOf: extraArgs)
}
@@ -44,6 +45,9 @@ public extension CliClient {
shell: shell.orDefault,
arguments
)
let configuration = try await configurationClient.findAndLoad()
try await playbookClient.installPlaybook(configuration)
}
func runPlaybookCommand(
@@ -53,11 +57,12 @@ public extension CliClient {
try await withLogger(loggingOptions) {
@Dependency(\.configurationClient) var configurationClient
@Dependency(\.logger) var logger
@Dependency(\.playbookClient) var playbookClient
let configuration = try await configurationClient.ensuredConfiguration(options.configuration)
logger.trace("Configuration: \(configuration)")
let playbookDirectory = try configuration.ensuredPlaybookDirectory(options.playbookDirectory)
let playbookDirectory = try await playbookClient.playbookDirectory(configuration)
let playbookPath = "\(playbookDirectory)/\(Constants.playbookFileName)"
logger.trace("Playbook path: \(playbookPath)")
@@ -144,30 +149,30 @@ public extension ConfigurationClient {
}
}
@_spi(Internal)
public extension Configuration {
// @_spi(Internal)
// public extension Configuration {
func defaultPlaybookDirectory() throws -> String {
let playbookDirectory = Bundle.module.url(
forResource: Constants.playbookBundleDirectoryName,
withExtension: nil
)
guard let playbookDirectory else {
throw CliClientError.playbookDirectoryNotFound
}
return playbookDirectory.cleanFilePath
}
// func defaultPlaybookDirectory() throws -> String {
// let playbookDirectory = Bundle.module.url(
// forResource: Constants.playbookBundleDirectoryName,
// withExtension: nil
// )
// guard let playbookDirectory else {
// throw CliClientError.playbookDirectoryNotFound
// }
// return playbookDirectory.cleanFilePath
// }
func ensuredPlaybookDirectory(_ optionalDirectory: String?) throws -> String {
guard let directory = optionalDirectory else {
guard let directory = playbook?.directory else {
return try defaultPlaybookDirectory()
}
return directory
}
return directory
}
}
// func ensuredPlaybookDirectory(_ optionalDirectory: String?) throws -> String {
// guard let directory = optionalDirectory else {
// guard let directory = playbook?.directory else {
// return try defaultPlaybookDirectory()
// }
// return directory
// }
// return directory
// }
// }
@_spi(Internal)
public extension Optional where Wrapped == String {

View File

@@ -5,4 +5,7 @@ enum Constants {
static let playbookFileName = "main.yml"
static let inventoryFileName = "inventory.ini"
static let vaultCommand = "ansible-vault"
static let brewPackages = [
"ansible", "imagemagick", "pandoc", "texLive"
]
}

View File

@@ -1,4 +0,0 @@
brew "ansible"
brew "imagemagick"
brew "pandoc"
brew "texlive"

View File

@@ -1,13 +0,0 @@
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false

View File

@@ -1,20 +0,0 @@
The MIT License (MIT)
Copyright (c) 2024 Michael Housh
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1 +0,0 @@
# ansible-hpa-playbook

View File

@@ -1,4 +0,0 @@
[defaults]
inventory = ./inventory.ini
roles_path = ./roles
interpreter_python = auto_silent

View File

@@ -1,3 +0,0 @@
[local]
127.0.0.1 ansible_connection=local

View File

@@ -1,36 +0,0 @@
[private]
default:
just --list
# Run the playbook with the passed in arguments.
[group('plays')]
run *ARGS:
@ansible-playbook ./main.yml \
--inventory ./inventory.ini \
{{ARGS}}
# Run the repo-template option in the `dir` with the passed in arguements.
[group('plays')]
create-repo-template dir *ARGS:
@just run \
--tags repo-template \
--extra-vars output_dir={{dir}} \
{{ARGS}}
# Run the build-project option in the `dir` with the passed in arguements.
[group('plays')]
build-project dir *ARGS:
@just run \
--tags build-project \
--extra-vars project_dir={{dir}} \
{{ARGS}}
# Setup a new consult project from a template repo.
[group('plays')]
setup-project repo-url version project-dir *ARGS:
@ansible-playbook ./main.yml \
--inventory ./inventory.ini \
--tags setup-project \
--extra-vars "{'template': {'repo': {'url': '{{repo-url}}', 'version': '{{version}}' }}}" \
--extra-vars "project_dir={{project-dir}}" \
{{ARGS}}

View File

@@ -1,15 +0,0 @@
---
- name: HPA Playbook
hosts: all
roles:
- role: repo-template
tags:
- repo-template
- never # makes it so a tag must be supplied to run.
- role: build-project
tags:
- build-project
- never # makes it so a tag must be supplied to run.
- role: setup-project
tags:
- never # makes it so a tag must be supplied to run.

View File

@@ -1,31 +0,0 @@
---
build_dir_name: ".build"
build_dir: "{{ project_dir }}/{{ build_dir_name }}"
project_dir: "{{ lookup('env', 'PWD') }}"
project_vars_dir: "{{ project_dir }}"
# template:
# path: "/path/to/template/dir
# vars: "repo_vars"
# repo: (optional if using a repo as a template)
template:
path: "/path/to/template/dir"
vars: "repo_vars"
# When using a repository as a template dir. In general, it's
# probably best to pin to a particular version of the repo template
# instead of a branch.
#
# repo:
# url: "https://example.com/repo.git"
# version: "main"
repo: {}
copy_on_build:
- "{{ template.path }}/head.tex"
- "{{ template.path }}/Definitions.md"
template_on_build:
- "{{ project_dir }}/Report.md"
- "{{ template.path }}/footer.tex"

View File

@@ -1,36 +0,0 @@
---
- name: Starting build project.
ansible.builtin.debug:
msg: "Build dir: {{ build_dir }}"
- name: Load project vars.
ansible.builtin.include_vars:
dir: "{{ project_vars_dir }}"
ignore_unknown_extensions: true
- name: Ensure build directory exists.
ansible.builtin.file:
path: "{{ build_dir }}"
state: directory
- name: Parse template facts.
ansible.builtin.include_role:
name: "prepare-template-facts"
- name: Load repo vars.
ansible.builtin.include_role:
name: "load-template-vars"
- name: Copy build files.
ansible.builtin.copy:
src: "{{ item }}"
dest: "{{ build_dir }}/{{ item | basename }}"
mode: '0600'
with_items: "{{ copy_on_build }}"
- name: Template build files.
ansible.builtin.template:
src: "{{ item }}"
dest: "{{ build_dir }}/{{ item | basename }}"
mode: '0600'
with_items: "{{ template_on_build }}"

View File

@@ -1,42 +0,0 @@
---
# Used internally to clone the template repo, if applicable and load it's
# variables.
#
# NOTE: This expects that you've called prepare-template-facts first.
- name: Check if template path exists.
ansible.builtin.stat:
path: "{{ template_dir }}"
register: template_dir_stat
tags:
- always
- name: Debug template variable.
ansible.builtin.debug:
var: template
tags:
- debug
- never
- name: Ensure repo.
ansible.builtin.git:
repo: "{{ template.repo.url }}"
dest: "{{ template_dir }}"
version: "{{ template.repo.version | default('main') }}"
when: template.repo.url is defined
tags:
- always
- name: Check for repo vars directory.
ansible.builtin.stat:
path: "{{ template_vars_path }}"
register: repo_vars
tags:
- always
- name: Load repo vars if available.
ansible.builtin.include_vars:
dir: "{{ template_vars_path }}"
when: repo_vars.stat.isdir is defined
tags:
- always

View File

@@ -1,21 +0,0 @@
---
# This role is used internally to parse template variables, depending on
# what is supplied.
#
- name: Set default template path.
ansible.builtin.set_fact:
repo_template_path: "{{ build_dir }}/template"
tags:
- always
- name: Parse template path.
ansible.builtin.set_fact:
template_dir: "{{ template.path | default(repo_template_path) }}"
tags:
- always
- name: Parse template vars path.
ansible.builtin.set_fact:
template_vars_path: "{{ template_dir }}/{{ template.vars | default('repo_vars') }}"
tags:
- always

View File

@@ -1,5 +0,0 @@
---
output_dir: "{{ lookup('env', 'PWD') }}"
use_vault: true
repo_vars_dir: "repo_vars"

View File

@@ -1,47 +0,0 @@
# Definitions
<!-- NOTE: Keep these at the very bottom, if any new definitions are added then
they should also be added as links to the content section near the top. -->
### CFM50 {#cfm50-definition}
Is a unit of measurement that is taken when a blower door test is performed on the home. Stands for
$C$ubic $F$eet per $M$inute at 50 pascals.
### IAQ {#iaq-definition}
Stands for $I$ndoor $A$ir $Q$uality. We spend most of our time indoors, so having good indoor air
quality can help reduce illness and potentially improve the quality and longevity of life.
### LAIR {#lair-definition}
Stands for $L$eakage $A$ir $I$nfiltration $R$ate, which is a metric that compares the square footage
of the home and it's blower door number. This aids in determining if a home is controllable by
properly sized HVAC equipment or if [shell] improvements (air sealing and insulation) should be
considered.
### Load Caclulation {#load-calculation-definition}
A load calculation, also called a Manual-J, calculates the heating and cooling requirements for a
home. Each home has a unique heating and cooling load based on the direction it faces, the air
leakage, the location, the insulation values, the types and sizes of windows, among other factors.
### Shell {#shell-definition}
Shell is a term used to describe the enclosure of the house. It is often used when talking about air
leakage and insulation levels.
### SHR {#shr-definition}
Stands for $S$ensible $H$eat $R$atio. Sensible heat ratio is the ratio of work required by the air
conditioner to remove moisture, as well as maintain the sensible temperature. The higher the number
the better.
### TESP {#tesp-definition}
Stands for $T$otal $E$xternal $S$tatic $P$ressure, which is a metric used to determine how much
resistance the blower motor has to overcome for the airflow requirement of the system.
### WC {#wc-definition}
Stands for $W$ater $C$olumn, which is a unit of measurement for pressure.

View File

@@ -1,307 +0,0 @@
---
title: "{{ document_title }}"
author: "{{ author_name }}"
date: \today{}
mainfont: Avenir Next
documentclass: article
fontsize: 12pt
# NOTE: The applegreen is a custom color defined in resources/head.tex
linkcolor: applegreen
urlcolor: applegreen
abstract: |
**Prepared For:**
{{ customer.name }}
{{ customer.address.street }}
{{ customer.address.city }}, {{ customer.address.state }} {{ customer.address.zip }}
```{=latex}
\begin{center}
```
<!-- below are just forced line breaks -->
\
\
\
[![logo]({{ links.images.logo }}){ width=30% }]({{ company.website }})
```{=latex}
\end{center}
```
---
<!-- NOTE: Table of contents section only needs changed if new sections are
added.
-->
# Contents
1. [Introduction](#home-performance-report)
1. [Your Goals](#your-goals)
1. [Leakage](#how-leaky-is-your-house)
1. [Observations](#observations)
1. [HVAC](#hvac-observations)
1. [Home](#home-observations)
1. [Load Calculations](#load-calculations)
1. [Your Loads](#your-loads)
1. [Airflow Assessment](#airflow-assessment)
1. [Your Static Measurements](#your-static-measurements)
1. [Static Pressure Forecast](#static-measurements-forecast)
1. [Summary](#summary)
1. [Definitions](#definitions)
1. [CFM50]
1. [IAQ]
1. [LAIR]
1. [Load Calculation][load-calculation]
1. [Shell][shell]
1. [SHR]
1. [TESP]
1. [WC][wc]
# Home Performance Report
Thank you for having us to your home for a home performance assessment. I hope that it was
beneficial. We learned a lot about your home and your goals during the brief visit. So, lets jump
in to some of the things discovered.
Below is a summary of your goals, house measurements, and our budget discussion as well as my
observations and some recommendations. As part of the service we ran load calculations that help
determine the right sized HVAC for your home as it stand and with some upgrades. Those are attached
as are bids for replacements.
Once you read the report youll have 3 options: do nothing, pick and choose upgrades, or do more
planning for more difficult goals or complex projects. These options are discussed at the end of the
report as well as our leanings for your home.
\goalsimage
## Your goals:
<!-- NOTE: This should probably be a table in most cases, with a rank.-->
<!--
| Goal | Rank |
| ------ | ---- |
| A goal | 8 |
| B goal | 5 |
-->
<!-- WARNING: Add goals here -->
1. Add air conditioning.
# How Leaky Is Your House
<!-- NOTE: Several of the variables used in this section are defined in vars.sh
Do not change them here, instead update them in the vars.sh file.
-->
One of the main objectives was to perform a blower door test and load calculations for the home. We
discovered that the blower door number was {{ home.cfm50 }} [CFM50] for the approximately
{{ home.square_feet }} \squarefoot ({{ home.lair }} [LAIR]). A leaky home, most often, is an
uncomfortable and uncontrollable home.
> _Air leakage tends to have one of the largest impacts on the load of a home._
>
> - _Around 1:1 homes begin to be more controllable / comfortable_
> - _your home is {{ home.cfm50 }}:{{ home.square_feet }} \squarefoot for a ratio of
> {{ home.lair }}_
> - _Leaky homes are very difficult to maintain comfort with HVAC alone._
# Observations
The below sections are observations about the current HVAC system and the home.
## HVAC Observations
<!-- WARNING: Add HVAC observations here. -->
1. Current furnace is sized appropriately based on the load of the home.
1. Current duct system is not sized adequately for the system.
1. Filter is too small for the system.
1. Return sizing is not adequate.
1. Supply sizing is marginal.
1. Current static pressure is already high.
1. These problems may become worse when AC is installed.
## Home Observations
<!-- WARNING: Add house observations here. -->
1. The house leakage is high for the size of the home.
1. May be hard to control comfort without [shell] improvements.
# Load Calculations
Several [load-calculations][load-calculation] were performed on your home to determine the proper
equipment sizing for this application. Below is a comparison of the
[load-calculations][load-calculation] with the current air leakage and several improved air leakage
targets.
## Your Loads
<!-- WARNING: Add the project loads here. -->
| [CFM50] | | Heating Total | Cooling Total | [SHR] | [LAIR] |
| -------------------- | ----------- | ------------- | ------------- | -------- | ------------------- |
| **{{ home.cfm50 }}** | **Current** | **55,102** | **20,726** | **0.79** | **{{ home.lair }}** |
| 2,000 | | 40,320 | 17,279 | 0.85 | 1.5:1 |
| 1,350 | | 35,885 | 16,245 | 0.88 | 1:1 |
> **Note:**
>
> 1. _The lower the heating and cooling total's the better._
> 1. _An undersized air conditioner is better than an oversized one._
> 1. _Supplemental dehumidification may be required for [SHR]'s below 0.83._
The above table shows the relationship between air leakage and the amount of heating and cooling
that is required for the home. A tight home is easier to control the comfort levels, offers superior
IAQ levels, and lower utility costs.
<!-- WARNING: Update this summary for the given project -->
The projected cooling size required for your home is around 2-Tons for the current leakage rate, or
1.5-Tons if [shell] improvements were made.
[Here is a link to your load calculation reports][loads-folder]
# Airflow Assessment
While on site, we also measured the total system airflow and static pressure of the system. Static
pressure is equivalent to the blood pressure of your system and gives us a better understanding of
the overall ability for the system to provide the proper amount of airflow, as well as how much it
may struggle to do so.
Static pressure is the amount of resistance that the blower has to work against in order to move air
through the system. Things that have an effect on the static pressure of the system include, air
filters (size and type), duct sizes, amount of ducts, length of ducts, duct fittings and
transitions, as well as internal system components. Each component of the system has a resistance
associated with it that the blower has to overcome, by taking some key measurements we are able to
determine the [TESP] of the system. While there are several static pressures in the system, when we
talk about static pressure we are generally referring to [TESP].
Static pressure ([TESP]) has a range of _low_, _acceptable_, or _high_. While these numbers are
specific to the actual equipment, most manufacturers follow similar standards. For the sake of
simplicity, 0.5" [wc] or under is an _acceptable_ target, 0.8" [wc] is generally the max acceptable
static pressure (although we like to stay well below this if possible), and above 0.8" [wc] is
considered _high_ and should be addressed. _Low_ is generally not common and is rarely problematic,
so it is not focused on much.
<!-- NOTE: Force a new page here, for images and sub-section heading to be on same page -->
\newpage
## Your Static Measurements
![True Flow Report][trueflow-image]{ height=50% }
<!-- WARNING: Summarize the static measurements here -->
The above image is a snapshot of the static pressures recorded for your system. This shows that the
static pressure of your system is very high (1.114" [wc]). The primary culprits for the high static
pressure are that the filter is undersized for the airflow required and the return duct sizing is
small.
These measurements were taken in the heating mode because your system does not currently have air
conditioning. Currently the heating airflow is on the low side for what is required for your system
(1200 CFM would be ideal). This should be adjusted if possible during the install to get better
performance and efficiency out of the system, given that some static pressures can be improved
during the project.
\
\
[Here is a link to the full airflow report.][trueflow-file]
<!-- NOTE: Force a new page here, for images and sub-section heading to be on same page -->
\newpage
## Static Measurements Forecast
![True Flow Forecast][trueflow-forecast-image]{ height=50% }
<!-- WARNING: Summarize the forecast results below. -->
The above image is a snapshot is of a forecast of the static pressures after adding air
conditioning. It should be noted that these measurements are based solely on the airflow required
for cooling mode, not for heating mode (in other words, heating mode is going to be higher because
the airflow requirement is higher).
This shows that with an upgraded filter we can get the static pressure below the 0.8" [wc] max
target while in cooling mode.
\
\
[Here is a link to the full forecast report.][trueflow-forecast-file]
# Summary
The purpose of the home performance assessment is to help find the overlap between the house needs,
the goals, and the budget to see if there's a viable project.
<!-- WARNING: Add summary here -->
The house is pretty leaky overall. This is due to the age and construction style of the house. This
may lead to comfort problems or trouble maintaining comfort in all areas of the house.
Based on the [load calculations][load-calculation], the previously quoted systems are too large for
the current load, so we need to update the proposals to be for 2-Ton systems. This will help with
the fact that the static pressure of the system is already really high. The static pressure for
heating will likely still be above the 0.8" [wc] max threshold.
An upgraded air filter is going to be required to help alleviate the blower motor. Another return
may be required in the living space to further drop the static pressure, however this could likely
be done in the future if desired. I would estimate that adding another return would be in the
**$800-1,200** range.
While on site it was mentioned that you would like some of the ducts to be sealed that go to the
second floor. This is something that is not included in our general proposals. I would estimate this
to be an additional **$150-300** and will add options in the updated proposals.
Since the goal is to add air conditioning, then I would recommend going with a 2-Ton system. If the
system does not maintain then you could look into [shell] improvements and air sealing the home.
Regards,
\
\
\
[Here is a link to all the documents][document-folder]
<!-- prettier-ignore-start -->
<!-- NOTE: These are reference links for short form linking in the document.
This allows all the links to be changed / updated in one place.
However most do not need updated unless sections are added, besides the
document links, which are defined / setup in vars.sh.
-->
[CFM50]: #cfm50-definition "CFM50"
[IAQ]: #iaq-definition "IAQ"
[LAIR]: #lair-definition "LAIR"
[load-calculation]: #load-calculation-definition "load calculation"
[shell]: #shell-definition "shell"
[SHR]: #shr-definition "SHR"
[wc]: #wc-definition "wc"
[TESP]: #tesp-definition "TESP"
<!-- IMPORTANT: Take screen shots of the TrueFlow reports and add to the img
folder with names / path setup in vars.sh -->
<!-- default is img/trueflow.png -->
[trueflow-image]: "{{ links.images.trueflow }}"
<!-- default is img/forecast.png -->
[trueflow-forecast-image]: "{{ links.images.trueflow_forecast }}"
<!-- NOTE: These links are defined in vars.sh -->
[loads-folder]: "{{ links.documents.loads_folder }}"
[trueflow-file]: "{{ links.documents.trueflow_file }}"
[trueflow-forecast-file]: "{{ links.documents.trueflow_forecast_file }}"
[document-folder]: "{{ links.documents.document_folder }}"
<!-- prettier-ignore-end -->

View File

@@ -1,26 +0,0 @@
% customize the footer
\usepackage{fancyhdr}
\pagestyle{fancy}
% clear all footers
\fancyfoot{}
% clear all headers
\fancyhead{}
% change font size on footer.
\newcommand{\changefont}{
\fontsize{8}{10}\selectfont
}
% NOTE: What is displayed in the footer of each page.
\fancyfoot[LE,LO]{
\href{ {{ company.website }} }
{\changefont\textbf{
{{ company.url_display_title }}
}}
}
\fancyfoot[RE,RO]{\textbf{
\changefont{
Phone: \href{tel:{{ company.phone }} }{
\color{orange}{{ company.phone }}
}
}
}}

View File

@@ -1,74 +0,0 @@
% https://jdhao.github.io/2019/05/30/markdown2pdf_pandoc/#the-top
% These were adapted from the above, which was very helpful in getting this
% functional.
% change style of quote, see also https://tex.stackexchange.com/a/436253/114857
\usepackage[most]{tcolorbox}
% change page margins
\usepackage[top=2cm, bottom=1.5cm, left=2cm, right=2cm]{geometry}
% change the line spacing
\usepackage{setspace}
\setstretch{1.25}
\usepackage[utf8]{inputenc}
% NOTE: This needs to stay above hyperref otherwise internal links break.
% start each section on new page and make section titles orange.
\usepackage{titlesec}
\titleformat{\section}
{\color{orange}\normalfont\Huge\bfseries}
\newcommand{\sectionbreak}{\clearpage}
% custom colors
\definecolor{applegreen}{rgb}{0.55,0.71,0.0}
% Remove figure from images
\usepackage[labelformat=empty]{caption}
\usepackage{fancyvrb,newverbs}
% see for different color codes https://rgbcolorcode.com/color/E6FFEA
\definecolor{linequote}{RGB}{224,215,188}
\definecolor{backquote}{RGB}{230,255,234} % background color of quotes
\definecolor{bordercolor}{RGB}{221,221,221}
% change left border: https://tex.stackexchange.com/a/475716/114857
% change left margin: https://tex.stackexchange.com/a/457936/114857
\newtcolorbox{myquote}[1][]{%
enhanced,
breakable,
size=minimal,
left=10pt,
top=5pt,
bottom=5pt,
frame hidden,
boxrule=0pt,
sharp corners=all,
colback=backquote,
borderline west={4pt}{0pt}{bordercolor},
#1
}
% redefine quote environment to use the myquote environment, see https://tex.stackexchange.com/a/337587/114857
\renewenvironment{quote}{\begin{myquote}}{\end{myquote}}
% remove the abstract title.
\usepackage{abstract}
\renewcommand{\abstractname}{}
\renewcommand{\absnamepos}{empty}
\def\squarefoot{$ft^{\text{2}}$ }
\def\goalsimage{
\begin{center}
\hfill\break
\hfill\break
\includegraphics[width=0.6\linewidth,height=\textheight,keepaspectratio]{img/goals.png}
\end{center}
}

View File

@@ -1,31 +0,0 @@
---
author_name: "Testy McTestface Jr"
document_title: "Home Performance Report"
company:
website: "https://example.com"
phone: "555-555-5555"
url_display_title: "www.Example.com"
links:
images:
logo: "img/logo.png"
trueflow: "img/trueflow.png"
trueflow_forecast: "img/forecast.png"
documents:
loads_folder: "https://example.com/path/to/loads/folder"
trueflow_file: "https://example.com/path/to/trueflow/file"
trueflow_forecast_file: "https://example.com/path/to/trueflow/forecast/file"
document_folder: "https://example.com/path/to/document/folder"
customer:
name: "Testy McTestface Sr"
address:
street: "1234 Seasme Street"
city: "No Mans Land"
state: "Foo"
zip: "55555"
home:
square_feet: "3,000"
cfm50: "3,000"
lair: "1:1"

View File

@@ -1,5 +0,0 @@
---
on_setup:
- "Report.md"
- "vars.yml"
- "vault.yml" # optional if using vault.

View File

@@ -1,18 +0,0 @@
---
author_name: "{{ vault_author_name }}"
document_title: "Home Performance Report"
company: "{{ vault_company }}"
links:
images:
logo: "img/logo.png"
trueflow: "img/trueflow.png"
trueflow_forecast: "img/forecast.png"
documents: "{{ vault_document_links }}"
customer: "{{ vault_customer }}"
home:
square_feet: "3,000"
cfm50: "3,000"
lair: "1:1"

View File

@@ -1,20 +0,0 @@
---
vault_author_name: "Testy McTestface Vault"
vault_company:
website: "https://vault.example.com"
phone: "555-555-5555"
url_display_title: "www.VaultExample.com"
vault_customer:
name: "Testy McTestface Sr Vault"
address:
street: "1234 Seasme Street"
city: "Vault"
state: "Foo"
zip: "55555"
vault_document_links:
loads_folder: "https://vault.example.com/path/to/loads/folder"
trueflow_file: "https://vault.example.com/path/to/trueflow/file"
trueflow_forecast_file: "https://vault.example.com/path/to/trueflow/forecast/file"
document_folder: "https://vault.example.com/path/to/document/folder"

View File

@@ -1,46 +0,0 @@
---
- name: Starting repo template role.
ansible.builtin.debug:
msg: "Output directory: {{ output_dir }}"
- name: Ensure output directory exists.
ansible.builtin.file:
path: "{{ output_dir }}"
state: directory
- name: Ensure repo vars directory.
ansible.builtin.file:
path: "{{ output_dir }}/{{ repo_vars_dir }}"
state: directory
- name: Copy general files.
ansible.builtin.copy:
src: "files/{{ item.src | default(item) }}"
dest: "{{ output_dir }}/{{ item.dest | default(item) }}"
with_items:
- "Definitions.md"
- "Report.md"
- "head.tex"
- "footer.tex"
- src: "vars.repo.yml"
dest: "{{ repo_vars_dir }}/vars.yml"
- name: Copy basic vars files.
ansible.builtin.copy:
src: "files/vars.default.yml"
dest: "{{ output_dir }}/vars.yml"
when: not 'with-vault' in ansible_run_tags or use_vault | bool == False
- name: Copy vault and vars files.
ansible.builtin.copy:
src: "files/{{ item.src }}"
dest: "{{ output_dir }}/{{ item.dest }}"
with_items:
- src: "vars.vault.yml"
dest: "vars.yml"
- src: "vault.default.yml"
dest: "vault.yml"
when: "'with_vault' in ansible_run_tags or use_vault | bool == True"
tags:
- with-vault
- never

View File

@@ -1,49 +0,0 @@
---
# template:
# path: "/path/to/local/template/dir
# vars: "repo_vars" (optional path inside template directory to find variables, defaults to 'repo_vars')
#
# OR
#
# template:
# repo:
# url: "https://example.com/template.git
# version: "1.0.0" or "branch" (tagging to a version is more ideal)
#
template:
# The preject directory to setup in.
project_dir: "{{ lookup('env', 'PWD') }}"
# This path get's setup / parsed based on the template variable,
# it will point to the directory of the template, which could be a
# local path on the system or inside of the project directory, depending
# on if the template is a repo or not.
#
# This is safe to use inside of the project or template specifications
# for paths to files that live in the template directory not the project
# directory.
#template_dir: ""
# Files or directories that are copied from the template directory to the project
# directory.
#
# These can be a simple item that is a path from the root of the template directory
# to a file, which will copy the file to the root of the project directory or
# in the form of:
#
# src: "path/in/template/dir"
# dest: "path/in/project/dir"
# mode: '0600' (optional mode of the file/dir to copy)
#
copy_on_setup: []
# Copies the entire contents of a directory to the root of the project directory.
#
# This is useful if you keep all the template files in a sub-directory of your project
# template, it will copy that entire directory over when setting up a new project.
#
# NOTE: If the project has been setup (indicated by a .setup file) this
# will be skipped so that it does not overwrite any changes to the
# project files. This ensures that a project is only setup once.
copy_directory_on_setup: []

View File

@@ -1,19 +0,0 @@
---
# NOTE: This is just an internal task that checks if a file exists
# before copying, to prevent changes from being overwritten.
#
- name: "Check if {{ destination | basename }} exists already."
ansible.builtin.stat:
path: "{{ destination }}"
register: filestat
tags:
- always
- name: "Copy {{ destination | basename }} file."
ansible.builtin.copy:
src: "{{ source }}"
dest: "{{ destination }}"
mode: "{{ mode | default('0600') }}"
when: not filestat.stat.exists
tags:
- always

View File

@@ -1,120 +0,0 @@
---
- name: Starting setup project.
ansible.builtin.debug:
msg: "Project dir: {{ project_dir }}"
tags:
- setup-project
- never
- name: Debug template vars pre parse.
ansible.builtin.debug:
var: template
tags:
- debug
- never
- name: Debug build dir.
ansible.builtin.debug:
var: build_dir
tags:
- debug
- never
- name: Parse template facts.
ansible.builtin.include_role:
name: "prepare-template-facts"
tags:
- setup-project
- never
- name: Debug template path post parse.
ansible.builtin.debug:
var: template_dir
tags:
- debug
- never
- name: Ensure output directory exists.
ansible.builtin.file:
path: "{{ project_dir }}"
state: directory
tags:
- setup-project
- never
- name: Load template vars.
ansible.builtin.include_role:
name: "load-template-vars"
tags:
- setup-project
- never
- name: Debug on_setup.
ansible.builtin.debug:
msg: "On setup vars: {{ copy_on_setup }}"
tags:
- debug
- never
- name: Debug copy directory contents.
ansible.builtin.debug:
var: copy_directory_on_setup
tags:
- debug
- never
- name: Check if project has been previously setup.
ansible.builtin.stat:
path: "{{ project_dir }}/.setup"
register: setup_file
tags:
- setup-project
- never
- name: Debug setup file stat.
ansible.builtin.debug:
var: setup_file.exists
tags:
- debug
- never
- name: Copy directory contents to project directory.
ansible.builtin.command: |
cp -r "{{ template_dir }}/{{ item.src | default(item) }}/." \
"{{ item.dest | default(project_dir) }}"
with_items: "{{ copy_directory_on_setup }}"
when: setup_file.stat.exists is false
register: copy_directory_stat
tags:
- setup-project
- never
- name: Debug copy directory stat.
ansible.builtin.debug:
var: copy_directory_stat
tags:
- debug
- never
- name: Copy project files.
ansible.builtin.include_tasks:
file: "copy_if_not_exists.yml"
vars:
source: "{{ template_dir }}/{{ item.src | default(item) }}"
destination: "{{ project_dir }}/{{ item.dest | default(item) }}"
mode: "{{ item.mode | default('0600') }}"
loop: "{{ copy_on_setup }}"
tags:
- setup-project
- never
- name: Create setup file.
ansible.builtin.template:
src: "templates/setup.txt"
dest: "{{ project_dir }}/.setup"
mode: '0600'
when: not setup_file.stat.exists
tags:
- setup-project
- never

View File

@@ -1,7 +0,0 @@
This file is managed by the ansible-hpa-playbook. It is an indication
that the project using the template.
{{ template.repo.url | default(template_dir) }}
Has already called setup. This file should not be removed or subsequent calls
to setup may overwrite existing data.

View File

@@ -1,3 +0,0 @@
[defaults]
inventory = ../inventory.ini
roles_path = ../roles

View File

@@ -1,2 +0,0 @@
test *ARGS:
@ansible-playbook ./test.yml {{ARGS}}

View File

@@ -1,61 +0,0 @@
---
- name: Test ansible-hpa-playbook
hosts: all
roles:
- role: setup-project
tags:
- setup-project
- never # force passing tags to run.
vars:
template_dir:
path: "/tmp/hpa-playbook-tmp"
vars: "repo_vars"
project_dir: "/tmp/hpa-setup-project-tmp"
- role: repo-template
tags:
- repo-template
- never # force passing tags to run.
- role: build-project
tags:
- build-project
- never # force passing tags to run.
vars:
template_dir:
path: "/tmp/hpa-playbook-tmp"
vars: "repo_vars"
project_dir: "/tmp/hpa-setup-project-tmp"
tasks:
- name: Test complex var, set fact.
ansible.builtin.set_fact:
template:
path: "{{ template.path | default('.build') }}"
vars:
template:
repo:
url: "https://example.com"
version: "main"
tags:
- vars
- never
- name: Test complex var, set fact when path is specified.
ansible.builtin.set_fact:
template_path_specified: true
vars:
template:
repo:
url: "https://example.com"
version: "main"
when: template.path is defined
tags:
- vars
- never
- name: Test complex var was set.
ansible.builtin.debug:
msg: "Template path: {{ template.path }}, specified: {{ template_path_specified | default(false) }}"
tags:
- vars
- never

View File

@@ -33,15 +33,19 @@ public struct Configuration: Codable, Equatable, Sendable {
}
public struct Playbook: Codable, Equatable, Sendable {
public let directory: String?
public let inventory: String?
public let version: String?
public init(
directory: String? = nil,
inventory: String? = nil
inventory: String? = nil,
version: String? = nil
) {
self.directory = directory
self.inventory = inventory
self.version = version
}
public static var mock: Self { .init() }

View File

@@ -15,6 +15,7 @@ useVaultArgs = true
#[playbook]
#directory = '/path/to/local/playbook-directory'
#inventory = '/path/to/local/inventory.ini'
#version = 'main'
# These are to declare where your template files are either on your local system or
# a remote git repository.

View File

@@ -0,0 +1,9 @@
// TODO: Use an actuall version tag for playbook repo.
public extension PlaybookClient {
@_spi(Internal)
enum Constants {
public static let defaultInstallationPath = "~/.local/share/hpa/playbook"
public static let playbookRepoUrl = "https://git.housh.dev/michael/ansible-hpa-playbook.git"
public static let playbookRepoVersion = "main"
}
}

View File

@@ -0,0 +1,72 @@
import ConfigurationClient
import Dependencies
import DependenciesMacros
import FileClient
import Foundation
import ShellClient
// TODO: Add update checks and pull for the playbook.
public extension DependencyValues {
var playbookClient: PlaybookClient {
get { self[PlaybookClient.self] }
set { self[PlaybookClient.self] = newValue }
}
}
@DependencyClient
public struct PlaybookClient: Sendable {
public var installPlaybook: @Sendable (Configuration) async throws -> Void
public var playbookDirectory: @Sendable (Configuration) async throws -> String
}
extension PlaybookClient: DependencyKey {
public static let testValue: PlaybookClient = Self()
public static var liveValue: PlaybookClient {
.init {
try await install(config: $0.playbook)
} playbookDirectory: {
$0.playbook?.directory ?? Constants.defaultInstallationPath
}
}
}
private func install(config: Configuration.Playbook?) async throws {
@Dependency(\.fileClient) var fileClient
@Dependency(\.logger) var logger
@Dependency(\.asyncShellClient) var shell
let path = config?.directory ?? PlaybookClient.Constants.defaultInstallationPath
let version = config?.version ?? PlaybookClient.Constants.playbookRepoVersion
let parentDirectory = URL(filePath: path)
.deletingLastPathComponent()
let exists = try await fileClient.isDirectory(parentDirectory)
if !exists {
try await fileClient.createDirectory(parentDirectory)
}
let playbookExists = try await fileClient.isDirectory(URL(filePath: path))
if !playbookExists {
try await shell.foreground(.init([
"git", "clone",
"--branch", version,
PlaybookClient.Constants.playbookRepoUrl, path
]))
} else {
logger.debug("Playbook exists, ensuring it's up to date.")
try await shell.foreground(.init(
in: path,
["git", "pull", "--tags"]
))
try await shell.foreground(.init(
in: path,
["git", "checkout", version]
))
}
}

View File

@@ -1,9 +1,16 @@
@_exported import Dependencies
import Foundation
import Logging
@_exported import ShellClient
public protocol TestCase {}
public extension TestCase {
// static var logLevel: Logger.Level = {
// let levelString = ProcessInfo.processInfo.environment["LOG_LEVEL"] ?? "debug"
// return Logger.Level(rawValue: levelString) ?? .debug
// }()
func withTestLogger(
key: String,
logLevel: Logger.Level = .debug,

View File

@@ -30,7 +30,7 @@ struct InstallDependenciesCommand: AsyncParsableCommand {
@Argument(
help: "Extra arguments / options to pass to the homebrew command."
)
var extraOptions: [String]
var extraOptions: [String] = []
mutating func run() async throws {
@Dependency(\.cliClient) var cliClient

View File

@@ -3,6 +3,7 @@ import ConfigurationClient
import Dependencies
import FileClient
import Foundation
import PlaybookClient
import ShellClient
import Testing
import TestSupport
@@ -104,19 +105,19 @@ struct CliClientTests: TestCase {
}
}
@Test
func ensuredPlaybookDirectory() throws {
let configuration = Configuration.mock
let playbookDir = try configuration.ensuredPlaybookDirectory("playbook")
#expect(playbookDir == "playbook")
do {
_ = try configuration.ensuredPlaybookDirectory(nil)
#expect(Bool(false))
} catch {
#expect(Bool(true))
}
}
// @Test
// func ensuredPlaybookDirectory() throws {
// let configuration = Configuration.mock
// let playbookDir = try configuration.ensuredPlaybookDirectory("playbook")
// #expect(playbookDir == "playbook")
//
// do {
// _ = try configuration.ensuredPlaybookDirectory(nil)
// #expect(Bool(false))
// } catch {
// #expect(Bool(true))
// }
// }
@Test
func shellOrDefault() {
@@ -295,6 +296,7 @@ struct CliClientTests: TestCase {
try await withTestLogger(key: key, logLevel: logLevel) {
$0.configurationClient = .mock(configuration)
$0.cliClient = .capturing(capturing)
$0.playbookClient = .liveValue
setupDependencies(&$0)
} operation: {
try await operation()

View File

@@ -0,0 +1,42 @@
import ConfigurationClient
import Dependencies
import FileClient
import Foundation
@_spi(Internal) import PlaybookClient
import ShellClient
import Testing
@Suite("PlaybookClientTests")
struct PlaybookClientTests {
@Test
func installation() async throws {
try await withDependencies {
$0.fileClient = .liveValue
$0.asyncShellClient = .liveValue
} operation: {
let tempDirectory = FileManager.default.temporaryDirectory
let pathUrl = tempDirectory.appending(path: "playbook")
let playbookClient = PlaybookClient.liveValue
let configuration = Configuration(playbook: .init(directory: pathUrl.cleanFilePath))
try? FileManager.default.removeItem(at: pathUrl)
try await playbookClient.installPlaybook(configuration)
let exists = FileManager.default.fileExists(atPath: pathUrl.cleanFilePath)
#expect(exists)
try FileManager.default.removeItem(at: pathUrl)
}
}
@Test(arguments: [
(Configuration(), PlaybookClient.Constants.defaultInstallationPath),
(Configuration(playbook: .init(directory: "playbook")), "playbook")
])
func playbookDirectory(configuration: Configuration, expected: String) async throws {
let client = PlaybookClient.liveValue
let result = try await client.playbookDirectory(configuration)
#expect(result == expected)
}
}