forked from neil/lxc-templates
517 lines
13 KiB
Plaintext
517 lines
13 KiB
Plaintext
|
#!/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 <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
|
||
|
|
||
|
|
||
|
#======================== 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
|
||
|
|
||
|
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
|
||
|
}
|
||
|
|
||
|
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
|
||
|
|
||
|
# 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
|