Configuration Templating

Overview

Templating is the mechanism by which we configure devices in Arigi. A device has one or more tags attached to it. These tags in turn match one or more configuration fragments that have the same tags. Arigi evaluates these fragments, assembles them in the order dictated by their priority, and thus creates a complete device configuration. If this configuration differs from what the device in question currently has, Arigi updates the device configuration accordingly. This is a powerful mechanism that allows reuse of configuration fragments between many devices, and automatically ensures that all affected devices are updated when a fragment changes.

digraph g {
    "Current Config" [style=filled, color="/accent3/1"]
    "Template A\nprio=20" [style=filled, color="/accent3/2"]
    "Template B\nprio=30" [style=filled, color="/accent3/2"]
    "Intermediate 1" [style=dashed]
    "Intermediate 2" [style=dashed]
    "New Config" [style=filled, color="/accent3/3"]

    {"Current Config", "Template A\nprio=20"} -> "Intermediate 1"
    {"Template B\nprio=30", "Intermediate 1"} -> "Intermediate 2"
    "Intermediate 2" -> "New Config"
}

Configuration fragments, in turn, can be dynamically built based on the information in Arigi. For example, a single fragment might define a folder shared with all devices having a certain tag. When a new device is added to this tag the fragment will change to accommodate it, and all devices attached to the fragment will be reconfigured to share the folder with the new device.

Updating by JSON

For simple cases, such as updating a config option, the best way is to provide the template in JSON format. For example, if we want to set the GUI port to 18384 instead of the default, a template definition would look like this:

Operation:

Set

Key:

gui.address

Template:

":18384"

Or, in the GUI:

../../_images/templates1.png

Note

The value :18384 is quoted, ":18384" because the template syntax is JSON and this is a string type value.

The GUI editor also shows the interpretation of the template itself in JSON format, and the resulting configuration after applying it to a device:

../../_images/templates2.png

Updating by Python (Starlark)

Language Basics

Arigi templates can use the Starlark templating language which is very similar to Python [1]. There are restrictions on the code in these templates – it is for example not possible to perform file I/O or launch external processes.

The configuration of the device being templated is available in the cfg variable and any changes to this object will be persisted as the result of the template. Python templates do therefore not have a “Key” setting like JSON templates. The ID of the current device is in the myID variable.

In the simplest case a Python template can be used to set static values, similar to a JSON template. For example, this template sets the GUI user ID and password hash to predetermined values:

../../_images/templates3.png

When developing Python templates it is sometimes useful to inspect values of variables. The debug function will output information so that it’s visible in the template result. For example:

../../_images/templates4.png

Functions and standard Python control structures can also be used to inspect and modify the configuration. One difference from standard Python is that loops and conditionals are only permitted inside functions.

../../_images/templates5.png

Database Access

To perform more advanced configuration templating we often need information about other devices than ourselves. For this purpose we have access to the db object, representing a read-only view of the Arigi database.

This allows to, for example, ensure that the default folder is shared will all devices. Here is a more involved example in text form:

# List all devices in the database.
all_devs = db.ListDevices()

# Process them into a list of {"devicID": "..."} objects as expected in the configuration.
# The string_dict() function forces the key to be of string type, which is required in the
# Syncthing configuration but not enforced by default by Starlark.
device_ids = [string_dict({ "deviceID": dev.DeviceID }) for dev in all_devs]

## All devices should share the folder.

# Find the folder in the current list of folders.
default_folder = [f for f in cfg["folders"] if f["id"] == "default"][0]

# Set the label and devices
default_folder["label"] = "Auto generated default folder"
default_folder["devices"] = device_ids

## All devices should be added as peers.

# This is myself. We take it from the existing config in order to preserve label and such.
my_dev = [d for d in cfg["devices"] if d["deviceID"] == myID]

# This is everyone else.
other_devs = [d for d in device_ids if d["deviceID"] != myID]

# Set the list of devices.
cfg["devices"] = my_dev + other_devs

The following methods are available on the database object:

GetDevice(id):

Returns the device object for the given device ID.

GetDeviceConfig(id, stage):

Returns the current device configuration for the given device ID. Stage is one of polled (the latest config returned by the device itself) or generated (the latest config we’ve generated due to template evaluation, but not necessarily pushed to the device yet).

GetDeviceVersion(id):

Returns the current device version object for the given device ID.

ListDevices():

Returns the list of all device objects.

ListDevicesWithTag(tag):

Returns the list of all device objects having the given tag.

ListDeviceTags(id):

Returns the list of tags for the given device ID.

ListFolders():

Returns the list of currently known folders.

ListTags():

Returns the list of all tags.

Using the debug(...) and debug(dir(...)) functions on returned objects can be useful in determining their structure and methods.