A semantic change guard for Ansible inventories that detects unexpected infrastructure changes before they reach production.
Inventory Guard compares two Ansible inventory files (current vs. new) and flags changes that exceed your configured thresholds. Instead of blindly accepting inventory updates, you can:
- Catch accidents: Detect when a typo removes 50 hosts instead of 5
- Prevent drift: Alert when variable changes exceed expected patterns
- Gate CI/CD: Block merges or deployments when changes look suspicious
- Audit changes: Generate reports showing exactly what changed
With uv (recommended):
uv add inventory-guardWith pip:
pip install inventory-guardCompare two inventory files (silent on success):
# Using long flags (explicit)
inventory-guard \
--current inventory/prod.yml \
--new inventory/prod-updated.yml \
--max-host-change-pct 5.0 \
--max-var-change-pct 5.0
# Using short flags (concise)
inventory-guard -c inventory/prod.yml -n inventory/prod-updated.ymlGet verbose output to see what's happening:
inventory-guard -v -c inventory/prod.yml -n inventory/prod-updated.ymlGet JSON summary for further processing:
inventory-guard --json -c inventory/prod.yml -n inventory/prod-updated.yml | jqBy default, successful runs produce no output (Unix philosophy: no news is good
news). Use -v for INFO logs or --json for machine-readable output.
Create inventory_semantic_guard.toml:
[inventory_guard]
current = "inventory/prod.yml"
new = "inventory/prod-updated.yml"
max_host_change_pct = 5.0
max_var_change_pct = 5.0
max_host_change_abs = 10
max_var_change_abs = 50
# Ignore volatile keys that change frequently
ignore_key_regex = [
"^build_id$",
"^last_updated$",
"^timestamp$"
]
# Treat these as unordered sets (order doesn't matter)
set_like_key_regex = [
"^foreman_host_collections$"
]
# Optional outputs
json_out = "changes.json"
report = "changes.md"Then run without arguments:
inventory-guard--config PATH Path to TOML config (default:
./inventory_semantic_guard.toml)
-c, --current PATH Current inventory file (required unless in config)
-n, --new PATH New inventory file (required unless in config)
--max-host-change-pct N Max % of hosts that can be added/removed
(default: 5.0)
--max-var-change-pct N Max % of variable keys that can change
(default: 5.0)
--max-host-change-abs N Absolute cap on host changes (default: 0 = disabled)
--max-var-change-abs N Absolute cap on variable changes
(default: 0 = disabled)
--ignore-key-regex REGEX Variable keys to ignore (repeatable)
--set-like-key-regex REGEX Treat list values as unordered sets (repeatable)
-v, --verbose Increase verbosity (-v for INFO, -vv for DEBUG)
--json Output JSON summary to stdout
--json-out PATH Write JSON summary to file
--report PATH Write Markdown report to file
- Loads both inventories: Parses YAML with Ansible vault tag support
- Computes effective variables: Merges group vars and host vars following Ansible precedence
- Compares hosts: Detects added/removed hosts
- Compares variables: For common hosts, counts variable key additions, removals, and value changes
- Applies thresholds: Fails if changes exceed configured limits
- Generates reports: Outputs JSON summary and optional Markdown report
0: Success - changes are within acceptable thresholds1: Error - file not found, invalid YAML, bad configuration, etc.2: Guard failure - changes exceed configured thresholds
Inventory Guard follows Unix conventions for output:
- Success (exit 0): Silent by default. Use
-vfor INFO logs,-vvfor DEBUG logs - Errors (exit 1, 2): Error messages logged to stderr as JSON
- JSON output: Only to stdout when
--jsonflag is used - Reports: Written to files when
--json-outor--reportspecified
Logs are structured JSON on stderr for easy parsing:
{"timestamp": "2025-11-09T21:46:08", "level": "INFO", "message": "Starting inventory comparison"}
{"timestamp": "2025-11-09T21:46:08", "level": "ERROR", "message": "File not found: inventory.yml"}GitHub Actions:
# .github/workflows/inventory-check.yml
name: Inventory Check
on: [pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install inventory-guard
- run: inventory-guard -c inventory/prod.yml -n inventory/prod-new.ymlGitLab CI:
# .gitlab-ci.yml
inventory-check:
script:
- pip install inventory-guard
- inventory-guard -c inventory/prod.yml -n inventory/prod-new.yml
only:
- merge_requests# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: inventory-guard
name: Check inventory changes
entry: inventory-guard
language: system
pass_filenames: false# Generate a detailed report for manual review
inventory-guard \
--current prod.yml \
--new prod-updated.yml \
--report changes.md \
--json-out changes.json
# Review the report
less changes.md- CLI arguments (highest priority)
- Config file values
- Built-in defaults (lowest priority)
Inventory Guard parses !vault tags as opaque strings. Encrypted values are
compared as-is:
all:
hosts:
app-1:
db_password: !vault |
$ANSIBLE_VAULT;1.2;AES256;dev
66643866353263333266393931336439623433646634303233663831316665663234...For variables like host collections where order doesn't matter:
--set-like-key-regex '^foreman_host_collections$'This treats [A, B, C] and [C, A, B] as identical.
Some keys change on every run (timestamps, build IDs). Ignore them:
--ignore-key-regex '^build_id$' \
--ignore-key-regex '^generated_at$'Clone and setup:
git clone https://github.com/maartenq/inventory-guard.git
cd inventory_guard
task installRun tests and checks:
# Run tests
task test
# Run type checking
task type
# Run linting
task lint
# Run all checks (lint + type)
task checkThis project uses GitHub Actions for continuous integration and deployment:
- Quality Assurance: Runs on every push and PR
- Linting (pre-commit hooks)
- Type checking (ty + mypy)
- Tests with coverage reporting
- Release: Triggered by version tags (e.g.,
0.2.0,1.0.0a1)- Validates version is newer than PyPI
- Runs full test suite
- Builds distribution packages
- Publishes to PyPI automatically
- Creates GitHub release with notes
To release a new version:
# Update version in pyproject.toml and src/inventory_guard/__init__.py,
# commit, then:
uv sync
git add .
git commit
git push
git tag 0.4.0
git push --tags
# GitHub Actions will handle the restMIT License (see LICENSE file)
Issues and pull requests welcome at https://github.com/maartenq/inventory-guard
Development setup:
- Fork the repository
- Clone your fork:
git clone https://github.com/YOUR_USERNAME/inventory-guard.git - Install dependencies:
uv sync - Run tests:
uv run pytest - Run checks:
uv run mypy src/ && pre-commit run --all-files - Submit a pull request