#!/usr/bin/env bash ### # # Options Section # ### set -e set -u set -o pipefail ### # # Function Section # ### function bind_chroot () { message "Bind file system '/dev' ..." if ! /usr/bin/grep --quiet "${CT_REALPATH}/dev" '/proc/mounts'; then /usr/bin/mount --bind '/dev' --target "${CT_REALPATH}/dev" &> '/dev/null' || \ error "\r\rBind file system '/dev' [ERROR]\n" \ " => The file system '/dev' could not be mounted.\n" fi message "\r\rBind filesystem '/dev' [OK]\n" message "Bind file system '/dev/pts' ..." if ! /usr/bin/grep --quiet "${CT_REALPATH}/dev/pts" '/proc/mounts'; then /usr/bin/mount --bind '/dev/pts' --target "${CT_REALPATH}/dev/pts" &> '/dev/null' || \ error "\r\rBind file system '/dev/pts' [ERROR]\n" \ " => The file system '/dev/pts' could not be mounted.\n" fi message "\r\rBind file system '/dev/pts' [OK]\n" message "Bind file system '/dev/mqueue' ..." if ! /usr/bin/grep --quiet "${CT_REALPATH}/dev/mqueue" '/proc/mounts'; then /usr/bin/mount --bind '/dev/mqueue' --target "${CT_REALPATH}/dev/mqueue" &> '/dev/null' || \ error "\r\rBind files ystem '/dev/mqueue' [ERROR]\n" \ " => The file system '/dev/mqueue' could not be mounted.\n" fi message "\r\rBind files ystem '/dev/mqueue' [OK]\n" message "Slave file system '/dev' ..." if ! /usr/bin/grep --quiet "${CT_REALPATH}/dev" '/proc/mounts'; then /usr/bin/mount --make-rslave --target "${CT_REALPATH}/dev" &> '/dev/null' || \ error "\r\rSlave file system '/dev' [ERROR]\n" \ " => The file system '/dev' could not be slaved.\n" fi message "\r\rSlave file system '/dev' [OK]\n" message "Bind file system '/proc' ..." if ! /usr/bin/grep --quiet "${CT_REALPATH}/proc" '/proc/mounts'; then /usr/bin/mount --types 'proc' --source '/proc' --target "${CT_REALPATH}/proc" &> '/dev/null' || \ error "\r\rBind file system '/proc' [ERROR]\n" \ " => The file system '/proc' could not be mounted.\n" fi message "\r\rBind file system '/proc' [OK]\n" message "Bind file system '/sys' ..." if ! /usr/bin/grep --quiet "${CT_REALPATH}/sys" '/proc/mounts'; then /usr/bin/mount --types 'sysfs' --source '/sys' --target "${CT_REALPATH}/sys" &> '/dev/null' || \ error "\r\rBind file system '/sys' [ERROR]\n" \ " => The file system '/sys' could not be mounted.\n" fi message "\r\rBind file system '/sys' [OK]\n" } function consistency () { message 'Check container consistency ...' /usr/bin/test -d "${CT_REALPATH}" || \ error '\r\rCheck container consistency [ERROR]\n' \ ' => The selected container does not exist.\n' /usr/bin/test -f "${CT_REALPATH}/etc/os-release" || \ error '\r\rCheck container consistency [ERROR]\n' \ ' => The selected container does not contain any system files.\n' message '\r\rCheck container consistency [OK]\n' } function error () { if [ -n "${1:-}" ]; then /usr/bin/echo -n -e "${1}" fi if [ -n "${2:-}" ]; then /usr/bin/echo -n -e "${2}" fi exit 1 } function help () { local HELP case "${1}" in command) read -r -d '' HELP << EOL Usage: pct-template (OPTION) (CONTAINER) Options: [ -c ] [ CONTAINER ] | Chroot Container [ -g ] [ CONTAINER ] | Generate Container Template [ -h ] | Help [ -p ] [ CONTAINER ] | Prepare Container [ -r ] [ CONTAINER ] | Container Repository EOL ;; chroot) read -r -d '' HELP << EOL Usage: pct-template -c (CONTAINER) Description: Change the root environment into the container. EOL ;; generate) read -r -d '' HELP << EOL Usage: pct-template -g (CONTAINER) Description: Compresses the container with the data compression format 'xz' and generates a hash sum of it. EOL ;; prepare) read -r -d '' HELP << EOL Usage: pct-template -p (CONTAINER) Description: Prepare and/or clean up the container. EOL ;; repository) read -r -d '' HELP << EOL Usage: pct-template -r (CONTAINER) Description: Download one of the following images from the repository. - Debian GNU/Linux - arm64_debian_bookworm - arm64_debian_trixie - Ubuntu GNU/Linux - arm64_ubuntu_noble-numbat EOL ;; esac /usr/bin/echo -e "${HELP}" } function message () { if [ -n "${1:-}" ]; then /usr/bin/echo -n -e "${1}" fi } function resolvconf () { local OPTION="${1}" case "${OPTION}" in prepare) message 'Prepare DNS-Resolver ...' if [[ -L "${CT_REALPATH}/etc/resolv.conf" ]]; then /usr/bin/unlink "${CT_REALPATH}/etc/resolv.conf" || \ error '\r\rPrepare DNS-Resolver [ERROR]\n' \ " => The 'resolv.conf' could not be unlinked.\n" fi /usr/bin/cp --force '/etc/resolv.conf' "${CT_REALPATH}/etc/resolv.conf" || \ error '\r\rPrepare DNS-Resolver [ERROR]\n' \ " => The 'resolv.conf' could not be copied into the container.\n" message '\r\rPrepare DNS-Resolver [OK]\n' ;; reset) message 'Reset DNS-Resolver ...' if [[ -L "${CT_REALPATH}/etc/resolv.conf" ]]; then /usr/bin/unlink "${CT_REALPATH}/etc/resolv.conf" || \ error '\r\rReset DNS-Resolver [ERROR]\n' \ " => The 'resolv.conf' could not be unlinked.\n" fi /usr/bin/cp --force '/usr/share/pct-template/etc/resolv.conf' "${CT_REALPATH}/etc/resolv.conf" || \ error '\r\rReset DNS-Resolver [ERROR]\n' \ " => The file '${CT_REALPATH}/etc/resolv.conf' could not be replaced.\n" message '\r\rReset DNS-Resolver [OK]\n' ;; esac } function unbind () { message "Unbind file system '/dev' ..." if /usr/bin/grep --quiet "${CT_REALPATH}/dev" '/proc/mounts'; then /usr/bin/umount --recursive "${CT_REALPATH}/dev" || \ error "\r\rUnbind file system '/dev' [ERROR]\n" \ " => The file system '/dev' could not be unmounted from the container.\n" fi message "\r\rUnbind file system '/dev' [OK]\n" message "Unbind file system '/proc' ..." if /usr/bin/grep --quiet "${CT_REALPATH}/proc" '/proc/mounts'; then /usr/bin/umount --recursive "${CT_REALPATH}/proc" || \ error "\r\rUnbind file system '/proc' [ERROR]\n" \ " => The file system '/proc' could not be unmounted from the container.\n" fi message "\r\rUnbind file system '/proc' [OK]\n" message "Unbind file system '/sys' ..." if /usr/bin/grep --quiet "${CT_REALPATH}/sys" '/proc/mounts'; then /usr/bin/umount --recursive "${CT_REALPATH}/sys" || \ error "\r\rUnbind file system '/sys' [ERROR]\n" \ " => The file system '/sys' could not be unmounted from the container.\n" fi message "\r\rUnbind file system '/sys' [OK]\n" } ### # # Runtime Environment # ### /usr/bin/echo -e 'Proxmox LXC Template Tool\n' if [[ "${EUID}" -ne '0' ]]; then /usr/bin/echo -e 'Error: Permission Denied' exit 1 fi while getopts ':c:g:hp:r:' OPT; do case "${OPT}" in c) CT_BASENAME=$(/usr/bin/basename "${OPTARG}") CT_REALPATH=$(/usr/bin/realpath "${OPTARG}") consistency trap 'unbind' EXIT bind_chroot trap "resolvconf 'reset' && unbind" EXIT resolvconf 'prepare' message '\n ... Enter chroot environment ... \n\n' /usr/bin/sleep '3s' /usr/sbin/chroot "${CT_REALPATH}" || true message '\n ... Exit chroot environment ... \n\n' /usr/bin/sleep '3s' exit 0 ;; g) CT_BASENAME=$(/usr/bin/basename "${OPTARG}") CT_REALPATH=$(/usr/bin/realpath "${OPTARG}") CT_DIRNAME=$(/usr/bin/dirname "${CT_REALPATH}") consistency trap "/usr/bin/rm --force ${CT_DIRNAME}/${CT_BASENAME}.tar.xz"* EXIT message 'Generate container template ...' /usr/bin/tar --create --use-compress-program="/usr/bin/xz -9 --extreme" --file "${CT_DIRNAME}/${CT_BASENAME}.tar.xz" --directory="${CT_REALPATH}" '.' || \ error '\r\rGenerate container template [ERROR]\n' \ ' => The container folder could not be compressed.\n' message '\r\rGenerate container template [OK]\n' message 'Generate container hashsum ...' /usr/bin/sha512sum "${CT_DIRNAME}/${CT_BASENAME}.tar.xz" | /usr/bin/sed --expression='s, .*/, ,' >> "${CT_DIRNAME}/${CT_BASENAME}.tar.xz.sha512sum" || \ error '\r\rGenerate container hashsum [ERROR]\n' \ ' => The container hashsum could not be generated.\n' message '\r\rGenerate container hashsum [OK]\n' trap '' EXIT exit 0 ;; h) set +e help 'command' exit 0 ;; p) CT_BASENAME=$(/usr/bin/basename "${OPTARG}") CT_REALPATH=$(/usr/bin/realpath "${OPTARG}") CT_DIRNAME=$(/usr/bin/dirname "${CT_REALPATH}") consistency trap "/usr/bin/rm --force ${CT_DIRNAME}/.${CT_BASENAME}" EXIT message 'Prepare the planned file remove list ...' if [ -f "/usr/share/pct-template/prepare/${CT_BASENAME}" ]; then /usr/bin/cp --update='none' "/usr/share/pct-template/prepare/${CT_BASENAME}" "${CT_DIRNAME}/.${CT_BASENAME}" || \ error '\r\rPrepare the planned file remove list [ERROR]\n' \ " => The remove list '${CT_BASENAME}' could not be copied.\n" elif [[ ! -f "/usr/share/pct-template/prepare/${CT_BASENAME}" ]]; then /usr/bin/cp --update='none' "/usr/share/pct-template/prepare/default" "${CT_DIRNAME}/.${CT_BASENAME}" || \ error '\r\rPrepare the planned file remove list [ERROR]\n' \ " => The remove list 'default' could not be copied.\n" fi /usr/bin/editor "${CT_DIRNAME}/.${CT_BASENAME}" || \ error '\r\rPrepare the planned file remove list [ERROR]\n' \ " => The remove list '.${CT_BASENAME}' could not be opened.\n" message '\r\rPrepare the planned file remove list [OK]\n' message 'Remove files from remove list ...' mapfile -t CLEANUP_LIST < <(/usr/bin/cat "${CT_DIRNAME}/.${CT_BASENAME}" | /usr/bin/sed '/#.*/d' | while read -r CLEANUP_PATH; do /usr/bin/echo "${CT_REALPATH}${CLEANUP_PATH}"; done) for CLEANUP_LIST in "${CLEANUP_LIST[@]}"; do /usr/bin/sh -c "/usr/bin/ls --directory ${CLEANUP_LIST} | /usr/bin/xargs /usr/bin/rm --force --recursive" &> '/dev/null' || \ error '\r\rRemove files from remove list [ERROR]\n' \ " => The file or directory '${CLEANUP_LIST}' could not be removed.\n" done unset CLEANUP_LIST message '\r\rRemove files from remove list [OK]\n' message 'Cleaning up the log files ...' mapfile -t CLEANUP_LIST < <(/usr/bin/find "${CT_REALPATH}/var/log" -type 'f') for CLEANUP_LIST in "${CLEANUP_LIST[@]}"; do /usr/bin/dd if=/dev/null of="${CLEANUP_LIST}" &> '/dev/null' || \ error '\r\rCleaning up the log files [ERROR]\n' \ " => The file '${CLEANUP_LIST}' could not be emptied.\n" done unset CLEANUP_LIST mapfile -t CLEANUP_LIST < <(/usr/bin/find "${CT_REALPATH}/var/logrotate" -type 'f') for CLEANUP_LIST in "${CLEANUP_LIST[@]}"; do /usr/bin/sh -c "/usr/bin/ls --directory ${CLEANUP_LIST} | /usr/bin/xargs /usr/bin/rm --force --recursive" &> '/dev/null' || \ error '\r\rCleaning up the log files [ERROR]\n' \ " => The file '${CLEANUP_LIST}' could not be removed.\n" done message '\r\rCleaning up the log files [OK]\n' message 'Load container information ...' source "${CT_REALPATH}/etc/os-release" || \ error '\r\rcontainer information [ERROR]\n' \ " => The system information '/etc/os-release' could not be loaded.\n" message '\r\rLoad container information [OK]\n' message 'Prepare container hostname ...' /usr/bin/echo 'LXC_NAME' > "${CT_REALPATH}/etc/hostname" || \ error '\r\rPrepare container hostname [ERROR]\n' \ " => The file '/etc/hostname' could not be written.\n" message '\r\rPrepare container hostname [OK]\n' message 'Prepare container hosts ...' /usr/bin/cp --force '/usr/share/pct-template/etc/hosts' "${CT_REALPATH}/etc/hosts" || \ error '\r\rPrepare container hosts [ERROR]\n' \ " => The file '/etc/hosts' could not be replaced.\n" message '\r\rPrepare container hosts [OK]\n' case "${NAME}" in 'Debian GNU/Linux') message 'Prepare container network interfaces ...' /usr/bin/cp --force '/usr/share/pct-template/etc/interfaces' "${CT_REALPATH}/etc/network/interfaces" || \ error '\r\rPrepare container network interfaces [ERROR]\n' \ " => The file '/etc/network/interfaces' could not be replaced.\n" message '\r\rPrepare container network interfaces [OK]\n' ;; esac resolvconf 'reset' exit 0 ;; r) message 'Download container template ...' if [[ -d "./${OPTARG}" ]]; then error '\r\rDownload container template [ERROR]\n' \ " => The container '${OPTARG}' already exists.\n" fi trap "/usr/bin/rm --force --recursive ./${OPTARG}.tar.xz"* EXIT /usr/bin/wget --quiet --output-document="./${OPTARG}.tar.xz" --no-hsts "https://repository.privlab.it/pct-template/${OPTARG}.tar.xz" &> '/dev/null' || \ error '\r\rDownload container template [ERROR]\n' \ " => The file '${OPTARG}.tar.xz' could not be downloaded.\n" /usr/bin/wget --quiet --output-document="./${OPTARG}.tar.xz.sha512sum" --no-hsts "https://repository.privlab.it/pct-template/${OPTARG}.tar.xz.sha512sum" &> '/dev/null' || \ error '\r\rDownload container template [ERROR]\n' \ " => The file '${OPTARG}.tar.xz.sha512sum' could not be downloaded.\n" message '\r\rDownload container template [OK]\n' message 'Security check of the container archive ...' /usr/bin/sha512sum --check --strict "./${OPTARG}.tar.xz.sha512sum" &> '/dev/null' || \ error '\r\rSecurity check of the container archive [ERROR]\n' \ " => The SHA512 hash value for '${OPTARG}.tar.xz' could not be verified.\n" message '\r\rSecurity check of the container archive [OK]\n' message 'Decompress container archive ...' /usr/bin/install --directory "./${OPTARG}" || \ error '\r\rDecompress container archive [ERROR]\n' \ " => The path '${OPTARG}' could not be installed.\n" /usr/bin/tar --extract --file="./${OPTARG}.tar.xz" --directory="./${OPTARG}" || \ error '\r\rDecompress container archive [ERROR]\n' \ " => The container archive '${OPTARG}.tar.xz' could not be decompressed.\n" /usr/bin/rm --force "./${OPTARG}.tar.xz" || \ error '\r\rDecompress container archive [ERROR]\n' \ " => The container archive '${OPTARG}.tar.xz' could not be removed.\n" message '\r\rDecompress container archive [OK]\n' trap "/usr/bin/rm --force --recursive ./${OPTARG}.tar.xz.sha512sum"* EXIT exit 0 ;; -*|*) set +e case "${1}" in -a) help 'archive' exit 0 ;; -c) help 'chroot' exit 0 ;; -p) help 'prepare' exit 0 ;; -r) help 'repository' exit 0 ;; esac help 'command' exit 0 ;; esac done set +e help 'command' shift $((OPTIND-1))