Skip to content

Deutsch

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 group bootstrap-agent is 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 .path unit (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, got could not connect once, waited and then succeeded in all three reboots. A repeat in curl does not help: curl --retry with --retry-connrefused does not retry a socket that does not exist yet (error 7 after 0.1 seconds, measured). After= alone does not help either, because Type=simple counts as started before the socket is open. Wait with a limit, as the example does; software that exits when the socket is missing can use Restart=on-failure with RestartSec=2.
  • Restart=on-failure is 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:

{"state":"connected"}

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-ready was written and complete was reported; none of the seven packages was acknowledged and no confirmed was sent.
  • The file acknowledges. The stand-in service (its own user ID, group wlpc, no root) wrote the file; 21 milliseconds later the agent logged target reached: acking the packages, then success 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:

d /run/wlpc 2775 root wlpc -
  • The group wlpc is the write grant. Your software's unit names it in SupplementaryGroups=wlpc; that works with DynamicUser=yes too.
  • A sandboxed unit also needs ReadWritePaths=/run/wlpc. DynamicUser=yes implies ProtectSystem=strict, which makes /run read-only for the unit.
  • Write a temporary file in /run/wlpc and rename it to target-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. The 2 in 2775 gives every file in the directory the group wlpc.

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: confirmed is billed, and it must mean "connected with the configuration the gateway holds now". The file says connected, not for which configuration.
  • When: after /etc/iot-config/profile-ready changes (your .path unit's PathChanged= starts your software then) and your software has connected with the new configuration. After a reboot /run is 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}, with N the profile_version your 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.