fix(azure-identityd): wait for a working resolver before starting identityd - #683
fix(azure-identityd): wait for a working resolver before starting identityd#683JanZachmann wants to merge 10 commits into
Conversation
…ntityd identityd enrolls the device-id cert via EST and reaches out to DPS right at start. When the resolver cannot answer yet it gets EAI_AGAIN, exits 1 and is restarted, which also fails the ADU agent health check and logs errors that are not in the CI syslog whitelist. Ordering after network-online.target does not cover this: the target only says an interface is configured. A new ExecStartPre blocks until the configured EST, DPS or iothub host resolves, capped at 30s, then starts the daemon regardless so a device that is really offline still relies on Restart=on-failure. The wait must delay the start and never skip it - a condition-skipped activation burns the socket's trigger limit and leaves no way back. Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
…ns gate toml exits non-zero with no output for an absent key and never prints "null", and its version is pinned by SRCREV, so the check could not fire. Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
The gate waited for the first configured endpoint only. With tpm attestation cert_issuance.est.urls.default holds the edge ca server, which identityd does not contact at start, so the wait covered the wrong host while the dps endpoint it does contact stayed unchecked. Every configured host is probed now, and each one still pending gets a try per pass, so a host that never resolves cannot eat the deadline of the others. The host is taken out of the url with userinfo and an ipv6 literal handled. The deadline was checked only after a lookup returned, so the wait really was bounded by the deadline plus one resolver timeout. The unit sets no TimeoutStartSec, so its ceiling is the 90s default and a slow resolver could fail the very start this gate protects. A single lookup is capped as well now, which bounds the whole wait by the deadline plus one pass. timeout comes from coreutils, busybox is built without the applet. toml prints a value raw with -r, so the tr process is gone, and the stderr redirect only hid a missing or unparsable config, which belongs in the journal. Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
Local SRC_URI files land in ${UNPACKDIR} since yocto 6, so do_install could not
stat the script and failed. The rest of the recipe reads its local files from
there once the branch is on yocto 6.
Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
An ip literal needs no lookup, and no endpoint is configured as one. Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
| -e 's#@libexecdir@#/usr/libexec#g' \ | ||
| -e '/Environment=\(.*\)$/d' \ | ||
| -e 's#^ExecStart=\(.*\)$#ExecStartPre=+-bash -c "/usr/sbin/omnect_service_log.sh start aziot-identityd"\nExecStart=\1\nExecStopPost=+-/bin/sh -c "/usr/sbin/omnect_service_log.sh stop aziot-identityd \\"$$SERVICE_RESULT\\" \\"$$EXIT_CODE\\" \\"$$EXIT_STATUS\\""#' \ | ||
| -e 's#^ExecStart=\(.*\)$#ExecStartPre=+-bash -c "/usr/sbin/omnect_service_log.sh start aziot-identityd"\nExecStartPre=+-/usr/bin/omnect_wait_dns_ready.sh\nExecStart=\1\nExecStopPost=+-/bin/sh -c "/usr/sbin/omnect_service_log.sh stop aziot-identityd \\"$$SERVICE_RESULT\\" \\"$$EXIT_CODE\\" \\"$$EXIT_STATUS\\""#' \ |
There was a problem hiding this comment.
why do we suppress a fail exit status of omnect_wait_dns_ready.sh?
as far as I can see it will always be 0? (ends explicitly with exit 0 only and doesn't terminate on failing commands)
There was a problem hiding this comment.
Correct that the script itself always exits 0 - it has no set -e and ends on exit 0. The - covers what the script cannot: systemd counts a failed exec as a failed ExecStartPre, so a missing, non-executable or killed script would fail the identityd start. That is the exact failure this PR removes, and the gate must never be able to cause it. It also matches the omnect_service_log.sh line above.
There was a problem hiding this comment.
I don't buy it: this PR adds both, the script to the read-only rootfs as well as the ExecStartPre extension to the service file, a non-existing (executable) script would be a system error that cannot be handled at all imho.
so, why/how should this - matter here?
There was a problem hiding this comment.
You are right, dropped both prefixes in 2b31533.
- only ever covered a missing or non-executable script, which is a broken image and should fail loud rather than be swallowed. The script itself cannot return non-zero, so nothing else was being suppressed.
+ is gone too, for the same "buys nothing" reason: /etc/aziot is 0775 root:aziot and config.toml is 0664 root:aziot, identityd reads that same file as aziotid, and the unit has no sandboxing directives - so toml, timeout and getent all work without privileges. Say if you would rather keep + as a guard against the perms changing.
There was a problem hiding this comment.
given that other service file changes do use + I would prefer leaving it here too for uniformity.
There was a problem hiding this comment.
Restored in a9548a0, so the line is now ExecStartPre=+/usr/bin/omnect_wait_dns_ready.sh - + back for uniformity, - gone.
There was a problem hiding this comment.
Pull request overview
This PR adds a startup gate for aziot-identityd.service to reduce first-boot flakiness by waiting (with caps) for DNS resolution of configured Azure IoT endpoint hostnames before identityd starts.
Changes:
- Added
omnect_wait_dns_ready.sh, which extracts relevant endpoint hosts from/etc/aziot/config.tomland polls DNS resolution with per-lookup and overall timeouts. - Updated the
aziot-identitydrecipe to install the script, add required runtime dependencies, and wire it intoaziot-identityd.servicevia an additionalExecStartPre.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| recipes-azure-iot/azure-identityd/aziot-identityd/omnect_wait_dns_ready.sh | New DNS readiness wait script that probes configured endpoint hosts before identityd starts. |
| recipes-azure-iot/azure-identityd/aziot-identityd.inc | Installs the script, adds RDEPENDS, and injects an ExecStartPre call into the identityd unit. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The three endpoints are different domains, so a host can only appear once. A duplicate would cost one extra lookup per pass and nothing else. Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
A missing or non-executable gate script means a broken image, and "-" would hide that. The script cannot fail on its own, so nothing else is suppressed. "+" is not needed either: identityd reads the same config as the same user. Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
HarryWaschkeit
left a comment
There was a problem hiding this comment.
see #683 (comment)
The other exec lines in this unit run with full privileges too. Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
|
Closing this without merging. The only trigger we can point at is our own lab resolver, and the log does not say why it went quiet - so merging this would ship a guess to every device. What the log does show ( Two things make the product-side wait not worth its cost:
And the gate only ever helped when the resolver was silent for less than 30 s. Past that it starts the daemon anyway, which is exactly today's behaviour. That is a narrow win for a script plus Not solved by closing: the nightly still fails on The review findings from this round were valid independently of the outcome and are worth keeping in mind for the next script of this shape: the URL parsing, the dedup array, and the |
Summary
aziot-identityd.servicegets a secondExecStartPrewhich waits until every endpoint host configured in/etc/aziot/config.tomlresolves -cert_issuance.est.urls.default,provisioning.global_endpointandprovisioning.iothub_hostname. A single lookup is capped at 5 s and the whole wait at 30 s. No endpoint configured means no wait, and on timeout the daemon starts anyway.Reason
identityd enrolls the
device-idcert via EST and reaches out to DPS right at start. When the resolver cannot answer it getsEAI_AGAIN, exits 1 and is restarted; the second attempt succeeds. This hits the first boot after flashing or a factory reset - the state every CI DUT boots in. The failed start also fails the ADU agent health check and logs[ERR!]lines that are not in the smoke-test syslog whitelist, while neither the crash-loop check norsystemctl is-system-runningcan see it: a unit that failed once and recovered isactivewithNRestarts=1.Seen twice on the rpi4 nightly, last in
test-image-job541. The lookup blocks for its full resolver timeout before it fails:aziot-certdstarts at 18:09:14 and reportsTemporary failure in name resolutionat 18:09:34, identityd exits, and the restart at 18:09:40 succeeds. The rest of the network worked for the whole of those 20 s - the test runner opened dozens of ssh connections to the DUT between 18:09:15 and 18:09:34, and timesyncd had reached the lab time server at 18:09:08, before the window. So the queries were sent and stayed unanswered, which is a different thing from resolved having no server at all - that case fails immediately instead of blocking. Why the resolver was silent for those 20 s is not established; it is the lab resolver handed out by DHCP, and it answers in a few milliseconds when warm.The unit already waits for
network-online.target, and that target only says a link is configured. DHCP hands out the address of a DNS server, not a DNS server that answers, and no ordering can express "a name resolves" - which is why the wait has to attempt a lookup. It also means the gate does not depend on which cause it was: resolver not warm, resolved not ready or an upstream that is slow all end in the same wait.Which endpoint identityd needs first depends on the provisioning method, and they are not the same domain, so every configured host is probed and each one still pending gets one try per pass - a host that never resolves must not starve the others. The single lookup is capped as well, because a failing lookup blocks for the resolver timeout: the unit sets no
TimeoutStartSec, so its ceiling is the 90 s default, and running into it would produce the very failed start this gate exists to prevent.The gate delays the start and never skips it. A condition-skipped activation would consume
aziot-identityd.socket's trigger limit of 20 within 2 s, and a failed socket is never activated again on its own (#680).