From 5464e1180a0da6b6d03a8da163675edf189acbaa Mon Sep 17 00:00:00 2001 From: Eric Dropps Date: Wed, 28 Oct 2026 18:22:52 -0600 Subject: [PATCH] Move site variables to group_vars/all/main.yml so Ansible loads them When an inventory has both group_vars/all.yml and a group_vars/all/ directory, Ansible's vars loader matches the directory first and stops, so all.yml is silently ignored (DataLoader.find_vars_files breaks on the first match). The example inventory shipped exactly that layout, with all/ holding vault.yml.example, so every inventory created from it lost its callsign, grid, timezone, and other site values and fell back to role defaults. - Move inventory/example/group_vars/all.yml to group_vars/all/main.yml. - Point scripts/configure.py, init-local-inventory.sh, the variable reference generator, README, and docs at the new path. - Fail fast in site.yml, bootstrap.yml, and network.yml, and in configure.py validation, while the old file sits next to all/, so existing local inventories get told to move it instead of staying silently broken. - Have qa.py reject reintroducing the old example path. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 7 +++++++ README.md | 2 +- docs/concepts.md | 2 +- docs/configuration.md | 13 ++++++++++--- docs/first-deployment.md | 2 +- docs/getting-started.md | 2 +- group_vars/all.yml | 2 +- .../example/group_vars/{all.yml => all/main.yml} | 0 playbooks/bootstrap.yml | 13 +++++++++++++ playbooks/network.yml | 13 +++++++++++++ playbooks/site.yml | 13 +++++++++++++ scripts/configure.py | 11 ++++++++++- scripts/generate-variable-reference.py | 4 ++-- scripts/init-local-inventory.sh | 2 +- scripts/qa.py | 3 +++ 15 files changed, 77 insertions(+), 12 deletions(-) rename inventory/example/group_vars/{all.yml => all/main.yml} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 344c50a..4d3708e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,13 @@ releases when needed to establish a durable public contract. ### Fixed +- Site variables moved from `group_vars/all.yml` to `group_vars/all/main.yml` in the + example inventory, configurator, and docs. Ansible loads only the `group_vars/all/` + directory when both exist, so the old file (callsign, grid, timezone, and other + site values) was silently ignored. Existing local inventories must move the file: + `mv inventory/local/group_vars/all.yml inventory/local/group_vars/all/main.yml`. + `site.yml`, `bootstrap.yml`, and `network.yml` now fail fast while the old file + is present next to `group_vars/all/`. - `digital/ft8web`: added the missing nginx template that prevented the role from deploying. - `scripts/qa.py` now fails when a role task references a template or file the diff --git a/README.md b/README.md index c52c929..e7b3602 100644 --- a/README.md +++ b/README.md @@ -198,7 +198,7 @@ cp -R inventory/example inventory/local Then edit: - `inventory/local/hosts.yml` for your real hosts; -- `inventory/local/group_vars/all.yml` for site-wide identity and defaults; +- `inventory/local/group_vars/all/main.yml` for site-wide identity and defaults; - `inventory/local/group_vars/all/vault.yml` for encrypted site secrets; - `inventory/local/host_vars/.yml` for attached hardware and per-node role settings. diff --git a/docs/concepts.md b/docs/concepts.md index 6bd740c..aa06cb1 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -102,7 +102,7 @@ The common split is: inventory/local/hosts.yml Which Linux nodes exist and how Ansible reaches them. -inventory/local/group_vars/all.yml +inventory/local/group_vars/all/main.yml Site-wide defaults such as callsign, timezone, event name. inventory/local/host_vars/.yml diff --git a/docs/configuration.md b/docs/configuration.md index 836bd7d..babfd62 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -27,16 +27,23 @@ Then: 1. Edit `inventory/local/hosts.yml` and replace the example node with your real host(s). 2. Rename `inventory/local/host_vars/example-node.yml` so the filename matches the inventory hostname it configures. -3. Edit `inventory/local/group_vars/all.yml` with site-wide values such as callsign, grid, and timezone. +3. Edit `inventory/local/group_vars/all/main.yml` with site-wide values such as callsign, grid, and timezone. 4. Edit each host's `host_vars/.yml` to describe attached hardware and host-specific role settings. 5. If a role needs credentials, create `inventory/local/group_vars/all/vault.yml` with Ansible Vault. 6. Test the inventory with `ansible all -m ping`. +Site variables live in `group_vars/all/main.yml`, inside the same directory as the +Vault file. When both a `group_vars/all.yml` file and a `group_vars/all/` directory +exist, Ansible loads only the directory and silently ignores the file. Inventories +created before this layout should move their site file into `group_vars/all/`; +`playbooks/site.yml`, `playbooks/bootstrap.yml`, `playbooks/network.yml`, and +`scripts/configure.py` validation refuse to continue while the old file is present. + `inventory/local/` is ignored by Git and is the normal place for real deployment data. ## Secrets -Do not put real credentials in `group_vars/all.yml`, role defaults, README files, or the tracked example inventory. +Do not put real credentials in `group_vars/all/main.yml`, role defaults, README files, or the tracked example inventory. Create an encrypted local vault: @@ -53,7 +60,7 @@ HamStack uses these conceptual layers: ```text role defaults ↓ -site variables in inventory/local/group_vars/all.yml +site variables in inventory/local/group_vars/all/main.yml ↓ host variables in inventory/local/host_vars/.yml ↓ diff --git a/docs/first-deployment.md b/docs/first-deployment.md index 1c3c593..1f9a65f 100644 --- a/docs/first-deployment.md +++ b/docs/first-deployment.md @@ -83,7 +83,7 @@ hamstack_gatus_tls_host: hamstack-demo hamstack_gatus_https_port: 8443 ``` -Edit `inventory/local/group_vars/all.yml` and set at least: +Edit `inventory/local/group_vars/all/main.yml` and set at least: ```yaml hamstack_site_name: My HamStack diff --git a/docs/getting-started.md b/docs/getting-started.md index db71380..1a3a834 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -64,7 +64,7 @@ At minimum: 2. set the node's real IP address or DNS name; 3. rename `inventory/local/host_vars/example-node.yml` to match your hostname; 4. set your callsign and timezone in - `inventory/local/group_vars/all.yml`; + `inventory/local/group_vars/all/main.yml`; 5. enable the role(s) you actually want in that host's variables. Do not enable every role just because it exists. diff --git a/group_vars/all.yml b/group_vars/all.yml index 277665c..c350320 100644 --- a/group_vars/all.yml +++ b/group_vars/all.yml @@ -2,6 +2,6 @@ # Intentionally contains no site-specific values. # # HamStack operator configuration belongs under: -# inventory/local/group_vars/all.yml +# inventory/local/group_vars/all/main.yml # # Start by copying inventory/example to inventory/local. diff --git a/inventory/example/group_vars/all.yml b/inventory/example/group_vars/all/main.yml similarity index 100% rename from inventory/example/group_vars/all.yml rename to inventory/example/group_vars/all/main.yml diff --git a/playbooks/bootstrap.yml b/playbooks/bootstrap.yml index 5556149..a9e43aa 100644 --- a/playbooks/bootstrap.yml +++ b/playbooks/bootstrap.yml @@ -4,6 +4,19 @@ become: true pre_tasks: + - name: Reject site variables that Ansible would ignore + # With both group_vars/all.yml and group_vars/all/ present, Ansible loads + # only the directory and silently drops the file. + ansible.builtin.assert: + that: >- + not ((inventory_dir ~ '/group_vars/all.yml') is file + and (inventory_dir ~ '/group_vars/all') is directory) + fail_msg: >- + {{ inventory_dir }}/group_vars/all.yml is ignored because + group_vars/all/ also exists. Move it to group_vars/all/main.yml. + quiet: true + run_once: true + - name: Check for optional HamStack Vault file ansible.builtin.stat: path: "{{ inventory_dir }}/group_vars/all/vault.yml" diff --git a/playbooks/network.yml b/playbooks/network.yml index 094cbe8..60a29d3 100644 --- a/playbooks/network.yml +++ b/playbooks/network.yml @@ -7,6 +7,19 @@ become: false pre_tasks: + - name: Reject site variables that Ansible would ignore + # With both group_vars/all.yml and group_vars/all/ present, Ansible loads + # only the directory and silently drops the file. + ansible.builtin.assert: + that: >- + not ((inventory_dir ~ '/group_vars/all.yml') is file + and (inventory_dir ~ '/group_vars/all') is directory) + fail_msg: >- + {{ inventory_dir }}/group_vars/all.yml is ignored because + group_vars/all/ also exists. Move it to group_vars/all/main.yml. + quiet: true + run_once: true + - name: Check for optional HamStack Vault file ansible.builtin.stat: path: "{{ inventory_dir }}/group_vars/all/vault.yml" diff --git a/playbooks/site.yml b/playbooks/site.yml index ca62b65..cda2288 100644 --- a/playbooks/site.yml +++ b/playbooks/site.yml @@ -4,6 +4,19 @@ become: true pre_tasks: + - name: Reject site variables that Ansible would ignore + # With both group_vars/all.yml and group_vars/all/ present, Ansible loads + # only the directory and silently drops the file. + ansible.builtin.assert: + that: >- + not ((inventory_dir ~ '/group_vars/all.yml') is file + and (inventory_dir ~ '/group_vars/all') is directory) + fail_msg: >- + {{ inventory_dir }}/group_vars/all.yml is ignored because + group_vars/all/ also exists. Move it to group_vars/all/main.yml. + quiet: true + run_once: true + - name: Check for optional HamStack Vault file ansible.builtin.stat: path: "{{ inventory_dir }}/group_vars/all/vault.yml" diff --git a/scripts/configure.py b/scripts/configure.py index 3c50461..207af43 100644 --- a/scripts/configure.py +++ b/scripts/configure.py @@ -35,8 +35,11 @@ LOCAL = ROOT / "inventory" / "local" HOSTS_FILE = LOCAL / "hosts.yml" NETWORK_FILE = LOCAL / "network.yml" -SITE_FILE = LOCAL / "group_vars" / "all.yml" VAULT_DIR = LOCAL / "group_vars" / "all" +# Site variables live inside group_vars/all/: when both group_vars/all.yml and +# group_vars/all/ exist, Ansible loads only the directory. +SITE_FILE = VAULT_DIR / "main.yml" +LEGACY_SITE_FILE = LOCAL / "group_vars" / "all.yml" VAULT_FILE = VAULT_DIR / "vault.yml" VAULT_TEMPLATE = VAULT_DIR / "vault.yml.example" HOST_VARS = LOCAL / "host_vars" @@ -657,6 +660,12 @@ def validate() -> int: if "example-node" in hosts: warnings.append("example-node is still present in the local inventory.") + if LEGACY_SITE_FILE.exists(): + errors.append( + "inventory/local/group_vars/all.yml is ignored by Ansible next to group_vars/all/; " + "move it to inventory/local/group_vars/all/main.yml." + ) + site = load_yaml(SITE_FILE, {}) if site.get("hamstack_callsign") in {"", "N0CALL", None}: warnings.append("hamstack_callsign is still unset/N0CALL.") diff --git a/scripts/generate-variable-reference.py b/scripts/generate-variable-reference.py index 872b069..30ce767 100644 --- a/scripts/generate-variable-reference.py +++ b/scripts/generate-variable-reference.py @@ -79,8 +79,8 @@ def infer_purpose(name: str, role: str) -> str: def collect() -> dict[str, list[tuple[str, Any, str]]]: groups: dict[str, list[tuple[str, Any, str]]] = {} - site = load_yaml(ROOT / "inventory" / "example" / "group_vars" / "all.yml") - groups["site/global"] = [(k, v, "inventory/example/group_vars/all.yml") for k, v in site.items() if k.startswith("hamstack_")] + site = load_yaml(ROOT / "inventory" / "example" / "group_vars" / "all" / "main.yml") + groups["site/global"] = [(k, v, "inventory/example/group_vars/all/main.yml") for k, v in site.items() if k.startswith("hamstack_")] for defaults in sorted((ROOT / "roles").rglob("defaults/main.yml")): role = defaults.parent.parent.relative_to(ROOT / "roles").as_posix() data = load_yaml(defaults) diff --git a/scripts/init-local-inventory.sh b/scripts/init-local-inventory.sh index 3f5f90d..7e98a7a 100755 --- a/scripts/init-local-inventory.sh +++ b/scripts/init-local-inventory.sh @@ -19,7 +19,7 @@ Created inventory/local from inventory/example. Next: 1. Edit inventory/local/hosts.yml. 2. Rename inventory/local/host_vars/example-node.yml to match your host. - 3. Edit inventory/local/group_vars/all.yml. + 3. Edit inventory/local/group_vars/all/main.yml. 4. Create inventory/local/group_vars/all/vault.yml with ansible-vault if you need secrets. Template: inventory/local/group_vars/all/vault.yml.example 5. Run: ansible all -m ping diff --git a/scripts/qa.py b/scripts/qa.py index 60eaae7..461b5be 100644 --- a/scripts/qa.py +++ b/scripts/qa.py @@ -227,6 +227,7 @@ def check_deprecated_paths() -> None: forbidden = [ "inventory/local/group_vars/vault.yml", "inventory/example/group_vars/vault.example.yml", + "inventory/example/group_vars/all.yml", "examples/dc34-linbpq.yml", "roles/services/hamstack-dashboard", "hamstack_dashboard_", @@ -251,6 +252,8 @@ def check_deprecated_paths() -> None: deprecated_files = [ ROOT / "group_vars" / "all" / "vault.yml.example", ROOT / "inventory" / "example" / "group_vars" / "vault.example.yml", + # Shadowed by group_vars/all/; site variables live in group_vars/all/main.yml. + ROOT / "inventory" / "example" / "group_vars" / "all.yml", ROOT / "roles" / "packet" / "graywolf" / "main.yml", ] for path in deprecated_files: