Skip to content

Mantle 2.0 Templates Library

Overview

This page provides reference templates that can be used when building or customizing deployment workflows in Mantle 2.0. Each example is presented with a brief summary, implementation notes, and direct links to the source files so you can download, modify, and upload them to Mantle Assets.

Available Templates

Red Hat Enterprise Linux Kickstart

The RHEL Kickstart template is intended for automated Red Hat Enterprise Linux provisioning in PXE-driven or unattended installation workflows. It includes pre-install disk and RAID cleanup logic, static management network configuration, root and user account handling through template variables, LVM-based partitioning, post-install service enablement, and package selection for virtualization-capable hosts.

Full file: rhel-kickstart.j2

Variants: Minimal | Server | Server with GUI | Virtualization Host

What this template does

  • Clears existing RAID and volume group metadata before installation.
  • Wipes detected installation disks to reduce conflicts from previous deployments.
  • Configures a static management interface using templated values.
  • Supports templated root and standard user password handling.
  • Creates a UEFI-compatible disk layout with LVM-backed logical volumes.
  • Enables Cockpit and SSH after installation.
  • Updates SSH settings to allow password-based root access.
  • Prepares the host for virtualization workloads.

Key implementation notes

  • Uses Jinja-style variables (for example, {{ ip }}, {{ hostname }}, {{ mgmt_interface|default("eth0") }}) and is meant to be rendered before use.
  • The install target is driven by {{ install_device }}, and Mantle binds that value for each host entry when it renders the kickstart file.
  • The default LVM layout consumes roughly 720 GB before --grow allocations, so adjust the logvol sizes to satisfy your operational and security requirements.
  • Two %post sections provide the same logic. If duplication is not required in your workflow, you can consolidate them before uploading.
  • SELinux is set to permissive and SSH password logins for root are enabled. Review those settings for production-facing environments.

Kickstart template requirements

Provide the data points below so the Enterprise Linux PXE workflow can render a valid ks file for every host.

Asset references

  • platform: set to enterprise_linux so Mantle runs the Linux build path.
  • platform_ver: Enterprise Linux ISO asset ID used for PXE boot/repo paths.
  • kickstart_ver: kickstart asset ID; Mantle validates it against the ISO's major version.

PXE and management networking

  • pxe_server: HTTP/TFTP endpoint used by %pre, PXELINUX, and GRUB.
  • netmask: subnet mask applied in the kickstart network --bootproto=static stanza.
  • gateway: default route for the management interface during installation.
  • dns_servers[0]: first DNS server passed via --nameserver; add additional entries as needed.

Per-host variables

Mantle renders the kickstart once per host and injects each entry's fields as top-level variables (for example, {{ ip }} and {{ install_device }}).

  • ip: static IP each host should assume while installing.
  • hostname: hostname injected into the kickstart network stanza.
  • username: username for the non-root admin account created post-install.
  • pw: password for that account; Mantle hashes it before injecting pwd.
  • mgmt_interface: interface name used in the network command and post-install nmcli.
  • install_device: limits ignoredisk/bootloader targets; required when multiple disks exist.

Optional but recommended

  • serial_console: device and baud pair for GRUB/UEFI console redirection.
  • extra_kernel_args: site-specific kernel parameters appended to the installer command line.

Build-level prerequisites

  • build_interface: interface Mantle uses during provisioning; required to pass pre-flight checks.
  • bios_devices: either one EL8000 entry or a list matching the host count; enforces BIOS automation alignment.

When to use it

Use this template when you need a repeatable RHEL host build process that supports unattended deployment, standardized partitioning, and post-install virtualization readiness.

Minimal

Download: rhel-kickstart-minimal.j2 Quick reference copy

%pre
wget http://{{ pxe_server }}/started || true

udevadm settle
dmsetup remove_all

# De-activate any exiting Volume Groups
vgchange -an system
vgchange -an os

# Clear software raid devices if any
raid_devices=$(mktemp /tmp/mdstat.XXXXXXXXX)
grep '^md' /proc/mdstat | cut -d: -f1 > "$raid_devices"

if [ -s "$raid_devices" ]; then
    for raid in $(cat "$raid_devices"); do
        wipefs -f -a "/dev/$raid"
        mdadm --stop -f "/dev/$raid"
        if [ $? != "0" ]; then
            udevadm settle
            dmsetup remove_all
            mdadm --stop -f "/dev/$raid"
        fi
    done
else
    echo "All RAID devices are cleared"
fi

rm -vf "$raid_devices"

