Skip to main content
Qualcomm Linux · OTA

有一件事曾经让我害怕:向放在某个角落架子上的设备推送软件更新,它没有键盘、没有显示器,如果出了问题也没人能插上 USB 线。一次糟糕的更新,那台设备就成了砖头。乘以整个设备群,您就能明白为什么人们会为此失眠。 好消息是,Qualcomm Linux 2.0 已经内置了一个非常可靠的答案,它构建在 OSTree 之上。当我真正坐下来在 IQ-8275 EVK 上使用它时,最令我惊讶的是我自己需要构建的东西是如此之少。大部分困难的部分已经完成了。 这是一篇动手教程。命令就在正文里,您可以直接通读,也可以在真实硬件上跟着做,最终得到一台能通过网络自我更新的设备。所展示的一切都实际运行过,我引用的数字都来自真实的开发板。 先说清楚一件事,以免有人不小心把它交付给客户:这是一个实验室概念验证。它使用明文 HTTP 并关闭了签名检查,以便您能真正看到齿轮的转动。这对学习来说很棒,对生产来说很糟糕。文末我会谈到成熟的生产版本。

我们实际要做的事情

计划很小,但回报很有趣。我们构建同一镜像的两个版本,把第一个刷到开发板上,然后不进行第二次刷写,通过空中下载把运行中的设备迁移到第二个版本。 IQ-8275 EVK running v1 on the bench 工作台上的 IQ-8275 EVK(左)及其运行 v1 的 Weston 桌面(右)。 然后我们:
  1. 用 v1 刷写设备一次,因为在第一个 SOTA 镜像之前没有 OTA。
  2. 搭建一个极其简单的更新服务器。
  3. 构建 v2,并通过设备自己的小网页(就像真实用户那样)将其推送到运行中的设备上,无需重新刷写。
  4. 全程观察旧版本作为回滚保留下来。
  5. 测量实际变更有多小。
最后一步是让我咧嘴笑的那一步,所以请坚持看到那里。

Qualcomm 完成了繁重工作的部分

坦白说,因为我在这里工作:这件事之所以容易,是因为工作已经在层里完成了。发行版由 meta-qcom-distro 提供(meta-qcom 本身只是 BSP),它有两种风格。默认的 qcom-distro 是普通的包管理根文件系统。有趣的是 qcom-distro-sota。SOTA 意思是 software over the air(空中软件下载),选择该发行版会为您启用整个更新栈。 “整个更新栈”是什么意思?当您用 qcom-distro-sota 构建时,您会得到内置于镜像的 OSTree、一个知道如何引导 OSTree 部署的 initramfs,并且构建会在您的普通可刷写镜像旁边生成一个 ostree_repo。设备就是从这个仓库拉取更新的。这些都不是您写的。您只是选择了一个不同的发行版。从这里开始,我将把结果称为 SOTA 镜像:一个恰好内置了 OTA 技术栈的普通可刷写镜像。 构建命令说实话几乎无聊,而对于构建命令来说,这是我能给出的最高赞美:
那个用冒号分隔的列表只是 kas 在堆叠配置片段。qcom-distro-sota.yml 完成了所有的魔法。把它换成 qcom-distro.yml,您就会得到一个没有 OTA 的普通镜像。同样的机器、同样的 recipe,只差一个片段。我非常喜欢 OTA 是一个构建时开关,而不是一个需要额外附加的巨大独立项目。(稍后构建 v1 和 v2 时,您会在那行命令上看到多一个片段 ota-demo.yml。那不是原厂 Qualcomm 片段,而是我们自己添加的小演示层,一会儿我们就创建它。) 有一件事值得提前了解:OTA 只有在设备上已经有 OSTree 技术栈时才有效,而普通的 qcom-distro 镜像没有。所以您需要通过 USB 刷写一次 SOTA 镜像,从那以后就可以通过空中下载更新了。

两分钟看懂 OSTree

