Skip to main content

Desired-state infrastructure convergence engine

Project description

CI Crates.io PyPI License: MIT codecov

verg

Desired-state infrastructure convergence engine. A fast, stateless alternative to Ansible, built in Rust.

Pre-1.0: verg is under active development. Expect breaking changes between minor versions.

Features

  • Fast - pushes a single static binary to each target over SSH; the agent executes locally, so there are no per-task SSH round-trips and no Python dependency on the target.
  • Stateless - every run reads actual system state and converges to desired state. No state files are kept on the control host between runs.
  • Simple - declare state in TOML files with Jinja2 templating for dynamic values. Strict config validation catches typos before any SSH connection is opened.
  • Agent-friendly - --json output, verg schema for machine-readable resource schemas, and structured exit codes make verg easy to drive from scripts and AI agents.
  • Secure by default - agent binary is checksum-verified before and after transfer; strict SSH host key checking; config validated locally before touching any target.

Install

# Cargo (crates.io)
cargo install verg

# PyPI
pip install verg

# Homebrew (tap)
brew install rvben/tap/verg

Quick Start

# Scaffold a new project in ./verg/
verg init

# Edit your inventory and desired state
$EDITOR verg/hosts.toml
$EDITOR verg/state/base.toml

# Preview what would change (dry-run, no modifications)
verg diff --targets all

# Apply to all hosts
verg apply --targets all

How It Works

verg (control CLI, runs on your machine) reads TOML state files and your host inventory, builds a host-specific bundle for each target, then pushes a compiled verg-agent binary over SSH. The agent reads the bundle from stdin, checks actual system state, converges each resource to desired state in dependency order, and returns a JSON summary to stdout. The control CLI aggregates results and presents them.

The agent is a static binary with no runtime dependencies. It is checksum-verified before deployment and runs entirely on the target - no per-resource SSH calls from the control host are needed once the agent is running.

Concepts

Project layout

A verg project lives in a directory named verg/ by default (override with --path or the VERG_PATH environment variable). Inside it:

verg/
  hosts.toml       # host inventory
  groups/          # one .toml per group, each with a [vars] table
  state/           # desired-state declarations; loaded in lexicographic order
  files/           # static files referenced by file resources
  templates/       # Jinja2 template files
  .verg/logs/      # structured apply logs (written automatically)

verg init scaffolds this layout with a starter hosts.toml and state/base.toml.

Inventory and selectors

Each host in hosts.toml has these fields:

Field Required Default Description
address yes - IP address or hostname
user no "root" SSH user
port no 22 SSH port
groups no [] Group memberships
[hosts.NAME.vars] no - Host-specific variables

The --targets flag accepts a selector expression:

Syntax Meaning
all Every host
web Hosts named web or in group web
a,b Union of selectors a and b
a:b Intersection (hosts in both a and b)
!x Exclude x
prod:!db In group prod but not group db

An unknown selector name is an error (exit code 6). Parentheses are not supported.

State files

State files live in state/ and are loaded in lexicographic order. Each file is a TOML document with this shape:

targets = ["web"]          # optional: limit to hosts in group/name "web"
                           # omit to apply to all hosts

[resource.<type>.<name>]   # one table per resource
property = "value"

A state file with no targets key applies to every host. A state file with targets = ["web"] applies only when the target host belongs to group web or has the name web. Only targets and resource are valid top-level keys; unrecognized keys are rejected by default.

See docs/resources.md for the full resource reference.

Variables and facts

Variables are interpolated using Jinja2 syntax ({{ var }}). Template sources:

Source Precedence
Host vars ([hosts.NAME.vars]) Highest
Group vars (groups/<g>.toml [vars]) Below host vars
Facts gathered at runtime Available as fact.arch, fact.os, etc.
Group membership Available as group.<name>
Inventory context (hosts.*, groups.*) Lowest

Available facts: fact.arch, fact.hostname, fact.os, fact.os_release, fact.os_version.

String values starting with $env. (e.g. password = "$env.MY_SECRET") are resolved from the environment at build time.

Inline content in a file resource is always interpolated. To interpolate a file loaded via source, set template = true.

cmd resources can register their stdout for use in downstream resources as {{ register.NAME }}. See docs/resources.md for details.

Ordering and handlers