# Wipe every detected block device that looks like an install target
install_disks=$(lsblk -ndo NAME,TYPE | awk '$2 == "disk" {print "/dev/" $1}')

if [ -z "$install_disks" ]; then
    echo "No block devices detected to wipe"
else
    for disk in $install_disks; do
        echo "Wiping signatures on $disk"
        wipefs -f -a "$disk"
    done
fi

%end

# Accept Eula
eula --agreed

# Firewall configuration
firewall --enabled --ssh

# Keyboard layouts
keyboard 'us'

# Timezone
timezone Etc/UTC --isUtc

# Network information
network  --bootproto=static --device={{ mgmt_interface|default("eth0") }} --gateway={{ gateway }} --ip={{ ip }} --netmask={{ netmask }} {% if dns_servers %}--nameserver={{ dns_servers | first }}{% endif %}  --hostname={{ hostname }}

# Root PW
{% if pwd is defined and pwd %}
rootpw --iscrypted {{ pwd }}
{% elif pw is defined and pw %}
rootpw {{ pw }}
{% else %}
# rootpw not set
{% endif %}

# Reboot after installation
reboot

# Use text mode install
text

# System language
lang en_US

# SELinux configuration
selinux --permissive

{% if pwd is defined and pwd %}
user --name={{ username }} --groups=wheel --password={{ pwd }} --iscrypted
{% elif pw is defined and pw %}
user --name={{ username }} --groups=wheel --password={{ pw }}
{% else %}
# user password not set
{% endif %}

# Partition clearing information
clearpart --all --initlabel

# Disk partitioning information

## EFI partition (required for UEFI)
part /boot/efi --fstype="efi" --size=600 --fsoptions="umask=0077,shortname=winnt" --ondisk={{ install_device }}

## Boot partition
part /boot --fstype="xfs" --size=1024 --ondisk={{ install_device }}

##Create LVM physical volume
part pv.01 --size=16384 --grow --ondisk={{ install_device }}

## Create volume group
volgroup vg0 pv.01

## Logical volumes
logvol / --vg=vg0 --name=lv_root --size=20480 --fstype=xfs
logvol /home --vg=vg0 --name=lv_home --size=102400 --fstype=xfs
logvol /var --vg=vg0 --name=lv_var --size=500000 --grow --fstype=xfs
logvol /var/log --vg=vg0 --name=lv_var_log --size=20480 --fstype=xfs
logvol /var/log/audit --vg=vg0 --name=lv_var_log_audit --size=20480 --fstype=xfs
logvol /var/tmp --vg=vg0 --name=lv_var_tmp --size=20480 --fstype=xfs
logvol /tmp --vg=vg0 --name=lv_tmp --size=20480 --fstype=xfs
logvol swap --vg=vg0 --name=lv_swap --size=15360 --fstype=swap

%post --log=/root/ks-post.log
systemctl enable --now sshd

# Allow root login with password over SSH
sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config
sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config
systemctl restart sshd

nmcli connection modify {{ mgmt_interface|default("eth0") }} ipv6.method ignore
%end

%packages
@^minimal-environment
aide
audit
expect
fapolicyd
firewalld
kexec-tools
opensc
openscap
openscap-scanner
openssh-server
python3
openssl-pkcs11
policycoreutils
postfix
rng-tools
rsyslog
rsyslog-gnutls
scap-security-guide
tmux
usbguard
-abrt
-abrt-addon-ccpp
-abrt-addon-kerneloops
-abrt-cli
-abrt-plugin-sosreport
-iprutils
-krb5-server
-krb5-workstation
-libreport-plugin-logger
-python3-abrt-addon
-rsh-server
-sendmail
-telnet-server
-tftp-server
-tuned
-vsftpd
%end

Server

Download: rhel-kickstart-server.j2 Quick reference copy

%pre
wget http://{{ pxe_server }}/started || true

udevadm settle
dmsetup remove_all

# De-activate any exiting Volume Groups
vgchange -an system
vgchange -an os

# Clear software raid devices if any
raid_devices=$(mktemp /tmp/mdstat.XXXXXXXXX)
grep '^md' /proc/mdstat | cut -d: -f1 > "$raid_devices"

if [ -s "$raid_devices" ]; then
    for raid in $(cat "$raid_devices"); do
        wipefs -f -a "/dev/$raid"
        mdadm --stop -f "/dev/$raid"
        if [ $? != "0" ]; then
            udevadm settle
            dmsetup remove_all
            mdadm --stop -f "/dev/$raid"
        fi
    done
else
    echo "All RAID devices are cleared"