人们称 OSTree 为”根文件系统的 git”,老实说,这个心智模型作为入门足够好了。它将文件系统的完整快照作为提交存储在仓库中。每个提交都是内容寻址的,因此相同的文件只存储一次并被共享。设备拉取一个新提交,从中构建一个新部署,并在下次重启时引导进入该部署。旧的部署保持原样。 最后那句话就是整个安全故事。您正在运行的系统永远不会被就地编辑。新版本完全在一旁组装,切换只在干净的重启时发生。如果新部署没能启动,您仍然停留在旧版本上,完好无损。 以下是更新落地后设备上的样子。星号是您引导的部署,第二行是您的安全网:
而 /usr,即实际的操作系统内容,是以只读方式挂载的,所以没有任何东西能偷偷地在上面涂写:
您的运行时配置和状态应该存放在那个不可变部分之外,在 /etc、/var 和 /home 之类的位置;这些运行时区域旨在跨更新保留,而不是被清除。这是一条设计规则,而不是对您在构建时随手丢进去的任何东西的魔法。我们会在结尾用一个小文件回到这个区别上。

OSTree 还是 A/B,以及我为什么选择 OSTree

这是每个人都会问的问题,所以让我认真回答一下。 A/B 更新,也叫双分区(dual bank),是把存储划分为系统的两个完整副本,槽位 A 和槽位 B。您从 A 运行,把整个新镜像写入 B,翻转一个标志,下次引导 B。它易于推理且坚如磐石,这就是手机一直采用它的原因。 但 A/B 有两项容易被忽视的成本。第一是空间。您要永久放弃足够容纳两份完整操作系统副本的闪存,时刻如此,尽管您只会运行其中一个。在成本敏感的边缘设备上,第二个槽位是实实在在的物料成本。第二是带宽。在最简单的 A/B 方案中,更新意味着传输一个全新的系统镜像,因为您要写入整个槽位。 OSTree 从另一个方向来解决问题。没有两个固定槽位。只有一个存储部署的仓库,它们共享所有共同的文件。更新时,设备只拉取真正变化的对象。有趣的地方就在这里,我稍后会给您一个我至今仍觉得有点离谱的真实数字。 因此,权衡大致是这样的。A/B 给您一个极其简单的心智模型和一个有保障的已知良好槽位,代价是双倍的闪存和大块头的更新。OSTree 给您微小的、节省带宽的更新以及带回滚安全的原子部署,无需保留第二个完整槽位。它要求两样东西作为回报:一个稍微不那么直观的心智模型,以及只读操作系统的纪律,这意味着您必须预先决定可写数据放在哪里(/etc、/var、/home),而不是散落在一个随处可编辑的文件系统上。对于定期发布功能的边缘 Linux 设备,OSTree 这边的权衡非常有吸引力,而且再说一次,Qualcomm 已经把它接好了。

环境搭建:三台机器

您需要三个角色。它们可以是三台机器,或者说实话,一块开发板加一台身兼两职的笔记本。
构建树位于 ~/yocto-build,最重要的目录是构建输出的位置:
需要明确的一个假设:本文从一个已有的 Qualcomm Linux Yocto 检出开始,其中已经有 meta-qcom 层和 kas 片段。它不涉及 repo 初始化或所有层的首次冷克隆,因此它不是一份从零开始的 SDK 搭建指南。如果您是第一次搭建构建树,请先用常规的 Qualcomm Linux 入门流程完成那部分 — 参见使用 Yocto 构建 Qualcomm Linux — 然后再回到这里。
在 IQ-8275 EVK 上使用 Qualcomm Linux 2.0 测试,meta-qcom / meta-qcom-distro 位于 qli-2.0 发布标签。本教程对层版本很敏感 — 如果路径或软件包名称在较新版本中发生了变动,请固定到此标签以便完全按照本文操作。
开始之前关于速度的一点提示:在 v1 和 v2 之间复用构建目录、sstate-cache 和 downloads。不要清除它们。有了热缓存,这里的每次重新构建大约需要八分钟,而不是数小时的冷构建。这就是快速迭代与出门喝咖啡的区别。

把一切串在一起的小 Yocto 层

