#!/bin/bash set -euo pipefail opts=$(getopt --name "$(basename "${0}")" --options 'hV:P:E:O:L:M:DFA' \ --longoptions 'help,to-version:,to-payload:,extension:,oem-payloads:,listen-port-1:,listen-port-2:,force-dev-key,force-flatcar-key,disable-afterwards' -- "${@}") eval set -- "${opts}" USER_PAYLOAD= PAYLOAD= VERSION= EXTENSIONS=() USE_OEM=yes FORCE_DEV_KEY= FORCE_FLATCAR_KEY= DISABLE_AFTERWARDS= LISTEN_PORT_1=9090 LISTEN_PORT_2=9091 while true; do case "$1" in -h|--help) echo "Usage: $(basename "${0}") --to-version VERSION [--to-payload FILENAME [--extension FILENAME]...] [--oem-payloads ] [--listen-port-1 PORT] [--listen-port-2 PORT] [--force-dev-key|--force-flatcar-key|--disable-afterwards]" echo " Updates Flatcar Container Linux through a temporary local update service on localhost." echo " The update-engine service will be unmasked (to disable updates again use -A)." echo " The reboot should be done after applying the update, either manually or through your reboot manager (check locksmithd/FLUO)." echo " An error will be reported if a previously applied update wasn't booted into yet (you may discard it with 'update_engine_client -reset_status')." echo " Warning: If you jump between channels, delete any GROUP configured in /etc/flatcar/update.conf for the new defaults to apply." echo "Options:" echo " -V, --to-version Updates to the version, by default using the matching release from update.release.flatcar-linux.net" echo " -P, --to-payload Updates to the given Flatcar base update payload file instead of downloading it" echo " (filename does not matter and internally flatcar_production_update.gz is used)" echo " -E, --extension Provides the given extension image as part of the update, required for -P if the system needs an OEM" echo " or a Flatcar extension, can/must be specified multiple times (filename matters and should end with" echo " either oem-OEMID.gz or flatcar-NAME.gz)" echo " -O, --oem-payloads Overwrites whether OEM payloads should be provided (default '${USE_OEM}')" echo " -D, --force-dev-key Bind-mounts the dev key over /usr/share/update_engine/update-payload-key.pub.pem" echo " -F, --force-flatcar-key Bind-mounts the Flatcar release key over /usr/share/update_engine/update-payload-key.pub.pem" echo " -A, --disable-afterwards Writes SERVER=disabled to /etc/flatcar/update.conf when done (this overwrites any custom SERVER)" echo " -L, --listen-port-1 Overwrites standard listen port ${LISTEN_PORT_1}" echo " -M, --listen-port-2 Overwrites standard listen port ${LISTEN_PORT_2}" echo echo "Example for updating to the latest Stable release and disabling automatic updates afterwards:" echo ' VER=$(curl -fsSL https://stable.release.flatcar-linux.net/amd64-usr/current/version.txt | grep FLATCAR_VERSION= | cut -d = -f 2)' echo " $(basename "${0}") -V \$VER -A" exit 1 ;; -V|--to-version) shift VERSION="$1" ;; -P|--to-payload) shift PAYLOAD="$1" USER_PAYLOAD=1 if [ "$PAYLOAD" = "" ]; then echo "Error: --to-payload must not have an empty value" > /dev/stderr ; exit 1 fi ;; -E|--extension) shift if [ "$1" = "" ]; then echo "Error: --extension must not have an empty value" > /dev/stderr ; exit 1 fi if [[ ! "$(basename -- "$1")" =~ ^(flatcar|oem).*gz$ ]]; then echo "Error: --extension expects paths to files named oem-OEMID.gz or flatcar-NAME.gz (with possible 'flatcar_test_update-' prefix), found: $1" > /dev/stderr ; exit 1 fi EXTENSIONS+=("$1") ;; -O|--oem-payloads) shift USE_OEM="$1" if [ "${USE_OEM}" != "yes" ] && [ "${USE_OEM}" != "no" ]; then echo "Error: --oem-payloads must be 'yes' or 'no'" > /dev/stderr ; exit 1 fi ;; -L|--listen-port-1) shift LISTEN_PORT_1="$1" if [ "$LISTEN_PORT_1" = "" ]; then echo "Error: --listen-port-1 must not have an empty value" > /dev/stderr ; exit 1 fi ;; -M|--listen-port-2) shift LISTEN_PORT_2="$1" if [ "$LISTEN_PORT_2" = "" ]; then echo "Error: --listen-port-2 must not have an empty value" > /dev/stderr ; exit 1 fi ;; -D|--force-dev-key) FORCE_DEV_KEY=1 KEY="https://raw.githubusercontent.com/flatcar-linux/coreos-overlay/main/coreos-base/coreos-au-key/files/developer-v1.pub.pem" ;; -F|--force-flatcar-key) FORCE_FLATCAR_KEY=1 KEY="https://raw.githubusercontent.com/flatcar-linux/coreos-overlay/flatcar-master/coreos-base/coreos-au-key/files/official-v2.pub.pem" ;; -A|--disable-afterwards) DISABLE_AFTERWARDS=1 ;; --) shift break;; esac shift done if [ "$#" != 0 ]; then echo "Error: unexpected extra argumuents: $*" > /dev/stderr ; exit 1 fi if [ "$PAYLOAD" = "" ] && [ "${#EXTENSIONS[@]}" != 0 ]; then echo "Error: local extensions are only supported with --to-payload" > /dev/stderr ; exit 1 fi if [ "${VERSION}" = "" ]; then echo "Error: must specify --to-version" > /dev/stderr ; exit 1 fi if [ "${FORCE_DEV_KEY}" = "1" ] && [ "${FORCE_FLATCAR_KEY}" = "1" ]; then echo "Error: must only specify one of --force-dev-key or --force-flatcar-key" > /dev/stderr ; exit 1 fi OEMID= if [ "${USE_OEM}" = "yes" ]; then # Use the old mount point for compatibility with old instances, where the script gets copied to OEMID=$({ grep -m 1 -o "^ID=.*" /usr/share/oem/oem-release 2> /dev/null || true ; } | cut -d = -f 2) fi # Determine what to download from release server if no local payload is given. # Using /usr/share/flatcar/oems/ from the currently running version means the download is only best-effort # to prevent a later fallback download when updating old instances that aren't fully migrated yet if [ "${OEMID}" != "" ] && { [ -e "/usr/share/flatcar/oems/${OEMID}" ] || [ -e "/usr/share/oem/sysext/active-oem-${OEMID}" ]; }; then if [ "$PAYLOAD" = "" ]; then EXTENSIONS+=("/var/tmp/flatcar-update/oem-${OEMID}.gz") elif ! echo " ${EXTENSIONS[*]} " | grep -q -P "[ /](flatcar_test_update-)?oem-${OEMID}.gz "; then # Surrounded with space to only match base name echo "Error: system requires '${OEMID}' OEM extension but not passed in --extension" > /dev/stderr ; exit 1 fi fi for NAME in $(grep -h -o '^[^#]*' /etc/flatcar/enabled-sysext.conf /usr/share/flatcar/enabled-sysext.conf 2> /dev/null | grep -v -x -f <(grep '^-' /etc/flatcar/enabled-sysext.conf 2> /dev/null | cut -d - -f 2-) | grep -v -P '^(-).*'); do if [ "$PAYLOAD" = "" ]; then EXTENSIONS+=("/var/tmp/flatcar-update/flatcar-${NAME}.gz") elif ! echo " ${EXTENSIONS[*]} " | grep -q -P "[ /](flatcar_test_update-)?flatcar-${NAME}.gz "; then echo "Error: system requires '${NAME}' Flatcar extension but not passed in --extension" > /dev/stderr ; exit 1 fi done [ "$EUID" = "0" ] || { echo "Need to be root: sudo $0 $opts" > /dev/stderr ; exit 1 ; } if mount | grep -q /usr/share/update_engine/update-payload-key.pub.pem; then echo "Warning: found a bind mount on /usr/share/update_engine/update-payload-key.pub.pem (will only unmount it if --force-dev-key|--force-flatcar-key is set)" fi if ss -tan | grep -q "LISTEN.*:${LISTEN_PORT_1}"; then echo "Error: some process is using port ${LISTEN_PORT_1}" > /dev/stderr ; exit 1 fi if ss -tan | grep -q "LISTEN.*:${LISTEN_PORT_2}"; then echo "Error: some process is using port ${LISTEN_PORT_2}" > /dev/stderr ; exit 1 fi # Migrate CoreOS machines to Flatcar if [ -d "/etc/coreos" ]; then mv /etc/coreos /etc/flatcar ln -s flatcar /etc/coreos fi HARDCODED_GROUP=$(grep -m 1 -o '^GROUP=.*' /etc/flatcar/update.conf 2> /dev/null || true) if [ "${HARDCODED_GROUP}" != "" ]; then echo "Warning: found hardcoded ${HARDCODED_GROUP} in /etc/flatcar/update.conf - make sure it fits the release channel you want to follow" > /dev/stderr fi systemctl unmask update-engine systemctl start update-engine STATUS=$(update_engine_client -status 2>/dev/null | { grep '^CURRENT_OP=UPDATE_STATUS_UPDATED_NEED_REBOOT$' || true ; }) if [ "$STATUS" != "" ]; then echo "Error: a previously downloaded update wasn't applied yet, you can discard it with 'update_engine_client -reset_status'" > /dev/stderr; exit 1 fi touch /etc/flatcar/update.conf PREV_SERVER=$(grep '^SERVER=' /etc/flatcar/update.conf || true) sed -i "/SERVER=.*/d" /etc/flatcar/update.conf echo "SERVER=http://localhost:${LISTEN_PORT_1}/update" >> /etc/flatcar/update.conf BOARD=$({ grep -m 1 BOARD= /usr/share/coreos/release || true ; } | cut -d = -f 2-) if [ "$BOARD" = "" ]; then echo "Error: could not find board from /usr/share/coreos/release" > /dev/stderr ; exit 1 fi mkdir -p "/var/tmp/flatcar-update" if [ "$PAYLOAD" = "" ]; then echo "Downloading update payloads..." PAYLOAD="/var/tmp/flatcar-update/flatcar_production_update.gz" # bash 4.3 compat for VAR[@] if VAR is () set +u for DOWNLOAD_FILE in "$PAYLOAD" "${EXTENSIONS[@]}"; do set -u rm -f "${DOWNLOAD_FILE}" BASEFILENAME="$(basename -- "${DOWNLOAD_FILE}")" curl -fsSL -o "${DOWNLOAD_FILE}" --retry-delay 1 --retry 60 --retry-connrefused --retry-max-time 60 --connect-timeout 20 "https://update.release.flatcar-linux.net/${BOARD}/${VERSION}/${BASEFILENAME}" SHA256_TO_CHECK=$(curl -fsSL --retry-delay 1 --retry 60 --retry-connrefused --retry-max-time 60 --connect-timeout 20 "https://update.release.flatcar-linux.net/${BOARD}/${VERSION}/${BASEFILENAME}.sha256" | cut -d " " -f 1) if [ "${SHA256_TO_CHECK}" = "" ]; then echo "Error: could not download sha256 checksum file" > /dev/stderr ; exit 1 fi SHA256_HEX=$(sha256sum -b "${DOWNLOAD_FILE}" | cut -d " " -f 1) if [ "${SHA256_TO_CHECK}" != "${SHA256_HEX}" ]; then echo "Error: mismatch with downloaded SHA256 checksum (${SHA256_TO_CHECK})" > /dev/stderr ; exit 1 fi done set -u echo "When restarting after an error you may reuse them with '--to-payload $PAYLOAD --extension ${EXTENSIONS[*]}' (add --extension before each extension)" else # bash 4.3 compat for VAR[@] if VAR is () set +u for DOWNLOAD_FILE in "$PAYLOAD" "${EXTENSIONS[@]}"; do set -u BASEFILENAME="$(basename -- "${DOWNLOAD_FILE}" | sed 's/flatcar_test_update-//g')" if [ "${DOWNLOAD_FILE}" = "${PAYLOAD}" ]; then BASEFILENAME="flatcar_production_update.gz" fi # The user may pass in the cached files on error if [ "${DOWNLOAD_FILE}" != "/var/tmp/flatcar-update/${BASEFILENAME}" ]; then ln -fs "$(readlink -f "${DOWNLOAD_FILE}")" "/var/tmp/flatcar-update/${BASEFILENAME}" fi done set -u fi BASE="http://localhost:${LISTEN_PORT_2}/" rm -f /tmp/response tee /tmp/response > /dev/null <<-EOF EOF # bash 4.3 compat for VAR[@] if VAR is () set +u for DOWNLOAD_FILE in "$PAYLOAD" "${EXTENSIONS[@]}"; do set -u HASH=$(openssl dgst -binary -sha1 < "${DOWNLOAD_FILE}" | base64) SIZE=$(stat -L --printf='%s\n' "${DOWNLOAD_FILE}") BASEFILENAME="$(basename -- "${DOWNLOAD_FILE}" | sed 's/flatcar_test_update-//g')" REQUIRED="false" OPTHASH256="" if [ "${DOWNLOAD_FILE}" = "${PAYLOAD}" ]; then # In case a local payload is given we have to use the correct name BASEFILENAME="flatcar_production_update.gz" REQUIRED="true" else HASH256=$(sha256sum -b "${DOWNLOAD_FILE}" | cut -d " " -f 1) OPTHASH256="hash_sha256=\"${HASH256}\"" fi tee -a /tmp/response > /dev/null <<-EOF EOF done set -u SHA256=$(openssl dgst -binary -sha256 < "$PAYLOAD" | base64) tee -a /tmp/response > /dev/null <<-EOF EOF # Cleanups for local socat servers below true > /tmp/payload-server-pids trap "umount /usr/share/update_engine/update-payload-key.pub.pem 2> /dev/null || true ; cat /tmp/payload-server-pids | xargs -r kill ; rm -f /tmp/response /tmp/payload-server /tmp/response-server /tmp/payload-server-pids" EXIT INT # Setup for XML response server # Helper script because inline quoting is insane tee /tmp/response-server > /dev/null <<'EOF' #!/bin/bash set -euo pipefail read -a WORDS if [[ ${#WORDS[@]} -ne 3 ]] || [[ ${WORDS[0]} != POST ]] || [[ ${WORDS[1]} != /update ]] ; then echo -ne "HTTP/1.1 400 Bad request\r\n\r\n"; exit 0 fi echo -ne "HTTP/1.1 200 OK\r\n" echo -ne "Content-Type: text/xml\r\n" LEN=$(stat --printf='%s\n' /tmp/response) echo -ne "Content-Length: ${LEN}\r\n" echo -ne "\r\n" cat /tmp/response EOF chmod +x /tmp/response-server socat TCP-LISTEN:"${LISTEN_PORT_1}",reuseaddr,fork SYSTEM:'/tmp/response-server' & CHILDPID="$!" echo "${CHILDPID}" >> /tmp/payload-server-pids # Setup for payload server # Helper script because inline quoting is insane tee /tmp/payload-server > /dev/null <<'EOF' #!/bin/bash set -euo pipefail SERVE="$1" TYPE="$2" read -a WORDS if [ "${#WORDS[@]}" != 3 ] || [ "${WORDS[0]}" != "GET" ]; then echo -ne "HTTP/1.1 400 Bad request\r\n\r\n"; exit 0 fi # Subfolders are not supported for security reasons as this avoids having to deal with ../../ attacks FILE="${SERVE}/$(basename -- "${WORDS[1]}")" if [ -d "${FILE}" ] || [ ! -e "${FILE}" ]; then echo -ne "HTTP/1.1 404 Not found\r\n\r\n" ; exit 0 fi echo -ne "HTTP/1.1 200 OK\r\n" echo -ne "Content-Type: ${TYPE};\r\n" LEN=$(stat -L --printf='%s\n' "${FILE}") echo -ne "Content-Length: ${LEN}\r\n" echo -ne "\r\n" cat "${FILE}" EOF chmod +x /tmp/payload-server socat TCP-LISTEN:"${LISTEN_PORT_2}",reuseaddr,fork SYSTEM:'/tmp/payload-server /var/tmp/flatcar-update/ application/gzip' & CHILDPID="$!" echo "${CHILDPID}" >> /tmp/payload-server-pids if [ "${FORCE_DEV_KEY}" = "1" ] || [ "${FORCE_FLATCAR_KEY}" = "1" ]; then rm -f /tmp/key curl -fsSL -o /tmp/key --retry-delay 1 --retry 60 --retry-connrefused --retry-max-time 60 --connect-timeout 20 "$KEY" umount /usr/share/update_engine/update-payload-key.pub.pem 2> /dev/null || true echo "Bind-mounting /usr/share/update_engine/update-payload-key.pub.pem" mount --bind /tmp/key /usr/share/update_engine/update-payload-key.pub.pem fi echo "Forcing update..." # Force an update if update_engine_client -update 2> /dev/null > /dev/null; then STATUS=$(update_engine_client -status 2>/dev/null | { grep '^CURRENT_OP=UPDATE_STATUS_UPDATED_NEED_REBOOT$' || true ; }) else STATUS= fi # Set previous or wanted SERVER setting sed -i "/SERVER=.*/d" /etc/flatcar/update.conf if [ "${DISABLE_AFTERWARDS}" = "1" ]; then echo "Setting SERVER=disabled in /etc/flatcar/update.conf" echo "SERVER=disabled" >> /etc/flatcar/update.conf elif [ "${PREV_SERVER}" != "" ]; then echo "${PREV_SERVER}" >> /etc/flatcar/update.conf fi if [ "$STATUS" = "" ]; then echo "Error: update failed" > /dev/stderr; exit 1 fi if [ "${USER_PAYLOAD}" = "" ]; then echo "Removing payload $PAYLOAD ${EXTENSIONS[*]}" fi # For the case that user payloads were given, this only removes the symlinks rm -rf "/var/tmp/flatcar-update" echo "Done, please make sure to reboot either manually or through your reboot manager (check locksmithd/FLUO)"