fi

rm -vf "$raid_devices"

# Wipe every detected block device that looks like an install target
install_disks=$(lsblk -ndo NAME,TYPE | awk '$2 == "disk" {print "/dev/" $1}')

if [ -z "$install_disks" ]; then
    echo "No block devices detected to wipe"
else
    for disk in $install_disks; do
        echo "Wiping signatures on $disk"
        wipefs -f -a "$disk"
    done
fi

%end

# Accept Eula
eula --agreed

# Firewall configuration
firewall --enabled --ssh

# Keyboard layouts
keyboard 'us'

# Timezone
timezone Etc/UTC --isUtc

# Network information
network  --bootproto=static --device={{ mgmt_interface|default("eth0") }} --gateway={{ gateway }} --ip={{ ip }} --netmask={{ netmask }} {% if dns_servers %}--nameserver={{ dns_servers | first }}{% endif %}  --hostname={{ hostname }}

# Root PW
{% if pwd is defined and pwd %}
rootpw --iscrypted {{ pwd }}
{% elif pw is defined and pw %}
rootpw {{ pw }}
{% else %}
# rootpw not set
{% endif %}

# Reboot after installation
reboot

# Use text mode install
text

# System language
lang en_US

# SELinux configuration
selinux --permissive

{% if pwd is defined and pwd %}
user --name={{ username }} --groups=wheel --password={{ pwd }} --iscrypted
{% elif pw is defined and pw %}
user --name={{ username }} --groups=wheel --password={{ pw }}
{% else %}
# user password not set
{% endif %}

# Partition clearing information
clearpart --all --initlabel

# Disk partitioning information

## EFI partition (required for UEFI)
part /boot/efi --fstype="efi" --size=600 --fsoptions="umask=0077,shortname=winnt" --ondisk={{ install_device }}

## Boot partition
part /boot --fstype="xfs" --size=1024 --ondisk={{ install_device }}

##Create LVM physical volume
part pv.01 --size=16384 --grow --ondisk={{ install_device }}

## Create volume group
volgroup vg0 pv.01

## Logical volumes
logvol / --vg=vg0 --name=lv_root --size=20480 --fstype=xfs
logvol /home --vg=vg0 --name=lv_home --size=102400 --fstype=xfs
logvol /var --vg=vg0 --name=lv_var --size=500000 --grow --fstype=xfs
logvol /var/log --vg=vg0 --name=lv_var_log --size=20480 --fstype=xfs
logvol /var/log/audit --vg=vg0 --name=lv_var_log_audit --size=20480 --fstype=xfs
logvol /var/tmp --vg=vg0 --name=lv_var_tmp --size=20480 --fstype=xfs
logvol /tmp --vg=vg0 --name=lv_tmp --size=20480 --fstype=xfs
logvol swap --vg=vg0 --name=lv_swap --size=15360 --fstype=swap

%post --log=/root/ks-post.log
systemctl enable --now sshd

# Allow root login with password over SSH
sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config
sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config
systemctl restart sshd

for drv in qemu network nodedev nwfilter secret storage interface; do systemctl start virt${drv}d{,-ro,-admin}.socket; done
   virt-host-validate

nmcli connection modify {{ mgmt_interface|default("eth0") }} ipv6.method ignore

%end

%post --log=/root/ks-post.log
systemctl enable --now sshd

# Allow root login with password over SSH
sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config
sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config
systemctl restart sshd

nmcli connection modify {{ mgmt_interface|default("eth0") }} ipv6.method ignore

%end

%packages
@^server-product-environment
@standard
aide
audit
expect
fapolicyd
firewalld
kexec-tools
opensc
openscap
openscap-scanner
openssh-server
python3
openssl-pkcs11
policycoreutils
postfix
rng-tools
rsyslog
rsyslog-gnutls
scap-security-guide
tmux
usbguard
-abrt
-abrt-addon-ccpp
-abrt-addon-kerneloops
-abrt-cli
-abrt-plugin-sosreport
-iprutils
-krb5-server
-krb5-workstation
-libreport-plugin-logger
-python3-abrt-addon
-rsh-server
-sendmail
-telnet-server
-tftp-server
-tuned
-vsftpd
%end

Server with GUI

Download: rhel-kickstart-server-with-gui.j2 Quick reference copy

%pre
wget http://{{ pxe_server }}/started || true

udevadm settle
dmsetup remove_all

# De-activate any exiting Volume Groups
vgchange -an system
vgchange -an os

