#!/bin/bash -e
# This script can be invoked as "edit-template" to make interactive changes
# to a PVE template, or as "commit-template" to commit changes to ZFS that
# have been made by external scripts.
#
# An optional command line argument specifies the id of the container.
#
# This script makes potentially invasive changes to the underlying ZFS
# storage side-stepping PVE's management of templates and containers. It is
# possible to seriously damage your cluster and you should only run this
# script if you have backups and feel comfortable with repairing any damage
# that could be caused.
export LC_ALL=C
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# If no arguments are given, assume that the default template has the id 100
templ="${1:-100}"
# Read a keypress without echo'ing nor requiring a RETURN
getkey() {
(
trap 'stty echo -iuclc icanon 2>/dev/null' EXIT INT TERM QUIT HUP
stty -echo iuclc -icanon 2>/dev/null
dd count=1 bs=1 2>/dev/null
)
}
# Find the local configuration file for this container
desc="/etc/pve/nodes/$(uname -n)/lxc/${templ}.conf"
[ -r "${desc}" ] || {
echo "Container ${templ} not found on $(uname -n)" >&2
exit 1
}
# Check that this is in fact an LXC template
host="$(sed 's/^hostname: //;t1;d;:1;q' "${desc}")"
grep -q '^template: 1' "${desc}" || {
echo "Container ${host} [${templ}] does not appear to be a template" >&2
exit 1
}
# Find the storage location for this template. This assumes that Proxmox
# uses ZFS. The rest of the script needs ZFS features to manage snapshots.
rootfs="$(sed 's/^rootfs: //;t1;d;:1;q' "${desc}")"
pool="${rootfs%%:*}"
rootfs="${rootfs#*:}"; rootfs="${rootfs%%,*}"
vol="$(sed -n "/^zfspool: ${pool}$/,/^$/{s/.*pool //;t1;d;:1;p;q}" \
/etc/pve/storage.cfg)"
rootdir="$(zfs list "${vol}" 2>/dev/null|awk 'END { print $5 }')/${rootfs##*/}"
zfspath="${vol}/${rootfs##*/}"
# If we couldn't find the ZFS directory where the template is mounted, abort
# now before things go wrong later.
[ -n "${rootdir}" -a "${rootdir}" != '/' -a -d "${rootdir}" ] &&
mountpoint -q "${rootdir}" || {
echo "Cannot find base disk for template ${host} [${templ}]" >&2
exit 1
}
# Another sanity check. Templates have a "__base__" snapshot. If that doesn't
# exist, we don't know how to commit any of our changes.
[[ "$(zfs list -t snapshot "${zfspath}@__base__" 2>&1 || :)" =~ @__base__ ]] ||{
echo "There is no \"__base__\" snapshot for template ${host} [${templ}]" >&2
echo "This doesn't look like a well-formed Proxmox template" >&2
exit 1
}
# If the script is invoked as "commit-template" instead "edit-template", don't
# bother with making any changes. Just move the ZFS snapshot and assume that
# the user made changes outside of this script.
if ! [[ "${0##*/}" =~ commit-template ]]; then
# Undo any system-wide changes, if we terminate unexpectedly.
trap 'trap "" INT TERM QUIT HUP EXIT ERR
pct stop "${templ}" || :
sed -i "s/template: 0/template: 1/" "${desc}"
exit 1' INT TERM QUIT HUP EXIT ERR
# Temporarily turn the template into a full container, start it, then
# after the user interactively made changes, turn it back into a template.
sed -i 's/template: 1/template: 0/' "${desc}"
echo "Entering container ${host} [${templ}]..."
pct start "${templ}"
pct enter "${templ}" || :
pct stop "${templ}"
sed -i "s/template: 0/template: 1/" "${desc}"
# Since we are using ZFS snapshots, we might as well give the user one
# last chance to abandon their changes.
echo -n "Commit changes to container" \
"$(sed 's/^hostname: //;t1;d;:1;q' "${desc}") [${templ}] (Y/n)"
while :; do
c="$(getkey | tr a-z A-Z)"
case "${c^^}" in
''|Y) echo " yes"
break
;;
N) echo " no"
zfs rollback "${zfspath}@__base__"
trap '' EXIT
exit 0
;;
*) tput bel
;;
esac
done
fi
# Clear out the log files and SSH host keys. Then leave a marker that this is
# a template. That information can be very useful in scripts that are invoked
# from /usr/share/lxc/hooks
find "${rootdir}/"{tmp,run,var/cache,var/log} -type f -print0 |
xargs -0 rm >&/dev/null || :
rm -rf "${rootdir}/"{etc/machine-id,var/log/journal/*,var/tmp/*}
find "${rootdir}/etc/ssh" -type f -name ssh_host\*key\* -print0 |
xargs -0 rm >&/dev/null || :
rm -f "${rootdir}/"root/.bash_history
touch "${rootdir}/.is-template"
# Move the "__base__" snapshot forward to the current state, thus committing all
# changes. In the easiest case, we delete the old snapshot and then create a new
# one.
#
# But this gets more complex, if there are linked clones referenced from other
# containers. Deletion isn't possible, but in that case, we can instead rename
# the snapshot to a new and unique name.
#
# This can leave orphaned snapshots, if the linked container is later deleted.
# Now, would be a good time to garbage collect.
zfs list -t snapshot "${zfspath}" |
sed 's/^[^@]*@\(__clone_[^_]*__\)\s.*/\1/;t;d' |
while read -r clone; do
# Any orphaned snapshot that matches the pattern __clone_XXX__ is destroyed.
zfs list -o origin | grep -qF "${clone}" ||
zfs destroy "${zfspath}@${clone}"
done
if zfs list -o origin -r "${vol}" | grep -qF "${zfspath}@__base__"; then
i=0
while zfs list -t snapshot "${zfspath}" |
sed '1d;s/^[^@]*@\(\S\+\).*/\1/' |
grep -qF "__clone_${i}__"; do
# Keep increasing the serial number, until we find a unique __clone_XXX__
i=$((i+1))
done
# Rename the current snapshot, so that we reuse the __base__ label. Future
# containers that are clone from this template will be based on the newly
# edited state of the template. Older linked containers are unaffected.
zfs rename "${zfspath}@__base__" "${zfspath}@__clone_${i}__"
else
# If there aren't any linked containers, simply delete and recreate the
# snapshot.
zfs destroy "${zfspath}@__base__" >&/dev/null || :
fi
# This is now the new state of the template.
zfs snapshot "${zfspath}@__base__"
trap '' EXIT
exit