Documentation

How Enroll works today

Enroll harvests selected Linux host state, validates it, and renders Ansible configuration-management output. This documentation reflects the current Ansible-only CLI.

Install

Install from your preferred channel. The Python package requires Python 3.10 or newer.

$ pip install enroll
$ pipx install enroll

$ sudo mkdir -p /usr/share/keyrings
$ curl -fsSL https://mig5.net/static/mig5.asc | sudo gpg --dearmor -o /usr/share/keyrings/mig5.gpg
$ echo "deb [arch=amd64 signed-by=/usr/share/keyrings/mig5.gpg] https://apt.mig5.net $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/mig5.list
$ sudo apt update
$ sudo apt install enroll

$ sudo rpm --import https://mig5.net/static/mig5.asc
$ sudo tee /etc/yum.repos.d/mig5.repo > /dev/null << 'EOF'
[mig5]
name=mig5 Repository
baseurl=https://rpm.mig5.net/$releasever/rpm/$basearch
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://mig5.net/static/mig5.asc
EOF
$ sudo dnf upgrade --refresh
$ sudo dnf install enroll

$ poetry install
$ poetry run enroll --help

Mental model

Enroll works in two main phases. Harvest collects host facts and relevant files into a bundle. Manifest turns that bundle into Ansible configuration-management code.

$ enroll harvest --out ./harvest
$ enroll validate ./harvest
$ enroll manifest --harvest ./harvest --out ./ansible

diff, explain, and validate all operate on harvest bundles too.

Output modes: standalone and multi-host

Standalone project

The default for one host or a reusable starting point. Output must be a new directory. Raw files live in role files/ directories and generated settings in role defaults.

$ enroll manifest --harvest ./harvest --out ./ansible

Multi-host project

Start with --host, then add harvests using --host and --extend. Host settings go in inventory. Matching files stay in the role; differing files move to per-host inventory storage.

$ enroll manifest --harvest ./web1.harvest --host web1 --out ./ansible
$ enroll manifest --harvest ./web2.harvest --host web2 --out ./ansible --extend

enroll harvest

Harvest writes a bundle containing state.json and captured artifacts. It detects packages, services, changed config, unowned service files, non-system users and SSH public keys, miscellaneous /etc, selected symlinks, Flatpak/Snap state, Docker/Podman image metadata, and /usr/local scripts/config. Live writable sysctls and firewall state are opt-in; persistent configuration remains part of normal collection.

$ enroll harvest --out /tmp/enroll-harvest
$ enroll harvest --remote-host host.example.net --remote-user admin --out /tmp/enroll-harvest
$ enroll harvest --out /tmp/enroll-harvest --include-path '/home/*/.bashrc' --exclude-path '/usr/local/bin/docker-*'

Common flags

  • --remote-host, --remote-user, --remote-port, --remote-ssh-config for SSH harvesting.
  • --ask-become-pass / -K for remote sudo prompts.
  • --ask-key-passphrase or --ssh-key-passphrase-env ENV_VAR for encrypted SSH private keys.
  • --no-sudo for a less complete remote harvest without sudo.
  • --include-path and --exclude-path for plain paths, globs, glob:, re:, or regex: patterns. Excludes win over includes.
  • --dangerous disables likely-secret filtering.
  • --harvest-sysctl captures live writable sysctl values; --harvest-firewall captures live ipset/iptables state when corresponding persistent files are absent. Both work with remote harvest and single-shot.
  • --sops GPG_FINGERPRINT... writes an encrypted harvest.tar.gz.sops.
  • --assume-safe-path skips the root unsafe-PATH prompt in trusted automation.

enroll manifest

Manifest validates a harvest and renders Ansible output.

$ enroll manifest --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible
$ enroll manifest --harvest /tmp/web1.harvest --out /tmp/enroll-multihost --host web1.example.net
$ enroll manifest --harvest /tmp/web2.harvest --out /tmp/enroll-multihost --host web2.example.net --extend
$ enroll manifest --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible-separate-roles --no-common-roles
Trust warning: Enroll validates structure, but it cannot prove the desired state is safe. Only apply manifests generated from harvests whose provenance you trust.

Ansible output

  • Standalone output contains playbook.yml, roles/, role files, defaults, templates, ansible.cfg, and requirements.yml.
  • Multi-host output adds inventory/hosts.yml, inventory/host_vars/<host>/main.yml, playbooks/<host>.yml, capture notes under host_notes/, and project metadata under .enroll/. The root playbook imports each host play.
  • Generated task and handler implementations are shared when they match. Identical files and templates stay in the role. When one differs, Enroll moves that artifact for all hosts using the role into inventory/host_files/<host>/<role>/; later hosts get their own copy. Different task logic gets a separate role named after its host, such as httpd__host_ashpool_mig5_net_34f62c26a93d.
  • Grouped service roles share one restart handler across hosts. When notified, it restarts each active unit in that host's <role>_restart_units inventory list. A host with no active units has an empty list.
  • Playbooks tag roles as role_<role_name>, such as role_users. Check your generated playbook for the roles it contains.

