How to make an apply-device-config script

tutorial

Installation

See this article on Mender Hub.

apply-device-config scripts

apply-device-config scripts are custom scripts, which can be installed into the /usr/lib/mender-configure/apply-device-config.d directory. They are then executed by the Configure Update Module, with the expected configuration parameters from the Mender server. In this way, you can extend Mender-Configure to you needs and keep your device configuration in sync.

How to write an apply-device-config script

apply-device-config scripts are simple shell (/bin/sh) scripts, which take one parameter: the JSON configuration values file. The script is then responsible for parsing the value from the JSON configuration, and then apply this to the system. See the examples in the examples section for inspiration.

Scripts are always run with the entire configuration, even if there is no configuration available for this particular script. This means that you should be careful to either: use a default value, or have it simply act as a no-op, and then exit with success.

All scripts in /usr/lib/mender-configure/apply-device-config.d are run in lexicographic order.

Examples

Device time zone configuration

To get a feel for the main parts of a configuration script, let us dissect the time zone script:

  1. shebang
#!/bin/sh
  1. Input parameter verification
if [ $# -ne 1 ]; then
    echo "Must be invoked with exactly one argument: The JSON configuration." 1>&2
    exit 2
fi

CONFIG="$1"

if ! [ -e "$CONFIG" ]; then
    echo "Error: $CONFIG does not exist." 1>&2
    exit 1
fi
  1. Extract and apply the configuration
TIMEZONE="$(jq -r -e .timezone < "$CONFIG")"
return_code=$?
case $return_code in
    0)
        timedatectl set-timezone "$TIMEZONE"
        exit $?
        ;;
    1)
        # Result was null, there is no timezone configuration, nothing to do.
        echo "No timezone configuration found."
        exit 0
        ;;
    *)
        exit $return_code
        ;;
esac

Note that this script relies on jq to parse the JSON input, which might not be present in all Linux distributions by default. All scripts must have some way of parsing the input JSON.

Simple wrapper which tweaks the parameters before calling a binary

#!/bin/sh
# Script which takes arguments from the configuration and runs COMMAND

if [ $# -ne 1 ]; then
    echo "Must be invoked with exactly one argument: The JSON configuration." 1>&2
    exit 2
fi

CONFIG="$1"

if ! [ -e "$CONFIG" ]; then
    echo "Error: $CONFIG does not exist." 1>&2
    exit 1
fi

ARGUMENTS="$(jq -r -e '."mender-demo-command-wrapper"' < "$CONFIG")"

return_code=$?
case $return_code in
    0)
        # Success, continue below.
        :
        ;;
    1)
        echo "No mender-demo-command-wrapper configuration found." >&2
        exit 0
        ;;
    *)
        exit $return_code
        ;;
esac

some-command "$ARGUMENTS"

return_code=$?
if [ $return_code -ne 0 ]; then
    echo "Applying the command with configuration $ARGUMENTS failed" >&2
fi

exit $return_code

Configure your device with different interpreters

Since the scripts interpreter is controlled by the header shebang, you can configure your device using any interpreter present on your device.

As an example of this, let us write a simple configuration program in Python, following the three main steps outlined above:

  1. shebang
#!/usr/bin/python

import json
import os.path
import subprocess
import sys
  1. Input parameter verification
if len(sys.argv) != 1:
    print("Must be invoked with exactly one argument: The JSON configuration.", file=sys.stderr)
    sys.exit(1)

config=sys.argv[0]

if not os.path.exists(config):
    print(f"Error: {config} does not exist.", file=sys.stderr)
    sys.exit(1)
  1. Extract and apply the configuration
try:
    configJSON = json.load(config)
    timezone = configJSON["timezone"]
    subprocess.run("timedatectl", "set-timezone", timezone, check=True)
except json.JSONDecodeError as e:
    print(f"Failed to parse the configuration JSON, error: {e}")
    sys.exit(1)
except subprocess.CalledProcessError as e:
    print(f"Setting the timezone with timedatectl failed with error: {e}")
    sys.exit(1)
except Exception as e:
    print(f"Failed with unhandled error: {e}")
    sys.exit(1)

Found errors? Think you can improve this documentation? Simply click the Edit link at the top of the page, and then the icon on Github to submit changes.