How to interact with the CommandRunner feature of OSConfig and Azure IoT

Most device properties exposed by OSConfig are discrete and abstracted from implementation details. For example, when you use HostName.desiredName to set a device's host name, you don't need to worry about the operating system flavor, about scripting runtime dependencies, or about timing nuances.

Sometimes, however, you may want to trade simplicity for flexibility. For example:

  • You need to retrieve custom information from the device -- such as wanting to report on the results of ping example.com from the device's perspective
  • You need to configure something on the device where there is no existing OSConfig desired/writable property

The CommandRunner feature of OSConfig enables custom configuration and reporting through shell commands/scripts. It also includes certain pre-defined actions (shutdown, reboot) which do not require any scripting.

Tip

This reference article describes CommandRunner concepts, interaction model, etc. For concrete usage examples, see:

The interaction model and key properties/fields

Summary of interaction model

While more nuanced usage is possible (and described later in this article), the core interaction model is straightforward:

  • You initiate a command request by writing to desired commandArguments in the twin.
  • You get the results by reading reported commandStatus from the twin.

Diagram showing commandArguments and commandStatus

Important

The payloads shown in this diagram are stripped down to emphasize in/out flow. For the full object model including required members not shown here, see: The full object model

The round-trip identifier is: commandId

In the diagram above, note that both commandArguments and commandStatus include a field called commandId. This is the round trip identifier linking request and outcome throughout this asynchronous operation.

The caller (admin or admin toolchain) chooses the commandId value.

Tip

commandId is required in commandArguments. Callers typically use one of these patterns when choosing values:

  • Monotonic values like "1" (for first request), "2" (for second request), etc. or descriptive values like "my_ping_command"

    These are well suited for ad-hoc exploration; they don't require planning, and they can be typed easily when editing twin contents

  • High entropy auto-generated values such as UUIDs

    These are well suited for large-scale programmatic usage where a cloud solution back-end will programmatically create and keep track of values

A new value of commandId in commandArguments signals that this is a new request. In the context of a single device's twin, populating a new commandId is roughly analogous to hitting 'Enter' in a shell environment.

commandId also enables keeping track of multiple requests. CommandRunner on each device can receive (and report status on) multiple requests over time. Meanwhile the commandArguments and commandStatus components in each device's twin can only describe one request at any moment. commandId is the round trip identifier enabling the caller to understand (and control) which which previous request is currently described by commandStatus. In effect, commandStatus communicates "for your request where the id was 'foo', the status is <...>".

When is commandStatus updated?

There are two triggers for CommandRunner on the device to update commandStatus.

  1. Background refresh

    commandStatus is updated at a regular interval (default is 30 seconds). By default this background refresh causes it to represent the status of the most recent request.

    In practice, this means that after submitting a request via commandArguments, you can expect to see corresponding status in commandStatus after approximately 30-60 seconds.

  2. refreshCommandStatus

    refreshCommandStatus is a mechanism which addresses two advanced use cases:

    1. You have issued multiple requests over time via commandArguments, and you want to see the status for a specific request, not the most recent request
    2. You don't want to wait for the background refresh, even if you are only interested in the latest request

    To trigger this mechanism, see the object model documentation below for commandArguments.

Good practices for custom configuration and reporting with CommandRunner

  1. Size considerations

    For short commands/scripts and short outputs you can work directly inline via the twin. This approach has advantages, especially having no network or authentication dependency outside of IoT Hub. The primary disadvantage of this approach is that the inputs (including your script/commands text) and outputs (including any captured console output) are each limited to 4KB.

    For longer scripts, you can store the script elsewhere (such as GitHub), and call that from a minimal wrapper command which you pass to OSConfig over the twin. For examples, see: Custom configuration and reporting with Azure IoT and OSConfig. Independent of size, you might also prefer to manage your scripts in GitHub or similar-- with twin contents simply pointing to those.

    For scripts/commands whose console output is too large for the twin, you can include logic in the script to send results to on-prem or cloud storage rather than to stdout. For examples, see: Custom configuration and reporting with Azure IoT and OSConfig.

  2. Use self-contained, non-interactive commands/scripts

    Fewer round trips are better, one round trip is best. OSConfig and CommandRunner are optimized for scale not interactivity. Although it is possible to use CommandRunner serially (one command after another, similar to a live synchronous terminal) the experience is not optimal. There is no way to handle interactive shell prompts, you must assign a commandId to each request, you must wait for twin synchronization, etc.

    Instead, think of CommandRunner as a way to achieve custom configuration and reporting at scale. You can asynchronously distribute a non-interactive script (or a pre-defined action like reboot) to one device or millions of devices, and can report on the results even as they evolve over time (as new devices join the fleet, for example).

    In other words, say you need to run four commands on all current and future Ubuntu 20.04 devices in Spain. Don't think of that as 4 discrete sequential uses of CommandRunner, think of it as a single use of CommandRunner where the payload contains your four commands. Meanwhile a cloud workflow such as IoT Hub Configurations can ensure this is distributed to all current and future devices which should be in scope.

