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 --helpMental 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 ./ansiblediff, 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 ./ansibleMulti-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.netenroll 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-configfor SSH harvesting.--ask-become-pass/-Kfor remote sudo prompts.--ask-key-passphraseor--ssh-key-passphrase-env ENV_VARfor encrypted SSH private keys.--no-sudofor a less complete remote harvest without sudo.--include-pathand--exclude-pathfor plain paths, globs,glob:,re:, orregex:patterns. Excludes win over includes.--dangerousdisables likely-secret filtering.--sops GPG_FINGERPRINT...writes an encryptedharvest.tar.gz.sops.--assume-safe-pathskips the root unsafe-PATHprompt 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-rolesAnsible output
- Single-site output contains
playbook.yml,roles/, role files, defaults, templates, andrequirements.yml. - Multi-site output uses host-specific inventory under
inventory/host_vars/<fqdn>/...and playbooks underplaybooks/. - Playbooks tag roles as
role_<role_name>, such asrole_usersorrole_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.netenroll 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 --sopsenroll 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-schemaRemote 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-jinjaturtleSupported 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