# Clear software raid devices if any
raid_devices=$(mktemp /tmp/mdstat.XXXXXXXXX)
grep '^md' /proc/mdstat | cut -d: -f1 > "$raid_devices"

if [ -s "$raid_devices" ]; then
    for raid in $(cat "$raid_devices"); do
        wipefs -f -a "/dev/$raid"
        mdadm --stop -f "/dev/$raid"
        if [ $? != "0" ]; then
            udevadm settle
            dmsetup remove_all
            mdadm --stop -f "/dev/$raid"
        fi
    done
else
    echo "All RAID devices are cleared"
fi

rm -vf "$raid_devices"

# Wipe every detected block device that looks like an install target
install_disks=$(lsblk -ndo NAME,TYPE | awk '$2 == "disk" {print "/dev/" $1}')

if [ -z "$install_disks" ]; then
    echo "No block devices detected to wipe"
else
    for disk in $install_disks; do
        echo "Wiping signatures on $disk"
        wipefs -f -a "$disk"
    done
fi

%end

# Accept Eula
eula --agreed

# Firewall configuration
firewall --enabled --ssh

# Keyboard layouts
keyboard 'us'

# Timezone
timezone Etc/UTC --isUtc

# Network information
network  --bootproto=static --device={{ mgmt_interface|default("eth0") }} --gateway={{ gateway }} --ip={{ ip }} --netmask={{ netmask }} {% if dns_servers %}--nameserver={{ dns_servers | first }}{% endif %}  --hostname={{ hostname }}

# Root PW
{% if pwd is defined and pwd %}
rootpw --iscrypted {{ pwd }}
{% elif pw is defined and pw %}
rootpw {{ pw }}
{% else %}
# rootpw not set
{% endif %}

# Reboot after installation
reboot

# Use text mode install
text

# System language
lang en_US

# SELinux configuration
selinux --permissive

{% if pwd is defined and pwd %}
user --name={{ username }} --groups=wheel --password={{ pwd }} --iscrypted
{% elif pw is defined and pw %}
user --name={{ username }} --groups=wheel --password={{ pw }}
{% else %}
# user password not set
{% endif %}

# Partition clearing information
clearpart --all --initlabel

# Disk partitioning information

## EFI partition (required for UEFI)
part /boot/efi --fstype="efi" --size=600 --fsoptions="umask=0077,shortname=winnt" --ondisk={{ install_device }}

## Boot partition
part /boot --fstype="xfs" --size=1024 --ondisk={{ install_device }}

##Create LVM physical volume
part pv.01 --size=16384 --grow --ondisk={{ install_device }}

## Create volume group
volgroup vg0 pv.01

## Logical volumes
logvol / --vg=vg0 --name=lv_root --size=20480 --fstype=xfs
logvol /home --vg=vg0 --name=lv_home --size=102400 --fstype=xfs
logvol /var --vg=vg0 --name=lv_var --size=500000 --grow --fstype=xfs
logvol /var/log --vg=vg0 --name=lv_var_log --size=20480 --fstype=xfs
logvol /var/log/audit --vg=vg0 --name=lv_var_log_audit --size=20480 --fstype=xfs
logvol /var/tmp --vg=vg0 --name=lv_var_tmp --size=20480 --fstype=xfs
logvol /tmp --vg=vg0 --name=lv_tmp --size=20480 --fstype=xfs
logvol swap --vg=vg0 --name=lv_swap --size=15360 --fstype=swap

%post --log=/root/ks-post.log
systemctl enable --now sshd

# Allow root login with password over SSH
sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config
sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config
systemctl restart sshd

nmcli connection modify {{ mgmt_interface|default("eth0") }} ipv6.method ignore

%end

%packages
@^server-with-gui-environment
@graphical-server-environment
@graphical-admin-tools
@gnome-desktop
aide
audit
expect
fapolicyd
firewalld
kexec-tools
opensc
openscap
openscap-scanner
openssh-server
python3
openssl-pkcs11
policycoreutils
postfix
rng-tools
rsyslog
rsyslog-gnutls
scap-security-guide
tmux
usbguard
-abrt
-abrt-addon-ccpp
-abrt-addon-kerneloops
-abrt-cli
-abrt-plugin-sosreport
-iprutils
-krb5-server
-krb5-workstation
-libreport-plugin-logger
-python3-abrt-addon
-rsh-server
-sendmail
-telnet-server
-tftp-server
-tuned
-vsftpd
%end

Virtualization Host

Download: rhel-kickstart-virtualization-host.j2 Quick reference copy

%pre
wget http://{{ pxe_server }}/started || true

