]> git.proxmox.com Git - mirror_lxc.git/blob - templates/lxc-alpine.in
templates: add support for new arch on Alpine Linux
[mirror_lxc.git] / templates / lxc-alpine.in
1 #!/bin/sh
2 # vim: set ts=4:
3
4 # Exit on error and treat unset variables as an error.
5 set -eu
6
7 #
8 # LXC template for Alpine Linux 3+
9 #
10
11 # Note: Do not replace tabs with spaces, it would break heredocs!
12
13 # Authors:
14 # Jakub Jirutka <jakub@jirutka.cz>
15
16 # This library is free software; you can redistribute it and/or
17 # modify it under the terms of the GNU Lesser General Public
18 # License as published by the Free Software Foundation; either
19 # version 2.1 of the License, or (at your option) any later version.
20 #
21 # This library is distributed in the hope that it will be useful,
22 # but WITHOUT ANY WARRANTY; without even the implied warranty of
23 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
24 # Lesser General Public License for more details.
25 #
26 # You should have received a copy of the GNU Lesser General Public
27 # License along with this library; if not, write to the Free Software
28 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
29
30
31 #=========================== Constants ============================#
32
33 # Make sure the usual locations are in PATH
34 export PATH=$PATH:/usr/sbin:/usr/bin:/sbin:/bin
35
36 readonly LOCAL_STATE_DIR='@LOCALSTATEDIR@'
37 readonly LXC_TEMPLATE_CONFIG='@LXCTEMPLATECONFIG@'
38 readonly LXC_CACHE_DIR="${LXC_CACHE_PATH:-"$LOCAL_STATE_DIR/cache/lxc"}/alpine"
39
40 # SHA256 checksums of GPG keys for APK.
41 readonly APK_KEYS_SHA256="\
42 9c102bcc376af1498d549b77bdbfa815ae86faa1d2d82f040e616b18ef2df2d4 alpine-devel@lists.alpinelinux.org-4a6a0840.rsa.pub
43 2adcf7ce224f476330b5360ca5edb92fd0bf91c92d83292ed028d7c4e26333ab alpine-devel@lists.alpinelinux.org-4d07755e.rsa.pub
44 ebf31683b56410ecc4c00acd9f6e2839e237a3b62b5ae7ef686705c7ba0396a9 alpine-devel@lists.alpinelinux.org-5243ef4b.rsa.pub
45 1bb2a846c0ea4ca9d0e7862f970863857fc33c32f5506098c636a62a726a847b alpine-devel@lists.alpinelinux.org-524d27bb.rsa.pub
46 12f899e55a7691225603d6fb3324940fc51cd7f133e7ead788663c2b7eecb00c alpine-devel@lists.alpinelinux.org-5261cecb.rsa.pub"
47
48 readonly APK_KEYS_URI='http://alpinelinux.org/keys'
49 readonly MIRRORS_LIST_URL='http://rsync.alpinelinux.org/alpine/MIRRORS.txt'
50
51 : ${APK_KEYS_DIR:=/etc/apk/keys}
52 if ! ls "$APK_KEYS_DIR"/alpine* >/dev/null 2>&1; then
53 APK_KEYS_DIR="$LXC_CACHE_DIR/bootstrap/keys"
54 fi
55 readonly APK_KEYS_DIR
56
57 : ${APK:=$(command -v apk || true)}
58 if [ ! -x "$APK" ]; then
59 APK="$LXC_CACHE_DIR/bootstrap/sbin/apk.static"
60 fi
61 readonly APK
62
63
64 #======================== Helper Functions ========================#
65
66 usage() {
67 cat <<-EOF
68 Template specific options can be passed to lxc-create after a '--' like this:
69
70 lxc-create --name=NAME [lxc-create-options] -- [template-options] [PKG...]
71
72 PKG Additional APK package(s) to install into the container.
73
74 Template options:
75 -a ARCH, --arch=ARCH The container architecture (e.g. x86, x86_64); defaults
76 to the host arch.
77 -d, --debug Run this script in a debug mode (set -x and wget w/o -q).
78 -F, --flush-cache Remove cached files before build.
79 -m URL --mirror=URL The Alpine mirror to use; defaults to random mirror.
80 -r VER, --release=VER The Alpine release branch to install; default is the
81 latest stable.
82
83 Environment variables:
84 APK The apk-tools binary to use when building rootfs. If not set
85 or not executable and apk is not on PATH, then the script
86 will download the latest apk-tools-static.
87 APK_KEYS_DIR Path to directory with GPG keys for APK. If not set and
88 /etc/apk/keys does not contain alpine keys, then the script
89 will download the keys from ${APK_KEYS_URI}.
90 LXC_CACHE_PATH Path to the cache directory where to store bootstrap files
91 and APK packages.
92 EOF
93 }
94
95 die() {
96 local retval=$1; shift
97
98 printf 'ERROR: %s\n' "$@" 1>&2
99 exit $retval
100 }
101
102 einfo() {
103 printf "\n==> $1\n"
104 }
105
106 fetch() {
107 if [ "$DEBUG" = 'yes' ]; then
108 wget -T 10 -O - $@
109 else
110 wget -T 10 -O - -q $@
111 fi
112 }
113
114 latest_release_branch() {
115 local arch="$1"
116 local branch=$(fetch "$MIRROR_URL/latest-stable/releases/$arch/latest-releases.yaml" \
117 | sed -En 's/^[ \t]*branch: (.*)$/\1/p' \
118 | head -n 1)
119 [ -n "$branch" ] && echo "$branch"
120 }
121
122 parse_arch() {
123 case "$1" in
124 x86 | i[3-6]86) echo 'x86';;
125 x86_64 | amd64) echo 'x86_64';;
126 aarch64 | arm64) echo 'aarch64';;
127 armv7) echo 'armv7';;
128 arm*) echo 'armhf';;
129 *) return 1;;
130 esac
131 }
132
133 random_mirror_url() {
134 local url=$(fetch "$MIRRORS_LIST_URL" | shuf -n 1)
135 [ -n "$url" ] && echo "$url"
136 }
137
138 run_exclusively() {
139 local lock_name="$1"
140 local timeout=$2
141 shift 2
142
143 mkdir -p "$LOCAL_STATE_DIR/lock/subsys"
144
145 local retval
146 {
147 echo -n "Obtaining an exclusive lock..."
148 if ! flock -x 9; then
149 echo ' failed.'
150 return 1
151 fi
152 echo ' done'
153
154 "$@"; retval=$?
155 } 9> "$LOCAL_STATE_DIR/lock/subsys/lxc-alpine-$lock_name"
156
157 return $retval
158 }
159
160
161 #============================ Bootstrap ===========================#
162
163 bootstrap() {
164 if [ "$FLUSH_CACHE" = 'yes' ] && [ -d "$LXC_CACHE_DIR/bootstrap" ]; then
165 einfo 'Cleaning cached bootstrap files'
166 rm -Rf "$LXC_CACHE_DIR/bootstrap"
167 fi
168
169 einfo 'Fetching and/or verifying APK keys'
170 fetch_apk_keys "$APK_KEYS_DIR"
171
172 if [ ! -x "$APK" ]; then
173 einfo 'Fetching apk-tools static binary'
174
175 local host_arch=$(parse_arch $(uname -m))
176 fetch_apk_static "$LXC_CACHE_DIR/bootstrap" "$host_arch"
177 fi
178 }
179
180 fetch_apk_keys() {
181 local dest="$1"
182 local line keyname
183
184 mkdir -p "$dest"
185 cd "$dest"
186
187 echo "$APK_KEYS_SHA256" | while read -r line; do
188 keyname="${line##* }"
189 if [ ! -f "$keyname" ]; then
190 fetch "$APK_KEYS_URI/$keyname" > "$keyname"
191 fi
192 echo "$line" | sha256sum -c -
193 done || exit 2
194
195 cd - >/dev/null
196 }
197
198 fetch_apk_static() {
199 local dest="$1"
200 local arch="$2"
201 local pkg_name='apk-tools-static'
202
203 mkdir -p "$dest"
204
205 local pkg_ver=$(fetch "$MIRROR_URL/latest-stable/main/$arch/APKINDEX.tar.gz" \
206 | tar -xzO APKINDEX \
207 | sed -n "/P:${pkg_name}/,/^$/ s/V:\(.*\)$/\1/p")
208
209 [ -n "$pkg_ver" ] || die 2 "Cannot find a version of $pkg_name in APKINDEX"
210
211 fetch "$MIRROR_URL/latest-stable/main/$arch/${pkg_name}-${pkg_ver}.apk" \
212 | tar -xz -C "$dest" sbin/ # --extract --gzip --directory
213
214 [ -f "$dest/sbin/apk.static" ] || die 2 'apk.static not found'
215
216 local keyname=$(echo "$dest"/sbin/apk.static.*.pub | sed 's/.*\.SIGN\.RSA\.//')
217 openssl dgst -sha1 \
218 -verify "$APK_KEYS_DIR/$keyname" \
219 -signature "$dest/sbin/apk.static.SIGN.RSA.$keyname" \
220 "$dest/sbin/apk.static" \
221 || die 2 'Signature verification for apk.static failed'
222
223 # Note: apk doesn't return 0 for --version
224 local out="$("$dest"/sbin/apk.static --version)"
225 echo "$out"
226
227 [ "${out%% *}" = 'apk-tools' ] || die 3 'apk.static --version failed'
228 }
229
230
231 #============================ Install ============================#
232
233 install() {
234 local dest="$1"
235 local arch="$2"
236 local branch="$3"
237 local extra_packages="$4"
238 local apk_cache="$LXC_CACHE_DIR/apk/$arch"
239 local repo_url="$MIRROR_URL/$branch/main"
240
241 if [ "$FLUSH_CACHE" = 'yes' ] && [ -d "$apk_cache" ]; then
242 einfo "Cleaning cached APK packages for $arch"
243 rm -Rf "$apk_cache"
244 fi
245 mkdir -p "$apk_cache"
246
247 einfo "Installing Alpine Linux in $dest"
248 cd "$dest"
249
250 mkdir -p etc/apk
251 ln -s "$apk_cache" etc/apk/cache
252 echo "$repo_url" > etc/apk/repositories
253
254 install_packages "$arch" alpine-base $extra_packages
255 make_dev_nodes
256 setup_inittab
257 setup_hosts
258 setup_network
259 setup_services
260
261 chroot . /bin/true \
262 || die 3 'Failed to execute /bin/true in chroot, the builded rootfs is broken!'
263
264 rm etc/apk/cache
265 cd - >/dev/null
266 }
267
268 install_packages() {
269 local arch="$1"; shift
270 local packages="$@"
271
272 $APK --arch="$arch" --root=. --keys-dir="$APK_KEYS_DIR" \
273 --update-cache --initdb add $packages
274 }
275
276 make_dev_nodes() {
277 mkdir -p -m 755 dev/pts
278 mkdir -p -m 1777 dev/shm
279
280 mknod -m 666 dev/zero c 1 5
281 mknod -m 666 dev/full c 1 7
282 mknod -m 666 dev/random c 1 8
283 mknod -m 666 dev/urandom c 1 9
284
285 local i; for i in $(seq 0 4); do
286 mknod -m 620 dev/tty$i c 4 $i
287 chown 0:5 dev/tty$i # root:tty
288 done
289
290 mknod -m 666 dev/tty c 5 0
291 chown 0:5 dev/tty # root:tty
292 mknod -m 620 dev/console c 5 1
293 mknod -m 666 dev/ptmx c 5 2
294 chown 0:5 dev/ptmx # root:tty
295 }
296
297 setup_inittab() {
298 # Remove unwanted ttys.
299 sed -i '/^tty[5-9]\:\:.*$/d' etc/inittab
300
301 cat <<-EOF >> etc/inittab
302 # Main LXC console console
303 ::respawn:/sbin/getty 38400 console
304 EOF
305 }
306
307 setup_hosts() {
308 # This runscript injects localhost entries with the current hostname
309 # into /etc/hosts.
310 cat <<'EOF' > etc/init.d/hosts
311 #!/sbin/openrc-run
312
313 start() {
314 local start_tag='# begin generated'
315 local end_tag='# end generated'
316
317 local content=$(
318 cat <<-EOF
319 $start_tag by /etc/init.d/hosts
320 127.0.0.1 $(hostname).local $(hostname) localhost
321 ::1 $(hostname).local $(hostname) localhost
322 $end_tag
323 EOF
324 )
325
326 if grep -q "^${start_tag}" /etc/hosts; then
327 # escape \n, busybox sed doesn't like them
328 content=${content//$'\n'/\\$'\n'}
329
330 sed -ni "/^${start_tag}/ {
331 a\\${content}
332 # read and discard next line and repeat until $end_tag or EOF
333 :a; n; /^${end_tag}/!ba; n
334 }; p" /etc/hosts
335 else
336 printf "$content" >> /etc/hosts
337 fi
338 }
339 EOF
340 chmod +x etc/init.d/hosts
341
342 # Wipe it, will be generated by the above runscript.
343 echo -n > etc/hosts
344 }
345
346 setup_network() {
347 # Note: loopback is automatically started by LXC.
348 cat <<-EOF > etc/network/interfaces
349 auto eth0
350 iface eth0 inet dhcp
351 hostname \$(hostname)
352 EOF
353 }
354
355 setup_services() {
356 local svc_name
357
358 # Specify the LXC subsystem.
359 sed -i 's/^#*rc_sys=.*/rc_sys="lxc"/' etc/rc.conf
360
361 # boot runlevel
362 for svc_name in bootmisc hosts syslog; do
363 ln -s /etc/init.d/$svc_name etc/runlevels/boot/$svc_name
364 done
365
366 # default runlevel
367 for svc_name in networking cron; do
368 ln -s /etc/init.d/$svc_name etc/runlevels/default/$svc_name
369 done
370 }
371
372
373 #=========================== Configure ===========================#
374
375 configure_container() {
376 local config="$1"
377 local hostname="$2"
378 local arch="$3"
379
380 cat <<-EOF >> "$config"
381
382 # Specify container architecture.
383 lxc.arch = $arch
384
385 # Set hostname.
386 lxc.utsname = $hostname
387
388 # If something doesn't work, try to comment this out.
389 # Dropping sys_admin disables container root from doing a lot of things
390 # that could be bad like re-mounting lxc fstab entries rw for example,
391 # but also disables some useful things like being able to nfs mount, and
392 # things that are already namespaced with ns_capable() kernel checks, like
393 # hostname(1).
394 lxc.cap.drop = sys_admin
395
396 # Include common configuration.
397 lxc.include = $LXC_TEMPLATE_CONFIG/alpine.common.conf
398 EOF
399 }
400
401
402 #============================= Main ==============================#
403
404 if [ "$(id -u)" != "0" ]; then
405 die 1 "This script must be run as 'root'"
406 fi
407
408 # Parse command options.
409 options=$(getopt -o a:dFm:n:p:r:h -l arch:,debug,flush-cache,mirror:,name:,\
410 path:,release:,rootfs:,help,mapped-uid:,mapped-gid: -- "$@")
411 eval set -- "$options"
412
413 # Clean variables and set defaults.
414 arch="$(uname -m)"
415 debug='no'
416 flush_cache='no'
417 mirror_url=
418 name=
419 path=
420 release=
421 rootfs=
422
423 # Process command options.
424 while [ $# -gt 0 ]; do
425 case $1 in
426 -a | --arch)
427 arch=$2; shift 2
428 ;;
429 -d | --debug)
430 debug='yes'; shift 1
431 ;;
432 -F | --flush-cache)
433 flush_cache='yes'; shift 1
434 ;;
435 -m | --mirror)
436 mirror_url=$2; shift 2
437 ;;
438 -n | --name)
439 name=$2; shift 2
440 ;;
441 -p | --path)
442 path=$2; shift 2
443 ;;
444 -r | --release)
445 release=$2; shift 2
446 ;;
447 --rootfs)
448 rootfs=$2; shift 2
449 ;;
450 -h | --help)
451 usage; exit 0
452 ;;
453 --)
454 shift; break
455 ;;
456 --mapped-[ug]id)
457 die 1 "This template can't be used for unprivileged containers." \
458 'You may want to try the "download" template instead.'
459 ;;
460 *)
461 echo "Unknown option: $1" 1>&2
462 usage; exit 1
463 ;;
464 esac
465 done
466
467 extra_packages="$@"
468
469 [ "$debug" = 'yes' ] && set -x
470
471 # Set global variables.
472 readonly DEBUG="$debug"
473 readonly FLUSH_CACHE="$flush_cache"
474 readonly MIRROR_URL="${mirror_url:-$(random_mirror_url)}"
475
476 # Validate options.
477 [ -n "$name" ] || die 1 'Missing required option --name'
478 [ -n "$path" ] || die 1 'Missing required option --path'
479
480 if [ -z "$rootfs" ] && [ -f "$path/config" ]; then
481 rootfs="$(sed -nE 's/^lxc.rootfs\s*=\s*(.*)$/\1/p' "$path/config")"
482 fi
483 if [ -z "$rootfs" ]; then
484 rootfs="$path/rootfs"
485 fi
486
487 arch=$(parse_arch "$arch") \
488 || die 1 "Unsupported architecture: $arch"
489
490 if [ -z "$release" ]; then
491 release=$(latest_release_branch "$arch") \
492 || die 2 'Failed to resolve Alpine last release branch'
493 fi
494
495 # Here we go!
496 run_exclusively 'bootstrap' 10 bootstrap
497 run_exclusively "$arch" 30 install "$rootfs" "$arch" "$release" "$extra_packages"
498 configure_container "$path/config" "$name" "$arch"
499
500 einfo "Container's rootfs and config have been created"
501 cat <<-EOF
502 Edit the config file $path/config to check/enable networking setup.
503 The installed system is preconfigured for a loopback and single network
504 interface configured via DHCP.
505
506 To start the container, run "lxc-start -n $name".
507 The root password is not set; to enter the container run "lxc-attach -n $name".
508 EOF