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: single-site and multi-site

Single-site mode

Default mode. Use it for one server or for a golden role set. Raw files live in each role's files/, and template variables live in defaults/main.yml.

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

Multi-site mode

Enable with --fqdn. Roles are generic; host-specific state lives under Ansible inventory host_vars. This mode implies --no-common-roles.

$ enroll manifest --harvest ./harvest --out ./ansible --fqdn host.example.net

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, /usr/local scripts/config, writable sysctls, and fallback iptables/ipset runtime state when persistent files are absent.

$ 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.
  • --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. It does not support --target; current Enroll has no Puppet or Salt renderer.

$ enroll manifest --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible
$ enroll manifest --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible --fqdn host.example.net
$ enroll manifest --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible --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

  • Single-site output contains playbook.yml, roles/, role files, defaults, templates, and requirements.yml.
  • Multi-site output uses host-specific inventory under inventory/host_vars/<fqdn>/... and playbooks under playbooks/.
  • Playbooks tag roles as role_<role_name>, such as role_users or role_services.

enroll single-shot

Single-shot runs harvest then manifest in one command. It supports the relevant harvest and manifest flags, including remote SSH, include/exclude paths, --dangerous, --sops, --fqdn, --no-common-roles, and JinjaTurtle flags.

$ enroll single-shot --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible --fqdn "$(hostname -f)"
$ enroll single-shot --remote-host host.example.net --remote-user admin --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible --fqdn host.example.net

enroll diff

Diff compares two harvest bundles and reports package, service, user, and managed-file 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-site
$ ansible-playbook playbooks/host.example.net.yml --check

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