Skip to content

maartenq/inventory-guard

Repository files navigation

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

  1. Loads both inventories: Parses YAML with Ansible vault tag support
  2. Computes effective variables: Merges group vars and host vars following Ansible precedence
  3. Compares hosts: Detects added/removed hosts
  4. Compares variables: For common hosts, counts variable key additions, removals, and value changes
  5. Applies thresholds: Fails if changes exceed configured limits
  6. Generates reports: Outputs JSON summary and optional Markdown report

Exit Codes

  • 0: Success - changes are within acceptable thresholds
  • 1: 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 -v for INFO logs, -vv for DEBUG logs
  • Errors (exit 1, 2): Error messages logged to stderr as JSON
  • JSON output: Only to stdout when --json flag is used
  • Reports: Written to files when --json-out or --report specified

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

  1. CLI arguments (highest priority)
  2. Config file values
  3. 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:

  1. Fork the repository
  2. Clone your fork: git clone https://github.com/YOUR_USERNAME/inventory-guard.git
  3. Install dependencies: uv sync
  4. Run tests: uv run pytest
  5. Run checks: uv run mypy src/ && pre-commit run --all-files
  6. Submit a pull request

About

Semantic change guard for Ansible inventories - detect unexpected infrastructure chan ges

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors