No description
  • Python 95.1%
  • Shell 4.1%
  • Jinja 0.8%
Find a file
Jesus Alejandro Sanchez Davila 7d4e340502
All checks were successful
Ansible Lint / ansible-lint (push) Successful in 1m4s
Merge pull request 'Make local password generation length policy-aware' (#13) from feature/openbao-policy-lengths into main
Reviewed-on: #13
Reviewed-by: Jesus Alejandro Sanchez Davila <jsanchezd@noreply.git.silvarion.org>
2026-10-12 17:38:30 +00:00
.forgejo/workflows Trim comments to short reasons instead of full explanations 2026-10-07 09:12:29 +02:00
changelogs Wire proxmox_ssh_automation_key into the container (sysadmin) connection too 2026-10-06 22:15:23 +02:00
inventory Trim comments to short reasons instead of full explanations 2026-10-07 09:12:29 +02:00
meta Initial commit 2026-05-19 23:16:43 +02:00
playbooks Trim comments to short reasons instead of full explanations 2026-10-07 09:12:29 +02:00
plugins Try OpenBao's own generate endpoint before falling back to local generation 2026-10-12 14:55:59 +02:00
roles Trim comments to short reasons instead of full explanations 2026-10-07 09:12:29 +02:00
scripts Add scripts/provision.sh to work around ansible.cfg's world-writable-mount gap 2026-10-01 22:51:48 +02:00
.ansible-lint.yml Trim comments to short reasons instead of full explanations 2026-10-07 09:12:29 +02:00
.gitignore inventory: replace scripts/provision.sh wrapper with a real inventory plugin 2026-09-15 23:32:52 +02:00
AGENTS.md docs: rename CLAUDE.md/AGENT.md to AGENTS.md 2026-09-24 09:37:21 +02:00
ansible.cfg inventory: replace scripts/provision.sh wrapper with a real inventory plugin 2026-09-15 23:32:52 +02:00
CHANGELOG.rst Release 1.2.0 2026-10-01 09:19:37 +00:00
galaxy.yml Release 1.2.0 2026-10-01 09:19:37 +00:00
README.md Add bootstrap_collection.yml as the true first step in main.yml 2026-10-06 20:59:56 +02:00
requirements.yml Trim comments to short reasons instead of full explanations 2026-10-07 09:12:29 +02:00

silvarion.provision

Ansible collection for declarative, parallelized infrastructure provisioning on Proxmox.

The collection reads a structured infrastructure repository (local path or git URL), dynamically registers matching hosts, and runs a three-play pipeline:

  1. Provision — for each registered host, run its silvarion.provision.* roles (e.g. proxmox) with connection: local to create the LXC container/VM.
  2. Prepare the controller — install (via ansible-galaxy) any tech role or collection referenced by a created host that isn't already installed.
  3. Configure — SSH into each created container/VM and run the rest of its ansible_roles_list (the tech roles: mysql, redis, etc.).

Requirements

Requirement Version
Ansible >= 2.14
community.proxmox >= 1.0.0

Installation

From a git repository

# Install directly from git
ansible-galaxy collection install \
  git+https://git.silvarion.org/ansible-collections/silvarion.provision.git

# Install a specific branch or tag
ansible-galaxy collection install \
  git+https://git.silvarion.org/ansible-collections/silvarion.provision.git,main

From a local clone

git clone https://git.silvarion.org/ansible-collections/silvarion.provision.git
ansible-galaxy collection install ./silvarion.provision/ --force

Create a requirements.yml in your project:

collections:
  - name: https://git.silvarion.org/ansible-collections/silvarion.provision.git
    type: git
    version: main
  - name: community.proxmox
    version: ">=1.0.0"

Then install:

ansible-galaxy collection install -r requirements.yml

Infrastructure Repository Structure

The inventory role (via the env_parser lookup plugin) expects a structured YAML repository that serves as the single source of truth for your infrastructure. playbook_env_source (or inventory_env_source when using the role directly) points to the root of an environment directory (e.g., infra/testlab/).

<env_dir>/
  metadata.yml                           # Layer 1: shared by every platform (optional)
  <platform>/                            # e.g., proxmox
    metadata.yml                         # Layer 2: provider-wide defaults
    defaults/
      <tech>.yml                         # Layer 3: per-tech resource profiles
    <node>/                              # e.g., pve01
      metadata.yml                       # Layer 4: node-specific overrides
      <category>/                        # e.g., base | monitoring | comms
        <server_type>/                   # e.g., lxc | vms
          <tech>/                        # e.g., mysql | redis | netbox
            <service>/                   # e.g., main | dev | public
              group_vars/
                all.yml                  # Layer 5: leaf instance variables (REQUIRED)

Legacy leaves (all.yml directly under <tech>/, with no <service> level) are still recognized — service defaults to main for those.

Variable Precedence (lowest → highest)

Layer File Purpose
1 metadata.yml (env_dir root) Shared by every platform — e.g. the fleet's ansible-collections org base URL, a default collection version. Optional; never required
2 <platform>/metadata.yml Provider-wide defaults (subnet, VMID range, SSH keys)
3 <platform>/defaults/<tech>.yml Per-tech resource profile (cores, memory, disk)
4 <platform>/<node>/metadata.yml Node-specific overrides
5 <platform>/<node>/<category>/<server_type>/<tech>/<service>/group_vars/all.yml Instance variables

Higher layers override lower layers via recursive merge. The fully merged variable dict is stored as provision_unit on each registered host.

Example all.yml (leaf)

# proxmox/pve01/base/lxc/mysql/main/group_vars/all.yml
ansible_host: 10.0.0.110
proxmox_node: pve01
ansible_roles_list:
  - name: my_namespace.mysql
    source: git
    path: "https://git.example.com/my-org/my_namespace.mysql.git"
    mysql_port: 3306
    mysql_root_pswd: "{{ vault_mysql_root_pswd }}"

source/path are optional — a role with neither is installed by plain name from the configured Galaxy server(s). source is git or localpath; path holds the actual location either way (a URL for git, a filesystem path for localpath) — one key for both, since ansible-galaxy itself doesn't distinguish "URL" from "path" as separate concepts, just a location string. version pins a specific ref/version; unset, a git-sourced entry falls back to playbook_ansible_collections_default_version.

silvarion.provision.proxmox (the platform provisioning role) is prepended to ansible_roles_list automatically — via inventory_default_provision_roles — so leaf files only need to list their tech roles.

To run only a subset of a host's ansible_roles_list for a given invocation (e.g. re-configuring just one tech role without touching the leaf all.yml), set ansible_target_roles_list to an allowlist of role names/roles as a host var or extra-var. It only affects the tech configuration play (roles applied on the target hosts via SSH); leaving it unset or empty runs every role in ansible_roles_list as before.

ansible-playbook silvarion.provision.main \
  -e playbook_env_source=/opt/infra/testlab \
  -e '{"ansible_target_roles_list": ["my_namespace.mysql"]}' \
  --tags tech

Example metadata.yml (platform level)

# proxmox/metadata.yml
proxmox_api_host: 192.168.1.10
proxmox_api_user: root@pam
proxmox_api_token_id: ansible
proxmox_subnet_prefix: "10.0.0"
proxmox_gateway: "10.0.0.1"
proxmox_bridge: vmbr0

Usage

Running the main playbook

Inventory is file-based (see "Key Design Decisions" in AGENT.md), rendered per-run into a directory that the silvarion.provision.render inventory plugin (plugins/inventory/render.py) creates and reads back itself, at -i-parse time — no external bookkeeping needed. The checked-in inventory/render.yml + ansible.cfg ([defaults] inventory = inventory/render.yml) make this collection's own directory the default -i source, so a plain ansible-playbook silvarion.provision.main call just works, provided ansible.cfg is actually picked up (see the note below).

Recommended: use scripts/provision.sh instead of a bare ansible-playbook call. The zero-flag convenience above has a real failure mode with no fallback: if ansible.cfg's auto-discovery doesn't fire (confirmed live, 2026-10-02, on a WSL drvfs/9p mount — Ansible silently refuses to load ansible.cfg found by searching a world-writable CWD), the render plugin never runs, _provision_inventory_render_dir never gets set, and the playbook hard-fails on its first argument-spec check with no inventory parsed at all. scripts/provision.sh passes -i and playbook_inventory_render_dir explicitly on the command line every time — bypassing ansible.cfg resolution entirely, since an explicit -i always wins regardless of CWD or filesystem — using a fresh mktemp -d per invocation, cleaned up on exit:

scripts/provision.sh \
  -e playbook_env_source=/opt/infra/testlab \
  -e playbook_platform=proxmox \
  -e playbook_node=pvetest \
  -e playbook_category=base \
  -e playbook_server_type=lxc \
  -e playbook_tech=mysql

Every example below works identically with scripts/provision.sh in place of ansible-playbook silvarion.provision.main (drop the -i/render-dir flags — the script adds its own).

# Deploy everything in testlab on all nodes
ansible-playbook silvarion.provision.main \
  -e playbook_env_source=/opt/infra/testlab \
  -e playbook_platform=proxmox \
  -e playbook_node=all \
  -e playbook_category=all \
  -e playbook_server_type=all \
  -e playbook_tech=all

# Deploy only MySQL containers on pvetest, base category
ansible-playbook silvarion.provision.main \
  -e playbook_env_source=/opt/infra/testlab \
  -e playbook_platform=proxmox \
  -e playbook_node=pvetest \
  -e playbook_category=base \
  -e playbook_server_type=lxc \
  -e playbook_tech=mysql

# Delete all base containers on pvetest (requires explicit confirmation)
ansible-playbook silvarion.provision.main \
  -e playbook_env_source=/opt/infra/testlab \
  -e playbook_platform=proxmox \
  -e playbook_node=pvetest \
  -e playbook_category=base \
  -e playbook_server_type=lxc \
  -e playbook_tech=all \
  -e playbook_action=delete \
  -e playbook_confirm_delete=true

# Run only the tech configuration phase (assumes containers/VMs already exist)
ansible-playbook silvarion.provision.main \
  -e playbook_env_source=/opt/infra/testlab \
  --tags tech

If ansible.cfg isn't being picked up — confirmed on a world-writable mount (e.g. WSL's drvfs/9p mounts, which report every file 777 regardless of real permissions): Ansible refuses to auto-load an ansible.cfg found by searching the current working directory when that directory is world-writable ([WARNING]: Ansible is being run in a world writable directory ... ignoring it as an ansible.cfg source). Point at it explicitly instead — this bypasses the auto-discovery check entirely:

