Template language
Templates and checks are written in Jinja. MiniPAM
adds a read-only data object for your inventory, a few helpers, and the require and warn tags.
Everything is scoped to the active site. Data reference in the header of the Templates and Asset integrity pages lists exactly what the version you run supports, with field descriptions.
The data object
| Name | Contains |
|---|---|
data.site | The site: id, name, location, timezone, tags |
data.generated_at | When the render started, as an ISO 8601 timestamp |
data.<collection> | A record set, one per collection below |
Collections
Each collection lists its main fields. A name in brackets is a shorter alias for the field before it, so
network.cidr and network.network_cidr are the same.
data.devices(loop namedevice):name,hostname,description,platform_identifier,os_info,parent_id. Linked records:device.device_type.data.interfaces(loop nameinterface):interface_name(name),mac_address,mac_vendor(vendor),ipv4_method,ipv6_method. Linked records:interface.device.data.device_types(loop namedevice_type):name,category,device_type(type),device_subtype(subtype),manufacturer.data.networks(loop namenetwork):network_name(name),network_cidr(cidr),gateway_ip(gateway),vlan_id,vlan_name,family.data.address_pools(loop namepool):pool_name(name),ip_start(start),ip_end(end),pool_type(type). Linked records:pool.network.data.ip_addresses(loop nameaddress):ip_address(address),usage_status(status),family,description,last_seen_at. Linked records:address.network.data.applications(loop nameapplication):app_name(name),port,port_protocol(transport),app_protocol(protocol),description,bind_ip,bind_scope,bind_ip_family,run_user,doc_url,source_url,netdevice_id(device_id),ip_address_id. Linked records:application.device,application.ip_address.data.services(loop nameservice):name,category,protocol_family,preferred_access_mode(access_mode),overview,description,doc_url,site_id.data.gateways(loop namegateway):name,overview,description,site_id.data.gateway_routes(loop nameroute):fqdn,path_prefix,route_priority(priority),service_id,service_gateway_id(gateway_id). Linked records:route.service,route.gateway.data.dns_records(loop namedns_record):record_name(name),record_type(type),record_value(value),ttl_seconds(ttl).
Every record also has id, created_at, and updated_at. All but gateways and gateway routes have tags.
Services and gateways include global ones, which have no site_id. data.applications.of(service) returns
every provider of the service as a plain application: which one is primary, and each provider's role and
priority, are not available in templates. data.ip_addresses.of(application) returns the application's IP
address, or all of its device's interface addresses; it ignores bind_ip, so read that field yourself when it
matters.
Record set methods
filter('key:op:value'): Matching records. Same syntax as structured filters, up to 5 criteria joined withand.of(record_or_set): Records related to a record or set, such asdata.ip_addresses.of(device)sort('field'): Records sorted ascending by a fieldlimit(n): At mostnrecords (1 to 2,000)first(): The first record, ornoneone(): The only record. Fails if there are none or several.exists():trueif there is at least one recordcount(): The number of records
Record sets can be looped over with {% for %} and chained:
{% set servers = data.devices.filter('tag.role:eq:server').sort('name').limit(50) %}
{% for device in servers %}
{{ device.name }}: {% for address in data.ip_addresses.of(device) %}{{ address.address }} {% endfor %}
{% endfor %}
of() follows these relationships, in both directions: devices and interfaces, interfaces and IP addresses,
devices and IP addresses, IP addresses and networks, pools and networks, applications and devices, IP
addresses, or services, gateways and devices, routes and gateways or services, device types and devices.
Filter operators
eq, ne, co (contains), nco, sw (starts with), ew (ends with), gt, gte, lt, lte, and for IP
fields in_cidr and not_in_cidr:
{% set lan = data.ip_addresses.filter('address:in_cidr:192.168.1.0/24') %}
{% set routers = data.devices.filter('tag.role:eq:router and name:sw:edge-') %}
Helpers
| Helper | Returns |
|---|---|
ip_in_cidr(value, cidr) | Whether an address is inside a CIDR |
cidr_contains(container, candidate) | Whether one CIDR contains another |
cidr_overlaps(left, right) | Whether two CIDRs overlap |
normalize_ip(value) | An address in canonical form |
normalize_cidr(value) | A CIDR in canonical form |
ip_version(value) | 4 or 6 |
has_tag(record, key, value=None) | Whether a record has a tag, optionally with a given value |
is_unique(records, field) | Whether no two records share a value for a field |
duplicate_values(records, field) | The values that appear more than once |
all_present(records, field) | Whether every record has a value for a field |
Assertions: require and warn
Available in templates and in checks:
{% require condition,
code='short_code',
message='What is wrong and what to fix',
expected='what should be true',
actual=what_was_found %}
{% warn condition, code='short_code', message='…' %}
requireblocks: a failingrequirefails the check, and blocks a template's output from being published.warnonly reports.code,message,expected, andactualare optional, but a clearmessagesaves the next person time.- A check must contain at least one
require. - Assertions produce no text in the output.
In an output check, artifact describes the generated file: artifact.content, artifact.filename,
artifact.media_type, and artifact.size (in bytes).
Jinja features
Standard Jinja statements and expressions work: set, if, for, macro, comments, whitespace control
({%-, -%}), and the ~ operator for joining strings.
Available filters: default, join, lower, upper, replace, trim, indent, length, sort,
unique, int, float, round, tojson, capitalize, title.
Templates are self-contained: include, import, and extends are not available.
Limits
Each render runs in a sandbox with these limits:
| Limit | Default | Configurable |
|---|---|---|
| Render time | 3 seconds | No |
| Output size | 2 MiB | No |
| Records returned by one query | 2,000 | Yes |
| Records loaded in one render | 10,000 | Yes |
| Queries in one render | 500 | Yes |
Admins can change the configurable limits, globally or per site, under Administration → System → Settings (category Templates). See Settings.
When a render hits a limit, the error says which one. Store a query with {% set %} instead of repeating it
inside a loop, and narrow large sets with filter() and limit().
How queries are counted
- Looping over a set runs one query, however many records it has.
of(record)inside a loop is batched: the first iteration runs its own query, and the second loads the lookups for every remaining record at once.data.ip_addresses.of(device)in a loop over 1,000 devices costs 3 queries, not 1,000. The same applies tocount(),first(), andexists()on that lookup.- Narrowing the lookup, as in
of(device).filter(...),.sort(...), or.limit(...), still runs one query per record. count()counts without loading the records, and a stored set remembers its count. If you also loop over the set, usedevices | lengthinstead: it loads the records once and reuses them for the loop.