#!/bin/sh # Copyright (C) 2014-2022 Daniel Baumann # # SPDX-License-Identifier: GPL-3.0+ # # 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 3 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 . set -e PROJECT="open-infrastructure" SOFTWARE="compute-tools" PROGRAM="container" VERSION="$(container version)" SCRIPT="${0}" export SCRIPT CACHE="/var/cache/${PROGRAM}/build-$(basename ${SCRIPT})" CONFIG="/etc/${SOFTWARE}/config" HOOKS="/etc/${SOFTWARE}/hooks" MACHINES="/var/lib/machines" VMS="/srv/container/vms" Parameters () { GETOPT_LONGOPTIONS="bind:,bind-ro:,boot-method:,script:,name:,preseed-file:,vm" GETOPT_OPTIONS="b:,s:,n:,p:,v:" PARAMETERS="$(getopt --longoptions ${GETOPT_LONGOPTIONS} --name=${SCRIPT} --options ${GETOPT_OPTIONS} --shell sh -- ${@})" if [ "${?}" != "0" ] then echo "'${SCRIPT}': getopt exit" >&2 exit 1 fi eval set -- "${PARAMETERS}" while true do case "${1}" in -b|--bind) # ignore shift 2 ;; --bind-ro) # ignore shift 2 ;; --boot-method) BOOT_METHOD="${2}" shift 2 ;; --cnt.auto) # ignore shift 2 ;; --cnt.container-server) # ignore shift 2 ;; -s|--script) # ignore shift 2 ;; -n|--name) NAME="${2}" shift 2 ;; --preseed-file) PRESEED_FILE="${2}" shift 2 ;; --vm) VM="TRUE" shift 1 ;; --) shift 1 break ;; *) echo "'${SCRIPT}': getopt error" >&2 exit 1 ;; esac done } Usage () { echo "Usage: container build -n|--name NAME -s|--script ${SCRIPT} [--boot-method systemd-nswawn|qemu] -- [-p|--preseed-file FILE]" >&2 exit 1 } Parameters "${@}" if [ -z "${NAME}" ] then Usage fi if [ -e "${MACHINES}/${NAME}" ] then echo "'${NAME}': container already exists" >&2 exit 1 fi if [ "$(id -u)" -ne 0 ] then echo "'${NAME}': need root privileges" >&2 exit 1 fi Mount () { # Mounting rw bind mounts if [ -n "${BIND}" ] then BINDS="$(echo ${BIND} | sed -e 's|;| |g')" for ENTRY in ${BINDS} do SOURCE="$(echo ${ENTRY} | awk -F: '{ print $1 }')" TARGET="$(echo ${ENTRY} | awk -F: '{ print $2 }')" mkdir -p "${SOURCE}" mkdir -p "${MACHINES}/${NAME}/${TARGET}" mount -o bind "${SOURCE}" "${MACHINES}/${NAME}/${TARGET}" done fi # Mounting ro bind mounts if [ -n "${BIND_RO}" ] then BINDS_RO="$(echo ${BIND_RO} | sed -e 's|;| |g')" for ENTRY in ${BINDS_RO} do SOURCE="$(echo ${ENTRY} | awk -F: '{ print $1 }')" TARGET="$(echo ${ENTRY} | awk -F: '{ print $2 }')" mkdir -p "${SOURCE}" mkdir -p "${MACHINES}/${NAME}/${TARGET}" mount -o rbind "${SOURCE}" "${MACHINES}/${NAME}/${TARGET}" done fi # Mounting overlay mounts if [ -n "${CNT_OVERLAY}" ] then CNT_OVERLAYS="$(echo ${CNT_OVERLAY} | sed -e 's|;| |g')" COUNT="0" for CNT_OVERLAY in ${CNT_OVERLAYS} do DIRECTORY_LOWER="$(echo ${CNT_OVERLAY} | awk -F: '{ print $1 }')" DIRECTORY_UPPER="$(echo ${CNT_OVERLAY} | awk -F: '{ print $2 }')" DIRECTORY_WORK="$(echo ${CNT_OVERLAY} | awk -F: '{ print $3 }')" DIRECTORY_MERGED="$(echo ${CNT_OVERLAY} | awk -F: '{ print $4 }')" COUNT="$((${COUNT} + 1))" CNT_OVERLAY_OPTION="$(echo ${CNT_OVERLAY_OPTIONS} | awk -F ';' "{ print \$${COUNT} }")" for DIRECTORY in "${DIRECTORY_LOWER}" "${DIRECTORY_UPPER}" "${DIRECTORY_WORK}" "${DIRECTORY_MERGED}" do mkdir -p "${DIRECTORY}" done if ! findmnt -n -o SOURCE "${DIRECTORY_MERGED}" | grep -qs '^overlay-' then if [ -n "${CNT_OVERLAY_OPTION}" ] then CNT_OVERLAY_OPTION="-o ${CNT_OVERLAY_OPTION}" fi mount -t overlay overlay-${NAME} ${CNT_OVERLAY_OPTION} -olowerdir="${DIRECTORY_LOWER}",upperdir="${DIRECTORY_UPPER}",workdir="${DIRECTORY_WORK}" "${DIRECTORY_MERGED}" fi done fi } Umount () { # Unmounting overlay mounts if [ -n "${CNT_OVERLAY}" ] then CNT_OVERLAYS="$(echo ${CNT_OVERLAY} | sed -e 's|;| |g')" for CNT_OVERLAY in ${CNT_OVERLAYS} do DIRECTORY_LOWER="$(echo ${CNT_OVERLAY} | awk -F: '{ print $1 }')" DIRECTORY_UPPER="$(echo ${CNT_OVERLAY} | awk -F: '{ print $2 }')" DIRECTORY_WORK="$(echo ${CNT_OVERLAY} | awk -F: '{ print $3 }')" DIRECTORY_MERGED="$(echo ${CNT_OVERLAY} | awk -F: '{ print $4 }')" umount -f "${DIRECTORY_MERGED}" > /dev/null 2>&1 || true for DIRECTORY in "${DIRECTORY_LOWER}" "${DIRECTORY_UPPER}" "${DIRECTORY_WORK}" "${DIRECTORY_MERGED}" do rmdir --ignore-fail-on-non-empty --parents ${DIRECTORY} > /dev/null 2>&1 || true done done # empty workdir otherwise there might happen stale file handles if [ -d "${DIRECTORY_WORK}" ] then rm --preserve-root --one-file-system -rf "${DIRECTORY_WORK}"/* fi fi # Unmounting ro bind mounts if [ -n "${BIND_RO}" ] then # unmount in reverse order to allow nested bind mounts BINDS_RO="$(echo ${BIND_RO} | sed -e 's|;| |g' | awk '{ for (i=NF; i>=1; i--) printf "%s ", $i; print ""}')" for ENTRY in ${BINDS_RO} do TARGET="$(echo ${ENTRY} | awk -F: '{ print $2 }')" umount -f "${MACHINES}/${NAME}/${TARGET}" > /dev/null 2>&1 || true done fi # Unmounting rw bind mounts if [ -n "${BIND}" ] then # unmount in reverse order to allow nested bind mounts BINDS="$(echo ${BIND} | sed -e 's|;| |g' | awk '{ for (i=NF; i>=1; i--) printf "%s ", $i; print ""}')" for ENTRY in ${BINDS} do TARGET="$(echo ${ENTRY} | awk -F: '{ print $2 }')" umount -f "${MACHINES}/${NAME}/${TARGET}" > /dev/null 2>&1 || true done fi # Unmounting pseudo-filesystems # FIXME this should not be necessary umount -f "${DIRECTORY}/dev" > /dev/null 2>&1 || true umount -f "${DIRECTORY}/dev/pts" > /dev/null 2>&1 || true umount -f "${DIRECTORY}/proc" > /dev/null 2>&1 || true umount -f "${DIRECTORY}/sys" > /dev/null 2>&1 || true # Unmounting disk images if [ "${BOOT_METHOD}" = "qemu" ] then umount -f "${DIRECTORY}/boot/efi" > /dev/null 2>&1 || true umount -f "${DIRECTORY}" > /dev/null 2>&1 || true rmdir "${MACHINES}/${NAME}" kpartx -d "${VMS}/${NAME}/root.img" fi } Chroot () { CHROOT="${1}" shift chroot "${CHROOT}" /usr/bin/env -i \ LC_ALL="C" PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/games:/usr/games" TERM="${TERM}" \ DEBIAN_FRONTEND="${DEBCONF_FRONTEND}" DEBIAN_PRIORITY="${DEBCONF_PRIORITY}" \ DEBCONF_NONINTERACTIVE_SEEN="true" DEBCONF_NOWARNINGS="true" \ ftp_proxy="${ftp_proxy}" http_proxy="${http_proxy}" \ ${@} } Upgrade_system () { DIRECTORY="${1}" # Mount pseudo-filesystems mount -o bind /dev/pts "${DIRECTORY}/dev/pts" mount -o bind /proc "${DIRECTORY}/proc" mount -o bind /sys "${DIRECTORY}/sys" # Disable dpkg syncing cat > "${DIRECTORY}/etc/dpkg/dpkg.cfg.d/${SOFTWARE}" << EOF force-unsafe-io EOF # Create policy-rc.d file cat > "${DIRECTORY}/usr/sbin/policy-rc.d" << EOF #!/bin/sh echo "All runlevel operations denied by policy" >&2 exit 101 EOF chmod 0755 "${DIRECTORY}/usr/sbin/policy-rc.d" # Upgrade system Chroot "${DIRECTORY}" "apt update" Chroot "${DIRECTORY}" "apt --yes --option Dpkg::Options::=--force-confnew upgrade" Chroot "${DIRECTORY}" "apt --yes --option Dpkg::Options::=--force-confnew dist-upgrade" # Install systemd support packages Chroot "${DIRECTORY}" "apt --yes install dbus libpam-systemd systemd-sysv" Chroot "${DIRECTORY}" "apt clean" } Cleanup_system () { DIRECTORY="${1}" Chroot "${DIRECTORY}" "apt --yes --purge autoremove" Chroot "${DIRECTORY}" "apt clean" # Cleanup rm -f "${DIRECTORY}/etc/dpkg/dpkg.cfg.d/${SOFTWARE}" rm -f "${DIRECTORY}/usr/sbin/policy-rc.d" # Unmount pseudo-filesystems umount -f "${DIRECTORY}/dev/pts" > /dev/null 2>&1 || true umount -f "${DIRECTORY}/proc" > /dev/null 2>&1 || true umount -f "${DIRECTORY}/sys" > /dev/null 2>&1 || true } Debconf () { # Configure local debconf mkdir -p "/tmp/${SOFTWARE}" DEBCONF_TMPDIR="$(mktemp -d -p "/tmp/${SOFTWARE}" -t $(basename ${0}).XXXX)" export DEBCONF_TMPDIR mkdir -p "${DEBCONF_TMPDIR}/debconf" cat > "${DEBCONF_TMPDIR}/debconf.systemrc" << EOF Config: configdb Templates: templatedb Name: config Driver: File Mode: 644 Reject-Type: password Filename: ${DEBCONF_TMPDIR}/debconf/config.dat Name: passwords Driver: File Mode: 600 Backup: false Required: false Accept-Type: password Filename: ${DEBCONF_TMPDIR}/debconf/passwords.dat Name: configdb Driver: Stack Stack: config, passwords Name: templatedb Driver: File Mode: 644 Filename: ${DEBCONF_TMPDIR}/debconf/templates.dat EOF DEBCONF_SYSTEMRC="${DEBCONF_TMPDIR}/debconf.systemrc" export DEBCONF_SYSTEMRC } Bootstrap () { DIRECTORY="${1}" EXCLUDE="ifupdown" #FIXME amd64 hardcoded INCLUDE="dbus,linux-image-amd64,grub-efi-amd64" # apt repositories INCLUDE="${INCLUDE},gnupg" if ( echo "${MIRROR}" | grep -qs '^https' ) || \ ( echo "${PARENT_MIRROR}" | grep -qs '^https' ) then INCLUDE="${INCLUDE},apt-transport-https,ca-certificates" fi case "${MODE}" in progress-linux) INCLUDE="${INCLUDE},progress-linux,gnupg" ;; esac mkdir -p "$(dirname ${DIRECTORY})" case "${BOOTSTRAP}" in debootstrap) debootstrap --verbose --arch=${ARCHITECTURE} --components=${PARENT_ARCHIVE_AREAS} \ --exclude=${EXCLUDE} --include=${INCLUDE} ${PARENT_DISTRIBUTION} "${DIRECTORY}" ${PARENT_MIRROR} ;; mmdebstrap) mmdebstrap --arch=${ARCHITECTURE} --components=${PARENT_ARCHIVE_AREAS} \ --format=directory --mode=root --aptopt='APT::Sandbox::User "root"' \ --include=${INCLUDE} ${PARENT_DISTRIBUTION} "${DIRECTORY}" ${PARENT_MIRROR} ;; *) echo "'${NAME}': ${BOOTSTRAP} - not supported" >&2 exit 1 ;; esac } Image () { DIRECTORY="${1}" FILES="${IMAGE}" for NUMBER in $(seq 1 ${IMAGE_NUMBER}) do eval FILES="${FILES} $`echo IMAGE${NUMBER}`" done for FILE in ${FILES} do case "${FILE}" in *.gz) TAR_OPTIONS="--gzip" if [ ! -e /bin/gzip ] then echo -en "\n" echo "'${NAME}': /bin/lzip - no such file." >&2 exit 1 fi ;; *.lz) TAR_OPTIONS="--lzip" if [ ! -e /usr/bin/lzip ] then echo -en "\n" echo "'${NAME}': /usr/bin/lzip - no such file." >&2 exit 1 fi ;; *.xz) TAR_OPTIONS="--xz" if [ ! -e /usr/bin/xz ] then echo -en "\n" echo "'${NAME}': /usr/bin/xz - no such file." >&2 exit 1 fi ;; *) TAR_OPTIONS="" ;; esac mkdir -p "${DIRECTORY}" echo "Using ${FILE}" if [ -e /usr/bin/pv ] then curl --fail --location --progress-bar --user-agent ${SOFTWARE}/${VERSION} --http2 ${CURL_TIME_COND} \ "${FILE}" -o - | \ pv --format '%p' --width 77 | \ tar -C "${DIRECTORY}" --strip 1 ${TAR_OPTIONS} -xf - #pv --format '%p' --width 77 "${CACHE}/${FILE}" | tar xf - ${TAR_OPTIONS} -C "${DIRECTORY}" --strip 1 else curl --fail --location --progress-bar --user-agent ${SOFTWARE}/${VERSION} --http2 ${CURL_TIME_COND} \ "${FILE}" -o - | \ tar -C "${DIRECTORY}" --strip 1 ${TAR_OPTIONS} -xf - fi echo " ok." done # Writing resolv.conf rm -f "${DIRECTORY}/etc/resolv.conf" cp /etc/resolv.conf "${DIRECTORY}/etc" } Configure_apt () { DIRECTORY="${1}" # Configure apt rm -f "${DIRECTORY}/etc/apt/sources.list" PARENT_AREA="$(echo ${PARENT_ARCHIVE_AREAS} | sed -e 's|,| |g')" PARENT_DIST="$(echo ${PARENT_DISTRIBUTION} | sed -e 's|-backports||')" cat > "${DIRECTORY}/etc/apt/sources.list.d/debian.list" << EOF # /etc/apt/sources.list.d/debian.list deb ${PARENT_MIRROR} ${PARENT_DIST} ${PARENT_AREA} EOF case "${MODE}" in progress-linux) cat > "${DIRECTORY}/progress-linux.cfg" << EOF progress-linux progress-linux/archives multiselect $(echo ${ARCHIVES} | sed -e 's| |, |g') progress-linux progress-linux/archive-areas multiselect $(echo ${ARCHIVE_AREAS} | sed -e 's|,| |g') EOF Chroot "${DIRECTORY}" "debconf-set-selections progress-linux.cfg" Chroot "${DIRECTORY}" "DEBIAN_FRONTEND=noninteractive DEBIAN_PRIORITY=criticial dpkg-reconfigure progress-linux" rm -f "${DIRECTORY}/progress-linux.cfg" case "${INSTALLER}" in bootstrap) Chroot "${DIRECTORY}" "apt update" ;; esac ;; esac } Deconfigure_system () { DIRECTORY="${1}" # Configure fstab cat > "${DIRECTORY}/etc/fstab" << EOF # /etc/fstab: static file system information. # # Use 'blkid' to print the universally unique identifier for a # device; this may be used with UUID= as a more robust way to name devices # that works even if disks are added and removed. See fstab(5). # # EOF # Fix /etc/mtab rm -f "${DIRECTORY}/etc/mtab" ln -s /proc/self/mounts "${DIRECTORY}/etc/mtab" # Removing machine-id rm -f "${DIRECTORY}/etc/machine-id" # Removing resolv.conf rm -f "${DIRECTORY}/etc/resolv.conf" cp /etc/resolv.conf "${DIRECTORY}/etc" # Removing hosts/hostname rm -f "${DIRECTORY}"/etc/hosts rm -f "${DIRECTORY}"/etc/hostname # Removing openssh-server host keys rm -f "${DIRECTORY}"/etc/ssh/ssh_host_*_key rm -f "${DIRECTORY}"/etc/ssh/ssh_host_*_key.pub } Configure_system () { DIRECTORY="${1}" # Overwrite resolv.conf from cache with hosts resolv.conf rm -f "${DIRECTORY}/etc/resolv.conf" cp /etc/resolv.conf "${DIRECTORY}/etc" echo "${NAME}" > "${DIRECTORY}/etc/hostname" # Configure apt rm -f "${DIRECTORY}/etc/apt/sources.list" PARENT_AREA="$(echo ${PARENT_ARCHIVE_AREAS} | sed -e 's|,| |g')" PARENT_DIST="$(echo ${PARENT_DISTRIBUTION} | sed -e 's|-backports||')" cat > "${DIRECTORY}/etc/apt/sources.list.d/debian.list" << EOF # /etc/apt/sources.list.d/debian.list deb ${PARENT_MIRROR} ${PARENT_DIST} ${PARENT_AREA} EOF for PARENT_REPO in ${PARENT_ARCHIVES} do case "${PARENT_REPO}" in buster-security) echo "deb ${PARENT_MIRROR_SECURITY} ${PARENT_DIST}/updates ${PARENT_AREA}" >> "${DIRECTORY}/etc/apt/sources.list.d/debian.list" ;; ${PARENT_DIST}-security) echo "deb ${PARENT_MIRROR_SECURITY} ${PARENT_DIST}-security ${PARENT_AREA}" >> "${DIRECTORY}/etc/apt/sources.list.d/debian.list" ;; ${PARENT_DIST}-updates) echo "deb ${PARENT_MIRROR} ${PARENT_DIST}-updates ${PARENT_AREA}" >> "${DIRECTORY}/etc/apt/sources.list.d/debian.list" ;; ${PARENT_DIST}-backports) echo "deb ${PARENT_MIRROR} ${PARENT_DIST}-backports ${PARENT_AREA}" >> "${DIRECTORY}/etc/apt/sources.list.d/debian.list" ;; ${PARENT_DIST}-proposed-updates) echo "deb ${PARENT_MIRROR} ${PARENT_DIST}-proposed-updates ${PARENT_AREA}" >> "${DIRECTORY}/etc/apt/sources.list.d/debian.list" ;; experimental) echo "deb ${PARENT_MIRROR} experimental ${PARENT_AREA}" >> "${DIRECTORY}/etc/apt/sources.list.d/debian.list" ;; esac done case "${MODE}" in progress-linux) cat > "${DIRECTORY}/progress-linux.cfg" << EOF progress-linux progress-linux/archives multiselect $(echo ${ARCHIVES} | sed -e 's| |, |g') progress-linux progress-linux/archive-areas multiselect $(echo ${ARCHIVE_AREAS} | sed -e 's|,| |g') EOF Chroot "${DIRECTORY}" "debconf-set-selections progress-linux.cfg" Chroot "${DIRECTORY}" "DEBIAN_FRONTEND=noninteractive DEBIAN_PRIORITY=criticial dpkg-reconfigure progress-linux" rm -f "${DIRECTORY}/progress-linux.cfg" ;; esac if [ "${APT_RECOMMENDS}" = "false" ] then cat > "${DIRECTORY}/etc/apt/apt.conf.d/recommends.conf" << EOF APT::Install-Recommends "false"; EOF fi # Add local archives configured from preseed file if ls "${DEBCONF_TMPDIR}/apt"/*.list > /dev/null 2>&1 then cp "${DEBCONF_TMPDIR}/apt"/*.list "${DIRECTORY}/etc/apt/sources.list.d" if ls "${DEBCONF_TMPDIR}/apt"/*.key > /dev/null 2>&1 then for KEY in "${DEBCONF_TMPDIR}/apt"/*.key do cp "${KEY}" "${DIRECTORY}" Chroot "${DIRECTORY}" "apt-key add $(basename ${KEY})" rm -f "${DIRECTORY}/$(basename ${KEY})" done fi if ls "${DEBCONF_TMPDIR}/apt"/*.pref > /dev/null 2>&1 then cp "${DEBCONF_TMPDIR}/apt"/*.pref "${DIRECTORY}/etc/apt/preferences.d" fi fi Upgrade_system "${DIRECTORY}" # Preseed system if [ -n "${PRESEED_FILE}" ] then for FILE in ${PRESEED_FILE} do if [ -e /usr/bin/kdig ] then DIG="/usr/bin/kdig" elif [ -e /usr/bin/dig ] then DIG="/usr/bin/dig" fi if [ -n "${DIG}" ] then IPV4_ADDRESS1="$(${DIG} A +short ${NAME} | tail -n1)" IPV4_ADDRESS1_PART1="$(echo ${IPV4_ADDRESS1} | cut -d. -f1)" IPV4_ADDRESS1_PART2="$(echo ${IPV4_ADDRESS1} | cut -d. -f2)" IPV4_ADDRESS1_PART3="$(echo ${IPV4_ADDRESS1} | cut -d. -f3)" IPV4_ADDRESS1_PART4="$(echo ${IPV4_ADDRESS1} | cut -d. -f4)" IPV6_ADDRESS1="$(${DIG} AAAA +short ${NAME} | tail -n1)" # FIXME: address parts export IPV4_ADDRESS1 IPV4_ADDRESS1_PART1 IPV4_ADDRESS1_PART2 IPV4_ADDRESS1_PART3 IPV4_ADDRESS1_PART4 export IPV6_ADDRESS1 fi sed -e "s|@FILE@|${FILE}|g" \ -e "s|@NAME@|${NAME}|g" \ -e "s|@IPV4_ADDRESS1@|${IPV4_ADDRESS1}|g" \ -e "s|@IPV4_ADDRESS1_PART1@|${IPV4_ADDRESS1_PART1}|g" \ -e "s|@IPV4_ADDRESS1_PART2@|${IPV4_ADDRESS1_PART2}|g" \ -e "s|@IPV4_ADDRESS1_PART3@|${IPV4_ADDRESS1_PART3}|g" \ -e "s|@IPV4_ADDRESS1_PART4@|${IPV4_ADDRESS1_PART4}|g" \ -e "s|@IPV6_ADDRESS1@|${IPV6_ADDRESS1}|g" \ "${FILE}" >> "${DIRECTORY}/preseed.cfg" done Chroot "${DIRECTORY}" "debconf-set-selections preseed.cfg" rm -f "${DIRECTORY}/preseed.cfg" fi # Manual hack to workaround broken preseeding in locales package if [ -n "${PRESEED_FILE}" ] then for FILE in ${PRESEED_FILE} do if grep -qs locales "${FILE}" then if Chroot "${DIRECTORY}" dpkg --get-selections | awk '{ print $1 }' | grep -qs '^locales$' then rm -f "${DIRECTORY}/etc/default/locale" "${DIRECTORY}/etc/locale.gen" Chroot "${DIRECTORY}" "DEBIAN_FRONTEND=noninteractive DEBIAN_PRIORITY=criticial dpkg-reconfigure locales" break fi fi done fi # Manual hack to create conffiles when using locales-all instead of locales if [ ! -e "${DIRECTORY}/etc/environment" ] then echo "LANG=C.UTF-8" >> "${DIRECTORY}/etc/environment" fi if [ ! -e "${DIRECTORY}/etc/default/locale" ] then echo "LANG=C.UTF-8" >> "${DIRECTORY}/etc/default/locale" fi # Manual hack to workaround broken preseeding in tzdata package if [ -n "${PRESEED_FILE}" ] then for FILE in ${PRESEED_FILE} do if grep -qs tzdata "${FILE}" then rm -f "${DIRECTORY}/etc/localtime" "${DIRECTORY}/etc/timezone" Chroot "${DIRECTORY}" "DEBIAN_FRONTEND=noninteractive DEBIAN_PRIORITY=criticial dpkg-reconfigure tzdata" break fi done fi if [ -n "${PACKAGES}" ] then Chroot "${DIRECTORY}" "apt --option Dpkg::Options::=--force-confnew --yes install ${PACKAGES}" fi # Manual hack to regenerate ssh keys if Chroot "${DIRECTORY}" dpkg --get-selections | awk '{ print $1 }' | grep -qs '^openssh-server$' && \ ! ls "${DIRECTORY}"/etc/ssh/ssh_host_*_key > /dev/null 2>&1 then Chroot "${DIRECTORY}" "DEBIAN_FRONTEND=noninteractive DEBIAN_PRIORITY=criticial dpkg-reconfigure openssh-server" fi # container command if [ -n "${CONTAINER_COMMAND}" ] then echo "${CONTAINER_COMMAND}" > "${DIRECTORY}/.container-command" chmod 0755 "${DIRECTORY}/.container-command" Chroot "${DIRECTORY}" "sh /.container-command" rm -f "${DIRECTORY}/.container-command" fi for NUMBER in $(seq 1 ${CONTAINER_COMMAND_NUMBER}) do eval COMMAND="$`echo CONTAINER_COMMAND${NUMBER}`" echo "${COMMAND}" > "${DIRECTORY}/.container-command" chmod 0755 "${DIRECTORY}/.container-command" Chroot "${DIRECTORY}" "sh /.container-command" rm -f "${DIRECTORY}/.container-command" done } Configure_network () { DIRECTORY="${1}" # Create /etc/resolv.conf rm -f "${DIRECTORY}/etc/resolv.conf.tmp" if [ -n "${NAMESERVER_DOMAIN}" ] then echo "domain ${NAMESERVER_DOMAIN}" >> "${DIRECTORY}/etc/resolv.conf.tmp" fi if [ -n "${NAMESERVER_SEARCH}" ] then echo "search ${NAMESERVER_SEARCH}" >> "${DIRECTORY}/etc/resolv.conf.tmp" fi if [ -n "${NAMESERVER_SERVER}" ] then if [ -e "${DIRECTORY}/etc/resolv.conf.tmp" ] then echo "" >> "${DIRECTORY}/etc/resolv.conf.tmp" fi for NAMESERVER in $(echo ${NAMESERVER_SERVER} | sed -e 's|,| |g') do echo "nameserver ${NAMESERVER}" >> "${DIRECTORY}/etc/resolv.conf.tmp" done fi if [ -n "${NAMESERVER_OPTIONS}" ] then if [ -e "${DIRECTORY}/etc/resolv.conf.tmp" ] then echo "" >> "${DIRECTORY}/etc/resolv.conf.tmp" fi echo "options ${NAMESERVER_OPTIONS}" >> "${DIRECTORY}/etc/resolv.conf.tmp" fi if [ -e "${DIRECTORY}/etc/resolv.conf.tmp" ] then mv "${DIRECTORY}/etc/resolv.conf.tmp" "${DIRECTORY}/etc/resolv.conf" fi # Create /etc/hosts rm -f "${DIRECTORY}/etc/hosts.tmp" if [ -n "${NETWORK1_IPV4_ADDRESS}" ] then cat >> "${DIRECTORY}/etc/hosts.tmp" << EOF ${NETWORK1_IPV4_ADDRESS} ${NAME} EOF fi if [ -n "${NETWORK1_IPV6_ADDRESS}" ] then cat >> "${DIRECTORY}/etc/hosts.tmp" << EOF ${NETWORK1_IPV6_ADDRESS} ${NAME} EOF fi if [ -n "${NETWORK1_IPV4_ADDRESS}" ] || [ -n "${NETWORK1_IPV6_ADDRESS}" ] then echo >> "${DIRECTORY}/etc/hosts.tmp" fi if [ -z "${NETWORK1_IPV4_ADDRESS}" ] && [ -z "${NETWORK1_IPV6_ADDRESS}" ] then # localhost only cat > "${DIRECTORY}/etc/hosts.tmp" << EOF 127.0.0.1 localhost ${NAME} ::1 localhost ${NAME} EOF else cat > "${DIRECTORY}/etc/hosts.tmp" << EOF 127.0.0.1 localhost ::1 localhost EOF fi cat >> "${DIRECTORY}/etc/hosts.tmp" << EOF # The following lines are desirable for IPv6 capable hosts ::1 ip6-localhost ip6-loopback fe00::0 ip6-localnet ff00::0 ip6-mcastprefix ff02::1 ip6-allnodes ff02::2 ip6-allrouters EOF mv "${DIRECTORY}/etc/hosts.tmp" "${DIRECTORY}/etc/hosts" } Configure_systemd_networkd () { DIRECTORY="${1}" # Enable systemd-networkd chroot "${DIRECTORY}" systemctl enable systemd-networkd for NUMBER in $(seq 1 ${NETWORK_NUMBER}) do eval IPV4_COMMENT="$`echo NETWORK${NUMBER}_IPV4_COMMENT`" eval IPV4_METHOD="$`echo NETWORK${NUMBER}_IPV4_METHOD`" eval IPV4_ADDRESS="$`echo NETWORK${NUMBER}_IPV4_ADDRESS`" eval IPV4_GATEWAY="$`echo NETWORK${NUMBER}_IPV4_GATEWAY`" eval IPV4_NETMASK="$`echo NETWORK${NUMBER}_IPV4_NETMASK`" eval IPV4_POST_UP="$`echo NETWORK${NUMBER}_IPV4_POST_UP`" eval IPV4_POST_DOWN="$`echo NETWORK${NUMBER}_IPV4_POST_DOWN`" eval IPV6_COMMENT="$`echo NETWORK${NUMBER}_IPV6_COMMENT`" eval IPV6_METHOD="$`echo NETWORK${NUMBER}_IPV6_METHOD`" eval IPV6_ADDRESS="$`echo NETWORK${NUMBER}_IPV6_ADDRESS`" eval IPV6_GATEWAY="$`echo NETWORK${NUMBER}_IPV6_GATEWAY`" eval IPV6_NETMASK="$`echo NETWORK${NUMBER}_IPV6_NETMASK`" eval IPV6_POST_UP="$`echo NETWORK${NUMBER}_IPV6_POST_UP`" eval IPV6_POST_DOWN="$`echo NETWORK${NUMBER}_IPV6_POST_DOWN`" if [ "${IPV4_METHOD}" != "none" ] || [ "${IPV6_METHOD}" != "none" ] then cat > "${DIRECTORY}/etc/systemd/network/eno${NUMBER}.network" << EOF [Match] Name=eno${NUMBER} EOF fi if [ -n "${IPV4_METHOD}" ] && [ "${IPV4_METHOD}" != "none" ] then cat >> "${DIRECTORY}/etc/systemd/network/eno${NUMBER}.network" << EOF [Network] EOF if [ -n "${IPV4_COMMENT}" ] then echo "Description=${IPV4_COMMENT}" >> "${DIRECTORY}/etc/systemd/network/eno${NUMBER}.network" fi case "${IPV4_METHOD}" in dhcp) cat >> "${DIRECTORY}/etc/systemd/network/eno${NUMBER}.network" << EOF DHCP=ipv4 EOF ;; static) cat >> "${DIRECTORY}/etc/systemd/network/eno${NUMBER}.network" << EOF DHCP=no Address=${IPV4_ADDRESS}/${IPV4_NETMASK} EOF if [ -n "${IPV4_GATEWAY}" ] then cat >> "${DIRECTORY}/etc/systemd/network/eno${NUMBER}.network" << EOF Gateway=${IPV4_GATEWAY} EOF fi ;; stub) cat >> "${DIRECTORY}/etc/systemd/network/eno${NUMBER}.network" << EOF DHCP=no EOF ;; esac if [ -n "${IPV4_POST_UP}" ] then cat > "${DIRECTORY}/etc/systemd/system/cnt-ipv4-post-up-eno${NUMBER}.service" << EOF [Unit] Description=${SOFTWARE} IPV4_POST_UP After=network-online.target Wants=network-online.target [Service] Type=oneshot ExecStart=/bin/sh -c "${IPV4_POST_UP}" [Install] WantedBy=multi-user.target EOF chroot "${DIRECTORY}" systemctl enable cnt-ipv4-post-up-eno${NUMBER}.service fi if [ -n "${IPV4_POST_DOWN}" ] then cat > "${DIRECTORY}/etc/systemd/system/cnt-ipv4-post-down-eno${NUMBER}.service" << EOF [Unit] Description=${SOFTWARE} IPV4_POST_DOWN After=network.target Wants=network.target [Service] Type=oneshot ExecStart=/bin/sh -c "${IPV4_POST_DOWN}" [Install] WantedBy=multi-user.target EOF chroot "${DIRECTORY}" systemctl enable cnt-ipv4-post-down-eno${NUMBER}.service fi fi if [ -n "${IPV6_METHOD}" ] && [ "${IPV6_METHOD}" != "none" ] then cat >> "${DIRECTORY}/etc/systemd/network/eno${NUMBER}.network" << EOF [Network] EOF if [ -n "${IPV6_COMMENT}" ] then echo "Description=${IPV6_COMMENT}" >> "${DIRECTORY}/etc/systemd/network/eno${NUMBER}.network" fi case "${IPV6_METHOD}" in dhcp) cat >> "${DIRECTORY}/etc/systemd/network/eno${NUMBER}.network" << EOF DHCP=ipv6 EOF ;; static) cat >> "${DIRECTORY}/etc/systemd/network/eno${NUMBER}.network" << EOF DHCP=no IPv6AcceptRA=no Address=${IPV6_ADDRESS}/${IPV6_NETMASK} EOF if [ -n "${IPV6_GATEWAY}" ] then cat >> "${DIRECTORY}/etc/systemd/network/eno${NUMBER}.network" << EOF Gateway=${IPV6_GATEWAY} EOF fi ;; stub) cat >> "${DIRECTORY}/etc/systemd/network/eno${NUMBER}.network" << EOF DHCP=no IPv6AcceptRA=no EOF ;; esac if [ -n "${IPV6_POST_UP}" ] then cat > "${DIRECTORY}/etc/systemd/system/cnt-ipv6-post-up-eno${NUMBER}.service" << EOF [Unit] Description=${SOFTWARE} IPV6_POST_UP After=network-online.target Wants=network-online.target [Service] Type=oneshot ExecStart=/bin/sh -c "${IPV6_POST_UP}" [Install] WantedBy=multi-user.target EOF chroot "${DIRECTORY}" systemctl enable cnt-ipv6-post-up-eno${NUMBER}.service fi if [ -n "${IPV6_POST_DOWN}" ] then cat > "${DIRECTORY}/etc/systemd/system/cnt-ipv6-post-down-eno${NUMBER}.service" << EOF [Unit] Description=${SOFTWARE} IPV6_POST_DOWN After=network.target Wants=network.target [Service] Type=oneshot ExecStart=/bin/sh -c "${IPV6_POST_DOWN}" [Install] WantedBy=multi-user.target EOF chroot "${DIRECTORY}" systemctl enable cnt-ipv6-post-down-eno${NUMBER}.service fi fi NUMBER="$((${NUMBER} + 1))" done } Configure_vm () { DIRECTORY="${1}" # FIXME duplicate definition EFI_PARTITION="p1" SYSTEM_PARTITION="p2" EFI_UUID="$(lsblk --output UUID --noheadings /dev/mapper/loop0${EFI_PARTITION})" ROOT_UUID="$(lsblk --output UUID --noheadings /dev/mapper/loop0${SYSTEM_PARTITION})" echo "UUID=${ROOT_UUID} / ext4 discard,noatime,errors=remount-ro 0 1" >> ${DIRECTORY}/etc/fstab echo "UUID=${EFI_UUID} /boot/efi vfat umask=0077 0 1" >> ${DIRECTORY}/etc/fstab mkdir -p ${DIRECTORY}/boot/efi/EFI/progress-linux cp ${DIRECTORY}/usr/lib/grub/x86_64-efi/monolithic/grubx64.efi ${DIRECTORY}/boot/efi/EFI/progress-linux/ cat > ${DIRECTORY}/boot/efi/EFI/progress-linux/grub.cfg << EOF search.fs_uuid ${ROOT_UUID} root set prefix=(\$root)'/boot/grub' configfile \$prefix/grub.cfg EOF cat >> ${DIRECTORY}/etc/initramfs-tools/modules << EOF # enable virtio virtio_pci virtio_blk virtio_net EOF Chroot ${DIRECTORY} update-initramfs -u mount -o bind /dev ${DIRECTORY}/dev Chroot ${DIRECTORY} update-grub # FIXME workaround for grub not being installed as EFI/boot/bootx86.efi mkdir "${DIRECTORY}/boot/efi/EFI/boot" echo 'fs0:\EFI\progress-linux\grubx64.efi' > "${DIRECTORY}/boot/efi/EFI/boot/startup.nsh" sed -i \ -e "s|@BOOT_METHOD@|${BOOT_METHOD}|g" \ -e "s|@CPU@|${CPU}|g" \ -e "s|@MEMORY@|${MEMORY}|g" \ -e "s|@DISPLAY@|${DISPLAY}|g" \ -e "s|@SERIAL@|${SERIAL}|g" \ -e "s|@MONITOR@|${MONITOR}|g" \ -e "s|@FIRMWARE@|${FIRMWARE}|g" \ -e "s|@DRIVE@|${DRIVE}|g" \ -e "s|@NETDEV@|${NETDEV}|g" \ -e "s|@QEMU_RAW@|${QEMU_RAW}|g" \ "${CONFIG}/${NAME}.conf" } Commands () { DIRECTORY="${1}" # maximum of 15 characters, prefix is 'veth-' HOSTNAME_SHORT="$(echo ${NAME} | cut -c-8)" HOST_INTERFACE_NAME="$(echo ${NETWORK1_VETH:-veth-${HOSTNAME_SHORT}-0})" sed -i -e "s|^cnt.auto=.*|cnt.auto=${CNT_AUTO}|g" \ -e "s|^cnt.container-server=.*|cnt.container-server=${CNT_CONTAINER_SERVER}|g" \ -e "s|^cnt.network-bridge=.*|cnt.network-bridge=${HOST_INTERFACE_NAME}:${NETWORK1_BRIDGE:-bridge0}|g" \ -e "s|^cnt.overlay=.*|cnt.overlay=${CNT_OVERLAY}|g" \ -e "s|^cnt.overlay-options=.*|cnt.overlay-options=${CNT_OVERLAY_OPTIONS}|g" \ -e "s|^bind=.*|bind=${BIND}|g" \ -e "s|^bind-ro=.*|bind-ro=${BIND_RO}|g" \ -e "s|^network-veth-extra=.*|network-veth-extra=${HOST_INTERFACE_NAME}:eno1|g" \ -e "s|@PRIMARY_TAP@|${HOST_INTERFACE_NAME}|g" \ "${CONFIG}/${NAME}.conf" if [ "${NETWORK_NUMBER}" -ge 2 ] then for NUMBER in $(seq 2 ${NETWORK_NUMBER}) do eval IPV4_METHOD="$`echo NETWORK${NUMBER}_IPV4_METHOD`" eval IPV6_METHOD="$`echo NETWORK${NUMBER}_IPV6_METHOD`" if [ -z "${IPV4_METHOD}" ] && [ -z "${IPV6_METHOD}" ] then continue fi eval HOST_INTERFACE_NAME="$`echo NETWORK${NUMBER}_VETH`" HOST_INTERFACE_NAME="$(echo ${HOST_INTERFACE_NAME:-veth-${HOSTNAME_SHORT}-${NUMBER}})" CONTAINER_INTERFACE_NAME="eno${NUMBER}" sed -i -e "/^register=.*/ a network-veth-extra=${HOST_INTERFACE_NAME}:${CONTAINER_INTERFACE_NAME}" "${CONFIG}/${NAME}.conf" eval BRIDGE="$`echo NETWORK${NUMBER}_BRIDGE`" sed -i -e "/^register=.*/ a cnt.network-bridge=${HOST_INTERFACE_NAME}:${BRIDGE:-bridge${NUMBER}}" "${CONFIG}/${NAME}.conf" done fi # Setting root password echo root:${ROOT_PASSWORD} | chroot "${DIRECTORY}" chpasswd # Host command if [ -n "${HOST_COMMAND}" ] then echo "${HOST_COMMAND}" > "${DIRECTORY}/.host-command" cd "${DIRECTORY}" sh "${DIRECTORY}/.host-command" cd "${OLDPWD}" rm -f "${DIRECTORY}/.host-command" fi for NUMBER in $(seq 1 ${HOST_COMMAND_NUMBER}) do eval COMMAND="$`echo HOST_COMMAND${NUMBER}`" echo "${COMMAND}" > "${DIRECTORY}/.host-command" cd "${DIRECTORY}" sh "${DIRECTORY}/.host-command" cd "${OLDPWD}" rm -f "${DIRECTORY}/.host-command" done # Show root password in case its automatically set case "${ROOT_RANDOM_PASSWORD}" in true) echo "${NAME}: root password set to '${ROOT_PASSWORD}'." ;; esac } Create_vm_disk () { mkdir -p "${VMS}/${NAME}" # FIXME drive-image file size as arg or preseed DRIVE_SIZE=12000 # FIXME image name dd if=/dev/zero of="${VMS}/${NAME}/root.img" bs=1024k count=0 seek="${DRIVE_SIZE}" parted "${VMS}/${NAME}/root.img" mklabel gpt # FIXME only for root image case "${FIRMWARE}" in legacy) # FIXME number partitions and size scrip and everything parted -a optimal "${VMS}/${NAME}/root.img" mkpart BIOS-BOOT 1MB 2MB parted "${VMS}/${NAME}/root.img" set 1 bios_grub on parted -a optimal "${VMS}/${NAME}/root.img" mkpart SYSTEM 2MB 100% SYSTEM_PARTITION="p2" ;; efi) parted -a optimal "${VMS}/${NAME}/root.img" mkpart EFI fat32 1M 1000M parted "${VMS}/${NAME}/root.img" set 1 esp on parted -a optimal "${VMS}/${NAME}/root.img" mkpart SYSTEM 1000MB 100% EFI_PARTITION="p1" SYSTEM_PARTITION="p2" # efi bios cp /usr/share/qemu/OVMF.fd "${VMS}/${NAME}/firmware.fd" ;; *) echo "Error, unrecognized firware." >&2 exit 1 esac kpartx -av "${VMS}/${NAME}/root.img" # FIXME loop number hardcoded mkfs.ext4 "/dev/mapper/loop0${SYSTEM_PARTITION}" if [ ! -z "${EFI_PARTITION}" ] then mkfs.vfat -F 32 "/dev/mapper/loop0${EFI_PARTITION}" fi # FIXME swap, more discs } Mount_vm_disk () { # FIXME probably not necessary mkdir -p "${MACHINES}/${NAME}" # FIXME partition number hardcoded mount "/dev/mapper/loop0${SYSTEM_PARTITION}" "${MACHINES}/${NAME}" mkdir -p "${MACHINES}/${NAME}/boot/efi" mount "/dev/mapper/loop0${EFI_PARTITION}" "${MACHINES}/${NAME}/boot/efi" } # Trap function trap 'Umount' EXIT HUP INT QUIT TERM umask 0022 export NAME Debconf # Pre hooks for FILE in "${HOOKS}/pre-${SCRIPT}".* "${HOOKS}/${NAME}.pre-${SCRIPT}" do if [ -x "${FILE}" ] then "${FILE}" fi done # Run debconf parts for DEBCONF_SCRIPT in "/usr/share/${SOFTWARE}/build-scripts/debconf.d"/* do if [ -x "${DEBCONF_SCRIPT}" ] then "${DEBCONF_SCRIPT}" fi done # Read-in configuration from debconf . "${DEBCONF_TMPDIR}/debconf.default" SYSTEM="${MACHINES}/${NAME}" if [ -z "${IMAGE}" ] && [ -z "${IMAGE1}" ] then INSTALLER="bootstrap" else INSTALLER="image" fi case "${BOOT_METHOD}" in qemu) Create_vm_disk Mount_vm_disk ;; esac case "${INSTALLER}" in bootstrap) ## Dependencies if [ -x /usr/bin/mmdebstrap ] then BOOTSTRAP="mmdebstrap" elif [ -x /usr/sbin/debootstrap ] then BOOTSTRAP="debootstrap" else echo "'${NAME}': /usr/bin/mmdebstrap or /usr/sbin/debootstrap - no such file." >&2 exit 1 fi ## Generic parts if [ ! -e "${CACHE}/${DISTRIBUTION}_${ARCHITECTURE}" ] then Bootstrap "${CACHE}/${DISTRIBUTION}_${ARCHITECTURE}.tmp" Configure_apt "${CACHE}/${DISTRIBUTION}_${ARCHITECTURE}.tmp" Deconfigure_system "${CACHE}/${DISTRIBUTION}_${ARCHITECTURE}.tmp" mv "${CACHE}/${DISTRIBUTION}_${ARCHITECTURE}.tmp" "${CACHE}/${DISTRIBUTION}_${ARCHITECTURE}" fi Upgrade_system "${CACHE}/${DISTRIBUTION}_${ARCHITECTURE}" || echo "W: If upgrading the system failed, try removing the cache for your distribution in /var/cache/${PROGRAM}" Cleanup_system "${CACHE}/${DISTRIBUTION}_${ARCHITECTURE}" ## Specific parts # FIXME is there a better way to do this? mkdir -p "${MACHINES}/${NAME}" cp -a "${CACHE}/${DISTRIBUTION}_${ARCHITECTURE}/"* "${MACHINES}/${NAME}/" ;; image) ## Dependencies if [ -x /usr/bin/curl ] then GET="curl" elif [ -x /usr/bin/wget ] then GET="wget" else echo "'${NAME}': /usr/bin/curl or /usr/bin/wget - no such file." >&2 exit 1 fi COMPRESSIONS="" if [ -x /usr/bin/lzip ] then COMPRESSIONS="${COMPRESSIONS} lz" fi if [ -x /usr/bin/xz ] then COMPRESSIONS="${COMPRESSIONS} xz" fi if [ -x /bin/gzip ] then COMPRESSIONS="${COMPRESSIONS} gz" fi if [ -z "${COMPRESSIONS}" ] then echo "'${NAME}': no supported compressor available (lz, xz, gz)." exit 1 fi ## Parts mkdir -p "${MACHINES}" Image "${MACHINES}/${NAME}" Configure_apt "${MACHINES}/${NAME}" Deconfigure_system "${MACHINES}/${NAME}" ;; esac Mount Configure_system "${MACHINES}/${NAME}" Configure_network "${MACHINES}/${NAME}" Configure_systemd_networkd "${MACHINES}/${NAME}" case "${BOOT_METHOD}" in qemu) echo DEBUG: Configure_vm start Configure_vm "${MACHINES}/${NAME}" echo DEBUG: Configure_vm end ;; esac Cleanup_system "${MACHINES}/${NAME}" Commands "${MACHINES}/${NAME}" # remove debconf temporary files rm --preserve-root --one-file-system -rf "${DEBCONF_TMPDIR}" rmdir --ignore-fail-on-non-empty "/tmp/${SOFTWARE}" 2>&1 || true # Post hooks for FILE in "${HOOKS}/post-${SCRIPT}".* "${HOOKS}/${NAME}.post-${SCRIPT}" do if [ -x "${FILE}" ] then "${FILE}" fi done