Run --extend from outside the project. Use consistent renderer options for each host. Enroll rejects duplicate hosts, edited roles or shared artifacts needed for reuse, namespace collisions, conflicting collection requirements and unsafe project entries. Extension requires Linux atomic directory exchange; a named encrypted project must be unpacked before extension. Projects from the earlier multi-host format must be regenerated from harvests.

For captured firewall snapshots, firewall_runtime_persist defaults to false. Enable it in the generated variables only after checking for another firewall persistence mechanism; it installs enroll-firewall.service to restore rules at boot and reconcile them on each playbook run.

enroll single-shot

Single-shot runs harvest then manifest in one command. It supports remote SSH, include/exclude paths, --dangerous, --sops, --host, --extend, --no-common-roles, the opt-in runtime capture flags and JinjaTurtle flags. Each harvest output path must be new.

$ enroll single-shot --harvest /tmp/web1.harvest --out /tmp/enroll-ansible --host web1.example.net
$ enroll single-shot --remote-host web2.example.net --remote-user admin --harvest /tmp/web2.harvest --out /tmp/enroll-ansible --host web2.example.net --extend

enroll diff

Diff compares two harvest bundles and reports package, service, user, managed-file, symlink, directory and application-inventory drift. Inputs can be bundle directories, state.json paths, tarballs, or SOPS bundles when --sops is enabled.

$ enroll diff --old ./baseline --new ./current --format markdown
$ enroll diff --old ./baseline --new ./current --ignore-package-versions --exclude-path /var/anacron
$ enroll diff --old ./baseline --new ./current --webhook https://example.net/hook --webhook-format json --webhook-header 'Authorization: Bearer ...'

Use --exit-code to return status 2 when differences exist. Use --notify-always to send webhook/email even when there are no differences.

enroll explain

Explain produces a human or JSON summary of a harvest: package counts, roles, included reasons, excluded reasons, and example paths.

$ enroll explain ./harvest
$ enroll explain ./harvest --format json --max-examples 25
$ enroll explain ./harvest.tar.gz.sops --sops

enroll validate

Validate checks that state.json exists, is valid JSON, matches the vendored JSON Schema unless skipped, and references artifacts that actually exist. It also reports unreferenced files in artifacts/.

$ enroll validate ./harvest
$ enroll validate ./harvest --format json --out validate.json
$ enroll validate ./harvest --fail-on-warnings
$ enroll validate ./harvest --schema ./state.schema.json
$ enroll validate ./harvest --schema https://enroll.sh/schema/state.schema.json --allow-remote-schema

Remote schemas are not fetched unless --allow-remote-schema is explicitly supplied. Use --no-schema to skip schema checks while keeping consistency checks.

Sensitive data

By default, Enroll tries not to harvest likely secrets. It denies known sensitive paths, private key material, common certificate/private-key locations, credential-looking assignments, credential-bearing URIs, authorization headers, service account key names, and other obvious sensitive values.

--dangerous disables those checks. It may copy private keys, API tokens, TLS key material, database passwords, and other secrets into the harvest output in plaintext. Use it only intentionally, and strongly consider --sops.

Value-less comment mentions such as # token are tolerated so stock config files do not become impossible to harvest. A commented-out populated credential assignment is still treated as sensitive.

JinjaTurtle integration

If the jinjaturtle executable is on PATH, Enroll can turn supported config files into Ansible templates. The default mode uses JinjaTurtle when available. --jinjaturtle makes it required; --no-jinjaturtle disables it.

$ enroll manifest --harvest ./harvest --out ./ansible --jinjaturtle
$ enroll manifest --harvest ./harvest --out ./ansible --no-jinjaturtle

Supported suffixes include INI, CFG, JSON, TOML, YAML, XML, repo files, and common systemd unit file suffixes. Templates live in role templates/; variables live in role defaults or host vars depending on output mode.

Configuration file

Enroll can load INI-style defaults from --config, ENROLL_CONFIG, $XDG_CONFIG_HOME/enroll/enroll.ini, or ~/.config/enroll/enroll.ini. Current-directory config files are not auto-loaded; pass them explicitly.

[enroll]
assume_safe_path = false

[harvest]
dangerous = false
include_path =
  /home/*/.bashrc
  /home/*/.profile
exclude_path = /usr/local/bin/docker-*, /usr/local/bin/some-tool
# remote_host = host.example.net
# remote_user = admin

[manifest]
no_jinjaturtle = true
# sops = 54A91143AE0AB4F7743B01FE888ED1B423A3BC99

[diff]
exclude_path = /var/anacron
ignore_package_versions = true

[single-shot]
include_path = re:^/home/[^/]+/\.config/myapp/.*$

Precedence is explicit CLI flags, then INI config, then argparse defaults. For hyphenated flags, use underscores in the INI file.

Run generated Ansible

Install generated collection requirements first, then run in check mode before applying for real.

$ cd /tmp/enroll-ansible
$ ansible-galaxy collection install -r requirements.yml
$ ansible-playbook -i "localhost," -c local playbook.yml --check
$ ansible-playbook -i "localhost," -c local playbook.yml

# multi-host project
$ ansible-playbook -i inventory/hosts.yml playbook.yml --limit web1.example.net --check

# targeted role tags
$ ansible-playbook -i "localhost," -c local playbook.yml --tags role_users