The full object model

Important

Version 1.0.3 (published 28 June 2022) includes breaking changes to member names which may impact existing users. For more information, see: Member names transition from PascalCase to camelCase in version 1.0.3

The following information applies to plain desired/reported views of the object model as well as DTDL enhanced views. For more information, see What OSConfig users need to know about "plain desired/reported" vs "DTDL enhanced" toolchains.

commandArguments (set by admin)

  • Description: Input from admin, triggers new command request or triggers refresh of commandStatus.

  • Path: properties.desired.CommandRunner.commandArguments (CommandRunner component --> commandArguments writable property)

  • Members

    Name Type Notes
    commandId string • Caller specified request ID; see above for background
    • commandArguments is ignored when commandId is blank
    • For interplay between commandId and action, see the following
    action enum/int The following should be coupled with a new value of commandId:
    • 1 (reboot)
    • 2 (shutdown)
    • 3 (runCommand); initiates shell command/script for custom configuration or reporting

    The following should be coupled with a previously used CommandID:
    • 4 (refreshCommandStatus) causes commandStatus to describe a specific request identified by commandId
    arguments string • When action is 1 (reboot) or 2 (shutdown), an optional number of seconds to delay before triggering the action
    • When action is 3 (RunCommand), the command line to execute, for example ping -c 2 example.com
    • Ignored for any other action type
    • Size of commandArguments (usually dominated by arguments text) in Azure IoT twins in limited to 4KB; for longer scripts see Good practices
    singleLineTextResult boolean • When action is 3 (RunCommand), optional toggle to specify whether newlines should be stripped from console output
    • Ignored for any other action type
    timeout int • When action is 3 (runCommand), long running requests are killed after this many seconds
    • Optional (default is 30)
    • Ignored for any other action type
  • Examples from IoT Hub and real devices

    • Reboot request:

      "CommandRunner": {
         "__t": "c",
         "commandArguments": {
            "commandId": "my_reboot_cmd",
            "arguments": "",
            "timeout": 30,
            "singleLineTextResult": false,
            "action": 1
         }
      }
      
    • Custom configuration (timezone) request:

      "CommandRunner": {
         "__t": "c",
         "commandArguments": {
            "commandId": "my_timezone_cmd",
            "arguments": "timedatectl set-timezone Etc/UTC; timedatectl | grep Time",
            "timeout": 30,
            "singleLineTextResult": false,
            "action": 3
         }
      }
      
    • For additional examples, see:

commandArguments (acknowledgement from device)

  • Description: This is the acknowledgement from the OSConfig agent on the device that it has received the commandArguments value from the admin side

  • Path: properties.reported.CommandRunner.commandArguments (the device acknowledgement portion of commandArguments writable property)

  • Members:

    Name Type Notes
    value map Should mirror properties.desired.CommandRunner.commandArguments, indicating the device in online and has received the request
    ac int Status code, nominal case is value 200

commandStatus

  • Description: Gives the status and output of a request previously submitted via commandArguments

  • Path: properties.reported.CommandRunner.commandStatus (CommandRunner component --> commandStatus read-only property)

  • Members

    Name Type Notes
    commandId string • Identifies which previously received request is currently described by Commandstatus
    • By default the most recent request is represented
    • Caller can change this focus to describe a specific request through the refreshCommandStatus mechanism described above
    resultCode int The exit code of the requested command. Analogous to echo $? in bash
    currentState enum/int • Nominal case is value 2 (succeeded)
    • See the metadata for full list of possible values
    textResult string • The console output of the requested command
    • Size of commandStatus (usually dominated by textResult component) in Azure IoT twins in limited to 4KB; for longer outputs see Good practices
  • Examples from IoT Hub and real devices

Important

Version 1.0.3 (published 28 June 2022) includes breaking changes to member names which may impact existing users. For more information, see: Member names transition from PascalCase to camelCase in version 1.0.3

Next steps

For an overview of OSConfig scenarios and capabilities, see:

For specific practical examples, see: