Start your software and confirm¶
For manufacturers whose own software (the operating software) uses the configuration the agent fetches. The agent does not start your software. Two things connect them: a marker file that tells your image the configuration is stored, and a second marker that your software writes once it is connected to its target.
Ready and confirmed¶
The agent reports two different things to the platform.
| Step | Meaning | Who sees it |
|---|---|---|
complete |
The configuration is stored on the gateway. This is "ready": your software may start. | The platform sets the device to PROVISIONED. Your image sees the file /etc/iot-config/profile-ready. |
confirmed |
Your software is connected to its target platform. | The platform sets the device to ACTIVE. This is the one step that is billed, once per device. |
Only confirmed is billed. Storing the configuration (complete) and any
number of check-ins are not. A device whose software never writes the
confirmation file below is never billed, and stays PROVISIONED.
Start your software with a path unit¶
The agent replaces /etc/iot-config/profile-ready after every newly stored
configuration, and only after the configuration itself is safely written.
After a reboot it restores the stored configuration and does not write the
marker again. A path unit therefore needs both PathExists= and
PathChanged=: the first starts your software at boot when a configuration is
already stored, the second starts it when a new version is stored later.
The release archive carries both units as examples under examples/; the
recipe does not install them. Copy them under your own names:
# your-software.path
[Unit]
Description=Start the operating software when a WLPC configuration is stored
[Path]
PathExists=/etc/iot-config/profile-ready
PathChanged=/etc/iot-config/profile-ready
Unit=your-software.service
[Install]
WantedBy=paths.target
# your-software.service (the example: one call to the local socket, then the marker)
[Unit]
Description=Operating software
After=bootstrap-agent.service
Wants=bootstrap-agent.service
[Service]
Type=oneshot
RemainAfterExit=yes
DynamicUser=yes
SupplementaryGroups=bootstrap-agent wlpc
ReadWritePaths=/run/wlpc
ExecStart=/bin/sh -c 'n=0; while :; do /usr/bin/curl -sS --max-time 10 --unix-socket /run/bootstrap-agent/api.sock http://localhost/v1/status && exit 0; rc=$$?; [ "$$rc" = 7 ] && [ "$$n" -lt 30 ] || exit "$$rc"; n=$$((n + 1)); sleep 2; done'
ExecStart=/bin/sh -c "echo '{\"state\":\"connected\"}' > /run/wlpc/.target-confirmed.tmp && mv -f /run/wlpc/.target-confirmed.tmp /run/wlpc/target-confirmed"
The example makes one call, writes the marker (see Confirm the target;
your software writes it only once it is connected, the example at once) and
ends. That is why it is Type=oneshot with
RemainAfterExit=yes: a service that simply ends while the marker still exists
is started again at once by PathExists=, until systemd's start limit stops it
and marks the .path unit as failed (measured). Your own software is a
long-running service (Type=simple, with Restart=on-failure) and needs
neither.
Enable the .path unit, not the service.
Three facts from a test on the lab device with a stand-in for your software:
- The marker is stored with mode 0600, owned by the agent. A service with
its own user ID, as above with
DynamicUser=yes, cannot read it. It does not have to: only its presence starts the service. The service reads the configuration over the socket, where membership in the groupbootstrap-agentis the grant (see Local API reference). - The service can start before the agent's socket exists. The agent unit
waits for
network-online.target, so after a reboot it starts about 18 seconds after your.pathunit (measured 18.5 to 18.7 s in three reboots). The socket is open 4.1 to 4.6 seconds after systemd started the agent (0.85 s on a later restart, when the board is idle). The example above, started 0.4 s after the agent, gotcould not connectonce, waited and then succeeded in all three reboots. A repeat incurldoes not help:curl --retrywith--retry-connrefuseddoes not retry a socket that does not exist yet (error 7 after 0.1 seconds, measured).After=alone does not help either, becauseType=simplecounts as started before the socket is open. Wait with a limit, as the example does; software that exits when the socket is missing can useRestart=on-failurewithRestartSec=2. Restart=on-failureis a proposal. The boot test ran with the example above, not with your software.
Confirm the target¶
When your software is connected to its target platform, it writes this to
/run/wlpc/target-confirmed:
The agent watches the file (no polling). When the content says connected, it
acknowledges every package of the stored configuration with applied: true,
and when all packages are acknowledged it reports confirmed. It also checks
the file once at start. On the lab device the acknowledgement went out in
under a second after the file was written, and the packages changed from none
acknowledged to all seven acknowledged; the device then showed ACTIVE and
HEALTHY.
The order, measured with cproto-v0.2.0-rc.2 on the lab gateway:
- Without the file, a new configuration is stored but not acknowledged.
With the file removed, a template change was stored,
profile-readywas written andcompletewas reported; none of the seven packages was acknowledged and noconfirmedwas sent. - The file acknowledges. The stand-in service (its own user ID, group
wlpc, no root) wrote the file; 21 milliseconds later the agent loggedtarget reached: acking the packages, thensuccess reported to the platform. All seven packages were acknowledged. - With the file present, a new configuration is acknowledged at once, in
the same second as it is stored (the releases up to
cproto-v0.2.0-rc.2; see Write the file again for every new configuration).
Who may write /run/wlpc¶
Your software writes the file without root, as a member of the group
wlpc. The agent only reads it. The directory does not exist by default; this
tmpfiles rule creates it:
- The group
wlpcis the write grant. Your software's unit names it inSupplementaryGroups=wlpc; that works withDynamicUser=yestoo. - A sandboxed unit also needs
ReadWritePaths=/run/wlpc.DynamicUser=yesimpliesProtectSystem=strict, which makes/runread-only for the unit. - Write a temporary file in
/run/wlpcand rename it totarget-confirmed, so the agent never reads half a file. - Leave the file readable for others (0644; the default umask 0022 gives
that). The agent is not in the group
wlpc. The2in2775gives every file in the directory the groupwlpc.
Measured on the lab gateway with a DynamicUser=yes unit (its own user ID,
no root): with the group and ReadWritePaths= the file was written (mode
0644, group wlpc); without ReadWritePaths= the write failed with
Read-only file system; without the group, with Permission denied.
In three reboots of the lab gateway the example service above wrote the file
each time under its own user ID (group wlpc, mode 0644, content
{"state":"connected"}). The agent's user, with the agent's groups and in
the agent's sandbox, could read it, and could not read a copy with mode 0600.
The agent reads the file only while packages are not acknowledged; in the
order test above it acted on a file written this way.
The agent also notices a directory that is created after it started: in an earlier test the directory was created after the agent's start, and the acknowledgement still came within a second.
Release status
From cproto-v0.2.0-rc.2 on, the archive's tmpfiles/bootstrap-agent.conf
carries the line above, and the example service in its examples/ folder
writes the file as shown here. The recipe in our layer creates the group
wlpc. An image that does not use the recipe creates the group itself.
Without this file nothing becomes ACTIVE, and nothing is billed
The agent never acknowledges on its own. If nothing on your image writes
the file, the device stays PROVISIONED, no confirmed is reported, and
the device is not billed. The setting GATEWAY_AGENT_CONFIRM_MARKER
changes the path; the name is historical.
Write the file again for every new configuration¶
Your software must write /run/wlpc/target-confirmed again after every
newly stored configuration, once it is connected to the target of that
configuration. A file written for an earlier configuration does not confirm
the new one.
- Why:
confirmedis billed, and it must mean "connected with the configuration the gateway holds now". The file saysconnected, not for which configuration. - When: after
/etc/iot-config/profile-readychanges (your.pathunit'sPathChanged=starts your software then) and your software has connected with the new configuration. After a reboot/runis empty, so your software writes the file at every start once it is connected, as the example does. - Optionally name the version:
{"state":"connected","profile_version":N}, withNtheprofile_versionyour software read from the local API. A file with a version confirms only that version, whenever it was written. Use it if your gateway can boot with a wrong clock (no real-time clock).
Releases up to cproto-v0.2.0-rc.2 still accept an old file
Up to cproto-v0.2.0-rc.2 the agent checks the file again after a new
configuration is stored, and an old connected acknowledges the new
configuration at once, before your software has applied it. The next
release counts the file only if it was written after the current
configuration was stored, or names its version. Write the file as
described above now; then nothing changes for you with the next release.
What is not reported¶
The agent only sends applied: true. A configuration that your software
cannot apply produces no acknowledgement and no error at the platform; the
platform only sees that confirmed does not come. Your software can send
applied: false to the local API; the agent keeps it on the device but does
not pass it on, and a connected file overrides it with applied: true. See
applied: false and
Two ways to confirm.