udevadm settle
dmsetup remove_all

# De-activate any exiting Volume Groups
vgchange -an system
vgchange -an os

# Clear software raid devices if any
raid_devices=$(mktemp /tmp/mdstat.XXXXXXXXX)
grep '^md' /proc/mdstat | cut -d: -f1 > "$raid_devices"

if [ -s "$raid_devices" ]; then
    for raid in $(cat "$raid_devices"); do
        wipefs -f -a "/dev/$raid"
        mdadm --stop -f "/dev/$raid"
        if [ $? != "0" ]; then
            udevadm settle
            dmsetup remove_all
            mdadm --stop -f "/dev/$raid"
        fi
    done
else
    echo "All RAID devices are cleared"
fi

rm -vf "$raid_devices"

# Wipe every detected block device that looks like an install target
install_disks=$(lsblk -ndo NAME,TYPE | awk '$2 == "disk" {print "/dev/" $1}')

if [ -z "$install_disks" ]; then
    echo "No block devices detected to wipe"
else
    for disk in $install_disks; do
        echo "Wiping signatures on $disk"
        wipefs -f -a "$disk"
    done
fi

%end

# Accept Eula
eula --agreed

# Firewall configuration
firewall --enabled --ssh

# Keyboard layouts
keyboard 'us'

# Timezone
timezone Etc/UTC --isUtc

# Network information
network  --bootproto=static --device={{ mgmt_interface|default("eth0") }} --gateway={{ gateway }} --ip={{ ip }} --netmask={{ netmask }} {% if dns_servers %}--nameserver={{ dns_servers | first }}{% endif %}  --hostname={{ hostname }}

# Root PW
{% if pwd is defined and pwd %}
rootpw --iscrypted {{ pwd }}
{% elif pw is defined and pw %}
rootpw {{ pw }}
{% else %}
# rootpw not set
{% endif %}

# Reboot after installation
reboot

# Use text mode install
text

# System language
lang en_US

# SELinux configuration
selinux --permissive

{% if pwd is defined and pwd %}
user --name={{ username }} --groups=wheel --password={{ pwd }} --iscrypted
{% elif pw is defined and pw %}
user --name={{ username }} --groups=wheel --password={{ pw }}
{% else %}
# user password not set
{% endif %}

# Partition clearing information
clearpart --all --initlabel

# Disk partitioning information

## EFI partition (required for UEFI)
part /boot/efi --fstype="efi" --size=600 --fsoptions="umask=0077,shortname=winnt" --ondisk={{ install_device }}

## Boot partition
part /boot --fstype="xfs" --size=1024 --ondisk={{ install_device }}

##Create LVM physical volume
part pv.01 --size=16384 --grow --ondisk={{ install_device }}

## Create volume group
volgroup vg0 pv.01

## Logical volumes
logvol / --vg=vg0 --name=lv_root --size=20480 --fstype=xfs
logvol /home --vg=vg0 --name=lv_home --size=102400 --fstype=xfs
logvol /var --vg=vg0 --name=lv_var --size=500000 --grow --fstype=xfs
logvol /var/log --vg=vg0 --name=lv_var_log --size=20480 --fstype=xfs
logvol /var/log/audit --vg=vg0 --name=lv_var_log_audit --size=20480 --fstype=xfs
logvol /var/tmp --vg=vg0 --name=lv_var_tmp --size=20480 --fstype=xfs
logvol /tmp --vg=vg0 --name=lv_tmp --size=20480 --fstype=xfs
logvol swap --vg=vg0 --name=lv_swap --size=15360 --fstype=swap

%post --log=/root/ks-post.log
systemctl enable --now cockpit.socket
systemctl enable --now sshd

# Allow root login with password over SSH
sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config
sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config
systemctl restart sshd

for drv in qemu network nodedev nwfilter secret storage interface; do systemctl start virt${drv}d{,-ro,-admin}.socket; done
   virt-host-validate

nmcli connection modify {{ mgmt_interface|default("eth0") }} ipv6.method ignore

%end

%packages
@^virtualization-host-environment
@^graphical-server-environment
@graphical-admin-tools
@virtualization-client
@virtualization-hypervisor
@virtualization-tools
aide
audit
expect
fapolicyd
firewalld
kexec-tools
qemu-kvm
cockpit-machines
virt-v2v
opensc
openscap
openscap-scanner
openssh-server
python3
openssl-pkcs11
policycoreutils
postfix
rng-tools
rsyslog
rsyslog-gnutls
scap-security-guide
tmux
usbguard
virt-install
virt-viewer
-abrt
-abrt-addon-ccpp
-abrt-addon-kerneloops
-abrt-cli
-abrt-plugin-sosreport
-iprutils
-krb5-server
-krb5-workstation
-libreport-plugin-logger
-python3-abrt-addon
-rsh-server
-sendmail
-telnet-server
-tftp-server
-tuned
-vsftpd
%end