我把所有自定义内容都放进一个小层 meta-ota-demo 中。它有三项工作:安装更新 agent、向 /etc/ota-demo-version 写入可见的版本标记,以及覆盖桌面壁纸,使更新在屏幕上一目了然。其中没有任何魔法,只是几个小文件。先在构建主机上创建层的目录布局:
下面的每个文件都在配套文件页面上完整列出并带有复制按钮 — 这里我只按创建顺序说明每个文件是什么并给出链接。 先从层配置 layer.conf 开始,保存到 meta-ota-demo/conf/layer.conf。它将 meta-ota-demo 注册到构建中。 接下来是版本标记 recipe ota-demo-version.bb,保存到 meta-ota-demo/recipes-ota/ota-demo-version/ota-demo-version.bb。稍后提升 OTA_DEMO_VERSION 就是”旋钮 1”;它只是把值写入 /etc/ota-demo-version,并作为 conffile 交付,使其在更新后仍保留。 然后是壁纸覆盖 weston-init.bbappend,保存到 meta-ota-demo/recipes-graphics/wayland/weston-init.bbappend。它前置了我们的文件路径,使我们的 qcom-background.png 优先于原厂的(“旋钮 2”)。 agent 的 recipe ota-agent_1.0.bb,保存到 meta-ota-demo/recipes-ota/ota-agent/ota-agent_1.0.bb。它唯一不太直观的部分是 RDEPENDS:agent 是纯 Python 标准库,但在最小化的 Yocto 镜像上标准库被拆分成多个软件包,因此您必须准确列出 agent 导入的那些(稍后详述)。 它的 systemd 单元 ota-agent.service,保存到 meta-ota-demo/recipes-ota/ota-agent/files/ota-agent.service。 agent 的配置文件 ota-agent.conf,保存到 meta-ota-demo/recipes-ota/ota-agent/files/ota-agent.conf。 agent 程序本身 ota-agent.py,保存到 meta-ota-demo/recipes-ota/ota-agent/files/ota-agent.py — 它是唯一长到值得单独一节的文件,我们稍后会回到它。最后是 kas 片段 ota-demo.yml,保存到 meta-qcom/ci/ota-demo.yml。它把该层拉入构建并向镜像添加软件包;注意它与其他 Qualcomm CI 片段放在一起,不在层内部。 我们会在每次构建时编辑该片段的 IMAGE_INSTALL:append 行来添加或移除 Vim。这是我们三个”旋钮”之一。 以下是所有文件的位置。八个文件在新的 meta-ota-demo 层里;第九个 ota-demo.yml 与 meta-qcom 中的原厂 Qualcomm CI 片段并列:
weston-init/qcom-background.png 不是您手工创建的文件 — 每次构建时将 wallpapers-v1.png 或 wallpapers-v2.png 复制到该路径,bbappend 中的 FILESEXTRAPATHS:prepend 就是这样拾取它的。我们会在下面每个版本的构建步骤中执行这个复制。 趁现在,把 agent 指向您的服务器。agent 读取设备上的 /etc/ota-agent.conf,它来自层中的文件:

简单说说这个 agent,因为它几乎什么都不是

OSTree 给了您引擎。它没有给您一个按钮。成熟的生产答案是 aktualizr-lite 与 SOTA 技术栈的 TUF 路径这样的真实后端通信,而且该客户端已经包含在发行版中。但我想看到齿轮的转动,所以我写了最小可能的东西:一个微型 Python 服务,没有框架,不需要 pip 安装任何东西,在 8088 端口提供一个网页。该页面显示正在运行什么,向服务器查询有什么可用,如果有新版本就显示一个按钮。按钮的底层恰好运行这些:
这就是整个更新。拉取新提交、部署它、重启进入它。网页只是这四行命令的一张友好面孔。 整个 agent 是一个文件,只用 Python 标准库,几分钟就能读完。完整源码在配套文件页面上的 ota-agent.py — 从那里复制,而不是重新输入约 320 行,并将其保存到 recipe 期望的位置:meta-ota-demo/recipes-ota/ota-agent/files/ota-agent.py。 如果您想不离开本页快速浏览,它的核心很小:一个 do_update(),它注册远端,运行 ostree pull 然后 ostree admin deploy 并重启,外面包着一个渲染状态卡片和按钮的单页 HTTP 处理程序。其余部分都是解析 ostree admin status 和格式化页面。 那个 recipe 里藏着一个值得指出的 Yocto 坑,因为它花了我一个小时。在最小镜像上,Python 标准库被拆分成一堆单独的软件包。所以即使 agent 只导入”内置”模块,镜像里实际上也没有它们,服务在首次启动时循环崩溃,报 ModuleNotFoundError: No module named 'html'——当您确信自己没有导入任何花哨的东西时,读到这个信息会非常困惑。解决办法是在 recipe 中准确声明需要哪些包:
一旦声明了这些,agent 就能在全新镜像上干净地启动,零手工修复,这才是您想要的标准。如果每次刷写后都得 SSH 进去修补点什么,那您并没有真正交付任何东西。

两张壁纸

我们希望变化是可见的,所以每个版本使用不同的壁纸。两张 PNG 如下所示 — 或者用您自己的任意两张图片。 wallpapers-v1.png — grey 版本 1 壁纸。下载:wallpapers-v1.png wallpapers-v2.png — purple 版本 2 壁纸。下载:wallpapers-v2.png 把两张 PNG 放进去:
每次构建会将其中一张复制到 recipe 的实际壁纸路径。这是我们的第二个旋钮。

构建 v1:标记 1、壁纸 A、无 Vim

三个旋钮,然后构建:
有了热缓存,这次构建对我来说大约花了八分钟,构建日志也确认几乎所有内容都被复用了:

搭建更新服务器

这部分简单得几乎让人不好意思。服务器是一个指向 Web 根目录的静态 HTTP 服务器,该目录包含两样东西:构建产生的 ostree_repo 的副本(我们发布 v2 时会同步过来),以及一个小小的 version.json,内容是”这是最新版本及其提交”。创建 Web 根目录并开始提供服务:
就这样。确保设备能访问到它:
如果现在返回 404,没关系;此阶段的重点只是服务器可达。我们在发布 v2 时才写入 version.json。 以后想让更新来自互联网而不是您的笔记本?将 agent 指向一个公网 URL,或把仓库放在 CDN 后面。设备端不需要任何改动,因为对 OSTree 来说远端就是一个 URL。

将 v1 刷写到设备

这是我们唯一一次通过 USB 刷写。在第一个 SOTA 镜像之前没有 OTA,所以 v1 必须用常规方式刷写。构建在 ostree_repo 旁边生成了一个可刷写镜像:
将开发板置于 EDL 模式,并用标准的 Qualcomm 刷写流程(qdl / 您已经在这块板上使用的 flat-build 刷写工具)刷写该镜像。我不打算在这里重新记录 EDL 刷写,它在开发板的入门材料中有介绍,而且无论镜像是否为 SOTA,流程都相同。 启动后,确认 v1 已生效且 agent 自行启动:
您刚刚通过网络将一台运行中的设备迁移到了一个新构建的操作系统版本,而旧版本就在那里作为回滚。没有线缆。单是这一点在一年前就会让我惊叹。

构建 v2 并以轻松的方式更新

同样的三个旋钮,不同的值:标记 2、壁纸 B,这次装上 Vim。
现在发布它。发布分两步:把构建新产生的 ostree_repo 复制到服务器的 Web 根目录,然后写入设备 agent 读取的 version.json。如果您的构建主机和更新服务器是同一台机器,复制就是本地 rsync;如果是分开的,脚本会通过 SSH 拉取(脚本中有一行注释代码用于此)。这是一个小脚本 — 完整源码在配套文件页面上的 publish.sh。 把它放到更新服务器上,使其可执行,并浏览一下脚本顶部,确认 DEPLOY_DIR 与您的构建输出位置匹配:
如果您的更新服务器和构建主机是不同的机器,请取消注释 publish.sh 顶部附近的 SSH rsync 行(就在本地那行的上方),并将 DEPLOY_DIR 设置为构建主机上的路径。
运行它来发布 v2:
现在以真实用户的方式进行更新。在浏览器中打开设备的网页:
页面显示设备在 v1,v2 可用,还有一个按钮。点击它。更新在后台运行,页面显示更新日志 — 刷新可查看进度。设备重启,大约两分钟后回到 v2。 OTA web UI before and after the update 左:Web UI 显示 v2 可用及”Download & install”按钮。右:更新后的同一页面,v2 正在运行,v1 保留为回滚。 如果您是无头运行,按钮只是一个 POST,您可以在终端里做完全相同的事情:
重新连接并确认新世界:
壁纸变了,vim 出现了,版本文件读出 2。视觉差异很难错过: Desktop before and after: v1 grey wallpaper vs v2 purple Supergraphic v1 桌面(左):灰色 Qualcomm 壁纸,无 vim。v2 桌面(右):紫色 Supergraphic 壁纸,已安装 vim。同一台设备,通过空中下载更新。 屏幕上的壁纸也变了。实际操作后的一点诚实说明:Web UI 显示的是 OSTree 的 Version: 字段,对于每次构建它都是发行版版本 2.0,而不是我的演示标记。它仍然正确地比较实际的提交,所以”已是最新”是对的,但如果我要将其产品化,我会从签名的元数据中显示真正的产品发布版本,而不是依赖那个字段。

我答应过您的那个数字

重点来了。添加一整个文本编辑器、更换壁纸并提升一个版本文件,实际变更有多大? 首先,在朴素的整镜像推送中我们要替换的操作系统内容的大小:
现在在两个提交之间生成静态增量,让 OSTree 告诉我们真正的变更有多大:
压缩后的增量为 8,218,038 字节。约 7.8 MB。 相对于 2.1 GB 的操作系统内容,这大约小了 260 倍。就为了添加 Vim 和更换壁纸。您还可以准确看到发生了什么变化:
关于那个数字的一点诚实说明。static-delta generate 测量的是变更压缩后有多小 — 约 7.8 MB。本文的极简 POC 流程没有把该增量发布到服务器,因此设备的 ostree pull 实际上是以松散对象方式获取变更(上面的 loose=40542896,约 40 MB),而不是一个整齐的 7.8 MB 文件。相对于 2.1 GB 这仍然很小,但要让设备在网络上拉取 7.8 MB 的增量,您需要在设备更新之前把静态增量生成到被服务的仓库中 — 这正是生产环境的做法。 现在想象一下,在按流量计费的蜂窝网络或不稳定的乡村链路上跨整个设备群。每台设备拉取个位数兆字节与几 GB 之间的差异,就是”周二随手推送一次更新”与”像军事行动一样安排更新”之间的差异。对于生产环境,您会在服务器上预生成这些静态增量,让每台设备得到一个整洁的文件。同样的思路,在线上更加紧凑。

验证持久性声明

我说过 /etc 在更新后仍会保留。让我实际演示而不是空口断言。在 v1 到 v2 更新之前,在 /etc 中放一个文件:
更新并重启后:
它保留下来了。这就是 OSTree 模型兑现承诺:/usr 中的操作系统被整体替换,但您在 /etc 中的东西被带了过来。构建会警告您一件事,而且您应该听:构建时放入 /var/lib 或 /home 之类位置的数据不会以您可能期望的方式保留。在 OSTree 系统中,初始镜像内容和持久运行时状态是两回事,决定什么放在哪里是产品的一个真正的设计步骤,而不是事后考虑。

下一步可以做什么

我构建的是有意为之的概念验证。它使用明文 HTTP,跳过了签名以保持机制可见,agent 以 root 运行,因为部署和重启需要这个权限。这些都不是您要交付的东西。对于生产环境,您会签名提交、通过 HTTPS 提供服务、给设备真正的身份,并依赖 SOTA 发行版已经捆绑的 aktualizr-lite 加 TUF 技术栈,让设备准确验证它们正在安装的内容,并获得包含发布策略和更新报告的真正设备群管理能力。您还会为已知的版本路径预生成静态增量。 但这一切的基础与我们刚刚使用的是同一个,并且随 Qualcomm Linux 2.0 开箱即用。原子部署、保留的回滚、微小的增量、只读操作系统与持久数据之间的清晰界线。那些困难的、吓人的、会把设备变砖的部分都被处理好了。剩下的是真正有趣的部分,决定您的设备应该如何以及何时更新,这部分您可以按产品的需要任意设计。 对于嵌入式客户来说,这里有趣的东西从来不是那个小小的 Python 网页。那个页面是用完即弃的。有趣的是,选择 qcom-distro-sota 会把镜像变成一个由 OSTree 管理的系统,一台真实设备可以在真实的 Yocto 构建的操作系统提交之间通过网络迁移而无需重新刷写,把旧版本留作安全网,只为变更的部分付出代价。这才是您可以在其上构建产品的部分。 如果您想亲自运行,较长文件的完整源码位于配套文件页面 — ota-agent.py Web agent 和 publish.sh 服务器脚本,每个都带复制按钮 — 而简短的 meta-ota-demo recipe 以可复制粘贴的 heredoc 形式内联在上文中。按所示创建它们,把三个 IP 地址指向您自己的设备,然后去给某个东西推送一次更新吧。