Semantic change guard for Ansible inventories - detect unexpected infrastructure changes
Project description
Inventory Guard
A semantic change guard for Ansible inventories that detects unexpected infrastructure changes before they reach production.
What It Does
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
Installation
With uv (recommended):
uv add inventory-guard
With pip:
pip install inventory-guard
Quick Start
Compare 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.yml
Get verbose output to see what's happening:
inventory-guard -v -c inventory/prod.yml -n inventory/prod-updated.yml
Get JSON summary for further processing:
inventory-guard --json -c inventory/prod.yml -n inventory/prod-updated.yml | jq
By default, successful runs produce no output (Unix philosophy: no news is good
news). Use -v for INFO logs or --json for machine-readable output.
Configuration File
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
CLI Options
--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
How It Works
- 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
Exit Codes
0: Success - changes are within acceptable thresholds1: Error - file not found, invalid YAML, bad configuration, etc.2: Guard failure - changes exceed configured thresholds
Output Behavior
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
Logging Format
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"}
Use Cases
CI/CD Pipeline
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.yml
GitLab 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 Hook
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: inventory-guard
name: Check inventory changes
entry: inventory-guard
language: system
pass_filenames: false
Manual Review
# 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
Configuration Precedence
- CLI arguments (highest priority)
- Config file values
- Built-in defaults (lowest priority)
Advanced Features
Ansible Vault Support
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...
Set-like Keys
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.
Ignoring Volatile Keys
Some keys change on every run (timestamps, build IDs). Ignore them:
--ignore-key-regex '^build_id$' \
--ignore-key-regex '^generated_at$'
Development
Clone and setup:
git clone https://github.com/maartenq/inventory-guard.git
cd inventory_guard
task install
Run tests and checks:
# Run tests
task test
# Run type checking
task type
# Run linting
task lint
# Run all checks (lint + type)
task check
CI/CD
This 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 rest
License
MIT License (see LICENSE file)
Contributing
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
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file inventory_guard-0.3.2.tar.gz.
File metadata
- Download URL: inventory_guard-0.3.2.tar.gz
- Upload date:
- Size: 12.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a6d09e900b651d8b43de12413b761cc6a06bd318f127a741ad4dadc66ed58125
|
|
| MD5 |
0f1dea32f7f865a6325d49d8114b5503
|
|
| BLAKE2b-256 |
1bff17cfc5ee8e69b77dc7d9749da908676aed9b36f120d148d4e8c2a6cda4c7
|
Provenance
The following attestation bundles were made for inventory_guard-0.3.2.tar.gz:
Publisher:
release.yaml on maartenq/inventory-guard
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
inventory_guard-0.3.2.tar.gz -
Subject digest:
a6d09e900b651d8b43de12413b761cc6a06bd318f127a741ad4dadc66ed58125 - Sigstore transparency entry: 1396602409
- Sigstore integration time:
-
Permalink:
maartenq/inventory-guard@30b8ce33344adbb1f83c4504310942184b445394 -
Branch / Tag:
refs/tags/0.3.2 - Owner: https://github.com/maartenq
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yaml@30b8ce33344adbb1f83c4504310942184b445394 -
Trigger Event:
push
-
Statement type:
File details
Details for the file inventory_guard-0.3.2-py3-none-any.whl.
File metadata
- Download URL: inventory_guard-0.3.2-py3-none-any.whl
- Upload date:
- Size: 14.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
243c55259a39ab3607e69fd3f22206f7912f51be443e4fe8eb89ca4edae7e986
|
|
| MD5 |
b77ea6c9a066dc81bc7bb3c2ef85a7df
|
|
| BLAKE2b-256 |
6e1cab83ff7dbb37d617b29f5ae387b0dda6de21777ba1a67cb21a808aee9777
|
Provenance
The following attestation bundles were made for inventory_guard-0.3.2-py3-none-any.whl:
Publisher:
release.yaml on maartenq/inventory-guard
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
inventory_guard-0.3.2-py3-none-any.whl -
Subject digest:
243c55259a39ab3607e69fd3f22206f7912f51be443e4fe8eb89ca4edae7e986 - Sigstore transparency entry: 1396602413
- Sigstore integration time:
-
Permalink:
maartenq/inventory-guard@30b8ce33344adbb1f83c4504310942184b445394 -
Branch / Tag:
refs/tags/0.3.2 - Owner: https://github.com/maartenq
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yaml@30b8ce33344adbb1f83c4504310942184b445394 -
Trigger Event:
push
-
Statement type: