forked from neil/lxc-templates
2163a7e4e0
Alpine started using https://git.alpinelinux.org/aports/plain/main/alpine-keys/ to publish their apk keys as of 3.15, so using the old URI will give errors about signature verification since a required key is missing Remove old key Fix #43 - let lxc config create character devices Creating lxc containers will error out when trying to create character devices with "file exists". This does not appear to be necessary for most of them since they get created when you actually start the container. Signed-off-by: 4oo4 <4oo4@users.noreply.github.com>
524 lines
14 KiB
Bash
524 lines
14 KiB
Bash
#!/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
|
|
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
|