ANSIBLE_CONFIG=./ansible.cfg ansible-playbook silvarion.provision.main -e ...

Overriding the render directory — pass -i/-e playbook_inventory_render_dir=<dir> (the old, pre-plugin flags) to bypass the plugin's own choice, e.g. for two fully isolated concurrent runs sharing nothing:

D=$(mktemp -d -p "${ANSIBLE_LOCAL_TEMP:-$HOME/.ansible/tmp}"); mkdir -p "$D/host_vars" "$D/group_vars" "$D/topology"
ansible-playbook silvarion.provision.main -i "$D" -e playbook_inventory_render_dir="$D" \
  -e playbook_env_source=/opt/infra/testlab ...

Using the inventory role individually

See roles/inventory/README.md's "Wiring it into a playbook" for the full -i/inventory_render_dir/meta: refresh_inventory pattern this role needs — it's the same requirement as running the main playbook above, just for a standalone use of this one role.

See roles/inventory/README.md and roles/proxmox/README.md for each role's full variable reference — kept there, next to the code, instead of duplicated here where it drifts out of sync.


Playbook Structure

File What it does
playbooks/main.yml Entry point — imports bootstrap_collection.yml, dynamic_inventory.yml, install_collections.yml, then infra_work.yml
playbooks/bootstrap_collection.yml Installs silvarion.provision itself (ansible.builtin only, no FQCN role/plugin references) — needed when this playbook was invoked by file path on a raw git checkout (e.g. a CI/CD runner) rather than by FQCN on an already-installed collection
playbooks/dynamic_inventory.yml Runs the inventory role to parse the env dir and register hosts
playbooks/install_collections.yml Installs every OTHER collection/role any matched host's ansible_roles_list references — before anything else runs, so Play 1 below has its own silvarion.provision.* roles available too, not just the tech roles Play 3 needs
playbooks/infra_work.yml 2 plays: provision (Play 1), configure via SSH (Play 3, tag tech) — numbered 1/3 not 1/2, since installing roles used to be this file's own Play 2 before it moved to install_collections.yml
playbooks/tech/run_role.yml Shared include_tasks — promotes one ansible_roles_list entry's vars, then include_role

Run only the tech-configuration phase (Play 3) with --tags tech, which assumes the containers/VMs from Play 1 already exist.


Playbook-level Variables (playbook_*)

Set as -e on silvarion.provision.main; each maps to the underlying role variable for that play.

Variable Default Description
playbook_env_source (required) Local path or git URL to the environment directory
playbook_inventory_render_dir _provision_inventory_render_dir (from the silvarion.provision.render inventory plugin) Directory to render host_vars/topology files into. Override only to bypass the plugin's own directory choice — see "Overriding the render directory" above
playbook_domain "" Domain suffix passed to the inventory role
playbook_platform proxmox Scope filter: platform subdirectory name, or all
playbook_node all Scope filter: node name, or all
playbook_category all Scope filter: category name, or all
playbook_server_type all Scope filter: lxc, vms, or all
playbook_tech all Scope filter: tech stack name, or all
playbook_service all Scope filter: service/purpose name, or all
playbook_action create create or delete
playbook_confirm_delete false Must be true to allow playbook_action=delete
playbook_throttle 1 Max concurrent provisioning operations
playbook_bootstrap false Run the proxmox role's LXC bootstrap steps, and (via env_bootstrap in the data repo's proxmox/metadata.yml) each tech role's own bootstrap steps (schema/user/root-password creation, etc.)
playbook_secure_logging true no_log on tasks handling secrets
playbook_debug false Verbose debug output
playbook_env_source_git_token "" Forgejo token to clone playbook_env_source with, when it's an HTTPS git URL rather than a local path
playbook_env_source_git_branch main Branch to check out when playbook_env_source is a git URL
playbook_env_source_cache_dir /tmp/silvarion-provision-env-source-cache Where playbook_env_source gets cloned to, when it's a git URL
playbook_ansible_collections_default_version (required — install_collections.yml only, when any role has source: git/localpath but no version of its own) Git ref to install a role at, when its ansible_roles_list entry doesn't pin its own version. Not a plain play var — resolved from the first matched host's own ansible_roles_list context (normally set once in the env_source's root metadata.yml, shared by every platform); -e still overrides

ansible_roles_list and ansible_target_roles_list are set per-host (leaf all.yml / host vars / -e), not as playbook_* vars — see Infrastructure Repository Structure above.


Sensitive Variables

Never store these in plaintext. Use ansible-vault, environment variables, or a secrets lookup (silvarion.provision.openbao / silvarion.secrets.openbao):

# Via ansible-vault encrypted group_vars
ansible-vault encrypt_string 'my-api-secret' --name proxmox_api_token_secret
# Or via an OpenBao lookup inline in a leaf all.yml — evaluated lazily,
# raises AnsibleError (not an empty string) if the lookup fails
proxmox_api_token_secret: "{{ lookup('silvarion.provision.openbao',
  server='https://openbao.example.com:8200',
  path='proxmox/ansible', key='api_token_secret',
  action='read') }}"

Collection Structure

silvarion.provision/
├── galaxy.yml                    # Collection metadata and dependencies
├── meta/
│   └── runtime.yml               # Ansible version requirement
├── plugins/
│   └── lookup/
│       ├── env_parser.py         # Walks the env dir, merges the 5-layer hierarchy
│       └── openbao.py            # OpenBao KV read/write/generate lookup
├── playbooks/
│   ├── main.yml                  # Entry point
│   ├── bootstrap_collection.yml  # Installs silvarion.provision itself, before anything else
│   ├── dynamic_inventory.yml     # Inventory registration
│   ├── install_collections.yml  # Installs every OTHER required collection/role
│   ├── infra_work.yml            # 2-play provision/configure pipeline
│   ├── tech/run_role.yml         # Shared per-role include
│   └── plugin_test.yml           # End-to-end test for the openbao lookup
├── requirements.yml               # Dependency installation shortcut
└── roles/
    ├── inventory/                 # Parse env dir, register hosts
    └── proxmox/                   # Create/delete LXC containers and VMs

License

MIT

Author

Jesus Alejandro Sanchez Davila jsanchez.consultant@gmail.com