Additional Quick Templates

The sections below provide smaller starter snippets for other Mantle workflows.

VMware Datacenter Build: Host & Network JSON

Match the VMware Datacenter Build form fields and keep a clean, version-controlled baseline for each program.

{
  "esxi": {
    "hosts": [
      {
        "host_ip": "10.20.30.11",
        "hostname": "esxi-a.mantle.lab",
        "username": "root",
        "password": "CHANGEME",
        "subnet_mask": "255.255.255.0",
        "default_gateway": "10.20.30.1",
        "dns_server_0": "10.20.1.10",
        "dns_server_1": "10.20.1.11",
        "ntp_server_0": "pool.ntp.org",
        "ntp_server_1": "time.navy.mil",
        "vlan": 20,
        "vmk0_mtu": 1500,
        "enable_ssh": true
      }
    ],
    "vSwitch0": {
      "standard_uplink": "vmnic0",
      "additional_uplinks": ["vmnic1"],
      "vm_network": "Mgmt",
      "vlan": 20,
      "mtu": 1500
    }
  }
}

Physical Network Device Template (Jinja2)

Use this baseline for serial-driven network builds referenced in Physical Network Builds.

hostname {{ device.hostname }}
!
interface mgmt0
  description Mantle Mgmt
  ip address {{ device.mgmt_ip }} {{ device.mgmt_mask }}
  no shutdown
!
interface {{ device.build_interface }}
  description Mantle Build VLAN {{ device.build_vlan }}
  switchport trunk encapsulation dot1q
  switchport trunk allowed vlan {{ device.allowed_vlans|join(',') }}
!
ip route 0.0.0.0/0 {{ device.default_gateway }}
username {{ device.username }} privilege 15 secret {{ device.password }}
ip domain-name {{ device.domain }}
ip name-server {{ device.dns_primary }} {{ device.dns_secondary }}
line vty 0 4
  transport input ssh
  login local
end

Cisco CSR1000v / C8000v Base Configuration

Leverage this IOS-XE template when staging Mantle-managed CSR1000v or C8000v routers. It aligns with the Physical Network Builds workflow, covering AAA, management plane hardening, core interfaces, and VTY controls. Download the full file at csr1000v-base-config.j2.

!
! Cisco CSR1000v / C8000v General Base Configuration Template
!
! Enable dna-advantage
license boot level network-advantage addon dna-advantage
!
hostname {{ hostname }}
!
service timestamps debug datetime msec
service timestamps log datetime msec
service password-encryption
no ip domain-lookup
ip domain name {{ domain_name }}
!
username {{ admin_username }} privilege 15 secret {{ admin_secret }}
enable secret {{ enable_secret }}
!
clock timezone UTC 0 0
!
! DNS / Name Resolution
!
ip name-server {{ dns_server_1 }}
!
! Local Banner
!
banner motd ^
{{ motd_banner }}
^
!
! SSH Configuration
!
crypto key generate rsa modulus {{ rsa_modulus }}
ip ssh version 2
ip ssh time-out {{ ssh_timeout }}
ip ssh authentication-retries {{ ssh_auth_retries }}
!
! AAA
!
aaa new-model
aaa authentication login default local
aaa authorization exec default local
!
! Logging
!
logging buffered 64000
logging console warnings
!
! NTP
!
ntp server {{ ntp_server_1 }}
!
! Loopback Interface
!
interface Loopback0
 description {{ loopback_description }}
 ip address {{ loopback_ip }} {{ loopback_mask }}
 no shutdown
!
! Management Interface
!
interface GigabitEthernet1
 description {{ mgmt_description }}
 ip address {{ mgmt_ip }} {{ mgmt_mask }}
 no shutdown
!
! WAN / Uplink Interface
!
interface GigabitEthernet2
 description {{ wan_description }}
 ip address {{ wan_ip }} {{ wan_mask }}
 no shutdown
!
! LAN Interface
!
interface GigabitEthernet3
 description {{ lan_description }}
 ip address {{ lan_ip }} {{ lan_mask }}
 no shutdown
