#!/bin/sh
+# vim: set ts=4:
-install_alpine() {
- rootfs="$1"
- mkdir -p "$rootfs"/etc/apk || return 1
- cp -r ${keys_dir:-/etc/apk/keys} "$rootfs"/etc/apk/
- if [ -n "$repository" ]; then
- echo "$repository" > "$rootfs"/etc/apk/repositories
- else
- cp /etc/apk/repositories "$rootfs"/etc/apk/repositories || return 1
- fi
- opt_arch=
- if [ -n "$apk_arch" ]; then
- opt_arch="--arch $apk_arch"
- fi
- ${APK:-apk} add -U --initdb --root $rootfs $opt_arch alpine-base
-}
-
-configure_alpine() {
- rootfs="$1"
- echo "Setting up /etc/inittab"
- cat >"$rootfs"/etc/inittab<<EOF
-::sysinit:/sbin/rc sysinit
-::wait:/sbin/rc default
-tty1:12345:respawn:/sbin/getty 38400 tty1
-::ctrlaltdel:/sbin/reboot
-::shutdown:/sbin/rc shutdown
-EOF
- # set up nameserver
- grep nameserver /etc/resolv.conf > "$rootfs/etc/resolv.conf"
+# Exit on error and treat unset variables as an error.
+set -eu
- # configure the network using the dhcp
- # note that lxc will set up lo interface
- cat <<EOF > $rootfs/etc/network/interfaces
-#auto lo
-iface lo inet loopback
+#
+# LXC template for Alpine Linux 3+
+#
-auto eth0
-iface eth0 inet dhcp
-EOF
+# Note: Do not replace tabs with spaces, it would break heredocs!
- # set the hostname
- echo $hostname > $rootfs/etc/hostname
-
- # missing device nodes
- echo "Setting up device nodes"
- mkdir -p -m 755 "$rootfs/dev/pts"
- mkdir -p -m 1777 "$rootfs/dev/shm"
- mknod -m 666 "$rootfs/dev/full" c 1 7
- mknod -m 666 "$rootfs/dev/random" c 1 8
- mknod -m 666 "$rootfs/dev/urandom" c 1 9
- mknod -m 666 "$rootfs/dev/tty0" c 4 0
- mknod -m 666 "$rootfs/dev/tty1" c 4 1
- mknod -m 666 "$rootfs/dev/tty2" c 4 2
- mknod -m 666 "$rootfs/dev/tty3" c 4 3
- mknod -m 666 "$rootfs/dev/tty4" c 4 4
-# mknod -m 600 "$rootfs/dev/initctl" p
- mknod -m 666 "$rootfs/dev/tty" c 5 0
- mknod -m 666 "$rootfs/dev/console" c 5 1
- mknod -m 666 "$rootfs/dev/ptmx" c 5 2
-
- # start services
- ln -s /etc/init.d/syslog "$rootfs"/etc/runlevels/default/syslog
-
- return 0
-}
-
-copy_configuration() {
- path=$1
- rootfs=$2
- hostname=$3
-
- grep -q "^lxc.rootfs" $path/config 2>/dev/null \
- || echo "lxc.rootfs = $rootfs" >> $path/config
- if [ -n "$lxc_arch" ]; then
- echo "lxc.arch = $lxc_arch" >> $path/config
- fi
- cat <<EOF >> $path/config
-lxc.tty = 4
-lxc.pts = 1024
-lxc.utsname = $hostname
-
-# When using LXC with apparmor, uncomment the next line to run unconfined:
-#lxc.aa_profile = unconfined
-
-# network interface
-lxc.network.name = eth0
-lxc.network.type = veth
-lxc.network.flags = up
-# enable for bridging
-#lxc.network.link = br0
-#lxc.network.ipv4 = n.n.n.n
-#lxc.network.ipv4.gateway = auto
-
-# devices
-lxc.cgroup.devices.deny = a
-# /dev/null and zero
-lxc.cgroup.devices.allow = c 1:3 rwm
-lxc.cgroup.devices.allow = c 1:5 rwm
-# consoles
-lxc.cgroup.devices.allow = c 5:1 rwm
-lxc.cgroup.devices.allow = c 5:0 rwm
-lxc.cgroup.devices.allow = c 4:0 rwm
-lxc.cgroup.devices.allow = c 4:1 rwm
-# /dev/{,u}random
-lxc.cgroup.devices.allow = c 1:9 rwm
-lxc.cgroup.devices.allow = c 1:8 rwm
-lxc.cgroup.devices.allow = c 136:* rwm
-lxc.cgroup.devices.allow = c 5:2 rwm
-# rtc
-lxc.cgroup.devices.allow = c 254:0 rwm
-
-# mounts point
-lxc.mount.entry=proc proc proc nodev,noexec,nosuid 0 0
-lxc.mount.entry=run run tmpfs nodev,noexec,nosuid,relatime,size=1m,mode=0755 0 0
-lxc.mount.entry=none dev/pts devpts gid=5,mode=620 0 0
+# Authors:
+# Jakub Jirutka <jakub@jirutka.cz>
+
+# 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
+2adcf7ce224f476330b5360ca5edb92fd0bf91c92d83292ed028d7c4e26333ab alpine-devel@lists.alpinelinux.org-4d07755e.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"
+
+readonly APK_KEYS_URI='http://alpinelinux.org/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
-EOF
- return 0
+#======================== 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() {
- echo "$@" >&2
- exit 1
+ local retval=$1; shift
+
+ printf 'ERROR: %s\n' "$@" 1>&2
+ exit $retval
}
-usage() {
- cat >&2 <<EOF
-Usage: $(basename $0) [-h|--help] [-r|--repository <url>] [-a|--arch <arch>]
- -p|--path <path> -n|--name <name>
+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
+
+ mknod -m 666 dev/zero c 1 5
+ mknod -m 666 dev/full c 1 7
+ mknod -m 666 dev/random c 1 8
+ mknod -m 666 dev/urandom c 1 9
+
+ 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
}
-usage_err() {
- usage
- exit 1
+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
}
-optarg_check() {
- if [ -z "$2" ]; then
- usage_err "option '$1' requires an argument"
- fi
+
+#=========================== 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
+
+ # Include common configuration.
+ lxc.include = $LXC_TEMPLATE_CONFIG/alpine.common.conf
+ EOF
}
-default_path=@LXCPATH@
+#============================= 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
- opt="$1"
- shift
- case "$opt" in
- -h|--help)
- usage
- exit 0
- ;;
- -n|--name)
- optarg_check $opt "$1"
- name=$1
- shift
- ;;
- -p|--path)
- optarg_check $opt "$1"
- path=$1
- shift
- ;;
- -r|--repository)
- optarg_check $opt "$1"
- repository=$1
- shift
- ;;
- -a|--arch)
- optarg_check $opt "$1"
- arch=$1
- shift
- ;;
- --)
- break;;
- --*=*)
- # split --myopt=foo=bar into --myopt foo=bar
- set -- ${opt%=*} ${opt#*=} "$@"
- ;;
- -?)
- usage_err "unknown option '$opt'"
- ;;
- -*)
- # split opts -abc into -a -b -c
- set -- $(echo "${opt#-}" | sed 's/\(.\)/ -\1/g') "$@"
- ;;
- *)
- usage
- exit 1
- ;;
- esac
+ 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="$@"
-[ -z "$name" ] && usage_err
+[ "$debug" = 'yes' ] && set -x
-if [ -z "${path}" ]; then
- path="${default_path}/${name}"
-fi
+# 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'
-rootfs=`awk -F= '$1 ~ /^lxc.rootfs/ { print $2 }' "$path/config" 2>/dev/null`
+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"
+ 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
-lxc_arch=$arch
-apk_arch=$arch
-
-case "$arch" in
- i[3-6]86)
- apk_arch=x86;;
- x86)
- lxc_arch=i686;;
- x86_64|"") ;;
- *) die "unsupported architecture: $arch";;
-esac
-
-install_alpine "$rootfs" || die "Failed to install rootfs for $name"
-configure_alpine "$rootfs" "$name" || die "Failed to configure $name"
-copy_configuration "$path" "$rootfs" "$name"
+# 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