#!/bin/sh # vim: set ts=4: # Exit on error and treat unset variables as an error. set -eu # # LXC template for Alpine Linux 3+ # # Note: Do not replace tabs with spaces, it would break heredocs! # Authors: # Jakub Jirutka # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA #=========================== Constants ============================# # Make sure the usual locations are in PATH export PATH=$PATH:/usr/sbin:/usr/bin:/sbin:/bin readonly LOCAL_STATE_DIR='@LOCALSTATEDIR@' readonly LXC_TEMPLATE_CONFIG='@LXCTEMPLATECONFIG@' readonly LXC_CACHE_DIR="${LXC_CACHE_PATH:-"$LOCAL_STATE_DIR/cache/lxc"}/alpine" # SHA256 checksums of GPG keys for APK. readonly APK_KEYS_SHA256="\ 9c102bcc376af1498d549b77bdbfa815ae86faa1d2d82f040e616b18ef2df2d4 alpine-devel@lists.alpinelinux.org-4a6a0840.rsa.pub ebf31683b56410ecc4c00acd9f6e2839e237a3b62b5ae7ef686705c7ba0396a9 alpine-devel@lists.alpinelinux.org-5243ef4b.rsa.pub 1bb2a846c0ea4ca9d0e7862f970863857fc33c32f5506098c636a62a726a847b alpine-devel@lists.alpinelinux.org-524d27bb.rsa.pub 12f899e55a7691225603d6fb3324940fc51cd7f133e7ead788663c2b7eecb00c alpine-devel@lists.alpinelinux.org-5261cecb.rsa.pub 73867d92083f2f8ab899a26ccda7ef63dfaa0032a938620eda605558958a8041 alpine-devel@lists.alpinelinux.org-58199dcc.rsa.pub 9a4cd858d9710963848e6d5f555325dc199d1c952b01cf6e64da2c15deedbd97 alpine-devel@lists.alpinelinux.org-58cbb476.rsa.pub 780b3ed41786772cbc7b68136546fa3f897f28a23b30c72dde6225319c44cfff alpine-devel@lists.alpinelinux.org-58e4f17d.rsa.pub 59c01c57b446633249f67c04b115dd6787f4378f183dff2bbf65406df93f176d alpine-devel@lists.alpinelinux.org-5e69ca50.rsa.pub db0b49163f07ffba64a5ca198bcf1688610b0bd1f0d8d5afeaf78559d73f2278 alpine-devel@lists.alpinelinux.org-60ac2099.rsa.pub 207e4696d3c05f7cb05966aee557307151f1f00217af4143c1bcaf33b8df733f alpine-devel@lists.alpinelinux.org-6165ee59.rsa.pub 128d34d4aec39b0daedea8163cd8dc24dff36fd3d848630ab97eeb1d3084bbb3 alpine-devel@lists.alpinelinux.org-61666e3f.rsa.pub 10877cce0a935e46ad88cb79e174a2491680508eccda08e92bf04fb9bf37fbc1 alpine-devel@lists.alpinelinux.org-616a9724.rsa.pub 4a095a9daca86da496a3cd9adcd95ee2197fdbeb84638656d469f05a4d740751 alpine-devel@lists.alpinelinux.org-616abc23.rsa.pub 0caf5662fde45616d88cfd7021b7bda269a2fcaf311e51c48945a967a609ec0b alpine-devel@lists.alpinelinux.org-616ac3bc.rsa.pub ebe717d228555aa58133c202314a451f81e71f174781fd7ff8d8970d6cfa60da alpine-devel@lists.alpinelinux.org-616adfeb.rsa.pub d11f6b21c61b4274e182eb888883a8ba8acdbf820dcc7a6d82a7d9fc2fd2836d alpine-devel@lists.alpinelinux.org-616ae350.rsa.pub 40a216cbd163f22e5f16a9e0929de7cde221b9cbae8e36aa368b1e128afe0a31 alpine-devel@lists.alpinelinux.org-616db30d.rsa.pub" readonly APK_KEYS_URI='https://git.alpinelinux.org/aports/plain/main/alpine-keys/' readonly DEFAULT_MIRROR_URL='http://dl-cdn.alpinelinux.org/alpine' : ${APK_KEYS_DIR:=/etc/apk/keys} if ! ls "$APK_KEYS_DIR"/alpine* >/dev/null 2>&1; then APK_KEYS_DIR="$LXC_CACHE_DIR/bootstrap/keys" fi readonly APK_KEYS_DIR : ${APK:=$(command -v apk || true)} if [ ! -x "$APK" ]; then APK="$LXC_CACHE_DIR/bootstrap/sbin/apk.static" fi readonly APK #======================== Helper Functions ========================# usage() { cat <<-EOF Template specific options can be passed to lxc-create after a '--' like this: lxc-create --name=NAME [lxc-create-options] -- [template-options] [PKG...] PKG Additional APK package(s) to install into the container. Template options: -a ARCH, --arch=ARCH The container architecture (e.g. x86, x86_64); defaults to the host arch. -d, --debug Run this script in a debug mode (set -x and wget w/o -q). -F, --flush-cache Remove cached files before build. -m URL --mirror=URL The Alpine mirror to use; defaults to $DEFAULT_MIRROR_URL. -r VER, --release=VER The Alpine release branch to install; default is the latest stable. Environment variables: APK The apk-tools binary to use when building rootfs. If not set or not executable and apk is not on PATH, then the script will download the latest apk-tools-static. APK_KEYS_DIR Path to directory with GPG keys for APK. If not set and /etc/apk/keys does not contain alpine keys, then the script will download the keys from ${APK_KEYS_URI}. LXC_CACHE_PATH Path to the cache directory where to store bootstrap files and APK packages. EOF } die() { local retval=$1; shift printf 'ERROR: %s\n' "$@" 1>&2 exit $retval } einfo() { printf "\n==> $1\n" } fetch() { if [ "$DEBUG" = 'yes' ]; then wget -T 10 -O - $@ else wget -T 10 -O - -q $@ fi } latest_release_branch() { local arch="$1" local branch=$(fetch "$MIRROR_URL/latest-stable/releases/$arch/latest-releases.yaml" \ | sed -En 's/^[ \t]*branch: (.*)$/\1/p' \ | head -n 1) [ -n "$branch" ] && echo "$branch" } parse_arch() { case "$1" in x86 | i[3-6]86) echo 'x86';; x86_64 | amd64) echo 'x86_64';; aarch64 | arm64) echo 'aarch64';; armv7) echo 'armv7';; arm*) echo 'armhf';; ppc64le) echo 'ppc64le';; *) return 1;; esac } run_exclusively() { local lock_name="$1" local timeout=$2 shift 2 mkdir -p "$LOCAL_STATE_DIR/lock/subsys" local retval { echo -n "Obtaining an exclusive lock..." if ! flock -x 9; then echo ' failed.' return 1 fi echo ' done' "$@"; retval=$? } 9> "$LOCAL_STATE_DIR/lock/subsys/lxc-alpine-$lock_name" return $retval } #============================ Bootstrap ===========================# bootstrap() { if [ "$FLUSH_CACHE" = 'yes' ] && [ -d "$LXC_CACHE_DIR/bootstrap" ]; then einfo 'Cleaning cached bootstrap files' rm -Rf "$LXC_CACHE_DIR/bootstrap" fi einfo 'Fetching and/or verifying APK keys' fetch_apk_keys "$APK_KEYS_DIR" if [ ! -x "$APK" ]; then einfo 'Fetching apk-tools static binary' local host_arch=$(parse_arch $(uname -m)) fetch_apk_static "$LXC_CACHE_DIR/bootstrap" "$host_arch" fi } fetch_apk_keys() { local dest="$1" local line keyname mkdir -p "$dest" cd "$dest" echo "$APK_KEYS_SHA256" | while read -r line; do keyname="${line##* }" if [ ! -s "$keyname" ]; then fetch "$APK_KEYS_URI/$keyname" > "$keyname" fi echo "$line" | sha256sum -c - done || exit 2 cd - >/dev/null } fetch_apk_static() { local dest="$1" local arch="$2" local pkg_name='apk-tools-static' mkdir -p "$dest" local pkg_ver=$(fetch "$MIRROR_URL/latest-stable/main/$arch/APKINDEX.tar.gz" \ | tar -xzO APKINDEX \ | sed -n "/P:${pkg_name}/,/^$/ s/V:\(.*\)$/\1/p") [ -n "$pkg_ver" ] || die 2 "Cannot find a version of $pkg_name in APKINDEX" fetch "$MIRROR_URL/latest-stable/main/$arch/${pkg_name}-${pkg_ver}.apk" \ | tar -xz -C "$dest" sbin/ # --extract --gzip --directory [ -s "$dest/sbin/apk.static" ] || die 2 'apk.static not found' local keyname=$(echo "$dest"/sbin/apk.static.*.pub | sed 's/.*\.SIGN\.RSA\.//') openssl dgst -sha1 \ -verify "$APK_KEYS_DIR/$keyname" \ -signature "$dest/sbin/apk.static.SIGN.RSA.$keyname" \ "$dest/sbin/apk.static" \ || die 2 'Signature verification for apk.static failed' # Note: apk doesn't return 0 for --version local out="$("$dest"/sbin/apk.static --version)" echo "$out" [ "${out%% *}" = 'apk-tools' ] || die 3 'apk.static --version failed' } #============================ Install ============================# install() { local dest="$1" local arch="$2" local branch="$3" local extra_packages="$4" local apk_cache="$LXC_CACHE_DIR/apk/$arch" if [ "$FLUSH_CACHE" = 'yes' ] && [ -d "$apk_cache" ]; then einfo "Cleaning cached APK packages for $arch" rm -Rf "$apk_cache" fi mkdir -p "$apk_cache" einfo "Installing Alpine Linux in $dest" cd "$dest" mkdir -p etc/apk ln -s "$apk_cache" etc/apk/cache local repo; for repo in main community; do echo "$MIRROR_URL/$branch/$repo" >> etc/apk/repositories done install_packages "$arch" "alpine-base $extra_packages" make_dev_nodes setup_inittab setup_hosts setup_network setup_services chroot . /bin/true \ || die 3 'Failed to execute /bin/true in chroot, the builded rootfs is broken!' rm etc/apk/cache cd - >/dev/null } install_packages() { local arch="$1" local packages="$2" $APK --arch="$arch" --root=. --keys-dir="$APK_KEYS_DIR" \ --update-cache --initdb add $packages } make_dev_nodes() { mkdir -p -m 755 dev/pts mkdir -p -m 1777 dev/shm local i; for i in $(seq 0 4); do mknod -m 620 dev/tty$i c 4 $i chown 0:5 dev/tty$i # root:tty done mknod -m 666 dev/tty c 5 0 chown 0:5 dev/tty # root:tty mknod -m 620 dev/console c 5 1 mknod -m 666 dev/ptmx c 5 2 chown 0:5 dev/ptmx # root:tty } setup_inittab() { # Remove unwanted ttys. sed -i '/^tty[5-9]\:\:.*$/d' etc/inittab cat <<-EOF >> etc/inittab # Main LXC console console ::respawn:/sbin/getty 38400 console EOF } setup_hosts() { # This runscript injects localhost entries with the current hostname # into /etc/hosts. cat <<'EOF' > etc/init.d/hosts #!/sbin/openrc-run start() { local start_tag='# begin generated' local end_tag='# end generated' local content=$( cat <<-EOF $start_tag by /etc/init.d/hosts 127.0.0.1 $(hostname).local $(hostname) localhost ::1 $(hostname).local $(hostname) localhost $end_tag EOF ) if grep -q "^${start_tag}" /etc/hosts; then # escape \n, busybox sed doesn't like them content=${content//$'\n'/\\$'\n'} sed -ni "/^${start_tag}/ { a\\${content} # read and discard next line and repeat until $end_tag or EOF :a; n; /^${end_tag}/!ba; n }; p" /etc/hosts else printf "$content" >> /etc/hosts fi } EOF chmod +x etc/init.d/hosts # Wipe it, will be generated by the above runscript. echo -n > etc/hosts } setup_network() { # Note: loopback is automatically started by LXC. cat <<-EOF > etc/network/interfaces auto eth0 iface eth0 inet dhcp hostname \$(hostname) EOF } setup_services() { local svc_name # Specify the LXC subsystem. sed -i 's/^#*rc_sys=.*/rc_sys="lxc"/' etc/rc.conf # boot runlevel for svc_name in bootmisc hosts syslog; do ln -s /etc/init.d/$svc_name etc/runlevels/boot/$svc_name done # default runlevel for svc_name in networking cron crond; do # issue 1164: alpine renamed cron to crond # Use the one that exists. if [ -e etc/init.d/$svc_name ]; then ln -s /etc/init.d/$svc_name etc/runlevels/default/$svc_name fi done } #=========================== Configure ===========================# configure_container() { local config="$1" local hostname="$2" local arch="$3" cat <<-EOF >> "$config" # Specify container architecture. lxc.arch = $arch # Set hostname. lxc.uts.name = $hostname # If something doesn't work, try to comment this out. # Dropping sys_admin disables container root from doing a lot of things # that could be bad like re-mounting lxc fstab entries rw for example, # but also disables some useful things like being able to nfs mount, and # things that are already namespaced with ns_capable() kernel checks, like # hostname(1). lxc.cap.drop = sys_admin # Comment this out if you have to debug processes by tracing. lxc.cap.drop = sys_ptrace # Comment this out if required by your applications. lxc.cap.drop = setpcap # Include common configuration. lxc.include = $LXC_TEMPLATE_CONFIG/alpine.common.conf EOF } #============================= Main ==============================# if [ "$(id -u)" != "0" ]; then die 1 "This script must be run as 'root'" fi # Parse command options. options=$(getopt -o a:dFm:n:p:r:h -l arch:,debug,flush-cache,mirror:,name:,\ path:,release:,rootfs:,help,mapped-uid:,mapped-gid: -- "$@") eval set -- "$options" # Clean variables and set defaults. arch="$(uname -m)" debug='no' flush_cache='no' mirror_url= name= path= release= rootfs= # Process command options. while [ $# -gt 0 ]; do case $1 in -a | --arch) arch=$2; shift 2 ;; -d | --debug) debug='yes'; shift 1 ;; -F | --flush-cache) flush_cache='yes'; shift 1 ;; -m | --mirror) mirror_url=$2; shift 2 ;; -n | --name) name=$2; shift 2 ;; -p | --path) path=$2; shift 2 ;; -r | --release) release=$2; shift 2 ;; --rootfs) rootfs=$2; shift 2 ;; -h | --help) usage; exit 0 ;; --) shift; break ;; --mapped-[ug]id) die 1 "This template can't be used for unprivileged containers." \ 'You may want to try the "download" template instead.' ;; *) echo "Unknown option: $1" 1>&2 usage; exit 1 ;; esac done extra_packages="$@" [ "$debug" = 'yes' ] && set -x # Set global variables. readonly DEBUG="$debug" readonly FLUSH_CACHE="$flush_cache" readonly MIRROR_URL="${mirror_url:-$DEFAULT_MIRROR_URL}" # Validate options. [ -n "$name" ] || die 1 'Missing required option --name' [ -n "$path" ] || die 1 'Missing required option --path' if [ -z "$rootfs" ] && [ -f "$path/config" ]; then rootfs="$(sed -nE 's/^lxc.rootfs.path\s*=\s*(.*)$/\1/p' "$path/config")" fi if [ -z "$rootfs" ]; then rootfs="$path/rootfs" fi arch=$(parse_arch "$arch") \ || die 1 "Unsupported architecture: $arch" if [ -z "$release" ]; then release=$(latest_release_branch "$arch") \ || die 2 'Failed to resolve Alpine last release branch' fi # Here we go! run_exclusively 'bootstrap' 10 bootstrap run_exclusively "$arch" 30 install "$rootfs" "$arch" "$release" "$extra_packages" configure_container "$path/config" "$name" "$arch" einfo "Container's rootfs and config have been created" cat <<-EOF Edit the config file $path/config to check/enable networking setup. The installed system is preconfigured for a loopback and single network interface configured via DHCP. To start the container, run "lxc-start -n $name". The root password is not set; to enter the container run "lxc-attach -n $name". EOF