!
! Default Route
!
ip route 0.0.0.0 0.0.0.0 {{ default_gateway }}
!
! VTY / Console Access
!
line console 0
 logging synchronous
 exec-timeout {{ console_exec_timeout_minutes }} {{ console_exec_timeout_seconds }}
 login local
!
line vty 0 4
 transport input ssh
 exec-timeout {{ vty_exec_timeout_minutes }} {{ vty_exec_timeout_seconds }}
 login local
!
line vty 5 15
 transport input ssh
 exec-timeout {{ vty_exec_timeout_minutes }} {{ vty_exec_timeout_seconds }}
 login local
!
! Save Configuration
!
end
wr mem

Mobile Provision Baseline Stub

Reference this JSON for Mobile Device Provisions baselines.

{
  "baseline_name": "Mantle-Mobile-Standard",
  "apks": ["ATAK-4.9.0.apk", "MantleMobile.apk"],
  "configs": ["atak_config.json", "vpn_profile.json"],
  "prep": {
    "wifi_ssid": "EDGE-LAB",
    "wifi_password": "SUPPORT123",
    "screen_timeout_minutes": 10
  },
  "devices": [
    {
      "serial": "RFCR91XYZ",
      "pin": "1234",
      "unique_files": ["/configs/unit1.json"]
    }
  ]
}

Mantle Flow DAG Skeleton

Use this Airflow DAG to validate Mantle Flow licensing and execution described in Mantle Flow.

"""
BASE ANSIBLE DAG TEMPLATE  (with XCom + remediation sample)
=========================================================

**Purpose**  Create Airflow DAGs that trigger Ansible playbooks through the FastAPI
backend, automatically pull structured output (`json_stdout`) into XCom, and
(optionally) remediate failed hosts.

**Core pieces**
1️⃣  **Trigger tasks** (`HttpOperator`) → POST payload to backend.  
2️⃣  **Sensor tasks** (`HttpSensor`) → poll status, push `json_stdout` to XCom.

**Key guarantees**
* Do **not** change *Section 1* constants — backend depends on them.
* Keep the helper factories intact; extend logic only in *Section 3*.

Down‑stream tasks read the playbook result with:

#python
#ti.xcom_pull(task_ids="wait_<task_id>", key="json_stdout")
#```
"""

# ─────────────────────────────────────────────────────────────────────────────
# SECTION 1  •  CONFIGURATION (DO NOT EDIT UNLESS YOU KNOW WHAT YOU’RE DOING)
# ─────────────────────────────────────────────────────────────────────────────
import json
from datetime import datetime, timezone
from typing import Any, Dict, List, Union, Any

from airflow import DAG
from airflow.exceptions import AirflowFailException
from airflow.providers.http.operators.http import HttpOperator
from airflow.providers.http.sensors.http import HttpSensor
from airflow.operators.python import PythonOperator, BranchPythonOperator
from airflow.operators.empty import EmptyOperator
from airflow.utils.trigger_rule import TriggerRule
from airflow.utils.context import Context

BACKEND_CONN_ID = "backend_http"  # Airflow connection id
TRIGGER_ENDPOINT = "api/ansible/airflow/playbook/run"  # Backend POST

HEADERS = {
    "Content-Type": "application/json",
    "Authorization": "Bearer {{ dag_run.conf.get('auth_token') }}",
}

DEFAULT_EXTRA_VARS: Dict[str, Any] = {}

# ─────────────────────────────────────────────────────────────────────────────
# SECTION 2  •  HELPERS  (DO NOT EDIT UNLESS YOU KNOW WHAT YOU’RE DOING)
# ─────────────────────────────────────────────────────────────────────────────


def create_trigger_task(
    playbook: str,
    task_id: str,
    *,
    extra_vars: Union[Any, None] = None,
) -> HttpOperator:
    """Return an `HttpOperator` that triggers *playbook* on the backend."""
    payload = {
        "dag_id": "{{ dag.dag_id }}",
        "dag_run_id": "{{ dag_run.conf.get('dag_run_id') }}",
        "task_id": task_id,
        "playbook": playbook,
        "extra_vars": extra_vars if extra_vars is not None else DEFAULT_EXTRA_VARS,
    }
    return HttpOperator(
        task_id=task_id,
        http_conn_id=BACKEND_CONN_ID,
        endpoint=TRIGGER_ENDPOINT,
        method="POST",
        data=json.dumps(payload),
        headers=HEADERS,
        log_response=True,
    )


