Skip to content

Instantly share code, notes, and snippets.

@haraldh
Created January 15, 2016 09:07
Show Gist options
  • Save haraldh/f48df9dbd661496caac1 to your computer and use it in GitHub Desktop.
Save haraldh/f48df9dbd661496caac1 to your computer and use it in GitHub Desktop.
#!/bin/bash
#
# Author: Harald Hoyer <harald@redhat.com>
# Copyright 2015 Red Hat, Inc. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
set -e
set -o pipefail
unset KEEP
usage()
{
{
echo "Usage: ${0##*/} [options] [<path to usr>]"
echo
echo "-h, --help print a help message and exit."
echo "--keep keep the working subvolume."
echo "--add-cmdline <options> add <options> to the kernel cmdline."
echo "--cmdline <options> set <options> as the kernel cmdline."
echo "--cleanup cleanup old subvolumes."
echo "--shell get a chroot shell."
echo "--update update the system."
echo
} >&2
}
TEMP=$(unset POSIXLY_CORRECT; \
getopt \
--options "h" \
--longoptions help,shell,keep,cleanup,update,cmdline:,add-cmdline: \
-- "$@")
if (( $? != 0 )); then
usage
exit 1
fi
eval set -- "$TEMP"
while (($# > 0)); do
case $1 in
--cleanup) CLEANUP=1;;
--update) CMD_UPDATE=1;;
--keep) KEEP=1;;
--shell) CHROOT_SHELL=1;;
--add-cmdline) CMDLINE_ADD="$2"; shift;;
--cmdline) CMDLINE="$2"; shift;;
--) shift; break;;
-h|--help) usage; exit 0;;
*) usage; exit 1;;
esac
shift
done
readonly TMPDIR="$(mktemp --tmpdir="/var/tmp/" -d -t updateusr.XXXXXX)"
mkdir -p "$TMPDIR/root"
readonly BTRFSROOT="$TMPDIR/btrfsroot"
mkdir -p "$TMPDIR/sysroot"
readonly SYSROOT="$TMPDIR/sysroot"
readonly USRDIR=$(readlink -f ${1:-/usr})
trap '
ret=$?;
set +e
cd /
if [[ -d "$SYSROOT" ]]; then
if [[ -d "$SYSROOT/srv" ]]; then
rm -fr "$SYSROOT/srv" || btrfs subvolume delete -c "$SYSROOT/srv" ||:
fi
if [[ -d "$SYSROOT/var/lib/machines" ]]; then
rm -fr "$SYSROOT/var/lib/machines" || btrfs subvolume delete -c "$SYSROOT/var/lib/machines" ||:
fi
for d in etc/resolv.conf usr proc dev sys run tmp; do
umount "$SYSROOT/$d"
done
rm -fr "$SYSROOT"
fi
if [[ $SNAPSHOT_WORK_NAME ]] && [[ $KEEP != 1 ]]; then
btrfs subvolume delete -c "$BTRFSROOT/$SNAPSHOT_WORK_NAME"
fi
mountpoint -q "$BTRFSROOT" && umount "$BTRFSROOT"
rmdir "$BTRFSROOT"
rm -fr "$TMPDIR"
exit $ret;
' EXIT
# clean up after ourselves no matter how we die.
trap 'exit 1;' SIGINT
mntp="$USRDIR"
while ! DEV=$(findmnt -e -v -n -o 'SOURCE' --target "$mntp"); do
mntp=${mntp%/*}
done
mkdir -p "$BTRFSROOT"
mount "$DEV" -o subvol=/ "$BTRFSROOT"
vercmp() {
local _n1=(${1//./ }) _op=$2 _n2=(${3//./ }) _i _res
for ((_i=0; ; _i++)); do
if [[ ! ${_n1[_i]}${_n2[_i]} ]]; then _res=0
elif ((${_n1[_i]:-0} > ${_n2[_i]:-0})); then _res=1
elif ((${_n1[_i]:-0} < ${_n2[_i]:-0})); then _res=2
else continue
fi
break
done
case $_op in
gt) ((_res == 1));;
ge) ((_res != 2));;
eq) ((_res == 0));;
le) ((_res != 1));;
lt) ((_res == 2));;
ne) ((_res != 0));;
esac
}
btrfs_subvolume_name() {
local key value where="$1"
while read key value; do
[[ "$key" != "Name:" ]] && continue
printf -- "%s" "$value"
return 0
done < <(btrfs subvolume show "$where")
return 1
}
btrfs_find_usr_os() {
local id gen level path where="$1" what="$2"
while read id gen level path; do
[[ "$level" != 5 ]] && continue
[[ "$path" != usr:$what:* ]] && continue
printf -- "%s\n" "${path##\<FS_TREE\>/}"
done < <(btrfs subvolume list -at "$where")
return 0
}
btrfs_find_newest() {
declare -a vols
readarray -t vols < <(btrfs_find_usr_os "$1" "$2")
maxversion=0
maxindex=-1
for (( i=0; i < ${#vols[@]}; i++)); do
IFS=: read -r name os arch version <<<"${vols[$i]}"
if vercmp $version gt $maxversion; then
maxindex="$i"
maxversion="$version"
fi
done
if (( $maxindex == -1 )); then
printf -- "btrfs subvolume for $1 not found\n" >&2
exit 1
fi
printf -- "${vols[$maxindex]}\n"
}
currentsubvol=$(btrfs_subvolume_name "$USRDIR")
USRVOL=${currentsubvol##usr:}
USRVOL=${USRVOL%:*}
[[ -d "$BTRFSROOT/$currentsubvol" ]] || exit 0
readonly SNAPSHOT_VERSION="$(date -u +'%Y%m%d%H%M%S')"
readonly SNAPSHOT_NAME="usr:$USRVOL:$SNAPSHOT_VERSION"
readonly SNAPSHOT_WORK_NAME="usr-work:$USRVOL"
usrsubvol="$SNAPSHOT_NAME"
clean_vols()
{
declare -a vols
readarray -t vols < <(btrfs_find_usr_os "$BTRFSROOT" "$USRVOL")
for (( i=0; i < ${#vols[@]}; i++)); do
[[ "${vols[$i]}" == "$usrsubvol" ]] && continue
[[ "${vols[$i]}" == "$currentsubvol" ]] && continue
printf -- "Removing old volume: %s\n" "${vols[$i]}"
for kdir in "$BTRFSROOT"/"${vols[$i]}"/lib/modules/*; do
[[ -d $kdir ]] || continue
(
cd "$kdir"
if [[ -f linux.efi ]]; then
EFI_NAME="${vols[$i]}-${kdir##*/}"
EFI_NAME="${EFI_NAME#usr:}"
EFI_NAME="${EFI_NAME//:/_}"
EFI_NAME="${EFI_NAME//./_}"
rm -f "/boot/EFI/Linux/$EFI_NAME.efi"
fi
for b in bootloader*.conf; do
[[ -f "$b" ]] || continue
# copy over the kernel and initrds
while read key val; do
case "$key" in
linux|initrd)
# replace \ with /
p=${val//\\//}
# create the base directory
rm -f "/boot/$p"
rmdir "/boot/${p%/*}" &>/dev/null || :
;;
esac
done < "$b"
rm -f /boot/loader/entries/"$b"
done
)
done
btrfs subvolume delete "$BTRFSROOT"/"${vols[$i]}"
done
}
setup_bootoptions()
{
local _SNAME="$1"
local -a BOOT_OPTIONS
local HASUSRFSFLAGS
local -a USRFSOPTS
local -a line
if ! [[ "${BOOT_OPTIONS[@]}" ]]; then
if [[ "$CMDLINE" ]]; then
line=( $CMDLINE )
else
read -r -d '' -a line < /proc/cmdline || :
fi
for i in "${line[@]}"; do
[[ "${i#initrd=*}" != "$i" ]] && continue
[[ "${i#rd.break*}" != "$i" ]] && continue
BOOT_OPTIONS+=("$i")
done
fi
[[ "$CMDLINE_ADD" ]] && BOOT_OPTIONS+=( $CMDLINE_ADD )
for ((i=0; i < ${#BOOT_OPTIONS[@]}; i++)); do
case "${BOOT_OPTIONS[$i]}" in
mount.usrflags=*)
_opts="${BOOT_OPTIONS[$i]#mount.usrflags=}"
IFS=, read -r -a USRFSOPTS <<<"$_opts" || :
for ((j=0; j < ${#USRFSOPTS[@]}; j++)); do
case ${USRFSOPTS[$j]} in
subvol=*)
USRFSOPTS[$j]="subvol=$_SNAME"
;;
esac
done
BOOT_OPTIONS[$i]="mount.usrflags=$(IFS=,; echo "${USRFSOPTS[*]}")"
HASUSRFSFLAGS=1
;;
esac
done
if ! [[ $HASUSRFSFLAGS = 1 ]]; then
BOOT_OPTIONS+=( "mount.usrflags=ro,subvol=$_SNAME" )
fi
printf -- "%s " "${BOOT_OPTIONS[@]}"
}
cd "$BTRFSROOT"
if ! [[ -d $SNAPSHOT_WORK_NAME ]]; then
btrfs subvolume snapshot "$currentsubvol" "$SNAPSHOT_WORK_NAME"
fi
cd "$SYSROOT"
mkdir -p "$SYSROOT"/{proc,dev,sys,run,usr,home,tmp,boot,home}
mount --bind "$BTRFSROOT/$SNAPSHOT_WORK_NAME" usr
for d in proc dev sys run tmp; do
mount --bind "/$d" "$SYSROOT/$d"
done
for d in var etc; do
mkdir -p "$SYSROOT/usr/share/factory/$d"
# FIXME
# better mount bind, but rpm file conflicts currently
cp -a --reflink=auto usr/share/factory/etc "$SYSROOT/"
cp -a --reflink=auto usr/share/factory/var "$SYSROOT/"
done
if [[ -e /etc/resolv.conf ]]; then
[[ -e etc/resolv.conf ]] || > "$SYSROOT/etc/resolv.conf"
mount --bind /etc/resolv.conf "$SYSROOT/etc/resolv.conf"
fi
for d in bin sbin lib lib64; do
ln -s "usr/$d" "$SYSROOT/$d"
done
[[ -e var/run ]] || ln -s ../run "$SYSROOT/var/run"
[[ -e var/lock ]] || ln -s ../run/lock "$SYSROOT/var/lock"
if [[ "$CMD_UPDATE" ]]; then
chroot . dnf update -y
fi
if [[ $CHROOT_SHELL == "1" ]]; then
chroot .
fi
[[ $KEEP == "1" ]] && exit 0
(
while read -r line || [[ $line ]]; do
case "$line" in
VERSION=*)
eval "$line"
line="VERSION=\"${VERSION%:*}:${SNAPSHOT_VERSION}\""
;;
VERSION_ID=*)
eval "$line"
line="VERSION_ID=\"${VERSION_ID%:*}:${SNAPSHOT_VERSION}\""
;;
PRETTY_NAME=*)
eval "$line"
line="PRETTY_NAME=\"${PRETTY_NAME%:*}:${SNAPSHOT_VERSION}\""
;;
esac
echo "$line"
done < "$SYSROOT/usr/lib/os-release"
) > "$SYSROOT/os-release" && {
rm -f "$SYSROOT/usr/lib/os-release"
mv "$SYSROOT/os-release" "$SYSROOT/usr/lib/os-release"
}
BOOT_OPTIONS="$(setup_bootoptions "$SNAPSHOT_NAME")"
# cleanup /var
rm -fr "$SYSROOT/var/cache"
rm -fr "$SYSROOT/var/log"
# FIXME
cp -a --reflink=auto etc "$SYSROOT/usr/share/factory/"
cp -a --reflink=auto var "$SYSROOT/usr/share/factory/"
mkdir -p "$SYSROOT/usr/etc"
if [ -d "$SYSROOT/usr/share/factory/etc/ld.so.conf.d" ]; then
rm -fr "$SYSROOT/usr/etc/ld.so.conf.d"
mv "$SYSROOT/usr/share/factory/etc/ld.so.conf.d" "$SYSROOT/usr/etc/ld.so.conf.d"
fi
cd "$SYSROOT/lib/modules"
declare -a kvers
kvers=(*/vmlinuz)
numkernels=$((-${#kvers[@]}))
numkernels=-1
for (( i=-1; i >= $numkernels; i-- )); do
v=${kvers[$i]]%/vmlinuz}
if [[ -d $v ]]; then
rm -f "$SYSROOT/etc/os-release"
(
while read -d '' -r line || [[ $line ]]; do
case "$line" in
VERSION=*)
eval "$line"
line="VERSION=\"${VERSION%:*}:${SNAPSHOT_VERSION}_${v//:/_}\""
;;
VERSION_ID=*)
eval "$line"
line="VERSION_ID=\"${VERSION_ID%:*}:${SNAPSHOT_VERSION}_${v//:/_}\""
;;
PRETTY_NAME=*)
eval "$line"
line="PRETTY_NAME=\"${PRETTY_NAME%:*}:${SNAPSHOT_VERSION}_${v//:/_}\""
;;
esac
echo "$line"
done < "$SYSROOT/usr/lib/os-release"
) > "$SYSROOT/etc/os-release"
chroot "$SYSROOT" \
dracut --force --uefi \
--kernel-image /lib/modules/$v/vmlinuz \
--kernel-cmdline "$BOOT_OPTIONS" \
-N --no-hostonly-cmdline --reproducible \
/lib/modules/$v/linux.efi "$v"
[[ -f $v/linux.efi ]]
mkdir -p /boot/EFI/Linux
EFI_NAME="${SNAPSHOT_NAME}-${v}"
EFI_NAME="${EFI_NAME#usr:}"
EFI_NAME="${EFI_NAME//:/_}"
EFI_NAME="${EFI_NAME//./_}"
cp $v/linux.efi "/boot/EFI/Linux/$EFI_NAME.efi"
fi
done
cd "$BTRFSROOT"
btrfs subvolume snapshot -r "$SNAPSHOT_WORK_NAME" "$SNAPSHOT_NAME"
[[ $CLEANUP == "1" ]] && clean_vols
exit 0
sync
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment