Skip to content

Commit 7b59d69

Browse files
committed
Support OEM systemd-sysext images and Flatcar extensions
The OEM software and future official Flatcar extensions will be shipped as systemd-sysext images, coupled to the Flatcar version and A/B updated. Instead of adding support for this in the update-engine C++ code the plan was to use a new helper binary in the post-inst action for downloading of the payload. Since this is not ready we use curl and a small script to decode the payload. The A/B update mechanism includes a migration path for old instances that first need a fallback download, because the old update-engine client doesn't pass the XML dump, then they need to go through another update cycle to have systemd-sysext images for both partitions, and then the migration can take place in the initrd where the old OEM contents get cleaned up.
1 parent 38c2ebe commit 7b59d69

File tree

6 files changed

+183
-5
lines changed

6 files changed

+183
-5
lines changed

decode_payload

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
#!/bin/bash
22
set -euo pipefail
33

4-
CHECKINPUT=0
4+
DEBUG="${DEBUG-0}"
5+
CHECKINPUT="${CHECKINPUT-0}"
56
SCRIPTFOLDER="$(dirname "$(readlink -f "$0")")"
67

8+
PROTOPATH="${PROTOPATH-"${SCRIPTFOLDER}"/src/update_engine}"
9+
710
if [ $# -lt 3 ] || [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
811
echo "Usage: $0 PUBKEY PAYLOAD OUTPUT [KERNELOUTPUT]"
912
echo "Decodes a payload, writing the decoded payload to stdout and payload information to stderr"
1013
echo "Only one optional kernel payload is supported (the output file will be zero-sized if no payload was found)."
1114
echo "Only a signle signature is supported as only one pubkey is accepted and in the multi-signature case only the second signature is looked at."
1215
echo "Pass CHECKINPUT=1 to process the inline checksums in addition to the final checksum."
16+
echo "Pass PROTOPATH=folder/ to specify the location of a directory with protobuf files."
1317
exit 1
1418
fi
1519

@@ -23,9 +27,11 @@ KERNEL="${4-}"
2327
MLEN=$(dd status=none bs=1 skip=12 count=8 if="${FILE}" | od --endian=big -An -vtu8 -w1024 | tr -d ' ')
2428

2529
# The manifest starts at offset 20 (with tail we do a +1 compared to dd) and we feed it into protoc for decoding (we assume that the text output format is stable)
26-
DESC=$(protoc --decode=chromeos_update_engine.DeltaArchiveManifest --proto_path "${SCRIPTFOLDER}"/src/update_engine "${SCRIPTFOLDER}"/src/update_engine/update_metadata.proto < <({ tail -c +21 "${FILE}" || true ; } |head -c "${MLEN}"))
30+
DESC=$(protoc --decode=chromeos_update_engine.DeltaArchiveManifest --proto_path "${PROTOPATH}" "${PROTOPATH}"/update_metadata.proto < <({ tail -c +21 "${FILE}" || true ; } |head -c "${MLEN}"))
2731

28-
echo "${DESC}" >&2
32+
if [ "${DEBUG}" = 1 ]; then
33+
echo "${DESC}" >&2
34+
fi
2935

3036
# Truncate
3137
true > "${PARTOUT}"
@@ -117,8 +123,12 @@ fi
117123

118124
# The signature protobuf message is at the signature offset
119125
# Decoding the "data" field caues some troubles and needs a workaround below for the dev key
120-
SIGDESC=$(protoc --decode=chromeos_update_engine.Signatures --proto_path "${SCRIPTFOLDER}"/src/update_engine "${SCRIPTFOLDER}"/src/update_engine/update_metadata.proto < <({ tail -c +$((21 + MLEN + SIGOFFSET)) "${FILE}" || true ; } |head -c "${SIGSIZE}"))
121-
echo "${SIGDESC}" >&2
126+
SIGDESC=$(protoc --decode=chromeos_update_engine.Signatures --proto_path "${PROTOPATH}" "${PROTOPATH}"/update_metadata.proto < <({ tail -c +$((21 + MLEN + SIGOFFSET)) "${FILE}" || true ; } |head -c "${SIGSIZE}"))
127+
128+
if [ "${DEBUG}" = 1 ]; then
129+
echo "${SIGDESC}" >&2
130+
fi
131+
122132
VERSION=2 # Init for the single-signature case, in the many-signature case the first signature ("version 1" but better would be "number 1") is,
123133
# at least for Flatcar production, a dummy and parsing it overwrites this variable with 1 and it will be ignored,
124134
# the second signature is "version 2" and the one we want to check. Even if only one signature is there it becomes "version 2",

flatcar-postinst

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ fi
3535
# shellcheck source=/dev/null
3636
source "${INSTALL_MNT}/lib/os-release"
3737
NEXT_VERSION_ID=${VERSION_ID}
38+
NEXT_VERSION="${VERSION}"
3839

3940
# shellcheck source=/dev/null
4041
source /usr/lib/os-release
@@ -43,6 +44,167 @@ tee_journal() {
4344
tee >(systemd-cat -t coreos-postinst)
4445
}
4546

47+
OEMID=$({ grep -m 1 -o "^ID=.*" "${OEM_MNT}"/oem-release || true ; } | cut -d = -f 2)
48+
49+
# Must not be used as "if sysext_download; then" or "sysext_download ||" because that makes set -e a no-op, and also must not use "( sysext_download )" because we want to set the global SUCCESS variable.
50+
sysext_download() {
51+
local name="$1" # Payload name
52+
local target="$2" # Path to write the payload to, writing does not need to be atomic because the caller later does an atomic move
53+
local from="${3-}" # Either path to XML dump or the constant "release-server"
54+
local base=""
55+
local entries=""
56+
local hash=""
57+
local size=""
58+
local url=""
59+
local ret
60+
SUCCESS=false
61+
set +e
62+
(
63+
set -e
64+
# TODO: Replace the below with invoking an ue-rs helper binary for downloading the payload "name", either from the XML data or the release server ("from"), and write unpacked, verified file to "target"
65+
if [ "${from}" = "release-server" ]; then
66+
url="https://update.release.flatcar-linux.net/${FLATCAR_BOARD}/${NEXT_VERSION}/${name}"
67+
elif [ "${from}" = "bincache-server" ]; then
68+
url="https://bincache.flatcar-linux.net/images/${FLATCAR_BOARD/-usr}/${NEXT_VERSION}/${name}"
69+
else
70+
base=$(grep -m 1 -o 'codebase="[^"]*"' "${from}" | cut -d '"' -f 2)
71+
entries=$(grep -m 1 -o "<package name=\"${name}\"[^>]*" "${from}")
72+
url="${base}/${name}"
73+
size=$(echo "${entries}" | grep -o 'size="[0-9]*' | cut -d '"' -f 2)
74+
hash=$(echo "${entries}" | grep -o -P 'hash="[^"]*' | cut -d '"' -f 2) # openssl dgst -binary -sha1 < "$PAYLOAD" | base64
75+
fi
76+
rm -f "${target}.tmp"
77+
curl -fsSL --retry-delay 1 --retry 60 --retry-connrefused --retry-max-time 60 --connect-timeout 20 -o "${target}.tmp" "${url}"
78+
if [ "${size}" != "" ] && [ "${hash}" != "" ]; then
79+
if [ "$(stat --printf='%s' "${target}.tmp")" != "${size}" ]; then
80+
echo "Size mismatch for ${name}" >&2
81+
return 1 # jump to ret=
82+
fi
83+
if [ "$(openssl dgst -binary -sha1 < "${target}.tmp" | base64)" != "${hash}" ]; then
84+
echo "Hash mismatch for ${name}" >&2
85+
return 1 # jump to ret=
86+
fi
87+
fi
88+
# Using "${INSTALL_MNT}" here is ok because it was verified first by update-engine
89+
PROTOPATH="${INSTALL_MNT}"/share/update_engine/ "${INSTALL_MNT}"/share/update_engine/decode_payload /usr/share/update_engine/update-payload-key.pub.pem "${target}.tmp" "${target}"
90+
)
91+
ret=$?
92+
set -e
93+
rm -f "${target}.tmp"
94+
if [ "${ret}" -eq 0 ]; then
95+
SUCCESS=true
96+
fi
97+
}
98+
99+
# To know whether an OEM update payload is expected we can't rely on checking if the Omaha response contains one
100+
# because users may run their own instance and forget to supply it, or this is an old instance that doesn't hand us
101+
# the XML dump over. In both cases we do a fallback download and rely on a hardcoded list of OEM sysexts which we
102+
# anyway need to maintain for the migration actions. Besides checking that an entry in the list exists we can also
103+
# check for the active-oem-OEM flag file to support custom OEMs (but they must be part of the Omaha response).
104+
if [ "${OEMID}" != "" ] && { [ -e "${INSTALL_MNT}/share/flatcar/oems/${OEMID}" ] || [ -e "${OEM_MNT}/sysext/active-oem-${OEMID}" ]; }; then
105+
mkdir -p "${OEM_MNT}"/sysext/ /etc/flatcar/oem-sysext/
106+
# Delete sysext images that belonged to the now overwritten /usr partition but keep the sysext image for the current version
107+
KEEP="${OEM_MNT}/sysext/oem-${OEMID}-${VERSION}.raw"
108+
if [ ! -e "${KEEP}" ]; then
109+
KEEP="/etc/flatcar/oem-sysext/oem-${OEMID}-${VERSION}.raw"
110+
fi
111+
if [ ! -e "${KEEP}" ]; then
112+
KEEP="${OEM_MNT}/sysext/oem-${OEMID}-initial.raw" # It may not exist as well but that's ok (also, it can only exist on the OEM partition)
113+
fi
114+
shopt -s nullglob
115+
for OLD_IMAGE in "${OEM_MNT}"/sysext/oem*raw /etc/flatcar/oem-sysext/oem*raw; do
116+
if [ "${OLD_IMAGE}" != "${KEEP}" ] && [ -f "${OLD_IMAGE}" ]; then
117+
rm -f "${OLD_IMAGE}"
118+
fi
119+
done
120+
# Note that in the case of VERSION=NEXT_VERSION we will replace the running sysext and maybe it's better
121+
# to do so than not because it allows to recover from a corrupted file (where the corruption happened on disk)
122+
SUCCESS=false
123+
# Preferred is to download from the location given by the Omaha response
124+
# which only works with a new update-engine client that creates "full-response",
125+
# and we also have to check that this file was created fresh for this update operation
126+
# (relies on the reset of /var/lib/update_engine/prefs/previous-version that old clients also do)
127+
if [ -e /var/lib/update_engine/prefs/full-response ] && [ $(stat -L --printf='%Y' /var/lib/update_engine/prefs/full-response) -gt $(stat -L --printf='%Y' /var/lib/update_engine/prefs/previous-version) ]; then
128+
rm -f "/var/lib/update_engine/oem-${OEMID}.raw"
129+
sysext_download "oem-${OEMID}.gz" "/var/lib/update_engine/oem-${OEMID}.raw" /var/lib/update_engine/prefs/full-response
130+
fi
131+
# If that was not provided due to updating from an old version or if the download failed, try the release server or bincache
132+
if [ "${SUCCESS}" = false ]; then
133+
rm -f "/var/lib/update_engine/oem-${OEMID}.raw"
134+
PAYLOADSERVER=release-server
135+
PAYLOADNAME="oem-${OEMID}.gz"
136+
if [ "$(md5sum /usr/share/update_engine/update-payload-key.pub.pem | cut -d " " -f 1)" = "7192addf4a7f890c0057d21653eff2ea" ]; then
137+
PAYLOADSERVER=bincache-server
138+
PAYLOADNAME="flatcar_test_update-oem-${OEMID}.gz"
139+
fi
140+
sysext_download "${PAYLOADNAME}" "/var/lib/update_engine/oem-${OEMID}.raw" "${PAYLOADSERVER}"
141+
fi
142+
if [ "${SUCCESS}" = false ]; then
143+
rm -f "/var/lib/update_engine/oem-${OEMID}.raw"
144+
echo "Failed to download required OEM update payload" >&2
145+
exit 1
146+
fi
147+
NEW_SYSEXT="${OEM_MNT}/sysext/oem-${OEMID}-${NEXT_VERSION}.raw"
148+
# We don't need to check if it's the initial MVP OEM because it's an update payload provided for a particular version
149+
echo "Trying to place /var/lib/update_engine/oem-${OEMID}-${NEXT_VERSION}.raw on OEM partition" >&2
150+
if ! mv "/var/lib/update_engine/oem-${OEMID}.raw" "${NEW_SYSEXT}"; then
151+
echo "That failed, moving it to right location on root partition" >&2
152+
NEW_SYSEXT="/etc/flatcar/oem-sysext/oem-${OEMID}-${NEXT_VERSION}.raw"
153+
mv "/var/lib/update_engine/oem-${OEMID}.raw" "${NEW_SYSEXT}"
154+
fi
155+
if [ -e "${KEEP}" ] && [ -e "${NEW_SYSEXT}" ] && [ ! -e "${OEM_MNT}/sysext/active-oem-${OEMID}" ]; then
156+
if [ -e "${INSTALL_MNT}/share/flatcar/oems/${OEMID}" ]; then
157+
touch "${OEM_MNT}/sysext/migrate-oem-${OEMID}"
158+
fi
159+
touch "${OEM_MNT}/sysext/active-oem-${OEMID}"
160+
fi
161+
fi
162+
163+
# Download official Flatcar extensions
164+
# The enabled-sysext.conf file is read from /etc and /usr and contains one name per line,
165+
# and when the name is prefixed with a "-" it means that the extension should be disabled if enabled by default in the file from /usr.
166+
# It may contain comments starting with "#" at the beginning of a line or after a name.
167+
# The file is also used in bootengine to know which extensions to enable.
168+
# Note that we don't need "{ grep || true ; }" to suppress the match return code because in for _ in $(grep...) return codes are ignored
169+
for NAME in $(grep -h -o '^[^#]*' /etc/flatcar/enabled-sysext.conf /usr/share/flatcar/enabled-sysext.conf | grep -v -x -f <(grep '^-' /etc/flatcar/enabled-sysext.conf | cut -d - -f 2-) | grep -v -P '^(-).*'); do
170+
KEEP="/etc/flatcar/sysext/flatcar-${NAME}-${VERSION}.raw"
171+
shopt -s nullglob
172+
# Delete sysext images that belonged to the now overwritten /usr partition but keep the sysext image for the current version
173+
for OLD_IMAGE in /etc/flatcar/sysext/flatcar*raw; do
174+
if [ "${OLD_IMAGE}" != "${KEEP}" ] && [ -f "${OLD_IMAGE}" ]; then
175+
rm -f "${OLD_IMAGE}"
176+
fi
177+
done
178+
# Note that in the case of VERSION=NEXT_VERSION we will replace the running sysext and maybe it's better
179+
# to do so than not because it allows to recover from a corrupted file (where the corruption happened on disk)
180+
SUCCESS=false
181+
# Preferred is to download from the location given by the Omaha response
182+
# which only works with a new update-engine client that creates "full-response",
183+
# and we also have to check that this file was created fresh for this update operation
184+
# (relies on the reset of /var/lib/update_engine/prefs/previous-version that old clients also do)
185+
if [ -e /var/lib/update_engine/prefs/full-response ] && [ $(stat -L --printf='%Y' /var/lib/update_engine/prefs/full-response) -gt $(stat -L --printf='%Y' /var/lib/update_engine/prefs/previous-version) ]; then
186+
rm -f "/var/lib/update_engine/flatcar-${NAME}.raw"
187+
sysext_download "flatcar-${NAME}.gz" "/var/lib/update_engine/flatcar-${NAME}.raw" /var/lib/update_engine/prefs/full-response
188+
fi
189+
# If that was not provided due to updating from an old version or if the download failed, try the release server or bincache
190+
if [ "${SUCCESS}" = false ]; then
191+
rm -f "/var/lib/update_engine/flatcar-${NAME}.raw"
192+
PAYLOADSERVER=release-server
193+
PAYLOADNAME="flatcar-${NAME}.gz"
194+
if [ "$(md5sum /usr/share/update_engine/update-payload-key.pub.pem | cut -d " " -f 1)" = "7192addf4a7f890c0057d21653eff2ea" ]; then
195+
PAYLOADSERVER=bincache-server
196+
PAYLOADNAME="flatcar_test_update-flatcar-${NAME}.gz"
197+
fi
198+
sysext_download "${PAYLOADNAME}" "/var/lib/update_engine/flatcar-${NAME}.raw" "${PAYLOADSERVER}"
199+
fi
200+
if [ "${SUCCESS}" = false ]; then
201+
rm -f "/var/lib/update_engine/flatcar-${NAME}.raw"
202+
echo "Failed to download required OEM update payload" >&2
203+
exit 1
204+
fi
205+
mv "/var/lib/update_engine/flatcar-${NAME}.raw" "/etc/flatcar/sysext/flatcar-${NAME}-${NEXT_VERSION}.raw"
206+
done
207+
46208
# Keep old nodes on cgroup v1
47209
if [[ "${BUILD_ID}" != "dev-"* ]]; then
48210
if [ "${VERSION_ID%%.*}" -lt 2956 ]; then

src/update_engine/omaha_request_action.cc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,9 @@ void OmahaRequestAction::TransferComplete(HttpFetcher *fetcher,
614614
string current_response(response_buffer_.begin(), response_buffer_.end());
615615
LOG(INFO) << "Omaha request response: " << current_response;
616616

617+
LOG_IF(WARNING, !system_state_->prefs()->SetString(kPrefsFullResponse, current_response))
618+
<< "Unable to write full response.";
619+
617620
// Events are best effort transactions -- assume they always succeed.
618621
if (IsEvent()) {
619622
CHECK(!HasOutputPipe()) << "No output pipe allowed for event requests.";

src/update_engine/omaha_request_action_unittest.cc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,7 @@ TEST(OmahaRequestActionTest, FormatUpdateCheckOutputTest) {
517517
NiceMock<PrefsMock> prefs;
518518
EXPECT_CALL(prefs, GetString(kPrefsPreviousVersion, _))
519519
.WillOnce(DoAll(SetArgumentPointee<1>(string("")), Return(true)));
520+
EXPECT_CALL(prefs, SetString(kPrefsFullResponse, _)).Times(1);
520521
EXPECT_CALL(prefs, SetString(kPrefsPreviousVersion, _)).Times(1);
521522
ASSERT_FALSE(TestUpdateCheck(&prefs,
522523
GetDefaultTestParams(),

src/update_engine/prefs.cc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const char kPrefsCurrentUrlIndex[] = "current-url-index";
3838
const char kPrefsCurrentUrlFailureCount[] = "current-url-failure-count";
3939
const char kPrefsBackoffExpiryTime[] = "backoff-expiry-time";
4040
const char kPrefsAlephVersion[] = "aleph-version";
41+
const char kPrefsFullResponse[] = "full-response";
4142

4243
bool Prefs::Init(const files::FilePath& prefs_dir) {
4344
prefs_dir_ = prefs_dir;

src/update_engine/prefs_interface.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ extern const char kPrefsCurrentUrlIndex[];
2828
extern const char kPrefsCurrentUrlFailureCount[];
2929
extern const char kPrefsBackoffExpiryTime[];
3030
extern const char kPrefsAlephVersion[];
31+
extern const char kPrefsFullResponse[];
3132

3233
// The prefs interface allows access to a persistent preferences
3334
// store. The two reasons for providing this as an interface are

0 commit comments

Comments
 (0)