This repository has been archived on 2026-06-24. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.
enroll.sh/src/content/_index.html
Miguel Jacq ef97d75b9f
All checks were successful
CI / test (push) Successful in 2m6s
More docs tweaks
2026-06-21 10:26:36 +10:00

424 lines
23 KiB
HTML

---
title: "Enroll"
html_title: "Enroll - Reverse-engineering servers into configuration management"
description: "Enroll inspects Debian-like and RedHat-like Linux hosts and generates Ansible, Puppet, or Salt manifests from what it finds. Harvest → Manifest → Manage."
og_title: "Enroll - Reverse-engineering servers into configuration management"
og_description: "Harvest a host's real configuration and turn it into Ansible, Puppet, or Salt code. Safe-by-default, with optional SOPS encryption."
og_type: "website"
---
<header class="hero py-5">
<div class="container py-4">
<div class="row align-items-center g-4">
<div class="col-lg-6">
<div class="kicker mb-3"><i class="bi bi-magic"></i> Reverse-engineering servers into Ansible, Puppet, or Salt</div>
<h1 class="display-5 fw-800 lh-1 mb-3" style="letter-spacing:-0.03em;">Get an existing Linux host into configuration management in seconds.</h1>
<p class="lead mb-4">Enroll inspects a Debian-like or RedHat-like system, harvests the state that matters, and generates Ansible roles, Puppet modules, or Salt states so you can bring snowflakes under management fast.</p>
<div class="d-flex flex-wrap gap-2 mb-4">
<a class="btn btn-dark btn-lg" href="#quickstart"><i class="bi bi-rocket-takeoff"></i> Quickstart</a>
<a class="btn btn-outline-dark btn-lg" href="#demos"><i class="bi bi-play-circle"></i> Watch demos</a>
<a class="btn btn-outline-secondary btn-lg" href="https://pypi.org/project/enroll/" target="_blank" rel="noreferrer"><i class="bi bi-box-seam"></i> PyPI</a>
<a class="btn btn-outline-secondary btn-lg" href="news.html"><i class="bi bi-megaphone"></i> News</a>
</div>
<div class="d-flex flex-wrap gap-2">
<span class="badge badge-soft rounded-pill px-3 py-2"><i class="bi bi-speedometer"></i> Super fast</span>
<span class="badge badge-soft rounded-pill px-3 py-2"><i class="bi bi-lock"></i> Optional SOPS encryption</span>
<span class="badge badge-soft rounded-pill px-3 py-2"><i class="bi bi-hdd-network"></i> Remote over SSH</span>
</div>
</div>
<div class="col-lg-6">
<div class="hero-card p-4 p-lg-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="fw-semibold"><i class="bi bi-terminal"></i> Config in the blink of an eye</div>
<div class="small text-muted">harvest once → render to your CM tool</div>
</div>
<div class="terminal">
<div class="mb-2"><span class="prompt">$</span> <code style="font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;">enroll harvest --out ./harvest</code></div>
<div class="mb-2"><span class="prompt">$</span> <code style="font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;">enroll manifest --harvest ./harvest --target ansible --out ./ansible</code></div>
<div class="mb-2"><span class="prompt">$</span> <code style="font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;">enroll manifest --harvest ./harvest --target puppet --out ./puppet</code></div>
<div class="mb-2"><span class="prompt">$</span> <code style="font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;">enroll manifest --harvest ./harvest --target salt --out ./salt</code></div>
<pre class="mb-0" style="white-space:pre-wrap; font-family:'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace; font-size:.9rem;">./ansible → playbook.yml, roles/, inventory/...
./puppet → manifests/site.pp, modules/, data/...
./salt → states/top.sls, states/roles/, pillar/...</pre>
</div>
<div class="smallprint mt-3">Tip: for multiple hosts, use <code>--fqdn</code> to generate target-native host data: Ansible inventory, Puppet Hiera, or Salt pillar.</div>
</div>
</div>
</div>
</div>
</header>
<section class="py-5" id="how" aria-label="How it works">
<div class="container">
<div class="row g-4 align-items-start">
<div class="col-lg-5">
<h2 class="section-title display-6 fw-bold mb-3">A simple mental model</h2>
<p class="mb-4">Enroll is built around two phases, plus optional drift and reporting tools:</p>
<div class="d-flex flex-column gap-3">
<div class="d-flex gap-3">
<div class="icon-pill"><i class="bi bi-bucket"></i></div>
<div>
<div class="fw-semibold">Harvest</div>
<div class="text-muted">Collect host facts + relevant files into a bundle.</div>
</div>
</div>
<div class="d-flex gap-3">
<div class="icon-pill"><i class="bi bi-diagram-3"></i></div>
<div>
<div class="fw-semibold">Manifest</div>
<div class="text-muted">Render Ansible roles/playbooks, Puppet modules/Hiera data, or Salt states/pillar from the harvest.</div>
</div>
</div>
<div class="d-flex gap-3">
<div class="icon-pill"><i class="bi bi-plus-slash-minus"></i></div>
<div>
<div class="fw-semibold">Diff</div>
<div class="text-muted">Compare two harvests and notify via webhook/email.</div>
</div>
</div>
<div class="d-flex gap-3">
<div class="icon-pill"><i class="bi bi-patch-question"></i></div>
<div>
<div class="fw-semibold">Explain</div>
<div class="text-muted">Analyze what's included/excluded in the harvest and why.</div>
</div>
</div>
<div class="d-flex gap-3">
<div class="icon-pill"><i class="bi bi-check-square"></i></div>
<div>
<div class="fw-semibold">Validate</div>
<div class="text-muted">Confirm that a harvest isn't corrupt or lacking artifacts.</div>
</div>
</div>
</div>
</div>
<div class="col-lg-7">
<div class="row g-3">
<div class="col-md-6">
<div class="card feature-card h-100">
<div class="card-body p-4">
<div class="fw-semibold mb-1">Safe-by-default harvesting</div>
<div class="text-muted">Enroll avoids likely secrets with a path denylist, content sniffing, and size caps - then lets you opt in to more aggressive collection when you're ready.</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card feature-card h-100">
<div class="card-body p-4">
<div class="fw-semibold mb-1">Ansible, Puppet, or Salt</div>
<div class="text-muted">The harvest bundle is renderer-neutral. Choose <code>--target ansible</code>, <code>--target puppet</code>, or <code>--target salt</code> at manifest time.</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card feature-card h-100">
<div class="card-body p-4">
<div class="fw-semibold mb-1">Fewer roles/modules where possible</div>
<div class="text-muted">By default, package/service snapshots are grouped by package <code>Section</code> or equivalent metadata to reduce role/module sprawl. Use <code>--no-common-roles</code>, or <code>--fqdn</code>, to keep output more host-specific.</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card feature-card h-100">
<div class="card-body p-4">
<div class="fw-semibold mb-1">Multi-site without "shared role broke host2"</div>
<div class="text-muted">In <code>--fqdn</code> mode, host-specific data moves into the target's native host-data layer: Ansible inventory, Puppet Hiera, or Salt pillar.</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card feature-card h-100">
<div class="card-body p-4">
<div class="fw-semibold mb-1">Remote over SSH</div>
<div class="text-muted">Harvest a remote host from your workstation, then render Ansible, Puppet, or Salt output locally.</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card feature-card h-100">
<div class="card-body p-4">
<div class="fw-semibold mb-1">Encrypt bundles at rest</div>
<div class="text-muted">Use <code>--sops</code> to store harvests/manifests as a single encrypted <code>.tar.gz.sops</code> file (GPG) for safer long-term storage as a DR strategy.</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="py-5 bg-light" id="quickstart" aria-label="Quickstart">
<div class="container">
<div class="row align-items-end g-3 mb-3">
<div class="col-lg-7">
<h2 class="section-title display-6 fw-bold mb-2">Quickstart</h2>
</div>
<div class="col-lg-5 text-lg-end">
<a class="btn btn-outline-dark" href="https://git.mig5.net/mig5/enroll#readme" target="_blank" rel="noreferrer"><i class="bi bi-journal-text"></i> Full README</a>
</div>
</div>
<ul class="nav nav-pills gap-2" id="qsTabs" role="tablist">
<li class="nav-item" role="presentation"><button class="nav-link active" id="qs1-tab" data-bs-toggle="tab" data-bs-target="#qs1" type="button" role="tab">Local</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" id="qs2-tab" data-bs-toggle="tab" data-bs-target="#qs2" type="button" role="tab">Remote</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" id="qs3-tab" data-bs-toggle="tab" data-bs-target="#qs3" type="button" role="tab">Multi-site</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" id="qs4-tab" data-bs-toggle="tab" data-bs-target="#qs4" type="button" role="tab">Diff</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" id="qs5-tab" data-bs-toggle="tab" data-bs-target="#qs5" type="button" role="tab">Explain</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" id="qs6-tab" data-bs-toggle="tab" data-bs-target="#qs6" type="button" role="tab">Validate</button></li>
</ul>
<div class="tab-content mt-3">
<div class="tab-pane fade show active" id="qs1" role="tabpanel" aria-labelledby="qs1-tab">
<div class="row g-3">
<div class="col-lg-6">
<div class="codeblock">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#qs1code"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="qs1code"># Harvest once
enroll harvest --out ./harvest
# Render and noop-test Ansible
enroll manifest --harvest ./harvest --target ansible --out ./ansible
ansible-playbook -i "localhost," -c local ./ansible/playbook.yml --check --diff
# Render and noop-test Puppet
enroll manifest --harvest ./harvest --target puppet --out ./puppet
puppet apply --modulepath ./puppet/modules ./puppet/manifests/site.pp --noop
# Render and noop-test Salt
enroll manifest --harvest ./harvest --target salt --out ./salt
salt-call --local --file-root ./salt/states state.apply test=True</code></pre>
</div>
</div>
<div class="col-lg-6">
<div class="callout p-4 h-100">
<div class="fw-semibold mb-2">Good for</div>
<div class="text-muted">Disaster recovery snapshots, "make this one host reproducible", and comparing how the same harvest looks in Ansible, Puppet, or Salt before you commit to a workflow.</div>
<hr>
<div class="smallprint">Using Ansible and want templates for structured configs? Install <a href="https://git.mig5.net/mig5/jinjaturtle" target="_blank" rel="noopener noreferrer">JinjaTurtle</a> and use <code>--jinjaturtle</code> (or let it auto-detect).</div>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="qs2" role="tabpanel" aria-labelledby="qs2-tab">
<div class="codeblock">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#qs2code"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="qs2code"># Remote harvest over SSH, then manifest locally
enroll single-shot \
--remote-host myhost.example.com \
--remote-user myuser \
--harvest /tmp/enroll-harvest \
--target ansible \
--out ./ansible \
--fqdn myhost.example.com</code></pre>
</div>
<div class="smallprint mt-3">If you don't want/need sudo on the remote host, add <code>--no-sudo</code> (expect a less complete harvest). For remote sudo prompts use <code>--ask-become-pass</code>/<code>-K</code>. If your SSH private key is encrypted, use <code>--ask-key-passphrase</code> (interactive) or <code>--ssh-key-passphrase-env ENV_VAR</code> (non-interactive/CI).</div>
</div>
<div class="tab-pane fade" id="qs3" role="tabpanel" aria-labelledby="qs3-tab">
<div class="codeblock">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#qs3code"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="qs3code"># Multi-site mode: one harvest, target-native host data
fqdn="$(hostname -f)"
enroll harvest --out /tmp/enroll-harvest
# Ansible: inventory/host_vars and per-host playbook
enroll manifest --harvest /tmp/enroll-harvest --target ansible --out ./ansible --fqdn "$fqdn"
ansible-playbook ./ansible/playbooks/${fqdn}.yml -i ./ansible/inventory/hosts.ini -c local --check --diff
# Puppet: Hiera node data, certname selects the host
enroll manifest --harvest /tmp/enroll-harvest --target puppet --out ./puppet --fqdn "$fqdn"
puppet apply --modulepath ./puppet/modules --hiera_config ./puppet/hiera.yaml --certname "$fqdn" ./puppet/manifests/site.pp --noop
# Salt: pillar node data, minion id selects the host
enroll manifest --harvest /tmp/enroll-harvest --target salt --out ./salt --fqdn "$fqdn"
salt-call --local --id "$fqdn" --file-root ./salt/states --pillar-root ./salt/pillar state.apply test=True</code></pre>
</div>
<div class="smallprint mt-3">Rule of thumb: single-site for "one server, easy-to-read roles/modules/states"; <code>--fqdn</code> for "many servers, host-specific data, fast adoption".</div>
</div>
<div class="tab-pane fade" id="qs4" role="tabpanel" aria-labelledby="qs4-tab">
<div class="codeblock">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#qs4code"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="qs4code"># Compare two harvests and get a human-friendly report (ignoring noise)
enroll diff --old /path/to/harvestA --new /path/to/harvestB --format markdown \
--exclude-path /var/anacron \
--ignore-package-versions
# Send a webhook when differences are detected
enroll diff \
--old /path/to/harvestA \
--new /path/to/harvestB \
--webhook https://example.net/webhook \
--webhook-format json \
--webhook-header 'X-Enroll-Secret: ...' \
--ignore-package-versions \
--exit-code
# Ignore a path and changes to package versions, and optionally
# enforce the old state locally (requires ansible-playbook)
enroll diff --old /path/to/harvestA --new /path/to/harvestB \
--exclude-path /var/anacron \
--ignore-package-versions \
--enforce</code></pre>
</div>
<div class="smallprint mt-3">E-mail notifications are also supported. Run it on a systemd timer to alert to drift!</div>
</div>
<div class="tab-pane fade" id="qs5" role="tabpanel" aria-labelledby="qs5-tab">
<div class="codeblock">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#qs5code"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="qs5code"># Explain what's in a harvest
enroll explain /path/to/harvest
# JSON format, and using a SOPS-encrypted harvest
enroll explain /path/to/harvest.sops \
--sops \
--format json
</code></pre>
</div>
<div class="smallprint mt-3">'explain' tells you why something was included, but also why something was <em>excluded</em>.</div>
</div>
<div class="tab-pane fade" id="qs6" role="tabpanel" aria-labelledby="qs6-tab">
<div class="codeblock">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#qs6code"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="qs6code"># Validate a harvest is correct.
enroll validate /path/to/harvest
# Check against the latest published version of the state schema specification
enroll validate /path/to/harvest --schema https://enroll.sh/schema/state.schema.json
</code></pre>
</div>
<div class="smallprint mt-3">'validate' makes sure the harvest's state conforms to Enroll's <a href="/schema.html">state schema</a>, doesn't contain orphaned artifacts and isn't missing any artifacts needed by the state. By default, it checks against the schema packaged with Enroll, but you can also check against the latest version on this site.</div>
</div>
</div>
</div>
</section>
<section class="py-5" id="demos" aria-label="Demos">
<div class="container">
<div class="row g-3 align-items-end mb-3">
<div class="col-lg-8">
<h2 class="section-title display-6 fw-bold mb-2">Demonstrations</h2>
</div>
</div>
<div class="row g-3">
<div class="col-md-6 col-lg-6">
<div class="card feature-card h-100">
<div class="card-body p-4">
<div class="fw-semibold mb-1">Harvest</div>
<div class="text-muted mb-3">Collect state into a bundle.</div>
<div class="asciicast" data-asciinema-id="765203"><script src="https://asciinema.org/a/765203.js" id="asciicast-765203" async="true"></script></div>
</div>
</div>
</div>
<div class="col-md-6 col-lg-6">
<div class="card feature-card h-100">
<div class="card-body p-4">
<div class="fw-semibold mb-1">Manifest</div>
<div class="text-muted mb-3">Render Ansible roles/playbooks, Puppet modules, or Salt states.</div>
<div class="asciicast" data-asciinema-id="765204"><script src="https://asciinema.org/a/765204.js" id="asciicast-765204" async="true"></script></div>
</div>
</div>
</div>
</div>
<div class="row g-3">
<div class="col-md-6 col-lg-6">
<div class="card feature-card h-100">
<div class="card-body p-4">
<div class="fw-semibold mb-1">Single-shot</div>
<div class="text-muted mb-3">Harvest → Manifest in one command.</div>
<div class="asciicast" data-asciinema-id="765127"><script src="https://asciinema.org/a/765127.js" id="asciicast-765127" async="true"></script></div>
</div>
</div>
</div>
<div class="col-md-6 col-lg-6">
<div class="card feature-card h-100">
<div class="card-body p-4">
<div class="fw-semibold mb-1">Diff</div>
<div class="text-muted mb-3">Drift report + webhook/email notifications, or optionally <em>enforce</em> the previous state!</div>
<div class="asciicast" data-asciinema-id="767081"><script src="https://asciinema.org/a/767081.js" id="asciicast-767081" async="true"></script></div>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="py-5" id="news" aria-label="News">
<div class="container">
<div class="callout p-4">
<div class="row align-items-center g-3">
<div class="col-lg-8">
<div class="kicker mb-2"><i class="bi bi-megaphone"></i> News</div>
<h2 class="h4 fw-bold mb-2">Enroll 0.7.0 adds Puppet and Salt rendering</h2>
<p class="text-secondary mb-0">The same harvest bundle can now render Ansible, Puppet, or Salt output, with target-native multi-host layouts and package-section grouping where possible.</p>
</div>
<div class="col-lg-4 text-lg-end">
<a class="btn btn-outline-dark" href="news.html"><i class="bi bi-newspaper"></i> Read release notes</a>
</div>
</div>
</div>
</div>
</section>
<section class="py-5 bg-light" id="install" aria-label="Install">
<div class="container">
<div class="row g-3 align-items-end mb-3">
<div class="col-lg-8">
<h2 class="section-title display-6 fw-bold mb-2">Install</h2>
<p class="text-muted mb-0">Use your preferred packaging. An AppImage is also available.</p>
</div>
<div class="col-lg-4 text-lg-end">
<a class="btn btn-outline-dark" href="https://git.mig5.net/mig5/enroll#install" target="_blank" rel="noreferrer"><i class="bi bi-link-45deg"></i>Other install steps</a>
</div>
</div>
<ul class="nav nav-pills gap-2" id="installTabs" role="tablist">
<li class="nav-item" role="presentation"><button class="nav-link active" data-bs-toggle="tab" data-bs-target="#inst1" type="button" role="tab">Debian/Ubuntu</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#inst2" type="button" role="tab">Fedora</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#inst3" type="button" role="tab">Pip</button></li>
</ul>
<div class="tab-content mt-3">
<div class="tab-pane fade show active" id="inst1" role="tabpanel">
<div class="codeblock">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#inst1code"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="inst1code">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</code></pre>
</div>
</div>
<div class="tab-pane fade" id="inst2" role="tabpanel">
<div class="codeblock">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#inst2code"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="inst2code">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</code></pre>
</div>
</div>
<div class="tab-pane fade" id="inst3" role="tabpanel">
<div class="codeblock">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#inst3code"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="inst3code">pip install enroll
# or: pipx install enroll</code></pre>
</div>
</div>
</div>
</div>
</section>