Resources declare dependencies with after = ["type.name", ...]. verg sorts resources into execution layers using Kahn's topological sort; within a layer, resources run in FQN order. A resource whose dependency fails is skipped with "dependency failed".

Resources with handler = true are skipped unless another resource lists their FQN in notify. Handlers run after all normal resources; shorthand notify actions (e.g. reload:nginx, daemon-reload) run after handlers. Each shorthand runs at most once per host per run.

when expressions conditionally skip resources based on facts and group membership (e.g. when = "fact.os == 'ubuntu'").

See docs/resources.md for the full common attributes reference.

Resource Types

Type Purpose
pkg Install or remove system packages (apt-get, dnf, pacman - auto-detected)
file Manage a file's content, permissions, and ownership
directory Ensure a directory exists with the right permissions and ownership
service Manage a systemd service's running state and boot enablement
cmd Run a shell command with an idempotency guard
user Create or remove a system user
cron Manage a cron job file in /etc/cron.d/
sysctl Set a Linux kernel parameter
apt_repo Add or remove an APT repository with its GPG key
docker_compose Manage a Docker Compose stack
download Download a file from a URL, optionally extracting an archive

Complete Example

A minimal nginx setup using pkg, file, and service resources with dependency ordering.

# verg/hosts.toml
[hosts.web1]
address = "192.0.2.10"
user = "root"
groups = ["web"]

[hosts.web1.vars]
http_port = "80"
server_name = "example.com"
# verg/state/nginx.toml
targets = ["web"]

[resource.pkg.nginx]
name = "nginx"
state = "present"

[resource.file.nginx-conf]
path = "/etc/nginx/sites-available/default"
content = """
server {
    listen {{ http_port }};
    server_name {{ server_name }};
    root /var/www/html;
}
"""
mode = "0644"
after = ["pkg.nginx"]
notify = ["reload:nginx"]

[resource.service.nginx]
name = "nginx"
state = "running"
enabled = true
after = ["file.nginx-conf"]

The FQN for each resource is type.name (e.g. pkg.nginx, file.nginx-conf, service.nginx). Every after reference in this example resolves to a resource defined in the same file.

Commands

Command Key args Description
verg apply --targets <TARGETS> (required) Converge targets to desired state
verg diff --targets <TARGETS> (default: all), --limit, --offset, --fields Show what would change without applying
verg check --targets <TARGETS> (default: all) Verify targets match desired state (exits 0 when drift found, 1 when all match)
verg schema - Print resource type schemas as JSON
verg init --force Scaffold a new project directory
verg completions <bash|fish|zsh|powershell|elvish> Generate shell completions

apply has no default target. --targets is required to prevent accidental mass applies. Running apply in a non-interactive pipeline without --yes exits with code 5 (confirmation required).

Global Flags

Flag Default Description
-o, --output <auto|text|json> auto Output format; auto selects JSON when stdout is not a TTY
--json - Force JSON output (hidden alias for -o json)
-q, --quiet - Suppress per-resource lines; print only the final summary
-y, --yes - Proceed when stdin is not a TTY (required for CI/pipelines)
--path <PATH> ./verg Path to the verg project directory (VERG_PATH)
--parallel <N> 10 Maximum parallel SSH connections
--ssh-config <PATH> - Path to SSH config file (VERG_SSH_CONFIG)
--agent-dir <PATH> - Directory containing verg-agent binaries per architecture (VERG_AGENT_DIR)
--host-key-checking <yes|accept-new|no> yes SSH host key checking policy
--ssh-known-hosts <PATH> - Path to a known_hosts file
--skip-agent-checksum - Skip agent binary checksum verification (air-gapped or local builds)
--lax-config - Downgrade config validation errors (unknown keys/types) to warnings
--timeout <SECONDS> 600 Per-host timeout in seconds

Exit Codes

Code Name Meaning
0 SUCCESS Resources changed or drift detected
1 NOTHING_CHANGED All resources already in desired state
2 PARTIAL_FAILURE Some resources failed; others succeeded
3 TOTAL_FAILURE All resources on all targets failed
4 CONNECTION_ERROR All failures were SSH connection errors
5 INVALID_CONFIG Config is invalid, or apply requires --yes (ConfirmationRequired)
6 TARGET_NOT_FOUND No hosts matched the given target selector
7 INTERNAL_ERROR Unexpected internal error (I/O or other)
8 CONFLICT State conflict that cannot be automatically resolved
130 INTERRUPTED Process received SIGINT (Ctrl-C)

