[{"content":"Ansible Navigator offers a new way to use familiar Ansible tools within execution environments. With it, you can run playbooks, view inventories, access Ansible documentation, and more. Let\u0026rsquo;s explore some of the basics of Ansible Navigator.\nThe purpose of Ansible Navigator is to enable running playbooks within execution environments, the same way that Ansible Automation Platform runs jobs inside them. This functionality means you can now develop and test your playbooks in the same environment you would be running in production.\nThis article gets you started with Navigator - we\u0026rsquo;ll explore more of its features in upcoming posts.\nPrerequisites You should have the following installed on your system:\nEither Podman or Docker Python pip ✏️ Note Red Hat subscribers can install Navigator via RPM packages, but this guide focuses on the pip installation method. Installation We will install Ansible Navigator using pip, the Python package manager. We will install it in a Python virtual environment to isolate the installation and prevent potential conflicts with system packages.\nCreate and activate a virtual environment:\n$ python -m venv venv $ source venv/bin/activate # On Windows: venv\\Scripts\\activate $ pip install ansible-navigator Exploring ansible-navigator With ansible-navigator installed you can run it by typing:\n❯ ansible-navigator ------------------------------------------------------------------------------------- Execution environment image and pull policy overview ------------------------------------------------------------------------------------- Execution environment image name: ghcr.io/ansible/community-ansible-dev-tools:latest Execution environment image tag: latest Execution environment pull arguments: None Execution environment pull policy: tag Execution environment pull needed: True ------------------------------------------------------------------------------------- Updating the execution environment ------------------------------------------------------------------------------------- Running the command: podman pull ghcr.io/ansible/community-ansible-dev-tools:latest [... container download output ...] The first time you run it, it will download a community-ansible-dev-tools container as shown above.\nThe Default View Once that\u0026rsquo;s done you will see the following screen: Ansible Navigator\nYou can see from the list that some tools incorporated into ansible-navigator are ansible-doc, ansible-playbook, and ansible-inventory. For now, let\u0026rsquo;s look at the images option.\nBefore we do that let\u0026rsquo;s exit out and download the homelab-ee that is an evolution of the execution environment I made in the article, Ansible Builder, by doing a podman pull.\nInspecting Execution Environment Images Now let\u0026rsquo;s reopen ansible-navigator and go to the images section. You do that by typing a colon followed by the option you want, so in our case :images.\nContainer Images\nWe have two container images on our system, as shown above. The default image is the creator-ee; we will look at one way to change that later. It also tells us if the container images we have on our system are usable as execution environments.\nLet\u0026rsquo;s examine homelab-ee; you do that by pressing the number next to the image name, in our case, 1.\nHomelab EE Image Info\nIt shows the image name at the top and gives us more choices.\nViewing Ansible Info Let\u0026rsquo;s look at info about Ansible and Python packages installed in the execution environment. You can select \u0026ldquo;Ansible version and collections\u0026rdquo; by pressing 2.\nHomelab EE Ansible Info\nHere, we can see all the collections and the version of Ansible installed in the execution environment.\nViewing Python Packages Let\u0026rsquo;s look at one more option. Hitting \u0026rsquo;esc\u0026rsquo; takes us back one page and then let\u0026rsquo;s select \u0026ldquo;Python packages\u0026rdquo; by hitting 3.\nThis gives us a list of all the Python packages installed on the system. If we use the up and down cursor keys it scrolls through the list, in this case there are 71 Python packages installed in the image. An interesting one to note is the proxmoxer package #37 which was installed in the execution environment to be able to run the proxmox modules.\nHomelab EE Python packages\nFeel free to explore the remaining image options on your own — there\u0026rsquo;s a lot to discover!\nConfiguring Ansible Navigator First, let\u0026rsquo;s see what kind of options we have, let\u0026rsquo;s run:\n$ ansible-navigator --help Usage: ansible-navigator [options] Options (global): -h --help Show this help message and exit --version Show the application version and exit [... additional configuration options ...] Subcommands: {subcommand} --help builder Build [execution environment] (https://ansible.readthedocs. io/en/latest/getting_started_ ee/index.html) (container image) [... additional configuration subcommands ...] # Full options and subcommands are very long There are numerous options and subcommands available for Ansible Navigator. Run this yourself to see the whole list. Let\u0026rsquo;s take a look at the help page for the settings subcommand.\n$ ansible-navigator settings --help Usage: ansible-navigator settings [options] settings: Review the current ansible-navigator settings Options (global): -h --help Show this help message and exit --version Show the application version and exit [... additional configuration options ...] Options (settings subcommand): --se --effective Show the effective settings. Defaults, CLI parameters, environment variables, and the settings file will be combined --gs --sample Generate a sample settings file --ss --schema Generate a schema for the settings file (\u0026#39;json\u0026#39;= draft-07 JSON Schema) (json) (default: json) --so --sources Show the source of each current settings entry Let\u0026rsquo;s look at the option --gs, which generates a sample Navigator configuration with all options commented out.\n$ ansible-navigator settings --gs --- ansible-navigator: # ansible: # config: # # Help options for ansible-config command in stdout mode # help: False # # Specify the path to the ansible configuration file # path: ./ansible.cfg # # zone [... additional configuration ...] # time-zone: UTC Here you can set all kinds of options, if you look under execution-environment you can see how we specify image the default image used by ansible-navigator. In this case its the creator-ee that navigator downloaded when we first started it up. Let\u0026rsquo;s make that our homelab-ee.\nBefore we create our config file, it\u0026rsquo;s worth knowing where Navigator looks for it: 💡 Config File Locations (first match wins) Location Description ANSIBLE_NAVIGATOR_CONFIG Environment variable ./ansible-navigator.\u0026lt;ext\u0026gt; Project directory ~/.ansible-navigator.\u0026lt;ext\u0026gt; User home directory \u0026lt;ext\u0026gt; can be yaml, yml, or json.\nIn our local directory we will add an ansible-navigator.yml file and add our custom ee.\n--- ansible-navigator: execution-environment: image: registry.patsbytes.net/homelab-ee:latest Running ansible-navigator and selecting images we see that our image is now default:\nHomelab EE as Default\nAs we wrap up this intro to Ansible Navigator, you now know how to install and perform basic configuration. Watch for future articles where we\u0026rsquo;ll dive deeper into other parts of Ansible Navigator. Until then, happy automating, and stay tuned for more insights and guides!\nReferences and further reading Ansible Navigator Documentation Installing Ansible Navigator Installing Packages (Python) Ansible Navigator Settings ","permalink":"https://patsbytes.net/posts/ansible-navigator-intro/","summary":"\u003cp\u003eAnsible Navigator offers a new way to use familiar Ansible tools within execution environments. With it, you can run playbooks, view inventories, access Ansible documentation, and more. Let\u0026rsquo;s explore some of the basics of Ansible Navigator.\u003c/p\u003e\n\u003cp\u003eThe purpose of Ansible Navigator is to enable running playbooks within execution environments, the same way that Ansible Automation Platform runs jobs inside them. This functionality means you can now develop and test your playbooks in the same environment you would be running in production.\u003c/p\u003e","title":"Ansible Navigator: Beginner's Guide"},{"content":"In general, we want to make our playbooks and tasks as simple as possible - but no simpler. Adding a little complexity at times can actually reduce our work and keep things readable. Here\u0026rsquo;s an example:\nIn the microsoft.ad.group Ansible module, there is an option to add users and a different option to remove users. That means you would have to write nearly identical tasks, one to add and one to remove. As shown below:\n--- - name: Add user from AD group hosts: all tasks: - name: Add members to the group, preserving existing membership microsoft.ad.group: name: \u0026#34;{{ groupname }}\u0026#34; scope: \u0026#34;{{ scope }}\u0026#34; members: add: \u0026#34;{{ username }}\u0026#34; when: user_option == \u0026#39;add\u0026#39; - name: Remove members from the group, preserving existing membership microsoft.ad.group: name: \u0026#34;{{ groupname }}\u0026#34; scope: \u0026#34;{{ scope }}\u0026#34; members: remove: \u0026#34;{{ username }}\u0026#34; when: user_option == \u0026#39;remove\u0026#39; But there is an alternative; you could use one task with a Jinja if statement.\n- name: Add or Remove members from the group, preserving existing membership microsoft.ad.group: name: \u0026#34;{{ groupname }}\u0026#34; members: add: \u0026#34;{{ username if user_option == \u0026#39;add\u0026#39; else omit }}\u0026#34; remove: \u0026#34;{{ username if user_option == \u0026#39;remove\u0026#39; else omit }}\u0026#34; Using Jinja templating like this should be used sparingly, but it reads well, and we don\u0026rsquo;t have to repeat code almost word for word.\n","permalink":"https://patsbytes.net/posts/ansible-jinja-conditional/","summary":"Use Jinja2 conditionals to reduce duplicate Ansible tasks without sacrificing readability.","title":"Reduce Duplicate Ansible Tasks with Jinja Conditional Logic"},{"content":"Manually setting up the same VM again and again gets old fast. HashiCorp Packer fixes this by automating the creation of reusable, templated “gold images”. In this post, we’ll walk through how to build a template on Proxmox VE.\nI\u0026rsquo;ve recently been learning Packer, and here I\u0026rsquo;ll share an example of using Packer to generate a Red Hat Enterprise Linux (RHEL) template on Proxmox. While I\u0026rsquo;m still exploring the intricacies of Packer, this article will remain at a fairly high level. I hope it gives you a solid starting point.\nPrerequisites: A Proxmox environment set up and running A RHEL ISO (we will use RHEL 8 in this article) Packer installed on your local machine (see references for installation docs) Step 1: Create the Packer Configuration File Configuration file named rhel-template.pkr.hcl:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 packer { required_plugins { proxmox = { version = \u0026#34;\u0026gt;= 1.1.3\u0026#34; source = \u0026#34;github.com/hashicorp/proxmox\u0026#34; } } } variable \u0026#34;proxmox_host\u0026#34; { type = string } variable \u0026#34;proxmox_node\u0026#34; { type = string } variable \u0026#34;iso_file\u0026#34; { type = string } variable \u0026#34;proxmox_user\u0026#34; { type = string } variable \u0026#34;proxmox_pass\u0026#34; { type = string } variable \u0026#34;vm_name\u0026#34; { type = string } variable \u0026#34;ssh_user\u0026#34; { type = string } variable \u0026#34;ssh_pass\u0026#34; { type = string } source \u0026#34;proxmox-iso\u0026#34; \u0026#34;rhel-tpl\u0026#34; { proxmox_url = \u0026#34;https://${var.proxmox_host}/api2/json\u0026#34; insecure_skip_tls_verify = true node = var.proxmox_node iso_file = var.iso_file vm_name = var.vm_name username = var.proxmox_user password = var.proxmox_pass cpu_type = \u0026#34;Nehalem\u0026#34; cores = \u0026#34;2\u0026#34; memory = \u0026#34;1024\u0026#34; disks { disk_size = \u0026#34;60G\u0026#34; storage_pool = \u0026#34;local-lvm\u0026#34; type = \u0026#34;virtio\u0026#34; } network_adapters { bridge = \u0026#34;vmbr0\u0026#34; } ssh_username = var.ssh_user ssh_password = var.ssh_pass ssh_timeout = \u0026#34;30m\u0026#34; ssh_handshake_attempts = \u0026#34;100\u0026#34; boot_command = [\u0026#34;\u0026lt;esc\u0026gt;\u0026lt;wait\u0026gt;\u0026#34;, \u0026#34;vmlinuz initrd=initrd.img \u0026#34;, \u0026#34;inst.ks=http://{{ .HTTPIP }}:{{ .HTTPPort }}/ks.cfg\u0026#34;, \u0026#34;\u0026lt;enter\u0026gt;\u0026#34;] http_directory = \u0026#34;http\u0026#34; } build { sources = [\u0026#34;source.proxmox-iso.rhel-tpl\u0026#34;] } There are four parts of the above config that I want to call out.\nThe top section (lines 1-8) is how we load the plugin for proxmox The following section (lines 10-40) defines the Packer variables. It doesn\u0026rsquo;t assign them; it just defines them—more on assigning later In the next section (lines 42-68), defines what Packer needs for connection and setting up the template on proxmox. Things like proxmox url, iso file to use, CPU/memory, disk, etc., are all assigned here The last section (lines 70-72) is the build section, where you can add provisioners to customize your template further—things like running shell scripts, Ansible playbooks, or other configuration management tools after the OS installation completes. For this walkthrough, I\u0026rsquo;m skipping additional provisioning and letting the template build with just the kickstart configuration Step 2: Install the Packer Proxmox Plugin With the above configuration file we can run:\npacker init . The packer init . command will install the Packer Proxmox plugin.\nStep 3: Set Up a Kickstart Configuration The configuration file\u0026rsquo;s boot_command above references a Kickstart file (ks.cfg). Create this in a directory named http:\nmkdir http Here\u0026rsquo;s a basic ks.cfg to get started:\n#version=RHEL8 lang en_US.UTF-8 keyboard us timezone America/Los_Angeles --isUtc rootpw your-rhel-root-password reboot text cdrom bootloader --append=\u0026#34;rhgb quiet crashkernel=1G-4G:192M,4G-64G:256M,64G-:512M\u0026#34; zerombr clearpart --all --initlabel autopart skipx firstboot --disable selinux --enforcing %packages @^minimal-environment %end Replace the values with your- with your actual values.\nStep 4: Create a vars file This file will assign values to the variables you define in the first configuration file. Here is an example:\nrhel_vars.pkrvars.hcl proxmox_host = your-proxmox-host proxmox_node = \u0026#34;pve01\u0026#34; iso_file = \u0026#34;isos:iso/rhel-8.6-x86_64-dvd.iso\u0026#34; proxmox_user = your-promox-user proxmox_pass = your-proxmox-user-password ssh_user = \u0026#34;root\u0026#34; ssh_pass = your-rhel-root-password vm_name = \u0026#34;rhel-tpl\u0026#34; Replace the values that start with \u0026ldquo;your-\u0026rdquo; with your values.\nStep 5: Run Packer With configurations set, initiate the Packer build:\npacker build -var-file=rhel_vars.pkrvars.hcl . Replace the values with your- with your actual values. Monitor Packer\u0026rsquo;s output. It will create the VM, provision it, and convert it into a Proxmox template.\nOnce Packer completes, log into the Proxmox web interface. You should see the new RHEL template. Clone this template whenever you need a new RHEL 8 instance.\nWith Packer, you can automate the creation of standardized machine images for Proxmox, ensuring consistent, repeatable deployments across your infrastructure.\n💡 Living Document This article is as much for my own reference as it is a tutorial for others. There are items I didn\u0026rsquo;t touch on and I know that. I\u0026rsquo;ll continue updating it as I discover new Packer techniques and refinements. References and Further Reading Packer Packer Install Docs Proxmox Developer Subscription for RHEL ","permalink":"https://patsbytes.net/posts/packer-proxmox/","summary":"Use HashiCorp Packer to automate the creation of a reusable RHEL template on Proxmox VE.","title":"How to Create a Template with HashiCorp Packer on Proxmox VE"},{"content":"Ansible has many built-in lookups and filters to simplify tasks. Today, we\u0026rsquo;ll explore the password lookup and the random_string lookup - both useful for generating random strings like user passwords.\nThe Password Lookup The password lookup generates random passwords; no surprise there.\nExample of generating a random password:\n--- - hosts: localhost vars: user_password: \u0026#34;{{ lookup(\u0026#39;password\u0026#39;, \u0026#39;./generated_password length=12 chars=ascii_letters,digits\u0026#39;) }}\u0026#34; tasks: - name: print out user_password debug: var: user_password Here, the password will be 12 characters long, composed of ASCII letters and digits. If you leave off the chars= option, the default generated passwords contain a random mix of ASCII letters, digits, and a few punctuation characters (”. , : - _”).\nThe password lookup can save generated passwords to a file, ensuring you have access to them later if needed. It is not necessarily recommended but useful in some cases.\n➜ cat generated_password Bq6KP6p0M1KH If you don\u0026rsquo;t need to keep them, replace the file path with \u0026lsquo;/dev/null\u0026rsquo;.\nNote: If you don\u0026rsquo;t use the password in your playbook, the lookup won\u0026rsquo;t generate the password file. The file would not have appeared if I had just had a message like \u0026ldquo;Password generated\u0026rdquo; instead of printing the password in the debug task.\nYou can also pass in an option to encrypt the password using the encrypt= option and pass a type of encryption, such as sha512_crypt.\nFor example:\n--- - hosts: localhost vars: user_password: \u0026#34;{{ lookup(\u0026#39;password\u0026#39;, \u0026#39;./generated_password encrypt=sha512_crypt length=12 chars=ascii_letters,digits\u0026#39;) }}\u0026#34; tasks: - name: print out user_password debug: var: user_password The playbook run prints the encrypted password not the actual password.\nIf you choose the encrypt option, the seed used to encrypt the password gets saved along with the unencrypted password.\n➜ cat generated_password 2z9cOpGZs7DM salt=lQWq1jbg04vWEIJO Of course, you don\u0026rsquo;t get the seed if you use \u0026lsquo;/dev/null\u0026rsquo; as the location. Passing a specific seed using seed=\u0026lt;some_seed\u0026gt; is also an option.\nThe password lookup does what it\u0026rsquo;s named for – it generates passwords. But this lookup might be limited if you need more control of the characters you want to use.\nThe Random String Lookup ✏️ Note The parameter names used in these examples may differ from the current Ansible documentation. They have been tested and confirmed working in my environment, but behavior may vary depending on the version of community.general you are using. I plan to revisit this article periodically to ensure the examples remain accurate. Let\u0026rsquo;s look at an example of the random_string lookup:\n--- - hosts: localhost vars: random_value: \u0026#34;{{ lookup(\u0026#39;community.general.random_string\u0026#39;, \u0026#39;length=12 chars=ascii_letters,digits\u0026#39;) }}\u0026#34; tasks: - name: print out random_value debug: var: random_value It looks similar to the password lookup.\nThe random_string lookup gives you more control over the string’s properties, including length and characters. Although it doesn’t allow automatically saving the strings it generates, you have more control over how many of each character type you want and can use a specific list of characters you set.\nTake the following example:\n--- - hosts: localhost vars: random_value: \u0026#34;{{ lookup(\u0026#39;community.general.random_string\u0026#39;, length=12,min_numeric=2, override_special=\u0026#39;@#$\u0026#39;) }}\u0026#34; tasks: - name: print out user_password debug: var: random_value Weighing the Pros and Cons When deciding between the password and random_string lookups in Ansible, it\u0026rsquo;s important to understand your needs.\nChoose password if: Your primary goal is to generate passwords for users or services. You find the auto-save feature valuable. You require less granular control over the generated string\u0026rsquo;s properties. Choose random_string if: Control over the string properties is important. You need to generate more than just passwords You don\u0026rsquo;t need the auto-save functionality Both the password and random_string lookups in Ansible cater to different needs. By understanding the features and limitations of each, you can make a choice based on the requirements of your project.\nReferences\nAnsible lookups: https://docs.ansible.com/ansible/latest/plugins/lookup.html\nPassword lookup: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/password_lookup.html\nRandom String lookup: https://docs.ansible.com/ansible/latest/collections/community/general/random_string_lookup.html\n","permalink":"https://patsbytes.net/posts/password_filters/","summary":"Generate secure random passwords in Ansible with password and random_string lookups.","title":"Ansible Lookups: Password vs Random_String"},{"content":"In Ansible, an execution environment is a container image that includes all the necessary dependencies, modules, and plugins needed to run automation tasks.\nWith Ansible Builder, you can build and customize these images, ensuring consistent and reproducible execution of playbooks and roles in a known environment.\nPrerequisites You should have the following installed on your system:\nEither Podman or Docker Python pip ✏️ Note Red Hat subscribers can install Builder via RPM packages, but this guide focuses on the pip installation method. Installation We will install using pip, the Python package manager. We will install it in a Python virtual environment to isolate the installation and prevent potential conflicts with system packages.\nCreate and activate a virtual environment:\n$ python3 -m venv venv $ source venv/bin/activate # On Windows: venv\\Scripts\\activate $ pip install ansible-builder Building an Execution Environment for Proxmox Creating the Build Files Let\u0026rsquo;s use Ansible Builder to build an execution environment for managing Proxmox servers. Proxmox VE is an open-source virtualization platform similar to VMware vSphere.\nWhile you can include various files in your execution environment build, the core file is execution-environment.yml.\n--- version: 3 images: base_image: name: quay.io/ansible/awx-ee:latest dependencies: ansible_core: package_pip: ansible-core==2.14.0 ansible_runner: package_pip: ansible-runner galaxy: collections: - community.general python: - proxmoxer - requests system: - gcc - python3-devel - libxml2-devel additional_build_files: - src: ansible.cfg dest: configs additional_build_steps: prepend_final: - ADD _build/configs/ansible.cfg /etc/ansible/ansible.cfg options: package_manager_path: /usr/bin/dnf ⚠️ Outdated Version This article uses ansible-core==2.14.0 as an example. You should check the Ansible documentation for the current supported release and update accordingly. The above execution-environment.yml has all the Ansible, Python, and system requirements in line. You can also specify them as separate files, such as requirements.yml (Ansible) and requirements.txt (Python).\n--snip-- dependencies: ansible_core: package_pip: ansible-core==2.14.0 ansible_runner: package_pip: ansible-runner galaxy: requirements.yml python: requirements.txt system: bindep.txt --snip-- ✏️ Requirements When building an execution environment, the final image requires both Ansible Core and Ansible Runner - this can be pre-installed in the base image or added during the build process, as shown above. I have included an empty ansible.cfg here to show how you include it in your image build. Feel free to put any options in this file that you want to use in your execution environment. Our execution-environment.yml will put this in the container image where we specify. We will look more at this later on.\n✏️ Possible Gotchas When I first wrote this article, the build process didn’t require any additional RPMs, and I had to call out microdnf as the package manager explicitly (it handles some RPM installs even if you don’t request extra packages). In the current version, the build looks for microdnf by default, but awx-ee now includes dnf. Because of this, I had to adjust the steps above to get everything working again. I’m calling this out since these details may continue to change in future releases. When the files are ready, execute the following command:\nansible-builder build -t proxmox-env\n-t tags the image that is created.\nThis is the message I see when running it:\nRunning command: podman build -f context/Containerfile -t promox-env context Complete! The build context can be found at: /home/pmartin/ansible-builder/article/context Verifying the Build After the build process finishes you should have the image available to use.\n$ podman images REPOSITORY TAG IMAGE ID CREATED SIZE localhost/proxmox-env latest 0ce7182e8bab 4 minutes ago 1.58 GB ---snip--- Let\u0026rsquo;s look at the collections using podman.\n$ podman run localhost/proxmox-env ansible-galaxy collection list # /usr/share/ansible/collections/ansible_collections Collection Version ----------------------- ------- amazon.aws 9.4.0 ansible.posix 2.0.0 ansible.windows 2.8.0 awx.awx 24.6.1 azure.azcollection 3.3.1 community.general 11.2.1 community.vmware 5.5.0 google.cloud 1.5.1 kubernetes.core 5.2.0 kubevirt.core 2.1.0 openstack.cloud 2.4.1 ovirt.ovirt 3.2.0 redhatinsights.insights 1.3.0 theforeman.foreman 5.3.0 vmware.vmware 1.11.0 For comparison this is a list of the collections that were already in the container we used as a base image.\n$ podman run quay.io/ansible/awx-ee ansible-galaxy collection list # /usr/share/ansible/collections/ansible_collections Collection Version ----------------------- ------- amazon.aws 9.4.0 ansible.posix 2.0.0 ansible.windows 2.8.0 awx.awx 24.6.1 azure.azcollection 3.3.1 community.vmware 5.5.0 google.cloud 1.5.1 kubernetes.core 5.2.0 kubevirt.core 2.1.0 openstack.cloud 2.4.1 ovirt.ovirt 3.2.0 redhatinsights.insights 1.3.0 theforeman.foreman 5.3.0 vmware.vmware 1.11.0 The base image included a lot of collections already. But for our custom execution environment, it needed the \u0026lsquo;community.general\u0026rsquo; collection, which contains the proxmox module.\nUsing our Execution Environment We have the collection we need for working with proxmox, now, let\u0026rsquo;s test it. We will be using ansible-navigator to execute the playbook using our execution environment. A future article will go deeper into ansible-navigator.\nThis is the simple playbook we will be running.\n--- - hosts: localhost connection: local vars_files: - vault.yml tasks: - name: Create vms from a list community.general.proxmox_kvm: api_user: \u0026#34;{{ api_user }}\u0026#34; api_password: \u0026#34;{{ api_password }}\u0026#34; api_host: \u0026#34;{{ api_host }}\u0026#34; validate_certs: false clone: rh8-tmplt name: prox-ee-test node: pve storage: Local_lun timeout: 300 Here is the playbook run.\nansible-navigator run --eei localhost/proxmox-env clone_vm_proxmox.yml --pp=never -m stdout -i inventory PLAY [localhost] *************************************************************** TASK [Gathering Facts] ********************************************************* ok: [localhost] TASK [Create vms from a list] ************************************************** changed: [localhost] PLAY RECAP ********************************************************************* localhost : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 For more details on ansible-navigator, check out this post.\nOption Description run Runs a playbook \u0026ndash;eei What execution environment image to use \u0026ndash;pp Pull policy, whether or not to pull the image -m Output mode, stdout disables the interactive TUI -i Inventory file or host pattern to use After the playbook run if we check proxmox we see our vm.\nNew VM\nIt works! Now we can share that execution environment with others and know that everyone is working in the same environment. Now let\u0026rsquo;s go back and check one last thing.\nWhen we ran the build command with ansible-builder it creates a directory, context, where it stores files used in the build.\ncontext ├── _build │ ├── bindep.txt │ ├── configs │ │ └── ansible.cfg │ ├── requirements.txt │ ├── requirements.yml │ └── scripts │ ├── assemble │ ├── check_ansible │ ├── check_galaxy │ ├── entrypoint │ ├── install-from-bindep │ ├── introspect.py │ └── pip_install └── Containerfile Some interesting files are the two requirement files and the bindep.txt - they contain exactly what you\u0026rsquo;d expect: Python modules, collections list, and system package list. You will also see our ansible.cfg has been copied into the _build directory along with a generated Containerfile that handles the actual creation of the image.\nTake some time to explore these copied and generated files - they reveal how ansible-builder translates your files into a working container image.\nAnsible Builder is a tool for creating your own execution environments. By leveraging its capabilities to bundle dependencies, modules, and plugins, you can ensure consistent and reliable execution of your automation workflows. This article scratches the surface of what you can do. Be sure to check out the documentation for more options.\nReferences and Further Reading Ansible Builder GitHub Installing Packages (Python) Proxmox Ansible Navigator GitHub Ansible Navigator (Pat\u0026rsquo;s Bytes) ","permalink":"https://patsbytes.net/posts/ansible-builder-intro/","summary":"Stop copying Python deps by hand. Build custom Ansible Execution Environments using Ansible Builder.","title":"Ansible Builder: A Beginners Guide"},{"content":"Ansible playbooks are just YAML files that you can edit anywhere. Using development tools like Visual Studio Code can improve your workflow, allowing you to write more effective automation code. This article will show you how to get started by setting up an Ansible development environment in Visual Studio Code.\nℹ️ Prerequisites To follow along, make sure you have these installed on your system:\nGit Visual Studio Code Ansible Ansible lint Windows Subsystem for Linux (if on Windows, optional) Once you have those installed, it\u0026rsquo;s time to get started!\nSetting up VS Code First, let\u0026rsquo;s install some extensions in VS Code.\nExtensions Install these core extensions for Ansible development:\nAnsible (by Red Hat) YAML (by Red Hat) GitLens (Optional but recommended) Optional Extensions (Depending on OS and situation) Depending on your setup, you may also want:\nWSL (VS Code Extenstion) - Useful if you\u0026rsquo;re developing on Windows and have installed Windows Subsystem for Linux Remote SSH - Allows for editing playbooks on remote machines via ssh To install extensions, open VS Code and use Ctrl+Shift+X (or Cmd+Shift+X on Mac) to access the Extensions marketplace.\nDetermining Where and How to Install Ansible The VS Code Ansible extension requires an installed instance of Ansible for documentation and linting features. Let\u0026rsquo;s look at some options based on your operating system:\nDirect Installation (Linux/macOS) If you\u0026rsquo;re using Linux or macOS, install Ansible directly on your local machine. This approach doesn\u0026rsquo;t require the optional remote connection extensions mentioned earlier.\nRemote Installation (Windows or Remote Development) If you\u0026rsquo;re using Windows or have Ansible installed on a remote server, you\u0026rsquo;ll need to connect VS Code to where Ansible is installed. This is where WSL and Remote - SSH extensions become useful.\nInstalling Ansible and Tools ✏️ Note You can also install Ansible using pip instead of package managers. This method works across Linux, macOS, and remote servers, but may require adjusting VS Code extension settings to point to the correct executable paths. WSL Setup Example (Windows Users) For this article, we\u0026rsquo;ll use WSL on Windows:\nInstall a current Linux distribution in WSL, such as Ubuntu or Fedora Using the distribution\u0026rsquo;s package manager, install Ansible, ansible-lint, and your collections within the WSL environment Connecting VS Code to our environment If you have installed the WSL extension in VS Code, you can click in the lower left this symbol:\nVSCode Remote connect\nThat will bring up a drop-down with a list of options. The one we want is either:\nConnect to WSL if you have one instance or Connect to WSL using Distro\u0026hellip; (and select your distro) if you have multiple WSLs on your system and want to connect to a specific one. When connected, go to the extensions tab, and click Install in WSL on the Ansible, YAML, and Gitlens extensions. Below is an example of what I see in the Ansible extension with my particular version of WSL:\nVSCode WSL extension install\nOnce installed on WSL, the Ansible extension should detect our previously installed tools. If not auto-detected or if you installed Ansible to a non-typical path (like by pip), the Ansible extension allows entry of paths to the executables for Ansible and Ansible Lint on the remote systems.\nOnce VS Code detects the Ansible installation, it will also see any collections installed on the system and be able to do autocomplete as Ansible playbook and role development happens. You should see the file type at the bottom as Ansible instead of just plain YAML.\nAnsible filetype in Code\nYou should now see autocompletion working.\nAnsible Intellisense\nSetting up an efficient Ansible development environment using Visual Studio Code can greatly enhance your ease of working with Ansible. You can now leverage the power of Ansible within the familiar interface of VS Code. Happy coding!\nNotes: Additional tools can be installed and used by the VS Code ansible extension, such as ansible-builder and ansible-navigator. These will require the machine you are connecting to be able to run containers. In future articles, I will feature some of these tools and how to use them in our development environment.\nReferences and further reading Deep Dive on Ansible VS Code Extension\n","permalink":"https://patsbytes.net/posts/ansible-vscode/","summary":"Setup an Ansible development environment in Visual Studio Code","title":"Ansible Development: A Visual Studio Code-based Environment Setup"},{"content":"Ansible content collections, often referred to simply as collections, enable the addition of content not included in Ansible core. Each collection bundles related roles, playbooks, modules, and plugins together. Let\u0026rsquo;s examine how to install and use Ansible collections.\nCollections first appeared in Ansible 2.8 as a technical preview. Ansible 2.10 moved most modules from the main Ansible repository to separate collections repositories. Because of this, collection updates no longer depend on the release schedule of Ansible itself and can be released more frequently if needed. This separation allows for faster bug fixes and updates.\nAlthough there are several ways to manage collections, this article focuses on installing and using Ansible collections from the command line.\nA variable that is useful to know Before we dive into installing and using collections, let\u0026rsquo;s take a moment and look at the COLLECTIONS_PATHS environment variable. This variable is a list of paths that Ansible uses to locate installed collections on the system.\n✏️ Note Ansible will search for a collections folder in the directory in which a playbook runs, regardless of COLLECTION_PATHS. Here are two ways to see this variable\u0026rsquo;s value:\n$ ansible-config dump | grep COLLECTIONS_PATHS COLLECTIONS_PATHS(default) = [\u0026#39;/home/pbytes/.ansible/collections\u0026#39;, \u0026#39;/usr/share/ansible/collections\u0026#39;] $ ansible --version ansible [core 2.13.2] config file = None configured module search path = [\u0026#39;/home/pbytes/.ansible/plugins/modules\u0026#39;, \u0026#39;/usr/share/ansible/plugins/modules\u0026#39;] ansible python module location = /home/pbytes/Projects/ansible/venv/lib/python3.9/site-packages/ansible ansible collection location = /home/pbytes/.ansible/collections:/usr/share/ansible/collections executable location = /home/pbytes/Projects/ansible/venv/bin/ansible python version = 3.9.2 (default, Feb 28 2021, 17:03:44) [GCC 10.2.1 20210110] jinja version = 3.1.2 libyaml = True $ The ansible-config dump command shows the defaults for COLLECTIONS_PATHS as a list of two directories. Similarly, the ansible --version command shows the same list but refers to it as ansible collection location and doesn\u0026rsquo;t indicate if it is set to defaults.\nWe can modify this list in our ansible.cfg. Under the [defaults] section, let\u0026rsquo;s add the key collections_paths and change one of the paths. We\u0026rsquo;ll keep the home directory path and replace the /usr/share... path /opt/ansible/collections.\n$ cat ansible.cfg [defaults] inventory=/etc/ansible/hosts collections_paths=~/.ansible/collections:/opt/ansible/collections Let\u0026rsquo;s run the ansible-config dump command again.\n$ ansible-config dump | grep COLLECTIONS_PATHS COLLECTIONS_PATHS(/home/pbytes/Projects/ansible/ansible.cfg) = [\u0026#39;/home/pbytes/.ansible/collections\u0026#39;, \u0026#39;/opt/ansible/collections\u0026#39;] Notice ansible-config dump shows which ansible.cfg is setting these new values, just like it showed it was using defaults before.\nWith the paths configured, let\u0026rsquo;s check if there are any collections in those paths. We do this by using ansible-galaxy collection list.\n$ ansible-galaxy collection list $ The ansible-galaxy collection list command found no collections in the defined paths, so the command returns no output. Depending on how you installed Ansible on your system, this might have a long list of collections that your package manager included during the Ansible install.\n✏️ Note There is a bug that might return the following error: **`ERROR! - None of the provided paths were usable. Please specify a valid path with --collections-path`** This error also means you have no collections in your defined paths, so you can safely ignore the error. For more on the error, see - https://github.com/ansible/ansible/issues/73127 Installing collections You install a collection using the format ansible-galaxy collection install \u0026lt;collection.name\u0026gt;.\n$ ansible-galaxy collection install ansible.windows Starting galaxy collection install process Process install dependency map Starting collection install process Downloading https://galaxy.ansible.com/download/ansible-windows-1.11.0.tar.gz to /home/pbytes/.ansible/tmp/ansible-local-37420dwgkpdj/tmpqwyitqni/ansible-windows-1.11.0-6w0zvtsa Installing \u0026#39;ansible.windows:1.11.0\u0026#39; to \u0026#39;/home/pbytes/.ansible/collections/ansible_collections/ansible/windows\u0026#39; ansible.windows:1.11.0 was installed successfully $ Here we installed the ansible.windows collection. The ansible-galaxy command installs it to the first path listed in COLLECTIONS_PATHS, in our case:\n/home/pbytes/.ansible/collections/\nSome options you can pass to the install command\n-p \u0026ldquo;path\u0026rdquo;: Path to install the collection -r \u0026ldquo;file\u0026rdquo;: Install collections based on a requirements file (see below) Let\u0026rsquo;s look at an example of a requirements.yml file:\n$ cat requirements.yml --- collections: # Can be a list of collections names - ansible.posix # Or Can be a list with name, version(opttional), and source(optional) - name: ansible.netcommon version: 2.6.1 source: https://galaxy.ansible.com Using this requirements file and the -p option let\u0026rsquo;s install those collections. We will install it in the directory we run our playbooks from in the collections folder. As noted before, even though this isn\u0026rsquo;t in our COLLECTIONS_PATHS, Ansible will still look there.\n$ ansible-galaxy collection install -r collections/requirements.yml -p ./collections Starting galaxy collection install process [WARNING]: The specified collections path \u0026#39;/home/pbytes/Projects/ansible/collections\u0026#39; is not part of the configured Ansible collections paths \u0026#39;/home/pbytes/.ansible/collections:/opt/ansible/collections\u0026#39;. The installed collection won\u0026#39;t be picked up in an Ansible run. Process install dependency map Starting collection install process Downloading https://galaxy.ansible.com/download/ansible-netcommon-2.6.1.tar.gz to /home/pbytes/.ansible/tmp/ansible-local-38033kku_abc/tmp1n81kclb/ansible-netcommon-2.6.1-zz9b60p6 Installing \u0026#39;ansible.netcommon:2.6.1\u0026#39; to \u0026#39;/home/pbytes/Projects/ansible/collections/ansible_collections/ansible/netcommon\u0026#39; Downloading https://galaxy.ansible.com/download/ansible-utils-2.6.1.tar.gz to /home/pbytes/.ansible/tmp/ansible-local-38033kku_abc/tmp1n81kclb/ansible-utils-2.6.1-wgeklro8 ansible.netcommon:2.6.1 was installed successfully Installing \u0026#39;ansible.utils:2.6.1\u0026#39; to \u0026#39;/home/pbytes/Projects/ansible/collections/ansible_collections/ansible/utils\u0026#39; Downloading https://galaxy.ansible.com/download/ansible-posix-1.4.0.tar.gz to /home/pbytes/.ansible/tmp/ansible-local-38033kku_abc/tmp1n81kclb/ansible-posix-1.4.0-dxj412mk ansible.utils:2.6.1 was installed successfully Installing \u0026#39;ansible.posix:1.4.0\u0026#39; to \u0026#39;/home/pbytes/Projects/ansible/collections/ansible_collections/ansible/posix\u0026#39; Downloading https://galaxy.ansible.com/download/community-general-5.4.0.tar.gz to /home/pbytes/.ansible/tmp/ansible-local-38033kku_abc/tmp1n81kclb/community-general-5.4.0-wmqxdqfe ansible.posix:1.4.0 was installed successfully Installing \u0026#39;community.general:5.4.0\u0026#39; to \u0026#39;/home/pbytes/Projects/ansible/collections/ansible_collections/community/general\u0026#39; community.general:5.4.0 was installed successfully $ This pulls in dependencies in addition to our requested collections. Notice that it warns us that this path is not in our current collection path. We will revisit this in a moment, but let\u0026rsquo;s look at what collections Ansible thinks are on our system.\n$ ansible-galaxy collection list # /home/pbytes/.ansible/collections/ansible_collections Collection Version ----------------- ------- ansible.windows 1.11.0 $ It only shows the ansible.windowscollection we initially installed. Let’s look at using collections and see if we can clear up the above warning and how even though those collections are not currently listed in our path, Ansible will still find them.\nUsing collections Here is a simple playbook that uses the dig lookup plugin in the community.general collection.\ncat dig_example.yml --- - hosts: localhost connection: local tasks: - name: Print out ansible.builtin.debug: msg: \u0026#34;{{ lookup(\u0026#39;community.general.dig\u0026#39;, \u0026#39;google.com\u0026#39;) }}\u0026#34; When referring to an item in a collection, we should specify the fully qualified collection name (FQCN), more on this later. We use two collections in this playbook. One is the community.general we installed from before. The other ansible.builtin comes included with Ansible core. There are still some modules that are built in. Check out the Ansible docs to see more.\nNow let\u0026rsquo;s run it.\nansible-playbook dig_example.yml PLAY [localhost] *************************************************************** TASK [Gathering Facts] ********************************************************* ok: [localhost] TASK [Print out] *************************************************************** ok: [localhost] =\u0026gt; { \u0026#34;msg\u0026#34;: \u0026#34;142.250.72.174\u0026#34; } PLAY RECAP ********************************************************************* localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 To clarify where we are, here is the current file structure of the folder where we ran the playbook.\n$ tree -L 3 . |-- ansible.cfg |-- collections | `-- ansible_collections | |-- ansible | |-- ansible.netcommon-2.6.1.info | |-- ansible.posix-1.4.0.info | |-- ansible.utils-2.6.1.info | |-- community | `-- community.general-5.4.0.info |-- dig_example.yml |-- inventory |-- requirements.yml 💡 Tip It is possible to type the module\u0026rsquo;s name without the Fully Qualified Collection Name (FQCN). However, because different collections could have modules with the same name, it is recommended to always use the FQCN in your Ansible code. The FQCN is required for all plugins and lookups from collections except ansible.builtin. With the information in this article, you should have a solid foundation for installing and using collections. Start exploring the collections available on Ansible Galaxy to see what other collections are out there.\nReferences On collections in general Builtin modules in the ansible.builtin collection For developing collections Ansible config settings Ansible Galaxy website ","permalink":"https://patsbytes.net/posts/ansible-collections/","summary":"Install and use Ansible collections to extend your automation with reusable content.","title":"Ansible Collections - A Primer"},{"content":"About Hey there!\nI\u0026rsquo;m Pat - Husband, Father, and perpetual tinkerer working in tech. This site is where I document what I learn, share solutions to problems I\u0026rsquo;ve encountered, and occasionally save future-me from having to figure something out twice. Most of my experiments happen in my homelab, where I break things, fix them, and sometimes even improve them.\nWhat I Do By day, I work as a consulting architect, helping organizations automate their infrastructure and streamline their operations. By night (and weekends), I\u0026rsquo;m usually knee-deep in my homelab, testing new tools, setting up services I probably don\u0026rsquo;t need, and learning things the hard way.\nWhy I Write We\u0026rsquo;ve all been there - you solve a tricky problem, feel pretty clever about it, then six months later you\u0026rsquo;re staring at the same issue wondering how you fixed it before. This site is my attempt to break that cycle. Writing things down helps me think through problems more clearly, and if it helps someone else avoid a few hours of debugging, even better.\nWhat You\u0026rsquo;ll Find Here Most of my content focuses on practical infrastructure automation, homelab adventures, and the occasional deep dive into tools I find genuinely useful. I write from a RHEL/Fedora perspective (though I dabble in Ubuntu/Debian), have strong opinions about YAML formatting, and believe documentation should be as reliable as the systems it describes.\nExpect:\nReal solutions to real problems I\u0026rsquo;ve actually encountered Honest takes on tools and technologies - including when they don\u0026rsquo;t work as advertised Step-by-step guides that include the gotchas and edge cases The occasional rant about why certain things in tech are unnecessarily complicated Beyond Tech When I\u0026rsquo;m not breaking things in the lab or helping clients automate theirs, I\u0026rsquo;m probably:\nTrying to explain why dad needs \u0026ldquo;just five more minutes\u0026rdquo; to finish this one thing Finding new ways to justify homelab purchases as \u0026ldquo;learning investments\u0026rdquo; Reading way too much about technologies I\u0026rsquo;ll probably never actually use Get In Touch Found something useful? Spotted an error? Have a better way to solve something I wrote about? I\u0026rsquo;d love to hear from you. The best part of writing in public is learning from the community.\nHead over to the contact page to get in touch!\nThanks for stopping by, and welcome to my corner of the internet!\n","permalink":"https://patsbytes.net/about/","summary":"\u003ch1 id=\"about\"\u003eAbout\u003c/h1\u003e\n\u003cp\u003eHey there!\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;m Pat - Husband, Father, and perpetual tinkerer working in tech. This site is where I document what I learn, share solutions to problems I\u0026rsquo;ve encountered, and occasionally save future-me from having to figure something out twice. Most of my experiments happen in my homelab, where I break things, fix them, and sometimes even improve them.\u003c/p\u003e\n\u003ch2 id=\"what-i-do\"\u003eWhat I Do\u003c/h2\u003e\n\u003cp\u003eBy day, I work as a consulting architect, helping organizations automate their infrastructure and streamline their operations. By night (and weekends), I\u0026rsquo;m usually knee-deep in my homelab, testing new tools, setting up services I probably don\u0026rsquo;t need, and learning things the hard way.\u003c/p\u003e","title":""},{"content":"Disclaimer for Pat\u0026rsquo;s Bytes This is my personal blog, which is entirely independent of my employer. My articles and comments are my own and don’t necessarily represent my employer positions, strategies, or opinions.\nAny mention of specific products, tools, or vendors is based on personal experience and does not constitute an endorsement.\nAll the information on this website is published in good faith and for general information purposes only. Pat’s Bytes does not make any warranties about the completeness, reliability, and accuracy of this information. Any action you take upon the information you find on this website (patsbytes.net), is strictly at your own risk. I will not be liable for any losses and/or damages in connection with the use of this website.\nFrom this website, you can visit other websites by following hyperlinks to such external sites. While I strive to provide only quality links to useful and ethical websites, I have no control over the content and nature of these sites. These links to other websites do not imply a recommendation for all the content found on these sites. Site owners and content may change without notice and occur before I can remove a link that may have gone ‘bad’.\nPlease also be aware that when you leave this website, other sites may have different privacy policies and terms beyond my control. Please be sure to check the Privacy Policies of these sites and their “Terms of Service” before engaging in any business or uploading any information.\nConsent By using this website, you hereby consent to this disclaimer and agree to its terms.\nUpdate Should I update, amend or make any changes to this document, those changes will be prominently posted here.\n","permalink":"https://patsbytes.net/disclaimer/","summary":"\u003ch1 id=\"disclaimer-for-pats-bytes\"\u003eDisclaimer for Pat\u0026rsquo;s Bytes\u003c/h1\u003e\n\u003cp\u003eThis is my personal blog, which is entirely independent of my employer. My articles and comments are my own and don’t necessarily represent my employer positions, strategies, or opinions.\u003c/p\u003e\n\u003cp\u003eAny mention of specific products, tools, or vendors is based on personal experience and does not constitute an endorsement.\u003c/p\u003e\n\u003cp\u003eAll the information on this website is published in good faith and for general information purposes only. Pat’s Bytes does not make any warranties about the completeness, reliability, and accuracy of this information. Any action you take upon the information you find on this website (patsbytes.net), is strictly at your own risk. I will not be liable for any losses and/or damages in connection with the use of this website.\u003c/p\u003e","title":""},{"content":"I'd love to hear from you! Whether you spot an error in a post, have a question about something I've written, or just want to share your own homelab adventures — drop me a message. Don't fill this out if you're human: Name Email Subject General question Post correction Homelab chat Other Message Send Message ✅ Message sent! Privacy note: This form is handled by Netlify and only collects what you provide above. No tracking, no nonsense.\n","permalink":"https://patsbytes.net/contact/","summary":"\u003cdiv id=\"contact-intro\"\u003eI'd love to hear from you! Whether you spot an error in a post, have a question about something I've written, or just want to share your own homelab adventures — drop me a message.\u003c/div\u003e\n\u003cstyle\u003e\n.contact-honeypot { position: absolute; left: -9999px; opacity: 0; height: 0; overflow: hidden; pointer-events: none; }\n.contact-form { display: flex; flex-direction: column; gap: 1.25rem; margin: 2rem 0 1.5rem; max-width: 680px; }\n.contact-field { display: flex; flex-direction: column; gap: 0.4rem; }\n.contact-field label { font-size: 0.9rem; font-weight: 600; color: var(--primary); }\n.contact-form input[type=\"text\"],\n.contact-form input[type=\"email\"],\n.contact-form select,\n.contact-form textarea {\n  width: 100%; padding: 0.55rem 0.75rem; border: 1px solid var(--border);\n  border-radius: 6px; background: var(--entry); color: var(--primary);\n  font-size: 0.95rem; font-family: inherit; box-sizing: border-box;\n  transition: border-color 0.15s ease, box-shadow 0.15s ease;\n  -webkit-appearance: none; appearance: none;\n}\n.contact-form select {\n  background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%23888' d='M1 1l5 5 5-5'/%3E%3C/svg%3E\");\n  background-repeat: no-repeat; background-position: right 0.75rem center; padding-right: 2rem; cursor: pointer;\n}\n.contact-form textarea { resize: vertical; min-height: 140px; }\n.contact-form input:focus, .contact-form select:focus, .contact-form textarea:focus {\n  outline: none; border-color: #4a90d9;\n  box-shadow: 0 0 0 3px rgba(74, 144, 217, 0.2);\n}\n.contact-submit {\n  display: inline-block; padding: 0.6rem 1.75rem; background: #1a73e8;\n  color: #fff; font-size: 1rem; font-weight: 600; font-family: inherit;\n  border: none; border-radius: 6px; cursor: pointer;\n  transition: background 0.15s ease, transform 0.1s ease;\n}\n.contact-submit:hover { background: #1558b0; }\n.contact-submit:active { transform: scale(0.98); }\n.contact-meta { margin-top: 1.75rem; padding-top: 1rem; border-top: 1px solid var(--border); font-size: 0.9rem; color: var(--secondary); }\n.contact-meta p { margin: 0.4rem 0; }\n.contact-meta strong { color: var(--primary); }\n\u003c/style\u003e\n\u003cform name=\"contact\" method=\"POST\" action=\"/contact/?success=true\" data-netlify=\"true\" netlify-honeypot=\"bot-field\" class=\"contact-form\"\u003e\n  \u003cinput type=\"hidden\" name=\"form-name\" value=\"contact\" /\u003e\n  \u003cp class=\"contact-honeypot\" aria-hidden=\"true\"\u003e\n    \u003clabel\u003eDon't fill this out if you're human: \u003cinput name=\"bot-field\" tabindex=\"-1\" autocomplete=\"off\" /\u003e\u003c/label\u003e\n  \u003c/p\u003e","title":"Get in Touch"}]