class HttpSensorWithXCom(HttpSensor):
    """Sensor that pushes `json_stdout` to XCom and fails fast on status==failed."""

    template_fields = HttpSensor.template_fields

    def poke(self, context: Context) -> bool:  # type: ignore[override]
        resp = self.hook.run(
            method=self.method, endpoint=self.endpoint, headers=self.headers
        )
        try:
            data: Dict[str, Any] = resp.json()
        except Exception as exc:
            raise AirflowFailException(
                f"{self.task_id}: response not JSON → {resp.text}"
            ) from exc

        status = str(data.get("status", "")).lower()
        if status == "failed":
            raise AirflowFailException(f"{self.task_id}: backend reported FAILURE")

        if (stdout := data.get("json_stdout")) is not None:
            context["ti"].xcom_push(key="json_stdout", value=stdout)

        return status == "success"


def create_sensor_task(task_id: str) -> HttpSensorWithXCom:
    """Factory for customised sensor (polls `/status/<dag_run_id>/<task_id>`)."""
    endpoint = (
        "api/ansible/airflow/playbook/status/"
        "{{ dag_run.conf.get('dag_run_id') }}/"
        f"{task_id}"
    )
    return HttpSensorWithXCom(
        task_id=f"wait_{task_id}",
        http_conn_id=BACKEND_CONN_ID,
        endpoint=endpoint,
        method="GET",
        headers=HEADERS,
        poke_interval=15,
        timeout=60 * 60,
        mode="reschedule",
    )


# Helper: build extra‑vars for remediation playbook


def _build_remediation_vars(failed_hosts: List[str]) -> Dict[str, Any]:
    return {"target_hosts": failed_hosts}


# ─────────────────────────────────────────────────────────────────────────────
# SECTION 3  •  DAG DEFINITION  (FREE TO EXTEND BELOW)
# ─────────────────────────────────────────────────────────────────────────────
"""
Workflow overview
-----------------
• step‑1 playbook runs → sensor grabs host statuses.
• Branch: if any host != "ok"  → run remediation playbook.
• Paths merge, then run step‑2 and step‑3 normally.
"""

with DAG(
    dag_id="dag_template",
    start_date=datetime.now(timezone.utc),
    schedule_interval=None,
    catchup=False,
    default_args={"owner": "airflow", "retries": 0, "depends_on_past": False},
    tags=["example", "ansible"],
) as dag:

    # ► Step 1
    trigger_1 = create_trigger_task("step01_dummy.yml", "run_step1")
    sensor_1 = create_sensor_task("run_step1")

    # ► Branch on host failures
    def _branch(**context) -> str:
        data: Dict[str, str] = (
            context["ti"].xcom_pull(task_ids="wait_run_step1", key="json_stdout") or {}
        )
        failed = [host for host, status in data.items() if status.lower() != "ok"]
        context["ti"].xcom_push(key="failed_hosts", value=failed)
        return "remediate_hosts" if failed else "skip_remediation"

    branch = BranchPythonOperator(task_id="branch_on_failures", python_callable=_branch)

    # ► Remediation path
    def _make_extra_vars(**context):
        failed: List[str] = (
            context["ti"].xcom_pull(key="failed_hosts", task_ids="branch_on_failures")
            or []
        )
        return _build_remediation_vars(failed)

    build_vars = PythonOperator(
        task_id="build_remediation_vars", python_callable=_make_extra_vars
    )

    remediation_trigger = create_trigger_task(
        "remediate_failed_hosts.yml",
        "remediate_hosts",
        extra_vars="{{ task_instance.xcom_pull(task_ids='build_remediation_vars') }}",
    )
    remediation_sensor = create_sensor_task("remediate_hosts")

    skip_remediation = EmptyOperator(task_id="skip_remediation")

    join = EmptyOperator(
        task_id="rejoin_paths", trigger_rule=TriggerRule.NONE_FAILED_MIN_ONE_SUCCESS
    )

    # ► Continue pipeline
    trigger_2 = create_trigger_task("step02_dummy.yml", "run_step2")
    sensor_2 = create_sensor_task("run_step2")

    trigger_3 = create_trigger_task("step03_dummy.yml", "run_step3")
    sensor_3 = create_sensor_task("run_step3")

    # Dependencies
    trigger_1 >> sensor_1 >> branch
    branch >> build_vars >> remediation_trigger >> remediation_sensor >> join
    branch >> skip_remediation >> join
    join >> trigger_2 >> sensor_2 >> trigger_3 >> sensor_3

# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
    dag.cli()  # `python <file>.py test` for local debug

Tip: Store the templates above in source control (Git, CodeCommit, etc.) so you always know which version was uploaded to Mantle and can track changes across programs.