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.
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
.
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.
refreshCommandStatus
refreshCommandStatus
is a mechanism which addresses two advanced use cases:- 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 - 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
.- You have issued multiple requests over time via
Good practices for custom configuration and reporting with CommandRunner
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.
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 followingaction 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 commandIdarguments 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 exampleping -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 practicessingleLineTextResult boolean • When action is 3 (RunCommand), optional toggle to specify whether newlines should be stripped from console output
• Ignored for any other action typetimeout int • When action is 3 (runCommand), long running requests are killed after this many seconds
• Optional (default is 30)
• Ignored for any other action typeExamples 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 ofcommandArguments
writable property)Members:
Name Type Notes value map Should mirror properties.desired.CommandRunner.commandArguments
, indicating the device in online and has received the requestac 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 aboveresultCode int The exit code of the requested command. Analogous to echo $?
in bashcurrentState enum/int • Nominal case is value 2
(succeeded
)
• See the metadata for full list of possible valuestextResult 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 practicesExamples from IoT Hub and real devices
Reboot result
"CommandRunner": { "__t": "c", "commandStatus": { "commandId": "my_reboot_cmd", "resultCode": 0, "textResult": "", "currentState": 2 } }
Custom configuration (timezone) result:
"CommandRunner": { "__t": "c", "commandStatus": { "commandId": "my_timezone_cmd", "testResult": "Time zone: UTC", "statusCode": 0 } }
For additional examples, see:
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: