- Python 95.1%
- Shell 4.1%
- Jinja 0.8%
|
All checks were successful
Ansible Lint / ansible-lint (push) Successful in 1m4s
Reviewed-on: #13 Reviewed-by: Jesus Alejandro Sanchez Davila <jsanchezd@noreply.git.silvarion.org> |
||
|---|---|---|
| .forgejo/workflows | ||
| changelogs | ||
| inventory | ||
| meta | ||
| playbooks | ||
| plugins | ||
| roles | ||
| scripts | ||
| .ansible-lint.yml | ||
| .gitignore | ||
| AGENTS.md | ||
| ansible.cfg | ||
| CHANGELOG.rst | ||
| galaxy.yml | ||
| README.md | ||
| requirements.yml | ||
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:
- Provision — for each registered host, run its
silvarion.provision.*roles (e.g.proxmox) withconnection: localto create the LXC container/VM. - Prepare the controller — install (via
ansible-galaxy) any tech role or collection referenced by a created host that isn't already installed. - 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
Via requirements.yml (recommended)
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