agama/live/live-root/usr/bin/live-self-update
Ladislav Slezák 3ba7513cda
Implement proxy support in the self-update, fixed handling fallback (#2845)
## Problem

- Missing HTTP proxy support in self-update
- Fallback for failing SCC/RMT was not handled correctly (I forgot about
the global `set -e` option which fails the whole script on any error
😟 )

## Solution

- Get the configured proxy
- Pass it to the `curl` call which contacts the SCC and zypper which
downloads the packages via `http_proxy` and `https_proxy` environment
variables.
- Fixed checking the result of the `curl` call
- Fixed keeping the systemd service status (workaround: enable the
service also in the root system, but due to `WantedBy=initrd.target` it
runs only once in initramfs.

## Testing

- Tested manually with `proxy=` boot option and also when setting the
proxy interactively with the dracut menu (invoked with the
`rd.cmdline=menu` boot option)
- Tested with my locally running Squid proxy server
- In both cases the proxy was use for contacting both the registration
server (SCC) and the self-update repository.
- So far tested with plain proxy without authentication, later I'll test
it with authenticated proxy

I checked the Squid access log and it contained these two lines
confirming that the proxy was really used for both connections:

```
1762155964.003    101 192.168.1.112 TCP_TUNNEL/200 5313 CONNECT scc.suse.com:443 - HIER_DIRECT/99.83.188.102 -
1762155967.332    277 192.168.1.112 TCP_TUNNEL/200 8724 CONNECT installer-updates.suse.com:443 - HIER_DIRECT/75.2.43.231 -
```


## Screenshots

Screenshot of the log:

<img width="1280" height="800" alt="agama-self-update-proxy-log"
src="https://github.com/user-attachments/assets/55a2aabc-c006-4724-80f6-8c5f3b622452"
/>

## Fixed systemd status

Self-update service status in initramfs (started from emergency shell
invoked with `rd.break` boot option):

<img width="1280" height="800" alt="self-update-status-initramfs"
src="https://github.com/user-attachments/assets/14a3cf44-88ee-4314-ad19-d1a62786b139"
/>

Self-update service status in the root image after fully booting the
system, the exit status is missing although the log is correctly kept:

<img width="1280" height="800" alt="self-update-status-root"
src="https://github.com/user-attachments/assets/8f4347e9-0db1-4a2c-b900-390558da84ab"
/>


The service status after enabling it also in the root image (after fully
booting the system):

<img width="1280" height="800" alt="self-update-status-root-fixed"
src="https://github.com/user-attachments/assets/cb63bf47-9abf-4149-a8d8-cf7c3237ee36"
/>


## Updates

- Set the proxy rather via environment variables than via the command
line options or URL query. That avoids showing or logging the proxy URL
with a password.
- Disable logging the proxy URL in the code
- Tested also with authenticated proxy, the Squid proxy then contains
these lines:
```
1762184588.402      0 192.168.1.105 TCP_DENIED/407 3892 CONNECT scc.suse.com:443 - HIER_NONE/- text/html
1762184588.763    359 192.168.1.105 TCP_TUNNEL/200 5313 CONNECT scc.suse.com:443 proxyuser HIER_DIRECT/99.83.188.102 -
1762184591.959      0 192.168.1.105 TCP_DENIED/407 3968 CONNECT installer-updates.suse.com:443 - HIER_NONE/- text/html
1762184592.517    555 192.168.1.105 TCP_TUNNEL/200 8724 CONNECT installer-updates.suse.com:443 proxyuser HIER_DIRECT/99.83.188.102 -
```

The "proxyuser" is the configured proxy account user name. The proxy is
connected twice, in the first attempt it checks whether a password is
required and which form (basic/digest) authentication is expected. In
the second request the actual user name and password is sent.
2025-11-04 10:00:40 +01:00

322 lines
10 KiB
Bash
Executable file

#! /bin/bash
shopt -s nullglob
[ -e /dracut-state.sh ] && . /dracut-state.sh
# Later we could disable "-x" or enable it on request, let's be more verbose in
# the initial implementation. Since we have enabled using the plymouth splash
# screen the verbose details will not be visible to users by default.
set -ex
. "$(dirname "$0")"/../lib/live-self-update/conf.sh
RESPONSE_FILE="$RUN_DIR/server_response"
REG_REPO_FILE="$RUN_DIR/reg_repos"
OSR="/etc/os-release"
ARCH=$(uname -m)
# do the cleanup at the end, trap ensures it runs even if something fails
trap cleanup_chroot EXIT
# helper function to log important steps (highlighted in the journalctl output)
# use "set +x" to avoid logging the message again (it is already logged in the
# original caller place so it would be actually logged three times...)
# see https://www.freedesktop.org/software/systemd/man/latest/sd-daemon.html
# for the log level numbers
milestone() {
({ set +x; } 2>/dev/null; echo "<5>$1")
}
# log warning
warning() {
({ set +x; } 2>/dev/null; echo "<4>$1")
}
# log error
error() {
({ set +x; } 2>/dev/null; echo "<3>$1")
}
# normal message
info() {
({ set +x; } 2>/dev/null; echo "<6>$1")
}
# the proxy might contain a password, make sure it is not displayed or logged
# when processing it
set +x
# get the configured network proxy (empty string if not configured)
# see ../lib/dracut/modules.d/98dracut-menu/dracut-cmdline-menu.sh
# get the last found, it has the highest priority
PROXY=$(grep "\bproxy=" /proc/cmdline /etc/cmdline.d/* | tail -n 1 | sed "s/.*\bproxy=\([^ \t]*\).*/\1/")
if [ -n "$PROXY" ]; then
if [[ "$PROXY" == *@* ]]; then
# looks like there is a username or password in the URL, do not print it
milestone "Using authenticated HTTP proxy"
else
milestone "Using proxy server $PROXY"
fi
# set the proxy for all zypper and curl calls, for security reasons curl
# uses only the lower case name, zypper accepts that as well,
# see https://everything.curl.dev/usingcurl/proxies/env.html
export http_proxy="$PROXY"
export https_proxy="$PROXY"
# hide the original URL to avoid leaking the password in URL later
PROXY=1
fi
set -x
# helper for running a command in the live root system, works both when running
# in initramfs ($NEWROOT is set) and in already mounted root ($NEWROOT unset)
run_command() {
if [ -z "$NEWROOT" ]; then
"$@"
else
"$NEWROOT/usr/bin/chroot" "$NEWROOT" "$@"
fi
}
# Prepare the system root to be used by scripts before switching to it.
prepare_chroot() {
if [ -n "$NEWROOT" ]; then
milestone "Preparing for the self update..."
# make system directories active also in the chroot directory
mount -o bind /run "$NEWROOT"/run
mount -o bind /sys "$NEWROOT"/sys
mount -o bind /proc "$NEWROOT"/proc
mount -o bind /dev "$NEWROOT"/dev
[ -f /run/NetworkManager/resolv.conf ] && cp /run/NetworkManager/resolv.conf "$NEWROOT"/etc/resolv.conf
fi
# remove any existing self-update repositories, probably left-overs from the previous run
rm -f "$NEWROOT"/etc/zypp/repos.d/self-update-*.repo
}
# Revert the changes done by prepare_chroot() and restore the system to the
# original state, called from bash "trap" handler
cleanup_chroot() {
local result="$?"
if [ "$result" != "0" ]; then
error "Error: Self update failed! (exit status: $result)"
else
milestone "Self update successfully finished"
fi
# log the result to be checked by openQA or Agama itself
echo "$result" > "$RESULT_FILE"
if [ -n "$NEWROOT" ]; then
rm -f "$NEWROOT"/etc/resolv.conf
# do not fail if not mounted
findmnt "$NEWROOT"/run > /dev/null && umount "$NEWROOT"/run
findmnt "$NEWROOT"/sys > /dev/null && umount "$NEWROOT"/sys
findmnt "$NEWROOT"/proc > /dev/null && umount "$NEWROOT"/proc
findmnt "$NEWROOT"/dev > /dev/null && umount "$NEWROOT"/dev
fi
}
# read the data from /etc/os-release file (source it)
read_os_release() {
# get the product data
if [ -f "$OSR" ]; then
. "$OSR"
else
error "File $OSR is missing!"
exit 1
fi
}
# ask the registration server (SCC or RMT) for the self-update URL
query_reg_server() {
local server
# custom server (RMT)
if [ -f "$SERVER_FILE" ]; then
server=$(< "$SERVER_FILE")
# default server (SCC)
elif [ -f "$CONFIG_DEFAULT_REG_SERVER_FILE" ]; then
server=$(< "$CONFIG_DEFAULT_REG_SERVER_FILE")
fi
if [ -n "$server" ]; then
read_os_release
local query
query="$server/connect/repositories/installer?identifier=$NAME&version=$VERSION_ID&arch=$ARCH"
info "Running query $query...."
local curl_options
curl_options=(--silent --globoff --location --retry 3 --retry-connrefused \
--connect-timeout 20 --max-time 10 --fail --show-error --output "$RESPONSE_FILE")
if grep -q "\binst.self_update_ssl=0\b" /run/agama/cmdline.d/agama.conf; then
warning "Disabling SSL check for $server"
curl_options+=("--insecure")
fi
if [ -n "$PROXY" ]; then
# automatically detect the proxy authentication method (basic/digest)
curl_options+=("--proxy-anyauth")
fi
if curl "${curl_options[@]}" -- "$query" && [ -s "$RESPONSE_FILE" ]; then
info "Found self-update repository: $(jq -r ".[] | select(.installer_updates==true) | .description" "$RESPONSE_FILE") "
jq -r ".[] | select(.installer_updates==true) | .url" "$RESPONSE_FILE" > "$REG_REPO_FILE"
else
warning "Contacting the registration server failed"
# SCC/RMT query failed and no fallback configured
if [ ! -s "$CONFIG_FALLBACK_FILE" ]; then
error "Cannot obtain the self-update repository"
exit 1
fi
fi
else
milestone "Registration server not defined"
fi
}
# add the self-update repositories into the system
add_update_repos() {
milestone "Adding self update repositories..."
# add the self-update repositories
while read -r update_url; do
if [ -n "$update_url" ]; then
local repo
repo="self-update-$index"
if grep -q "\binst.self_update_ssl=0\b" /run/agama/cmdline.d/agama.conf; then
warning "Disabling SSL check for repository $update_url"
# add "ssl_verify=no" query to the repository URL
update_url=$(url_add_query "$update_url" "ssl_verify=no")
fi
local zypper_options
zypper_options=()
# allow using unsigned repository
if grep -q "\binst.self_update_unsigned_repo=1\b" /run/agama/cmdline.d/agama.conf; then
warning "Unsigned repository is allowed"
zypper_options+=("--gpgcheck-allow-unsigned-repo")
fi
# completely disable GPG checks, not recommended, if possible the options
# above should be preferred
if grep -q "\binst.self_update_gpg=0\b" /run/agama/cmdline.d/agama.conf; then
warning "Disabling all GPG checks!"
zypper_options+=("--no-gpgcheck")
fi
run_command zypper addrepo "${zypper_options[@]}" -f "$update_url" "$repo"
self_update_repos+=("$repo")
((++index))
fi
done < "$1"
}
# Reads the configured fallback repository, the URL might contain several
# placeholders. This way, it works across multiple architectures and does not
# contain a hardcoded version (the fallback URL should work with SLES-16.0,
# SLES-16.1, SLES-16.2...)
read_fallback_repos() {
# Substituting $arch pattern with the architecture of the current system.
# Substituting these variables with the /etc/os-release content:
# $os_release_name => NAME
# $os_release_id => ID
# $os_release_version => VERSION
# $os_release_version_id => VERSION_ID
# IMPORTANT NOTE: the "VERSION_ID" replacement must happen *before*
# the "VERSION" replacement because it partially matches that string!
sed -e "s/\$os_release_name/$NAME/g" -e "s/\$os_release_id/$ID/g" \
-e "s/\$os_release_version_id/$VERSION_ID/g" \
-e "s/\$os_release_version/$VERSION/g" \
-e "s/\$arch/$ARCH/g" "$CONFIG_FALLBACK_FILE" > "$FALLBACK_REPOS_FILE"
}
# Applies all the updates
#
# Reads the URL of the updates from $AGAMA_DUD_INFO and process each one.
install_updates() {
index=0
self_update_repos=()
if [ -f "$REPO_FILE" ]; then
prepare_chroot
add_update_repos "$REPO_FILE"
else
# try SCC/RMT
query_reg_server
# repo from the registration server
if [ -s "$REG_REPO_FILE" ]; then
prepare_chroot
add_update_repos "$REG_REPO_FILE"
# use a fallback URL
elif [ -s "$CONFIG_FALLBACK_FILE" ]; then
milestone "Using fallback repository"
read_fallback_repos
prepare_chroot
add_update_repos "$FALLBACK_REPOS_FILE"
# no self-update repo found, exit (in the openSUSE installer)
else
milestone "No self-update repository configured, skipping self-update"
exit 0
fi
fi
local zypper_options
# automatically import the repository GPG key
if grep -q "\binst.self_update_import_key=1\b" /run/agama/cmdline.d/agama.conf; then
warning "Enabled GPG key autoimport"
zypper_options="--gpg-auto-import-keys"
fi
milestone "Loading repositories..."
run_command zypper $zypper_options refresh "${self_update_repos[@]}"
# build the list of repository options passed to zypper
repo_options=()
for repo in "${self_update_repos[@]}"; do
repo_options+=("--repo" "$repo")
done
milestone "Updating packages..."
# update the system only using the predefined self-update repositories,
# allow downgrading and changing vendor (can be useful for testing)
run_command zypper --non-interactive update --allow-downgrade --allow-vendor-change --details --no-recommends "${repo_options[@]}"
milestone "Cleaning the system..."
# delete the repository caches, not needed anymore, saves space in RAM disk
run_command zypper clean --all
# disable the self update repositories (keep them for debugging purposes)
for repo in "${self_update_repos[@]}"; do
run_command zypper modifyrepo --disable "$repo"
done
}
# add a query parameter to URL
# $1 - base URL ("http://example.com")
# $2 - the query part ("foo=bar")
# returns "http://example.com?foo=bar"
url_add_query() {
local base
local query
# split the string at the "?" separator
query="${1#*\?}"
base="${1%\?*}"
# if the "?" separator is missing then "$query" equals to "$base"
if [ -z "$query" ] || [ "$query" = "$base" ]; then
# no query present, add a query part to the URL
echo "${base}?${2}"
else
# a query is already present, append the new part
echo "${base}?${query}&${2}"
fi
}
install_updates