#!/bin/ksh93 -e # Template script for generating ubuntu container for LXC. # Copy it to the GZ's /usr/share/lxc/templates/lxc-ubuntu-csmd to be able to # use just lxc-create -t ubuntu-csmd ... # This script consolidates and extends the existing lxc ubuntu scripts. # It tries to be as compatible as possible to the limited setup script coming # with LXC/Ubuntu#s LXC package. Incompatible differences are: # Since dumb pseudo intelligence is disliked, this script does not contain any # squid auto-detection brain damage/functions. Instead one can specify them # directly (if any) on the CLI using the -X and -x options or the env variables # http_proxy and https_proxy. Container specific, i.e. non-miniroot packages to # install can be specified using option -i instead of --packages. To flush the # cached [partial] miniroot, use -f instead of -F. If you want to debug one or # more parts (i.e. functions) of the script, use -T fname instead of -d. The # special fname 'ALL' can be used to enable debugging for all functions and thus # would be the alternative for -d. Last but not least this script avoids using # global vars and passing operands by value - usually the associative array # SOPTS is passed as reference (usually CFG) to most functions and can be used # to obtain/store script related settings. # Last but not least this template supports full plumbing of the rootfs BEFORE # installation starts (i.e. inherit datasets from the GZ, do zone internal # mounts) and of course the unplumbing on exit. For more information see $0 -h. # Copyright 2014 Jens Elkner # see $0 -h typeset -r VERSION='2.0' typeset -r FPROG="${.sh.file}" typeset -r PROG="${FPROG##*/}" export PATH="${PATH}:/usr/sbin:/usr/bin:/sbin:/bin" POSIXLY_CORRECT=1 unset SOPT DEFAULT MIRROR SECURITY_MIRROR RELNAME2NUM; typeset -A SOPT DEFAULT typeset -Ai -r RELNAME2NUM=( ['warty']=410 ['hoary']=504 ['breezy']=510 ['dapper']=606 ['edgy']=610 ['feisty']=704 ['gutsy']=710 ['hardy']=804 ['intrepid']=810 ['jaunty']=904 ['karmic']=910 ['lucid']=1004 ['maverick']=1010 ['natty']=1104 ['oneiric']=1110 ['precise']=1204 ['quantal']=1210 ['raring']=1304 ['saucy']=1310 ['trusty']=1404 ['utopic']=1410 ['vivid']=1504 ['wily']=1510 ['xenial']=1604 ['yakkety']=1610 ['zesty']=1704 ['artful']=1710 ['bionic']=1804 ['cosmic']=1810 ['dicso']=1904 ['eoan']=1910 ['focal']=2004 ['groovy']=2010 ['hirsute']=2104 ['impish']=2110 ['jammy']=2204 ) # Miniroot packages, only. Any change here requires '-f' to reflect changes! # The previous/packaged ubuntu template honors packages_template env var :( # for whatever reason. So to be compatible ... # ulogd2 is needed for proper iptables logging via -j NFLOG. If one uses just # -j LOG iptables logging becomes absolutely unreliable, i.e. some log msg # never show up in any log and log messages from the GZ ends up in a zone's # log, but one can't control into which one it goes (appears to be randomly # distrubuted over all zones). # liblockfile1 is required by sendmail, but because the debian/ubuntu package # is a nightmare, we use the original uncluttered/unbroken version from # http://pkg.cs.ovgu.de/LNF/linux/ubuntu/last/, which is linked to liblockfile. # This is also the reason, why we NOT include bsd-mailx here, because this would # suck in postfix and related crap as long as sendmail is not installed! # NOTE: Debian idiots made nfs-common depend on dmsetup ... A=( ${packages_template:-'ssh vim ksh gawk xz-utils libcap-ng-utils acl iptables ulogd2 ca-certificates autofs nfs-common lsof strace patch ed ldap-utils m4 procmail liblockfile1 -mawk -debconf-i18n -e2fsprogs -e2fslibs -isc-dhcp-client -isc-dhcp-common -ntpdate -sudo -console-setup -kmod -ubuntu-advantage-tools -dmsetup'} ) X="${A[*]}" unset A DEFAULT=( [release]='jammy' [pkgs.t]=",${X// /,}" [user]='admin' [user.pw]='ubuntu' [user.shell]='/usr/bin/tcsh' [user.gcos]='Administrator' # the real "physical" base directory for local homes. /home is usually an # automounter maintained directory - keep it empty [homebase]='/local/home' # make sure, these locales are defined within the new container. # space separated! [locales]='en_US.UTF-8 de_DE.UTF-8' # [langs]=',en' # language-packs [proxy]="${http_proxy:-none}" [proxys]="${https_proxy:-none}" [mirror.x86]='http://de.archive.ubuntu.com/ubuntu' [mirrors.x86]='http://security.ubuntu.com/ubuntu' [mirror.ports]='http://de.ports.ubuntu.com/ubuntu-ports' [mirrors.ports]='http://de.ports.ubuntu.com/ubuntu-ports' [csmd]='http://pkg.cs.ovgu.de/LNF/linux/ubuntu' [statedir]='/var' [templatecfg]='/usr/share/lxc/config' [lock.timeout]=120 # in seconds [mac.prefix]='00:16:3e' ) DEFAULT[cache]="${DEFAULT[statedir]}/share/lxc" # Yes, one may "hijack" DEFAULT[] and SOPTS[] here. For now, it's a feature ;-) [[ -r /etc/default/lxc ]] && . /etc/default/lxc # Backward compat: [[ -n ${MIRROR} ]] && DEFAULT[mirror.x86]="${MIRROR}" && unset MIRROR [[ -n ${MIRRORS} ]] && DEFAULT[mirrors.x86]="${MIRRORS}" && unset MIRRORS # to be able to distinguish this scripts messages from other output typeset -T LogObj_t=( typeset -Sh 'Color for info messages' GREEN='38;5;232;48;5;118' typeset -Sh 'Color for warning messages' BLUE='38;5;21;48;5;118' typeset -Sh 'Color for fatal messages' RED='38;5;9;48;5;118' function log { print -u2 "\E[1;$2m${ date +%T; } $1:\E[0m $3" } typeset -Sfh ' log a message' log function info { _.log "INFO" ${_.GREEN} "$*" } typeset -Sfh ' log a info message' info function warn { _.log "WARN" ${_.BLUE} "$*" } typeset -Sfh ' log a warning message' warn function fatal { _.log "FATAL" ${_.RED} "$*" } typeset -Sfh ' log a fatal error message' fatal function printMarker { typeset COLOR="$1" print -f '\E[1;%sm----------------------------------------------------------------------------\E[0m\n' "${COLOR:-${_.GREEN}}" } typeset -Sfh ' print a marker line' printMarker ) LogObj_t Log function showUsage { [[ -z $1 ]] && X='-?' || X='--man' getopts -a ${PROG} "${ print ${USAGE}; }" OPT $X } function pressKey2continue { typeset A [[ -z $1 || ${SOPT[ftrace]} =~ @$1@ ]] || return 0 print "\E[1m$1 done. \E[38;5;9mHit to continue!\E[0m" read A return 0 } # Check if given path is on a ZFS function is_on_zfs { [[ -e $1 && ${ stat -f -c '%T' "$1" ; } == 'zfs' ]] } # Get the DS associated with the given path. # arg1 .. vname - where to store the determined dataset name, which contains # the given path. Set to '' on fail. # arg2 .. vname - where to store the mountpoint of the related zfs. Set to '' # on fail. # arg3 .. canonical absolute path, e.g. /zones/${zone_name} # return 0 if the given path is the mountpoint of a mounted DS, 1 otherwise. function get_zfs { typeset -n DS=$1 typeset -n MP=$2 typeset A P="$3" DS= ; MP= [[ -d $P && ${ stat -f -c '%T' "$P" ; } == 'zfs' ]] || return 31 # we do not allow /${zname} i.e. w/o a parent (usually ${lxc.lxcpath}) A=( ${ /bin/df "$P" | tail -1 ; } ) [[ -z $A ]] && return 32 DS=$A MP=${ zfs get -H -o value mountpoint $A ; } [[ $MP == $P ]] } function try_mk_zfs { typeset PP= DS MP P="${1%%/}" X B [[ -d $P ]] && return 0 [[ ${P:0:1} != '/' ]] && Log.fatal "'$P' is not an absolute path." && \ return 2 if [[ -n ${SOPT[zfs]} ]]; then PP="${P%/}" # strip off '/rootfs' tail # need an existing path to be able to determine the fstype in use while [[ -n ${PP} && ! -e ${PP} ]]; do PP="${PP%/*}" done [[ -n ${PP} ]] && PP=${ realpath "${PP}" ; } fi if [[ -n ${PP} ]] && is_on_zfs "${PP}" ; then get_zfs DS MP "${PP}" # sanity check - should not happen, but we are paranoid a little bit ;-) [[ -z $DS || -z $MP || ${P:0:${#MP}} != ${MP} ]] && return 2 # we inherit the mountpoint, but this does not mean, that the DS name # has the same *prefix ... B=${P:${#MP}} # workaround for the buggy util-linux mount [[ -n ${POSIXLY_CORRECT} ]] && X=${POSIXLY_CORRECT} || X= unset POSIXLY_CORRECT zfs create -p "${DS}$B" X=$? POSIXLY_CORRECT=$X else mkdir -p "$P" X=$? fi pressKey2continue ${.sh.fun} return $X } # try to destroy the DS with the given mountpoint, or if not a DS, just rm -rf function try_del_zfs { typeset P="${1%%/}" DS MP X=0 [[ -d $P ]] || return 0 if [[ -n ${SOPT[zfs]} ]] && get_zfs DS MP "$P" ; then zfs destroy ${DS} || \ { X=$? ; Log.warn "failed CWD='${PWD}'"; pressKey2continue; } else rm -rf "$P" || X=$? fi pressKey2continue ${.sh.fun} return $X } function umountPkgCache { typeset X P= if [[ -n ${SOPT[cache.apt.pkg]} ]]; then P=${SOPT[cache.apt.pkg]%/*/*/*} umount -f ${SOPT[cache.apt.pkg]} && SOPT[cache.apt.pkg]= fi if [[ -n ${SOPT[cache.apt.lib]} ]]; then [[ -z $P ]] && P=${SOPT[cache.apt.pkg]%/*/*/*} umount -f ${SOPT[cache.apt.lib]} && SOPT[cache.apt.lib]= fi if [[ -n $P ]]; then typeset A # the install mounts /proc and other stuff so make it disappear as well mount | while read -A A ; do [[ ${A[2]} =~ ^$P ]] || continue [[ ${A[4]} == 'zfs' || ${A[4]} == 'xfs' ]] && continue print ${A[2]} done | sort -r | while read X ; do umount $X done fi pressKey2continue ${.sh.fun} } function cleanup { umountPkgCache try_del_zfs ${SOPT[cache.partial]} try_del_zfs ${SOPT[cache.rootfs]} Log.printMarker pressKey2continue ${.sh.fun} } function disable_svcs { typeset X="$1/usr/sbin/policy-rc.d" print '#!/bin/sh\n# return: action forbidden by policy\nexit 101' >"$X" chmod 0755 "$X" pressKey2continue ${.sh.fun} } function enable_svcs { rm -f "$1/usr/sbin/policy-rc.d" pressKey2continue ${.sh.fun} } function write_sourceslist { typeset -n CFG=$1 typeset DIR="$2" # path to the partial cache or the rootfs typeset ARCH="$3" # architecture we want to add typeset MULTI="$4" # whether to use the multi-arch syntax or not typeset REPOS='main restricted universe multiverse' REL="${CFG[release]}" [[ -z ${ARCH} ]] && ARCH="${CFG[arch]}" [[ -n ${MULTI} ]] && MULTI="[arch=${ARCH}] " if [[ -n ${CFG[proxy]} && ${CFG[proxy]} != 'none' ]]; then mkdir -p "${DIR}/etc/apt/apt.conf.d" print "Acquire::http::Proxy \"${CFG[proxy]}\" ;" \ >"${DIR}/etc/apt/apt.conf.d/70proxy" fi cat >>"${DIR}/etc/apt/sources.list"< ${TMPROOT}/etc/apt/sources.list write_sourceslist CFG ${TMPROOT}/ typeset -x LC_CTYPE=C LANGUAGE= LANG= if ! chroot ${TMPROOT} apt-get update ; then Log.fatal "Failed to update miniroot's apt cache." return 3 # JEL-TBD: hmm, really? fi Log.info "Trying to remove from miniroot '${UPKGS// / }'" H=${RELNAME2NUM[${CFG[release]}]} (( H > 2004 )) && X='--allow-remove-essential -y' || X='--force-yes -y' print '#!/bin/sh\nexport SUDO_FORCE_REMOVE=yes' \ "\nprintf 'Yes, do as I say!\n'|apt-get purge ${X} ${UPKGS}" \ >${TMPROOT}/var/tmp/pkgrm chmod 755 ${TMPROOT}/var/tmp/pkgrm chroot ${TMPROOT} /var/tmp/pkgrm && \ Log.info 'Done.' || Log.warn "Failed with exit code $?" disable_svcs ${TMPROOT} # stupid adduser bullshit mv ${TMPROOT}/usr/sbin/adduser ${TMPROOT}/usr/sbin/adduser.orig ln -s /bin/true ${TMPROOT}/usr/sbin/adduser if ! lxc-unshare -s MOUNT -- chroot ${TMPROOT} apt-get dist-upgrade -y then Log.warn 'Miniroot upgrade failed. The miniroot cache may be out' \ 'of date, in which case flushing the cache (option -f) may help.' fi mv ${TMPROOT}/usr/sbin/adduser.orig ${TMPROOT}/usr/sbin/adduser enable_svcs ${TMPROOT} umountPkgCache [[ ! -e ${TMPROOT}/var/lib/apt/extended_states ]] && \ cp -a ${PKGCACHE}/lib/* ${TMPROOT}/var/lib/apt/ if [[ -n ${SOPT[zfs]} ]] && get_zfs DS_T MP_T ${TMPROOT} && \ get_zfs DS_R MP_R ${CFG[cache.rootfs]} then zfs destroy ${DS_R} zfs rename ${DS_T} ${DS_R} else mv ${TMPROOT}/* ${CFG[cache.rootfs]}/ fi trap - EXIT SIGINT SIGTERM SIGHUP Log.info 'Miniroot is complete and ready for use.' pressKey2continue ${.sh.fun} return 0 } function check_lock { typeset -n CFG=$1 typeset LOCK="${CFG[lockdir]}/ubuntu-${CFG[release]}" typeset I K=$$ S X LC_NUMERIC=C # no flock subshell bullshit - the simple way is sufficient if [[ -e ${LOCK} ]]; then I=$(<${LOCK}) X=${ ps -hp $I ; } if [[ -z $X ]]; then rm -f ${LOCK} else Log.warn 'There is probably another zone install running.' X= while [[ -z $X ]]; do X='n' read -t 10 X?'Kill the running install? [y/N]: ' [[ -z $X ]] && X='n' # should not happen S=${X:0:1} if [[ $S == 'y' || $S == 'Y' ]]; then kill -9 $I X=${ ps -hp $I ; } [[ -z $X ]] && rm -f ${LOCK} || X= fi done fi fi (( S=${CFG[lock.timeout]}/0.2 )) for (( I=0; I < S; I++ )); do if [[ -e ${LOCK} ]]; then if (( I )); then (( I%5 == 1 )) && print -n '.' else Log.warn 'Waiting for another pkg cache using process to' \ 'finish ...' fi sleep 0.2 continue fi print $K >>${LOCK} X=$(<${LOCK}) [[ $X == $K ]] && trap "rm -f ${LOCK}" EXIT && K=0 && break done if (( K )); then Log.fatal 'Package cache is still busy - giving up.' return 1 fi (( I )) && print pressKey2continue ${.sh.fun} return 0 } # On demand drop old package cache and miniroot for the related release # and re-create them if needed. function create_miniroot { typeset -n CFG=$1 if [[ -n ${CFG[cache.apt.clear]} ]]; then Log.info 'Flushing package cache ...' try_del_zfs ${CFG[cache.apt]} && CFG[cache.apt.clear]= || \ Log.warn 'failed. Continue using remaining package cache ...' fi if [[ -n ${CFG[flush]} ]]; then typeset X= Log.info 'Flushing cached miniroot ...' try_del_zfs ${CFG[cache.partial]} || \ X+="\tzfs umount ${CFG[cache.partial]}\n" try_del_zfs ${CFG[cache.rootfs]} || \ X+="\tzfs umount ${CFG[cache.rootfs]}\n" [[ -z $X ]] && CFG[flush]= || { Log.warn 'failed. Run the following command[s] in the global zone' \ 'first and retry:\n' "$X" return 2 } fi if [[ ! -e ${CFG[cache.rootfs]}/bin/sh ]]; then if ! download_ubuntu CFG ; then Log.fatal 'Download failed.' return 2 fi fi pressKey2continue ${.sh.fun} return 0 } function plumb_zonefs { typeset -n CFG=$1 typeset RFS="${CFG[rootfs]}" E S D A P integer RES=0 try_mk_zfs ${RFS} || return $? CFG[mounts.ext]= ; CFG[mounts.int]= if [[ -n ${CFG[fstab.ext]} ]]; then while read -A E ; do [[ -z $E || ${E[0]:0:1} == '#' ]] && continue if [[ ! -d ${RFS}/${E[1]} ]]; then mkdir -p ${RFS}/${E[1]} || { RES=$? ; break; } fi # we've already checked them in read_configuration mount -o ${E[3]} "${E[0]}" "${RFS}/${E[1]}" || { RES=$? ; break; } CFG[mounts.ext]+="${RFS}/${E[1]} " done <${CFG[fstab.ext]} fi (( RES )) && { pressKey2continue ${.sh.fun}; return ${RES}; } [[ -z ${CFG[fstab.int]} ]] && return 0 print "${CFG[fstab.int]}" | while read -A E ; do [[ -z $E || ${E:0:1} == '#' ]] && continue if [[ ${E[2]} != 'tmpfs' ]]; then S="${RFS}/$E" if [[ ! -d $S ]]; then # Actually systemd would create it on zone startup anyway, if # it doesn't exists. So we do it for consistency reasons here, # and can be sure, that the zone comes up, even with sysv or # upstart based init. mkdir -p $S || { RES=$? ; break; } fi else S="$E" fi D="${RFS}/${E[1]}" if [[ ! -d $D ]]; then # Systemd would create it as well, if it doesn't exist on # startup. So for consistency reasons, we create it here, too. mkdir -p "$D" || { RES=$? ; break; } fi # we've already checked them in read_configuration mount -o ${E[3]} "$S" "$D" || { RES=$? ; break; } CFG[mounts.int]+="$D " done pressKey2continue ${.sh.fun} return ${RES} } function unplumb_zonefs { typeset -n CFG=$1 typeset A integer K=0 I if [[ -n ${CFG[mounts.int]} ]]; then A=( ${CFG[mounts.int]} ) I=${#A[@]} for (( I-=1; I >= 0; I-- )); do umount -f "${A[I]}" || (( K++ )) done fi if [[ -n ${CFG[mounts.ext]} ]]; then A=( ${CFG[mounts.ext]} ) I=${#A[@]} for (( I-=1; I >= 0; I-- )); do umount -f "${A[I]}" || (( K++ )) done fi pressKey2continue ${.sh.fun} return $K } function copy_miniroot2zone { typeset -n CFG=$1 typeset ZFS="${CFG[cfgdir]}/rootfs" RFS="${CFG[rootfs]}" \ MINIROOT="${CFG[cache.rootfs]}" \ DS_MR MP_MR DS_RF MP_RF TS Log.info "Transfering miniroot '${MINIROOT}' to new container's '${RFS}'" # Actually clone and promote the miniroot to the new rootfs makes things # complicated and isn't really worth, because history has shown, that # sooner or later all packages of an image get replaced by updates - so no # space savings but the opposite: one may run short of space on updates. # So what we can do is to use zfssend/receive for transfer. # if [[ -n ${CFG[zfs]} ]] && get_zfs DS_MR MP_MR ${MINIROOT%/*} && \ # get_zfs DS_RF MP_RF ${RFS%/*} # then # [[ ${RFS} == ${ZFS} ]] || umount ${RFS} || return 1 # zfs destroy ${ZFS} || return 1 # TS=${ date '+%Y-%m-%d_%H:%M:%S.%N' ; } # zfs snapshot ${MINIROOT}@${TS} # zfs clone ${MINIROOT}@${TS} ${ZFS} || return 1 # zfs promote ${ZFS} || return 1 # else # Because there might be already some dirs dueto ext->int mounts, a # simple mv might produce unwanted results. rsync -Ha ${MINIROOT}/ ${RFS}/ || return 1 [[ -z ${CFG[keep.miniroot]} ]] && rm -rf ${MINIROOT} # fi pressKey2continue ${.sh.fun} return 0 } function checkFstabEntry { typeset -n MSG=$1 integer INTERNAL=$2 I typeset X P E=( $3 ) F=( 'resource' 'point' ) MSG='' P="${E[0]}" [[ -z $P || ${P:0:1} == '#' ]] && return 0 # comment or empty lines are ok if (( ${#E[@]} != 6 )); then MSG='Mount entry is invalid - field count != 6' return 1 fi I=0 for P in ${E[0..1]}; do X=${P//*([-_.a-zA-Z0-9\/])} # extract invalid chars if [[ -n $X ]]; then MSG+="Mount ${F[I]} '$P' contains invalid characters '$X'.\n" elif [[ ${P:0:1} =~ [-.] ]]; then MSG+="Mount ${F[I]} '$P' starts with a dash or slash or dot " MSG+='(not allowed).\n' elif [[ $P =~ \.\. ]]; then MSG+="Mount ${F[I]} '$P' contains '..' (not allowed).\n" fi if (( I > 0 )); then if (( INTERNAL )); then [[ ${P:0:1} == '/' ]] || \ MSG+="Mount point '$P' is not absolute.\n" else # makes life much easier [[ ${P:0:1} == '/' ]] && \ MSG+="Absolut mount points not supported ($P).\n" fi fi (( I++ )) done # for now we only support 'binds' and tmpfs, only. TBD: ZFS [[ ${E[2]} != 'none' && ${E[2]} != 'tmpfs' ]] && \ MSG+="FS type '${E[2]}' is corrently not supported.\n" [[ ${E[3]} =~ ^[-a-zA-Z0-9,.=]+$ ]] || \ MSG+="Mount option '${E[3]}' contains invalid characters.\n" [[ ${E[4]} =~ ^[0-9]+$ ]] || \ MSG+="Dump field contains not a number (${E[4]}).\n" [[ ${E[5]} =~ ^[0-9]+$ ]] || \ MSG+="Pass field contains not a number (${E[5]}).\n" [[ -n ${MSG} ]] && return 2 return 0 } function read_configuration { typeset -n CFG=$1 typeset CONFIG="${CFG[cfgdir]}/config" RCFG= NETCFG= AUTOCFG= X LINE \ KEY VAL FSTAB= A NKEY='network.' NLEN=8 integer I=0 K=0 M=0 IDX=-1 # number of veth, hwaddr, macvlan entries found typeset -A ZBASES CFG[rootfs.seen]= CFG[rootfs.options.seen]= (( LXCVERS >= 30000 )) && NKEY='net.' && NLEN=4 while read LINE ; do [[ ${LINE:0:4} != 'lxc.' ]] && RCFG+="${LINE}\n" && continue # normalize, so that '=' is surrounded by exactly one space char, only X="${LINE/#~(E)([[:space:]]*)=([[:space:]]*)/ = }" if [[ ${X:4:NLEN} == ${NKEY} ]]; then LINE="$X" X=${X:4+NLEN} VAL=${X#*= } KEY=${.sh.match% = } # add to config just in case CFG[${NKEY}${KEY}]="${VAL}" (( LXCVERS >= 30000 )) && KEY=${KEY#*([0-9])\.} && IDX=${.sh.match%.} if [[ ${KEY} == 'type' && ${VAL} =~ ^(veth|macvlan)$ ]]; then [[ ${VAL} == 'veth' ]] && (( I++ )) || (( M++ )) elif [[ ${KEY} == 'hwaddr' ]]; then (( K++ )) elif [[ ${KEY} =~ ^ipv(4|6)(\.|$) ]]; then NETCFG+='# Disabled: may cause ~ 2 min startup delay!\n' NETCFG+='# Configure from inside: /etc/network/interfaces\n#' fi NETCFG+="${LINE}\n" else typeset RFSKEY='rootfs' MNTKEY='mount' integer RLEN=8 MLEN=7 if (( LXCVERS >= 30000 )); then RFSKEY+='.path' && (( RLEN+=5 )) MNTKEY+='.fstab' && (( MLEN+=6 )) fi if [[ ${X:4:RLEN} == "${RFSKEY} =" ]]; then CFG[rootfs.seen]=1 elif [[ ${X:4:16} == 'rootfs.options =' ]]; then CFG[rootfs.opts.seen]=1 elif [[ ${X:4:MLEN} == "${MNTKEY} =" ]]; then ((MLEN += 4 + 1 )) # redefine as offset lxc. + ' =' [[ -r ${X:MLEN} ]] && CFG[fstab.ext]="${X:MLEN}" continue elif [[ ${X:4:13} == 'mount.entry =' ]]; then FSTAB+="${X:18}\n" && continue fi AUTOCFG+="$X\n" fi done <${CONFIG} if (( LXCVERS >= 30000 )); then (( IDX < 0 )) && IDX=0 NKEY+="${IDX}." fi if (( K == 0 && I+M == 1 )); then # If there is exactly one veth|macvlan network entry, make sure it has # an associated hwaddr. To get something stable, we try to deduce it # from the host's IP and number of configured zones and fallback to # simple random bytes if required info's are n/a. I=0 X= if [[ -n ${CFG[mac.prefix]} ]]; then VAL=( ${CFG[mac.prefix]//:/ } ) S=${#VAL[@]} (( S > 5 )) && S=5 for (( I=0 ; I < S; I++ )); do [[ ${VAL[I]} =~ ^[0-9a-fA-F][0-9a-fA-F]?$ ]] || break X+=":${VAL[I]}" done fi if [[ -z $X ]]; then X=':00:16:3e' I=3 fi KEY=${ hostname ; } VAL=( ${ getent hosts ${KEY} ; } ) if (( ${#VAL[@]} < 2 )); then for (( ; I < 5; I++ )) ; do X+=${ printf ':%02x' ${RANDOM} ; } done else VAL=( ${VAL[0]//./ } ) for (( ; I < 5 ; I++ )); do X+=${ printf ':%02x' ${VAL[I-1]} ; } done fi ZBASES["${CFG[cfgdir]%/*}"]=1 while read -A A ; do Y="${A[7]}" [[ ${Y:0:1} == '@' && ${Y: -8} == '/command' ]] || continue Y="${Y:1:${#Y}-9}" [[ -f $Y/config ]] && ZBASES[${Y%/*}]=1 done 1 )); then [[ -z ${CFG[${NKEY}ipv4]} ]] && CFG[${NKEY}ipv4]="${VAL[0]}" I=${#VAL[@]} [[ -z ${CFG[${NKEY}hostnames]} ]] && \ CFG[${NKEY}hostnames]="${VAL[1..I]}" fi fi if [[ -z ${CFG[${NKEY}ifname]} ]]; then X=${CFG[zname]%%.*} [[ $X =~ [0-9]$ ]] && X+=_0 || X+=0 CFG[${NKEY}ifname]=$X NETCFG+="lxc.${NKEY}name = $X\n" fi # make sure all "implanted" mount entries are valid K=0 if [[ -n ${FSTAB} ]]; then I=1 print "${FSTAB}" | while read LINE ; do if ! checkFstabEntry X 1 "${LINE}" ; then Log.info "Internal mount entry $I (${LINE}) is invalid: $X" (( K++ )) fi done fi (( K )) || CFG[fstab.int]="${FSTAB}" if [[ -n ${CFG[fstab.ext]} ]]; then I=1 while read LINE ; do if ! checkFstabEntry X 0 "${LINE}" ; then Log.info "Virtual fstab mount entry $I (${LINE}) is invalid: $X" (( K++ )) fi done <${CFG[fstab.ext]} fi CFG[cfg.auto]="${AUTOCFG}" CFG[cfg.net]="${NETCFG}" pressKey2continue ${.sh.fun} return $K } function write_configuration { typeset -n CFG=$1 typeset RCFG= CONFIG="${CFG[cfgdir]}/config" FKEY= MKEY= NKEY='utsname' (( LXCVERS >= 30000 )) && FKEY='.path' && MKEY='.fstab' && NKEY='uts.name' # Create the fstab - per default empty. NOTE: Each entry with an absolute # mountpoint not containing ${lxc.rootfs.path} or ${lxc.rootfs.mount} gets # ignored! [[ -n ${CFG[fstab.ext]} ]] && \ cp "${CFG[fstab.ext]}" "${CFG[cfgdir]}/fstab" || \ touch ${CFG[cfgdir]}/fstab ## Add all the includes RCFG+='\n' RCFG+='# Common configuration\n' [[ -e ${CFG[templatecfg]}/ubuntu.common.conf ]] && \ RCFG+="lxc.include = ${CFG[templatecfg]}/ubuntu.common.conf\n" [[ -e ${CFG[templatecfg]}/ubuntu.${CFG[release]}.conf ]] && \ RCFG+="lxc.include = ${CFG[templatecfg]}/ubuntu.${CFG[release]}.conf\n" ## Add the container-specific config RCFG+='\n' RCFG+='# Container specific configuration\n' RCFG+="${CFG[cfg.auto]}" [[ -n ${CFG[rootfs.seen]} ]] || RCFG+="lxc.rootfs${FKEY} = ${CFG[rootfs]}\n" [[ -n ${CFG[rootfs.opts.seen]} ]] || RCFG+='lxc.rootfs.options = noatime\n' RCFG+="lxc.mount${MKEY} = ${CFG[cfgdir]}/fstab\n" RCFG+="lxc.${NKEY} = ${CFG[zname]%%.*}\n" RCFG+="lxc.arch = ${CFG[arch]}\n" ## Re-add the previously removed network config RCFG+='\n' RCFG+='# Network configuration\n' RCFG+="${CFG[cfg.net]}" print "${RCFG}" >${CONFIG} I=$? (( I )) && Log.fatal "Failed to write container config '${CONFIG}'" pressKey2continue ${.sh.fun} return $I } function configure_zone { typeset -n CFG=$1 typeset RFS="${CFG[rootfs]}" X Y Z LINE NKEY='network.' integer I write_configuration CFG || return 1 print ${CFG[zname]%%.*} >${RFS}/etc/hostname (( LXCVERS >= 30000 )) && NKEY='net.0.' [[ -n ${CFG[${NKEY}ifname]} ]] && X="${CFG[${NKEY}ifname]}" || X="eth0" Y="[Match]\nName=$X\n\n[Network]\n" if [[ -z ${CFG[${NKEY}ipv4]} || -z ${CFG[${NKEY}type]} ]]; then [[ -z ${CFG[${NKEY}ipv4]} ]] && CFG[${NKEY}ipv4]='127.0.1.1' Y+='DHCP=ipv4\n# OR\n#Address=a.b.c.h/24\n' else Y+="#DHCP=ipv4\n" # a subnet mask of 24 is usually wanted, even for class A or B networks Y+="Address=${CFG[${NKEY}ipv4]}/24\n" fi Y+='LinkLocalAddressing=no\nIPv6AcceptRouterAdvertisements=no\n' Y+='#Gateway=a.b.c.d\n' Y+='#Domains=mydomain.de\n' Y+='#DNS=n.m.o.p q.r.s.t\n' Log.warn 'You probably need to adjust' \ "'${RFS}/etc/systemd/network/40-${X}.network'" \ 'before the container gets started for the first time!' print -n "$Y"> "${RFS}/etc/systemd/network/40-${X}.network" Y=${ whence systemctl ; } [[ -n $Y && -d ${RFS}/etc/systemd/system ]] && \ $Y --root=${RFS} enable systemd-networkd cat >${RFS}/etc/hosts< 64000000 )); then Z='1g' elif (( I > 16000000 )); then Z='512m' elif (( I > 1000000 )); then Z='128m' else Z= fi X="${CFG[fstab.int]}" Y='/etc/apparmor.d/lxc/lxc-default' if [[ -n $X && -e $Y ]]; then while read LINE ; do [[ ${LINE} =~ mount[[:space:]]+options=\((rw,?|rbind,?).*\) ]] \ && Y='' && break done <$Y [[ -n $Y ]] && \ Log.printMarker ${Log.RED} && \ Log.warn "Make sure, that '$Y' (or whatever profile you" \ 'have set) allows rbind mounts within the zone, i.e. contains a' \ 'rule like this "mount options=(rw, rbind),". Otherwise the zone' \ 'may not come up properly (mountall might hang, network' \ 'interface(s) get not initialized, etc.)!' && \ Log.printMarker ${Log.RED} # Since apparmor is so immature, we can't simply find out, which file # to change and reload it properly on demand... Furthermore the only # way seems to reboot to get apparmor into a clean state after changes # (sorry for not wasting more time for this toy) ... #sed -e '/}/ i\\tmount options=(rw, rbind),' \ # -i /etc/apparmor.d/lxc/lxc-default fi [[ -n $Z ]] && X+="tmpfs /tmp tmpfs defaults,size=$Z\n" [[ -n $X ]] && print -n "$X" >${RFS}/etc/fstab # suppress log level output for udev sed -e 's/="err"/=0/' -i ${RFS}/etc/udev/udev.conf || true # remove jobs for consoles 5 and 6 since we only create 4 consoles in # this template rm -f ${RFS}/etc/init/tty{5,6}.conf # no "pseudo-intelligent" aka dumb guesses about locales. Either they are # set or we leave as is! [[ -n ${CFG[locales]} ]] && \ chroot ${RFS} /usr/sbin/locale-gen ${CFG[locales]} [[ -x ${RFS}/var/lib/dpkg/info/openssh-server.postinst ]] || return 0 # generate new SSH keys disable_svcs ${RFS} rm -f ${RFS}/etc/ssh/ssh_host_*key* X="${RFS}/etc/init/ssh.conf" [[ -e $X ]] && mv $X ${X}.disabled typeset -x DPKG_MAINTSCRIPT_PACKAGE=openssh DPKG_MAINTSCRIPT_NAME=postinst chroot ${RFS} /var/lib/dpkg/info/openssh-server.postinst configure [[ -e ${X}.disabled ]] && mv ${X}.disabled $X enable_svcs ${RFS} sed -e "s/root@${ hostname ; }/root@${CFG[zname]%%.*}/g" \ -i ${RFS}/etc/ssh/ssh_host_*.pub || true pressKey2continue ${.sh.fun} return 0 } function install_packages { typeset -n CFG=$1 shift typeset PKGS="$@" X='--no-install-recommends' integer braindamaged_debian_scripts=0 [[ -z ${PKGS} ]] && return 0 [[ -n ${CFG[recommended]} ]] && X= if [[ ! -d ${CFG[rootfs]}/var/cache/apt/archives \ && -d ${CFG[cache.apt]}/cache ]] then if mount --bind ${CFG[cache.apt]}/cache ${CFG[rootfs]}/var/cache/apt then CFG[cache.apt.pkg]=${CFG[rootfs]}/var/cache/apt trap umountPkgCache EXIT fi fi if [[ -n ${CFG[needupdate]} ]]; then chroot ${CFG[rootfs]} apt-get update CFG[needupdate]='' fi # WTF: openjdk-8-jre-headless && ca-certificates-java wanna have /proc # mounted for no reason. Hopefully we catch all Depandants with this regex: if [[ ${PKGS} =~ (^|\s)(openjdk-8|ant|cup|.*-(jdk|jre)|jlex|.*java.*) ]] then braindamaged_debian_scripts=1 mv ${CFG[rootfs]}/bin/mountpoint ${CFG[rootfs]}/bin/mountpoint.orig ln -s true ${CFG[rootfs]}/bin/mountpoint fi pressKey2continue ${.sh.fun} ${ ls -l ${CFG[rootfs]}/bin/mountpoint ; } # abgefucktes man update dauert ewig chroot ${CFG[rootfs]} debconf-set-selections <${RFS}/etc/apt/sources.list.d/csmd.list fi if [[ ! -f ${RFS}/etc/init/container-detect.conf ]]; then # JEL-TBD: actually lame but probably works for most users # Make sure we have a working resolv.conf [[ -e ${RESOLVCONF} ]] && mv ${RESOLVCONF} ${RESOLVCONF}.lxcbak cat /etc/resolv.conf > ${RESOLVCONF} # for lucid, if not trimming, then add the ubuntu-virt # ppa and install lxcguest if [[ ${CFG[release]} == 'lucid' ]]; then install_packages CFG 'python-software-properties' chroot ${RFS} add-apt-repository 'ppa:ubuntu-virt/ppa' CFG[needupdate]=1 fi if (( RELNAME2NUM[${CFG['release']}] < 1604 )) ; then CFG[recommended]=1 install_packages CFG lxcguest CFG[recommended]= fi # Restore old resolv.conf rm -f ${RESOLVCONF} [[ -e ${RESOLVCONF}.lxcbak ]] && mv ${RESOLVCONF}.lxcbak ${RESOLVCONF} fi # If the container isn't running a native architecture, setup multiarch X= for Y in ~(N)${RFS}/usr/bin/qemu-*-static ; do [[ -x $Y ]] && X="$Y" && break done if [[ -n $X ]]; then Y=${ chroot ${RFS} dpkg-query -W -f='${Version}' dpkg ; } X="${Y//~(E)[^.0-9].*}" # normalize to N(.M)* A=( ${X//./ } 0 0 0 ) if (( A[0] < 1 || A[1] < 16 || A[2] < 2 )); then mkdir -p ${RFS}/etc/dpkg/dpkg.cfg.d print "foreign-architecture ${HARCH}" \ >${RFS}/etc/dpkg/dpkg.cfg.d/lxc-multiarch else chroot ${RFS} dpkg --add-architecture ${HARCH} fi # Write a new sources.list containing both native and multiarch entries > ${RFS}/etc/apt/sources.list write_sourceslist CFG ${RFS} ${CFG[arch]} native [[ ${CFG[arch]} == ${HARCH} ]] || \ write_sourceslist CFG ${RFS} ${HARCH} multiarch CFG[needupdate]=1 # Finally update the lists and install host platform related stuff (( RELNAME2NUM[${CFG['release']}] < 1604 )) && X="upstart:${HARCH}" ||X= X+=" mountall:${HARCH} isc-dhcp-client:${HARCH}" if [[ -d ${RFS}/etc/iproute2 ]]; then X+=" iproute2:${HARCH}" else X+=" iproute:${HARCH}" fi install_packages CFG $X fi # Install Packages in container X="${CFG[pkgs.i]:1}" if [[ -n $X ]]; then Log.info "Installing packages: $X" install_packages CFG ${X//,/ } fi # Set initial timezone as on host X='' if [[ -f /etc/timezone ]]; then X=$( ${RFS}/etc/timezone [[ ! -e ${RFS}/etc/timezone ]] && \ Log.warn 'Timezone not configured. Adjust it manually.' || \ chroot ${RFS} dpkg-reconfigure -f noninteractive tzdata X="${CFG[pkgs.u]:1}" if [[ -n $X ]]; then Log.info "Trying to uninstall packages $X ..." chroot ${RFS} apt-get -y purge ${X//,/ } || true fi # add possibly missing groups # crazy debian/ubuntu has no default for smmsp - windows freaks ... # and thus we don't care either and enforce compliance! X=${ grep ':25:' ${RFS}/etc/group ; } integer LAST=25 if [[ -n $X && ${X:0:6} != 'smmsp:' ]]; then sort -k3,3n -t: ${RFS}/etc/group | while read Y ; do Y=${Y%:*} Y=${Y##*:} (( Y <= LAST )) && continue (( Y-1 != LAST )) && (( Y-- )) && break LAST=$Y done sed -i -e "/:25:/ s,:25:,:$Y:," ${RFS}/etc/group sed -i -r -e "/:[0-9]+:25:/ s,([0-9]):25:,\\1:${Y}:," ${RFS}/etc/passwd fi groupadd -R ${RFS} -g 25 --system smmsp X=${ grep ':x:25:' ${RFS}/etc/passwd ; } if [[ -n $X && ${X:0:6} != 'smmsp:' ]]; then LAST=25 sort -k3,3n -t: ${RFS}/etc/passwd | while read Y ; do A=( ${Y//:/ } ) Y=${A[2]} (( Y <= LAST )) && continue (( Y-1 != LAST )) && (( Y-- )) && break LAST=$Y done sed -i -e "s,:x:25:,:x:${Y}:," ${RFS}/etc/passwd fi useradd -R ${RFS} -u 25 -g 25 -d / -M -N -r \ -c 'SendMail Mail Submission Program' smmsp X="${CFG[grps]:1}" if [[ -n $X ]]; then for Y in ${X//,/ } ; do A=( ${Y//:/ } ) if [[ -z ${A[1]} ]]; then groupadd -R ${RFS} $Y continue fi G=${A[0]} A[0]='-g' (( A[1] <= 100 )) && A[2]='--system' groupadd -R ${RFS} ${A[@]} $G done fi # just make sure, sudo group exists unless sudo is not installed [[ -e ${RFS}/usr/bin/sudo ]] && groupadd -R ${RFS} --system sudo 2>/dev/null # rmdir /dev/shm for containers that have /run/shm # I'm afraid of doing rm -rf ${RFS}/dev/shm, in case it did # get bind mounted to the host's /run/shm. So try to rmdir # it, and in case that fails move it out of the way. # NOTE: This can only be removed once 12.04 goes out of support if [[ -e ${RFS}/dev/shm && ! -h ${RFS}/dev/shm ]]; then rmdir ${RFS}/dev/shm 2>/dev/null || mv ${RFS}/dev/shm ${RFS}/dev/shm.bak ln -s /run/shm ${RFS}/dev/shm fi pressKey2continue ${.sh.fun} enable_svcs ${RFS} } function finalize_user { typeset -n CFG=$1 typeset RFS=${CFG[rootfs]} A U X integer I # setup the account A=( '-s' ${CFG[user.shell]} ) if [[ -z ${CFG[bindhome]} ]]; then U="${CFG[homebase]##/}/${CFG[user]}" # it might already exist if the caller has set it up e.g. as ZFS [[ ! -d ${RFS}/$U ]] && A+=( '--create-home' ) A+=( '--home' "/$U" ) else A+=( '--home' "${CFG[bindhome]}" ) U="${CFG[bindhome]##/}" print "/$U $U none bind 0 0" >> "${CFG[cfgdir]}/fstab" fi [[ -n ${CFG[user.gcos]} ]] && A+=( '--comment' "${CFG[user.gcos]}" ) [[ -n ${CFG[user.gid]} ]] && A+=( '--gid' ${CFG[user.gid]} ) && (( I++ )) if [[ -n ${CFG[user.uid]} ]]; then A+=( '--uid' ${CFG[user.uid]} ) (( I++ )) (( ${CFG[user.uid]} < 1000 )) && A+=( '--system' ) fi [[ ! -d ${RFS}/${U%/*} ]] && mkdir -p ${RFS}/${U%/*} # we can't use -R ${RFS} because the bogus useradd wants to create user # wrt. / and not ${RFS} and thus would fail chroot ${RFS} useradd "${A[@]}" ${CFG[user]} # make sure we have a UID:GID if (( I != 2 )); then X=${ chroot ${RFS} id -u ${CFG[user]} ; } CFG[user.uid]=${X:-1000} X=${ chroot ${RFS} id -g ${CFG[user]} ; } CFG[user.gid]=${X:-1000} fi # set password explicitly, since it might be already encrypted X="${CFG[user.pw]}" Y="${X:0:1}" [[ $Y == '$' || $Y == '!' || $Y == '*' ]] && Y='-e' || Y='' print "${CFG[user]}:$X" | chpasswd -R ${RFS} $Y [[ -e ${RFS}/usr/bin/sudo ]] && chroot ${RFS} adduser ${CFG[user]} sudo # copy ssh key file but only for newly created home (avoid overwrite) X="${CFG[keyfile]}" if [[ -z ${CFG[bindhome]} && -n $X && -f $X ]]; then U+='/.ssh' mkdir -p "${RFS}/$U" cp "$X" "${RFS}/$U/authorized_keys2" chown -R ${CFG[user.uid]}:${CFG[user.gid]} "${RFS}/$U" chmod 0700 "${RFS}/$U" Log.info "Public key file '$X' copied to '/$U'" fi pressKey2continue ${.sh.fun} return 0 } function getPkgArch { typeset X="$1" # Code taken from debootstrap [[ -z $X && -x /usr/bin/dpkg ]] && \ X=${ /usr/bin/dpkg --print-architecture 2>/dev/null ; } if [[ -z $X ]]; then X=${ whence udpkg ; } [[ -n $X ]] && X=${ $X --print-architecture 2>/dev/null ; } || X='' fi [[ -z $X ]] && X=${ uname -p ; } if [[ $X =~ ^(i[3-6]86|x86|athlon|linux32)$ ]]; then X='i386' elif [[ $X =~ ^(x86_64|amd64|linux64)$ ]]; then X='amd64' elif [[ $X == 'armv7l' ]]; then X='armhf' elif [[ $X == 'aarch64' ]]; then X='arm64' elif [[ $X == 'ppc64le' ]]; then X='ppc64el' fi print -- "$X" } function normalizeArch { typeset -n X="$1" if [[ ${X:0:1} == 'i' ]]; then X='ipc32' elif [[ $Y == 'amd64' ]]; then X='ipc64' elif [[ $X == 'armhf' || $X == 'armel' ]]; then X='arm32' fi } function dumpVars { typeset -n CFG=$1 typeset A X Y integer I=0 Log.printMarker Log.info 'Defaults:' Log.printMarker Y="${!DEFAULT[@]}" if [[ -n ${ whence sort ; } ]]; then A=( ${ print "${Y// /$'\n'}" | sort ; } ) Y="${A[@]}" I=1 fi for X in $Y ; do print "$X = '${DEFAULT[$X]}'" done Log.printMarker Log.info 'Used:' Log.printMarker Y="${!CFG[@]}" if (( I )) ; then A=( ${ print "${Y// /$'\n'}" | sort ; } ) Y="${A[@]}" fi for X in $Y ; do print "$X = '${CFG[$X]}'" done pressKey2continue ${.sh.fun} } function allValid { typeset -n CFG=$1 typeset A X Y H integer L if [[ -n ${CFG[muid]} || -n ${CFG[mgid]} ]]; then # userNS unsupported Log.fatal "This template can't be used for unprivileged containers." \ '\n\tYou may want to try "--template=download" instead.' return 1 fi # normalize trace function list if [[ -n ${CFG[ftrace]} ]]; then A=( ${CFG[ftrace]} ) X="${A[@]}" CFG[ftrace]="@${X// /@}@" fi # make sure a user and distro name is set [[ -z ${CFG[user]} ]] && CFG[user]="${DEFAULT[user]}" [[ -z ${CFG[release]} ]] && CFG[release]="${DEFAULT[release]}" if [[ -z ${CFG[release]} ]]; then Log.fatal 'Strange things happen and release is not set (use -r ...)!' return 2 fi if ! [[ ${CFG[release]} =~ ^[a-z0-9]+$ ]]; then Log.fatal "Invalid release codename '${CFG[release]}'!" return 2 fi [[ -z ${CFG[csmd]} ]] && CFG[csmd]=${DEFAULT[csmd]} [[ ${CFG[csmd]} == '-' ]] && CFG[csmd]= [[ -n ${CFG[defdir]} && ! -d ${CFG[defdir]} ]] && \ Log.warn "Ignoring '${CFG[defdir]}' - not a directory." && CFG[defdir]= # check, whether to bind its home and if so, whether it is available if [[ -n ${CFG[bindhome]} ]]; then X="${ getent passwd ${CFG[user]} ; }" if [[ -z $X ]]; then Log.fatal "${CFG[user]} seems to be not a valid/local account in" \ 'the parent container!' return 3 fi Y=${X##*:} # shell X=${.sh.match%:} H=${X##*:} # home A=${.sh.match%:} if [[ -z $X ]] ; then Log.fatal "${CFG[user]} seems to not have a home directory set!" return 4 elif [[ ! -d $H ]]; then Log.fatal "${CFG[user]}'s home directory '$H' not found!" return 5 fi A=( ${A//:/ } ) CFG[bindhome]="$H" # if we use the same home, it make sense to use the same shell as well CFG[user.shell]="$Y" CFG[user.uid]="${A[2]}" CFG[user.gid]="${A[3]}" X=${ id -gn ${CFG[user]} 2>/dev/null ; } [[ -n $X ]] && CFG[grps]+=",${X}:${A[3]}" # add gname:GID CFG[user.gcos]="${A[4..${#A[@]}-1]}" fi [[ -z ${CFG[user.shell]} ]] && CFG[user.shell]="${DEFAULT[user.shell]}" # just in case someone defines it as defaults for the "ubuntu" user [[ -z ${CFG[user.uid]} && -n ${DEFAULT[user.uid]} ]] && \ CFG[user.uid]=${DEFAULT[user.uid]} [[ -z ${CFG[user.gid]} && -n ${DEFAULT[user.gid]} ]] && \ CFG[user.gid]=${DEFAULT[user.gid]} [[ -z ${CFG[user.gcos]} && -n ${DEFAULT[user.gcos]} ]] && \ CFG[user.gcos]=${DEFAULT[user.gcos]} # make sure a password is set if [[ -z ${CFG[user.pw]} ]]; then X="${ getent shadow ${CFG[user]} ; }" A=( ${X//:/ } ) if [[ -n ${A[1]} ]]; then CFG[user.pw]="${A[1]}" Log.info "Password from parent for ${CFG[user]} will be used." else CFG[user.pw]="${DEFAULT[user.pw]}" Log.info "The default password for ${CFG[user]} for will be used." fi fi # debootstrap is required to populate the rootfs X=${ whence qemu-debootstrap ; } [[ -z $X ]] && X=${ whence debootstrap ; } if [[ -z $X ]]; then Log.fatal "'debootstrap' command not found! This is required" return 6 fi CFG[bootstrap]="$X" if (( ${ id -u ; } != 0 )); then Log.fatal "This script should be run as 'root'!" return 7 fi # lxc-create always supplies a container name if ! [[ ${CFG[zname]%%.*} =~ ^[a-z][-a-z0-9]*$ ]]; then Log.fatal 'No valid container name aka cid is set (see option --name).' return 8 fi # lxc-create always supplies a config base path if [[ -z ${CFG[cfgdir]} ]]; then Log.fatal 'No config base path is set (see option --path).' return 9 fi if [[ ! -e ${CFG[cfgdir]}/config ]]; then Log.fatal "The config directory ${CFG[cfgdir]} does not contain a" \ "'config' file!" return 10 fi # lxc-create always supplies a rootfs path if [[ -z ${CFG[rootfs]} ]]; then Log.warn 'No rootfs path is set (see option --rootfs).' \ 'Trying to determine it automagically ...' H= ; L=11 (( LXCVERS >= 30000 )) && H='.path' && (( L+=5 )) while read X Y ; do [[ ${X} == "lxc.rootfs$H" || ${X:0:$L} == "lxc.rootfs${H}=" ]] || \ continue [[ ${Y:0:1} == '=' ]] && A=( ${Y:1} ) || A=( ${X:11} ) if (( ${#A[@]} != 1 )); then Log.fatal 'Sorry, you should play with other toys!' return 10 fi if [[ ! -d $A ]]; then Log.fatal "Found 'lxc.rootfs$H = $A', but this directory" \ 'does not exist!' return 11 fi CFG[rootfs]="$A" break done <${CFG[cfgdir]}/config [[ -z ${CFG[rootfs]} ]] && Log.fatal 'failed.' && return 10 fi # make sure a proper, platform compatible arch is set X=${DEFAULT[arch]} Y="${CFG[arch]}" if [[ -z $Y ]]; then CFG[arch]=$X elif [[ $X != $Y ]]; then H="$X" normalizeArch X normalizeArch Y if [[ $X == $Y ]]; then : # ok elif [[ ${X:0:1} != ${Y:0:1} ]]; then # arm|ipc|powerpc Log.fatal "${CFG[arch]} containers cannot be run on $H platforms" return 12 elif [[ ${X:3:2} == '32' && ${Y:3:2} == '64' ]] ; then Log.fatal '64bit containers cannot be run on 32bit platforms.' return 13 elif ! [[ $Y =~ ^[a-z][-0-9A-Za-z]*$ ]]; then Log.fatal "Invalid architecture name '$Y'!" return 14 fi fi # verify user script if [[ -n ${CFG[script]} && ! -x ${CFG[script]} ]]; then Log.warn "Secondary hook script '${CFG[script]}' is not executable!" return 15 fi [[ -z ${CFG[mac.prefix]} ]] && CFG[mac.prefix]="${DEFAULT[mac.prefix]}" [[ ${CFG[mac.prefix]} == {1,2}([[:xdigit:]]){0,5}(:{1,2}([[:xdigit:]])) ]] \ || { Log.fatal "Invalid MAC prefix '${CFG[mac.prefix]}'"; return 16 ; } # setup some internals [[ -z ${CFG[statedir]} ]] && CFG[statedir]=${DEFAULT[statedir]:-/var} [[ -z ${CFG[cache]} ]] && CFG[cache]="${DEFAULT[cache]:-/var/share/lxc}" CFG[cache.release]="${CFG[cache]}/${CFG[release]}" CFG[cache.rootfs]="${CFG[cache.release]}/rootfs-${CFG[arch]}" CFG[cache.partial]="${CFG[cache.release]}/partial-${CFG[arch]}" CFG[cache.apt]="${CFG[cache.release]}/pkg" CFG[lockdir]="${CFG[statedir]}/lock/subsys/lxc" CFG[zfs]=${ whence zfs ; } # We do not make stupid guesses for redundant paper weight. Either the user # tells us what he needs, or we use the default, minimal set. [[ -z ${CFG[langs]} ]] && CFG[langs]="${DEFAULT[langs]}" Y=${CFG[langs]} H='' for X in ${Y//,/ } ; do [[ ${X:0:14} == 'language-pack-' ]] && \ H+=",$X" || H+=",language-pack-$X" done CFG[langs]="$H" [[ -z ${CFG[locales]} ]] && CFG[locales]="${DEFAULT[locales]}" if [[ -z ${CFG[pkgs.t]} ]]; then X="${DEFAULT[pkgs.t]}" H=${RELNAME2NUM[${CFG[release]}]} # in focal (20.04) ksh is ksh2020 bullshit (( ${H//.} == 2004 )) && X=",ksh93${X//,ksh,/,}" CFG[pkgs.t]="$X" fi X=${CFG[user.shell]} if [[ -n $X ]] && [[ $X == 'ksh93' || ${X: -2:2} == 'sh' ]]; then # make sure that the corresponding package gets installed H= X=${ readlink -m $X ; } [[ -n $X ]] && H=${ dpkg -S $X 2>/dev/null ; } # since focal those stupid packers put tcsh into /usr/bin/ but record # the path as /bin/ (which is symlinked to /usr/bin since focal). # So dpkg would not find it. Therefore the 2nd try w/o /usr: [[ -z $H && ${X:0:4} == '/usr' ]] && H=${ dpkg -S ${X:4} 2>/dev/null ; } [[ -n $H ]] && CFG[pkgs.t]+=",${H%%:*}" fi [[ -z ${CFG[homebase]} ]] && CFG[homebase]="${DEFAULT[homebase]%%/}" [[ -z ${CFG[proxy]} ]] && CFG[proxy]="${DEFAULT[proxy]}" [[ ${CFG[proxy]} == 'none' ]] && CFG[proxy]='' [[ -z ${CFG[proxys]} ]] && CFG[proxy]="${DEFAULT[proxys]}" [[ ${CFG[proxys]} == 'none' ]] && CFG[proxys]='' [[ -z ${CFG[mirror]} ]] && CFG[mirror]="${DEFAULT[mirror]}" [[ -z ${CFG[mirrors]} ]] && CFG[mirrors]="${DEFAULT[mirrors]}" [[ -z ${CFG[templatecfg]} ]] && CFG[templatecfg]="${DEFAULT[templatecfg]}" # just make sure, that we do not need to quote and get no other trouble # because of dumb names H='' for X in user.shell cfgdir rootfs statedir cache templatecfg homebase ; do [[ ${CFG[$X]} =~ ^[_a-zA-Z0-9/][-_a-zA-Z0-9/]*$ ]] || \ { Log.fatal "Invalid character for '$X' ('${CFG[$X]}')!" ; H=1 ; } done [[ -n $H ]] && return 20 CFG[needupdate]=1 if [[ -n ${CFG[dump]} ]]; then dumpVars CFG return 999 # notify lxc-create: nothing done fi [[ -z ${CFG[lock.timeout]} ]] && CFG[lock.timeout]=120 [[ ${CFG[lock.timeout]} =~ ^[0-9]+$ && ${CFG[lock.timeout]} -gt 0 ]] || \ CFG[lock.timeout]=120 [[ ! -d ${CFG[lockdir]} ]] && { mkdir -p ${CFG[lockdir]} || return 13 ; } pressKey2continue ${.sh.fun} return 0 } function mount_zfs_cache { typeset -n CFG=$1 typeset T X D P M A B C PC="${POSIXLY_CORRECT}" typeset -i I L ERR=0 [[ -n ${CFG[zfs]} ]] || return 0 unset POSIXLY_CORRECT for X in cache cache.release cache.apt cache.partial cache.rootfs ; do P=${CFG["$X"]} # find the first mounted ancestor PP=$P while [[ -n ${PP} && ! -d ${PP} ]]; do PP="${PP%/*}" done [[ -z ${PP} ]] && continue get_zfs D M ${PP} (( $? > 1 )) && continue L=${#P} # we do not read blindly the whole zfs list, because there might be # several thousands of zfs ... zfs list -r -H -o name,mountpoint,mounted -s mountpoint $D | \ while read A B C T; do [[ $C == 'yes' ]] && continue I=${#B} if (( I == L )); then [[ $P == $B ]] || continue I=0 # same path elif (( I < L )) && [[ "$B/" == ${P:0:I+1} ]]; then I=0 # parent of the path elif (( I > L )) && [[ "$P/" == ${B:0:L+1} ]]; then I=0 # child of the path fi if (( ! I )); then zfs mount $A || { ERR=51 ; break 2 ; } fi done done [[ -n ${PC} ]] && POSIXLY_CORRECT=${PC} (( ERR )) && Log.fatal 'There is probably another zone install or' \ 'a child of it still running.' pressKey2continue ${.sh.fun} return ${ERR} } function doMain { typeset -n CFG=$1 integer RES allValid CFG || exit $? check_lock CFG || exit $? mount_zfs_cache CFG || exit $? read_configuration CFG || { RES=$? ; (( RES+=100 )) ; return ${RES} ; } create_miniroot CFG || { RES=$? ; (( RES+=200 )) ; return ${RES} ; } plumb_zonefs CFG || { RES=$? ; (( RES+=300 )) ; return ${RES} ; } copy_miniroot2zone CFG || { RES=$? ; (( RES+=400 )) ; return ${RES} ; } configure_zone CFG || { RES=$? ; (( RES+=500 )) ; return ${RES} ; } post_install CFG || { RES=$? ; (( RES+=600 )) ; return ${RES} ; } [[ -n ${SOPT[noadm]} ]] && return 0 finalize_user CFG } # version 2.1.0+ introduced config keyword changes, and 3.0.0 finally dropped # the support for the "old" keywords. Affected by this script are: # lxc.aa_profile => lxc.apparmor.profile # lxc.mount => lxc.mount.fstab # lxc.network.$prop => lxc.net.$idx.$prop # lxc.rootfs => lxc.rootfs.path # lxc.utsname => lxc.uts.name # Details: https://discuss.linuxcontainers.org/t/lxc-2-1-has-been-released/487 function getLxcVersion { typeset -n RES=$1 RES=0 typeset X X=${ lxc-info --version ; } X="${X%%+([^0-9.])*}" # Idiots added git revision in jammy typeset -ia V V=( ${X//./ } 0 0 ) integer N # we assume max. 2 digits for each version part number (( RES=${V[0]}*10000 + ${V[1]} * 100 + ${V[2]} )) } # MAIN if [[ -r /etc/lsb-release ]]; then # On ubuntu machines use per default the same OS release as in the GZ Y='' while read X ; do if [[ ${X:0:11} == 'DISTRIB_ID=' ]]; then Y="${X:11}" DEFAULT[dist.id]="$Y" elif [[ ${X:0:17} == 'DISTRIB_CODENAME=' ]]; then Y="${X:17}" DEFAULT[dist.name]="$Y" fi done