check exits 0 when drift is found and 1 when everything already matches. diff exits 0 on any successful run (even with no changes); only connection or partial failures yield 4 or 2.

Security

verg pushes a compiled agent binary to target hosts and runs it as the configured SSH user (root by default). The agent binary is SHA-256 verified locally before transfer and remotely after transfer using sha256sum -c. SSH host key checking is strict by default (StrictHostKeyChecking=yes). All state files are validated locally before any SSH connection is opened. Resources marked sensitive = true have their payload redacted from output and the apply changelog.

See SECURITY.md for the full threat model, supply chain details, secret handling, and vulnerability reporting.

License

MIT

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

verg-0.7.0.tar.gz (126.3 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

verg-0.7.0-py3-none-manylinux_2_28_x86_64.whl (2.4 MB view details)

Uploaded Python 3manylinux: glibc 2.28+ x86-64

verg-0.7.0-py3-none-manylinux_2_28_aarch64.whl (2.2 MB view details)

Uploaded Python 3manylinux: glibc 2.28+ ARM64

verg-0.7.0-py3-none-macosx_11_0_arm64.whl (2.1 MB view details)

Uploaded Python 3macOS 11.0+ ARM64

verg-0.7.0-py3-none-macosx_10_12_x86_64.whl (2.2 MB view details)

Uploaded Python 3macOS 10.12+ x86-64

File details

Details for the file verg-0.7.0.tar.gz.

File metadata

  • Download URL: verg-0.7.0.tar.gz
  • Upload date:
  • Size: 126.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for verg-0.7.0.tar.gz
Algorithm Hash digest
SHA256 8d6b93450691e979366bab921aa9b379ce2abbd62d5f0f59435501de7e5ab975
MD5 9a0e6c85d9a810fbb8e71e1506f23508
BLAKE2b-256 b5ee48295eaa47a300e1974f3001a1e65d7a10e5de8142c930fa715d18b4aebe

See more details on using hashes here.

File details

Details for the file verg-0.7.0-py3-none-manylinux_2_28_x86_64.whl.

File metadata

  • Download URL: verg-0.7.0-py3-none-manylinux_2_28_x86_64.whl
  • Upload date:
  • Size: 2.4 MB
  • Tags: Python 3, manylinux: glibc 2.28+ x86-64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for verg-0.7.0-py3-none-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 eff8a7f8c8b9fc2550376c28f0b8e4416390be50e51f9a41614dfa631bb795d5
MD5 16c8a5bd857e093ea160f5bf5dfd31d5
BLAKE2b-256 165e796d9fea3c3f401969a60f525d58b13168567939e39204acfb56e564519d

See more details on using hashes here.

File details

Details for the file verg-0.7.0-py3-none-manylinux_2_28_aarch64.whl.

File metadata

File hashes

Hashes for verg-0.7.0-py3-none-manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 06013d20aba922e26b8b369dd936d36bb6e81965d34ec3e2539ffd377647af78
MD5 c6e7232c0e3c36f685f0582008660943
BLAKE2b-256 1869cc7f88bc55625ddf375931e4e0f547ae5e88e6c166c62d329d06d5f0b538

See more details on using hashes here.

File details

Details for the file verg-0.7.0-py3-none-macosx_11_0_arm64.whl.

File metadata

  • Download URL: verg-0.7.0-py3-none-macosx_11_0_arm64.whl
  • Upload date:
  • Size: 2.1 MB
  • Tags: Python 3, macOS 11.0+ ARM64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for verg-0.7.0-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 7c50e14dc09be9ae9c45119ba16eccfec9dc7b9e65993b43711ae9288fedf420
MD5 1ca74814ffc1735f4817bd095ccac1ec
BLAKE2b-256 264e939e1775acab42717085b298a3e22b7cf21c4dd0a2cb8ecd8c5893f63726

See more details on using hashes here.

File details

Details for the file verg-0.7.0-py3-none-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for verg-0.7.0-py3-none-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 4b770f75b70398f90c279b20f2b6e85e2526985ad3fd048be2333894b433e78a
MD5 132862abdbc7cee3f970ae8b110b8b2c
BLAKE2b-256 7a09990cc94d17105bd3ff7236381a9439e9ddafd36def4b33c80beca5896020

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page