diff --git a/bluez.sysext/create.sh b/bluez.sysext/create.sh new file mode 100644 index 0000000..00fbaa4 --- /dev/null +++ b/bluez.sysext/create.sh @@ -0,0 +1,212 @@ +#!/usr/bin/env bash +# vim: et ts=2 syn=bash +# +# The BlueZ sysext. +# +# BlueZ is the Linux Bluetooth userspace: bluetoothd plus the bluetoothctl / +# btmon / btmgmt tools. It has no upstream static or portable release, and it +# links glib, dbus and readline, so this extension bundles the binaries with +# their shared-library closure (including glibc + the dynamic loader) extracted +# from a Debian container. tools/flix.sh resolves the closure and patchelf's the +# binaries onto a private loader/rpath, isolating them from the host's libraries +# (same approach as qemu.sysext and tilde.sysext). Debian packages BlueZ, so the +# version parameter selects the Debian suite (stable/testing) that ships it. +# +# The kernel side is not part of this extension: Bluetooth modules have to match +# the running kernel exactly, so they belong in the Flatcar image rather than in +# a sysext. Flatcar images built after flatcar/scripts#4197 carry them. + +RELOAD_SERVICES_ON_MERGE="true" + +DEBIAN_BLUEZ_API="https://sources.debian.org/api/src/bluez/" + +# A transient network hiccup should not fail a build; same retry policy the +# other recipes in this repo use (consul, haproxy, nomad, vault). +CURL_RETRY=(--retry-delay 1 --retry 60 --retry-connrefused + --retry-max-time 60 --connect-timeout 20) + +# Resolve a Debian suite alias (stable/testing) to its codename from the archive +# Release file, e.g. stable -> trixie. The sources API tags versions by codename +# only, so we need this mapping. Fails if the lookup can't be made. +function _debian_codename() { + local alias="$1" codename + codename="$(curl -fsSL "${CURL_RETRY[@]}" "https://deb.debian.org/debian/dists/${alias}/Release" 2>/dev/null \ + | sed -nE 's/^Codename:[[:space:]]*([^[:space:]]+).*/\1/p')" || return 1 + [[ -n "${codename}" ]] || return 1 + printf '%s\n' "${codename}" +} +# -- + +# Read a Debian sources API response on stdin and print the x.y BlueZ versions +# shipped by the given suite codenames (args), newest first. +function _bluez_versions_in() { + jq -r --args ' + ($ARGS.positional) as $cn + | .versions[]? + | select((.suites // []) | any(. as $s | $cn | index($s))) + | .version' "$@" \ + | sed -nE 's/^([0-9]+\.[0-9]+).*/\1/p' \ + | sort -Vru +} +# -- + +# Return 0 if the given Debian codename ships the requested x.y BlueZ version. +function _bluez_version_in() { + local api="$1" ver="$2" codename="$3" + echo "${api}" | jq -e --arg v "${ver}" --arg c "${codename}" ' + .versions[]? + | select((.suites // []) | index($c)) + | select(.version | test("^" + ($v | gsub("\\.";"\\.")) + "([-.+~]|$)")) + ' >/dev/null +} +# -- + +function list_available_versions() { + local api stable testing + stable="$(_debian_codename stable)" && testing="$(_debian_codename testing)" || { + echo "ERROR: failed to resolve Debian stable/testing codenames." >&2 + return 1 + } + api="$(curl -fsSL "${CURL_RETRY[@]}" "${DEBIAN_BLUEZ_API}")" || { + echo "ERROR: failed to query the Debian sources API (${DEBIAN_BLUEZ_API})." >&2 + return 1 + } + echo "${api}" | _bluez_versions_in "${stable}" "${testing}" +} +# -- + +function populate_sysext_root() { + local sysextroot="$1" + local arch="$2" + local version="$3" + + local img_arch + img_arch="$(arch_transform 'x86-64' 'amd64' "${arch}")" + img_arch="$(arch_transform 'arm64' 'arm64' "${img_arch}")" + + # Map the stable/testing aliases to codenames (the sources API only tags by + # codename) and fetch the version list ONCE. Fail hard on any lookup/parse + # error so a transient failure can't silently change which suite (and thus + # which BlueZ version) we build against. + local stable testing api + stable="$(_debian_codename stable)" && testing="$(_debian_codename testing)" || { + echo "ERROR: failed to resolve Debian stable/testing codenames." >&2 + return 1 + } + api="$(curl -fsSL "${CURL_RETRY[@]}" "${DEBIAN_BLUEZ_API}")" || { + echo "ERROR: failed to query the Debian sources API (${DEBIAN_BLUEZ_API})." >&2 + return 1 + } + echo "${api}" | jq -e '.versions' >/dev/null 2>&1 || { + echo "ERROR: unexpected response from the Debian sources API." >&2 + return 1 + } + + # 'latest' resolves to the newest version in stable/testing so the checks below + # have a concrete target. + if [[ "${version}" == "latest" ]]; then + version="$(echo "${api}" | _bluez_versions_in "${stable}" "${testing}" | head -1)" + [[ -n "${version}" ]] || { + echo "ERROR: could not determine the latest bluez version from Debian." >&2 + return 1 + } + fi + + # Choose the suite that actually ships the requested version: prefer stable, + # else testing, else fail. Never fall back blindly. + local suite="" + if _bluez_version_in "${api}" "${version}" "${stable}"; then + suite="stable" + elif _bluez_version_in "${api}" "${version}" "${testing}"; then + suite="testing" + else + echo "ERROR: bluez ${version} is not in Debian stable (${stable}) or testing (${testing})." >&2 + return 1 + fi + + announce "Building bluez ${version} (Debian ${suite}) for ${arch}" + + # Build the sysext inside a Debian container of the chosen suite: install + # BlueZ, verify bluetoothd actually matches the requested version (a moved + # suite must not silently produce a different build), then hand the binaries + # and the D-Bus policy to tools/flix.sh, which resolves the library closure + # and patchelf's them onto a private loader/rpath. + docker run --rm -i \ + -v "${scriptroot}/tools/":/tools \ + -v "${sysextroot}":/install_root \ + --platform "linux/${img_arch}" \ + --pull always \ + --network host \ + -e WANT_VERSION="${version}" \ + "docker.io/debian:${suite}-slim" bash -euc ' + export DEBIAN_FRONTEND=noninteractive + apt-get update -qq + apt-get install -y -qq --no-install-recommends bluez patchelf >/dev/null + + # bluetoothd moved from /usr/lib to /usr/libexec across Debian releases. + bluetoothd="" + for c in /usr/libexec/bluetooth/bluetoothd /usr/lib/bluetooth/bluetoothd; do + [ -x "${c}" ] && bluetoothd="${c}" && break + done + if [ -z "${bluetoothd}" ]; then + echo "ERROR: bluetoothd not found in the bluez package." >&2 + exit 1 + fi + + got="$("${bluetoothd}" --version | sed -nE "s/^([0-9]+\.[0-9]+).*/\1/p" | head -1)" + if [ "${got}" != "${WANT_VERSION}" ]; then + echo "ERROR: requested bluez ${WANT_VERSION} but Debian installs ${got}." >&2 + exit 1 + fi + + # bluetoothd plus every CLI tool this suite's bluez package ships. Take + # the list from the package rather than hardcoding it: the tool set drifts + # across Debian releases (the legacy hciconfig/hcitool/... tools are + # deprecated upstream and already gone from newer ones), and a hardcoded + # subset silently drops tools that are still shipped -- hciattach in + # particular, which is what attaches a UART controller. + paths="${bluetoothd}" + for b in $(dpkg -L bluez | sed -nE 's|^(/usr/s?bin/[^/]+)$|\1|p' | sort -u); do + [ -f "${b}" ] && [ -x "${b}" ] && paths="${paths} ${b}" + done + + # bluetoothd will not take its name on the system bus without this policy. + dbus_conf="" + for c in /usr/share/dbus-1/system.d/bluetooth.conf \ + /etc/dbus-1/system.d/bluetooth.conf; do + [ -f "${c}" ] && dbus_conf="${c}" && break + done + if [ -z "${dbus_conf}" ]; then + echo "ERROR: the bluez D-Bus policy file was not found." >&2 + exit 1 + fi + # flix.sh only accepts the "source:target" form for paths outside /usr; + # a path that is already under /usr must be passed as-is. + case "${dbus_conf}" in + /usr/*) paths="${paths} ${dbus_conf}" ;; + *) paths="${paths} ${dbus_conf}:/usr/share/dbus-1/system.d/bluetooth.conf" ;; + esac + + cd /install_root + /tools/flix.sh / bluez ${paths} + + # bluetoothd is looked up at a fixed path by our unit; normalise the + # location so the unit does not have to care which Debian release we used. + if [ "${bluetoothd}" != "/usr/libexec/bluetooth/bluetoothd" ]; then + mkdir -p bluez/usr/libexec/bluetooth + mv "bluez${bluetoothd}" bluez/usr/libexec/bluetooth/bluetoothd + fi + + owner="$(stat -c "%u:%g" /install_root)" + if [ "${owner}" != "$(id -u):$(id -g)" ]; then + chown -R "${owner}" /install_root/bluez + fi + ' + + # Merge rather than move: the bakery has already copied this extension's + # static files/ tree into ${sysextroot}/usr before calling us. + mkdir -p "${sysextroot}/usr" + cp -a "${sysextroot}/bluez/usr/." "${sysextroot}/usr/" + rm -rf "${sysextroot}/bluez" +} +# -- diff --git a/bluez.sysext/files/usr/lib/systemd/system/bluetooth.service b/bluez.sysext/files/usr/lib/systemd/system/bluetooth.service new file mode 100644 index 0000000..b31ddf2 --- /dev/null +++ b/bluez.sysext/files/usr/lib/systemd/system/bluetooth.service @@ -0,0 +1,50 @@ +[Unit] +Description=Bluetooth service +Documentation=man:bluetoothd(8) +# Only present once a Bluetooth controller has been detected and its driver +# (btusb, hci_uart, ...) has loaded. Without an adapter the unit is skipped +# rather than failed, so the Upholds= drop-in does not retry in a loop. A +# controller that shows up later is caught by the udev rule this sysext ships, +# since a Condition= is only evaluated when the unit is queued. +ConditionPathIsDirectory=/sys/class/bluetooth +After=dbus.service +Requires=dbus.service + +[Service] +Type=dbus +BusName=org.bluez +# The sysext drops a new D-Bus policy into /usr/share/dbus-1/system.d, which a +# running dbus-daemon has not read yet. Without this reload bluetoothd cannot +# take its name on the system bus on first merge. busctl ships with systemd, so +# it is always present on Flatcar. +ExecStartPre=-/usr/bin/busctl call org.freedesktop.DBus /org/freedesktop/DBus org.freedesktop.DBus ReloadConfig +ExecStart=/usr/libexec/bluetooth/bluetoothd +NotifyAccess=main +LimitNPROC=1 +Restart=on-failure +RestartSec=2 + +# Adapter settings and pairing keys. Replaces a tmpfiles.d rule: systemd creates +# the directory before the daemon starts, and ProtectSystem=strict below needs +# it declared here to leave it writable. +StateDirectory=bluetooth +StateDirectoryMode=0700 + +# Sandboxing, as shipped by upstream BlueZ in src/bluetooth.service.in. +CapabilityBoundingSet=CAP_NET_RAW CAP_NET_ADMIN CAP_NET_BIND_SERVICE +NoNewPrivileges=true +ProtectHome=true +ProtectSystem=strict +PrivateTmp=true +ProtectKernelTunables=true +ProtectControlGroups=true +MemoryDenyWriteExecute=true +RestrictRealtime=true + +[Install] +# Upstream BlueZ uses bluetooth.target here. systemd does ship that target, but +# nothing pulls it in by default, so 'systemctl enable bluetooth.service' would +# leave the service unstarted. multi-user.target matches what the other sysexts +# in this repo do and makes enabling behave as expected. +WantedBy=multi-user.target +Alias=dbus-org.bluez.service diff --git a/bluez.sysext/files/usr/lib/systemd/system/multi-user.target.d/10-bluez.conf b/bluez.sysext/files/usr/lib/systemd/system/multi-user.target.d/10-bluez.conf new file mode 100644 index 0000000..777b7ae --- /dev/null +++ b/bluez.sysext/files/usr/lib/systemd/system/multi-user.target.d/10-bluez.conf @@ -0,0 +1,2 @@ +[Unit] +Upholds=bluetooth.service diff --git a/bluez.sysext/files/usr/lib/udev/rules.d/99-bluez.rules b/bluez.sysext/files/usr/lib/udev/rules.d/99-bluez.rules new file mode 100644 index 0000000..b6ede3d --- /dev/null +++ b/bluez.sysext/files/usr/lib/udev/rules.d/99-bluez.rules @@ -0,0 +1,8 @@ +# A controller can appear long after multi-user.target has been reached: a USB +# dongle gets hot-plugged, or btusb/hci_uart loads late. bluetooth.service is +# guarded by ConditionPathIsDirectory=/sys/class/bluetooth, and a Condition= is +# only evaluated when the unit is queued -- nothing re-queues it when the +# directory finally shows up, so the daemon would stay stopped indefinitely. +# Pulling the unit in from the device event closes that gap; starting an +# already-running service is a no-op, so this is safe to fire per controller. +ACTION=="add", SUBSYSTEM=="bluetooth", TAG+="systemd", ENV{SYSTEMD_WANTS}+="bluetooth.service" diff --git a/bluez.sysext/test.sh b/bluez.sysext/test.sh new file mode 100644 index 0000000..e69de29 diff --git a/docs/bluez.md b/docs/bluez.md new file mode 100644 index 0000000..1cc315e --- /dev/null +++ b/docs/bluez.md @@ -0,0 +1,105 @@ +# BlueZ sysext + +This extension ships [BlueZ](http://www.bluez.org/), the Linux Bluetooth userspace: the `bluetoothd` daemon plus the `bluetoothctl`, `btmon`, `btmgmt` and `btattach` tools. + +BlueZ has no upstream static or portable release and it links glib, dbus and readline, so this extension bundles the binaries together with their shared-library closure (including glibc and the dynamic loader) extracted from a Debian container, the same way the `qemu` and `tilde` extensions do. Because of that the version numbers you can build are the BlueZ versions Debian stable/testing ship, not arbitrary upstream releases. + +## Kernel requirements + +The extension ships **userspace only**. Bluetooth kernel modules have to match the running kernel exactly, so they belong in the Flatcar image rather than in a sysext. + +Flatcar images built before [flatcar/scripts#4197](https://github.com/flatcar/scripts/pull/4197) contain no Bluetooth support at all — `CONFIG_BT` was never enabled — so `bluetoothd` has nothing to talk to. On such an image the `bluetooth.service` unit shipped here is skipped rather than failed, because it is guarded by `ConditionPathIsDirectory=/sys/class/bluetooth`. + +On an image that does have the modules, the driver for your controller (`btusb` for USB adapters, `hci_uart` for UART-attached ones) is autoloaded when the device is detected, `/sys/class/bluetooth` appears, and `bluetooth.service` starts. + +## Usage + +Download and merge the sysext at provisioning time using the below butane snippet. + +The snippet includes automated updates via systemd-sysupdate. +Sysupdate will stage updates and request a reboot by creating a flag file at `/run/reboot-required`. +You can deactivate updates by changing `enabled: true` to `enabled: false` in `systemd-sysupdate.timer`. + +Note that the snippet is for the x86-64 version of BlueZ 5.82. Other architectures are also available. + +Check out the metadata release at https://github.com/flatcar/sysext-bakery/releases/tag/bluez for a list of all versions available in the bakery. + +```yaml +variant: flatcar +version: 1.0.0 + +storage: + files: + - path: /opt/extensions/bluez/bluez-5.82-x86-64.raw + mode: 0644 + contents: + source: https://extensions.flatcar.org/extensions/bluez-5.82-x86-64.raw + - path: /etc/sysupdate.bluez.d/bluez.conf + contents: + source: https://extensions.flatcar.org/extensions/bluez.conf + - path: /etc/sysupdate.d/noop.conf + contents: + source: https://extensions.flatcar.org/extensions/noop.conf + links: + - target: /opt/extensions/bluez/bluez-5.82-x86-64.raw + path: /etc/extensions/bluez.raw + hard: false +systemd: + units: + - name: systemd-sysupdate.timer + enabled: true + - name: systemd-sysupdate.service + dropins: + - name: bluez.conf + contents: | + [Service] + ExecStartPre=/usr/bin/sh -c "readlink --canonicalize /etc/extensions/bluez.raw > /tmp/bluez" + ExecStartPre=/usr/lib/systemd/systemd-sysupdate -C bluez update + ExecStartPost=/usr/bin/sh -c "readlink --canonicalize /etc/extensions/bluez.raw > /tmp/bluez-new" + ExecStartPost=/usr/bin/sh -c "if ! cmp --silent /tmp/bluez /tmp/bluez-new; then touch /run/reboot-required; fi" +``` + +## What the extension starts + +`bluetooth.service` is upheld by `multi-user.target`, so it starts on merge and is restarted if it dies. It runs `bluetoothd` as a D-Bus service under the `org.bluez` name. + +The extension drops a D-Bus policy file into `/usr/share/dbus-1/system.d/`, which a *running* `dbus-daemon` has not read yet. The unit therefore asks D-Bus to reload its configuration before starting `bluetoothd`; without that, `bluetoothd` cannot take its name on the system bus until the next reboot. + +Adapter settings and pairing keys are stored under `/var/lib/bluetooth`, created by the unit's `StateDirectory=`. + +The unit is guarded by `ConditionPathIsDirectory=/sys/class/bluetooth`, so on a host without a controller it is skipped rather than restarted in a loop. A controller that appears later — a hot-plugged USB dongle, or `btusb` loading after boot — is picked up by the udev rule the extension ships, which pulls the unit in from the device event. + +## Configuration + +`bluetoothd` reads `/etc/bluetooth/main.conf` if present and runs with built-in defaults otherwise. To change settings, drop your own file there via Ignition, for example to power adapters on automatically: + +```yaml +storage: + files: + - path: /etc/bluetooth/main.conf + mode: 0644 + contents: + inline: | + [Policy] + AutoEnable=true +``` + +## Checking it works + +```sh +# the controller shows up once the kernel driver has bound it +bluetoothctl list + +# live HCI trace, useful when a controller misbehaves +btmon +``` + +If `bluetoothctl list` prints nothing, check that a driver actually bound the controller: + +```sh +systemctl status bluetooth +ls /sys/class/bluetooth +journalctl -u bluetooth +``` + +An empty `/sys/class/bluetooth` means no Bluetooth driver is loaded — either the hardware is absent, or the image predates the kernel change linked above. diff --git a/docs/index.md b/docs/index.md index 1f95155..5fb93ee 100644 --- a/docs/index.md +++ b/docs/index.md @@ -61,6 +61,7 @@ Check out documentation on specific extensions at the navigation menu on the lef | Extension | Availability | Versions available | | ---------------- | ------------ | ------------- | | `bird` | released | [bird versions](https://github.com/flatcar/sysext-bakery/releases/tag/bird) | +| `bluez` | released | [bluez versions](https://github.com/flatcar/sysext-bakery/releases/tag/bluez) | | `chrony` | released | [chrony versions](https://github.com/flatcar/sysext-bakery/releases/tag/chrony) | | `cilium` | released | [cilium versions](https://github.com/flatcar/sysext-bakery/releases/tag/cilium) | | `cloud-hypervisor` | released | [cloud-hypervisor versions](https://github.com/flatcar/sysext-bakery/releases/tag/cloud-hypervisor) | diff --git a/release_build_versions.txt b/release_build_versions.txt index bc25a94..8fa6691 100644 --- a/release_build_versions.txt +++ b/release_build_versions.txt @@ -109,6 +109,9 @@ haproxy latest bird 3.1.2 bird latest +bluez 5.82 +bluez latest + btop 1.4.0 btop latest