In-house Alpine builder
This commit is contained in:
parent
ee447087f6
commit
f9cdbe5ac9
17 changed files with 315 additions and 678 deletions
|
|
@ -1,630 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
# vim: set ts=4 sw=4:
|
|
||||||
# SPDX-FileCopyrightText: © 2017 Jakub Jirutka <jakub@jirutka.cz>
|
|
||||||
# SPDX-License-Identifier: MIT
|
|
||||||
#---help---
|
|
||||||
# Usage: alpine-make-vm-image [options] [--] <image> [<script> [<script-opts...>]]
|
|
||||||
#
|
|
||||||
# This script creates a bootable Alpine Linux disk image for virtual machines.
|
|
||||||
# If running on Alpine system (detected by file /etc/alpine-release), then it
|
|
||||||
# also installs needed packages on the host system. On other systems you must
|
|
||||||
# install them yourself: qemu-img, qemu-nbd, rsync, sfdisk, and mkfs utility
|
|
||||||
# for the chosen ROOTFS. If $APK is not available on the host system, then
|
|
||||||
# static apk-tools specified by $APK_TOOLS_URI is downloaded and used.
|
|
||||||
#
|
|
||||||
# Note that it does not create any partitions by default (it's really not
|
|
||||||
# needed), filesystem is created directly on the image. If you must use some
|
|
||||||
# platform that foolishly requires partitions in images, use --partition.
|
|
||||||
#
|
|
||||||
# Arguments:
|
|
||||||
# <image> Path of disk image to use or create if not exists.
|
|
||||||
#
|
|
||||||
# <script> Path of script to execute after installing base system in
|
|
||||||
# the mounted image and before umounting it.
|
|
||||||
#
|
|
||||||
# <script-opts> Arguments to pass to the script.
|
|
||||||
#
|
|
||||||
# Options and Environment Variables:
|
|
||||||
# -b --branch ALPINE_BRANCH Alpine branch to install; used only when
|
|
||||||
# --repositories-file is not specified. Default is
|
|
||||||
# latest-stable.
|
|
||||||
#
|
|
||||||
# -S --fs-skel-dir FS_SKEL_DIR Path of directory which content to recursively copy
|
|
||||||
# (using rsync) into / in the image.
|
|
||||||
#
|
|
||||||
# --fs-skel-chown FS_SKEL_CHOWN Force all files from FS_SKEL_DIR to be owned by the
|
|
||||||
# specified USER:GROUP.
|
|
||||||
#
|
|
||||||
# -P --partition (PARTITION) Create GUID Partition Table (GPT) with a single partition.
|
|
||||||
#
|
|
||||||
# -f --image-format IMAGE_FORMAT Format of the disk image (see qemu-img --help).
|
|
||||||
#
|
|
||||||
# -s --image-size IMAGE_SIZE Size of the disk image to create in bytes or with suffix
|
|
||||||
# (e.g. 1G, 1024M). Default is 2G.
|
|
||||||
#
|
|
||||||
# -i --initfs-features INITFS_FEATURES List of additional mkinitfs features (basically modules)
|
|
||||||
# to be included in initramfs (see mkinitfs -L). "base" and
|
|
||||||
# $ROOTFS is always included, don't specify them here.
|
|
||||||
# Default is "scsi virtio".
|
|
||||||
#
|
|
||||||
# -k --kernel-flavor KERNEL_FLAVOR The kernel flavour to install; virt (default), lts
|
|
||||||
# (Alpine >=3.11), or vanilla (Alpine <3.11).
|
|
||||||
#
|
|
||||||
# --keys-dir KEYS_DIR Path of directory with Alpine keys to copy into the image.
|
|
||||||
# Default is /etc/apk/keys. If does not exist, keys for
|
|
||||||
# x86_64 embedded in this script will be used.
|
|
||||||
#
|
|
||||||
# -m --mirror-uri ALPINE_MIRROR URI of the Aports mirror to fetch packages; used only
|
|
||||||
# when --repositories-file is not specified. Default is
|
|
||||||
# http://dl-cdn.alpinelinux.org/alpine.
|
|
||||||
#
|
|
||||||
# -C --no-cleanup (CLEANUP) Don't umount and disconnect image when done.
|
|
||||||
#
|
|
||||||
# -p --packages PACKAGES Additional packages to install into the image.
|
|
||||||
#
|
|
||||||
# -r --repositories-file REPOS_FILE Path of the repositories file to copy into the rootfs.
|
|
||||||
# If not provided, Alpine's main and community repositories
|
|
||||||
# on --mirror-uri will be used.
|
|
||||||
#
|
|
||||||
# --rootfs ROOTFS Filesystem to create on the image. Default is ext4.
|
|
||||||
#
|
|
||||||
# -c --script-chroot (SCRIPT_CHROOT) Bind <script>'s directory at /mnt inside image and chroot
|
|
||||||
# into the image before executing <script>.
|
|
||||||
#
|
|
||||||
# -t --serial-console (SERIAL_CONSOLE) Add configuration for a serial console on ttyS0.
|
|
||||||
#
|
|
||||||
# -h --help Show this help message and exit.
|
|
||||||
#
|
|
||||||
# -v --version Print version and exit.
|
|
||||||
#
|
|
||||||
# APK APK command to use. Default is "apk".
|
|
||||||
#
|
|
||||||
# APK_OPTS Options to pass into apk on each execution.
|
|
||||||
# Default is "--no-progress".
|
|
||||||
#
|
|
||||||
# APK_TOOLS_URI URL of apk-tools binary to download if $APK is not found
|
|
||||||
# on the host system. Default is x86_64 apk.static from
|
|
||||||
# https://gitlab.alpinelinux.org/alpine/apk-tools/-/packages.
|
|
||||||
#
|
|
||||||
# APK_TOOLS_SHA256 SHA-256 checksum of $APK_TOOLS_URI.
|
|
||||||
#
|
|
||||||
# Each option can be also provided by environment variable. If both option and
|
|
||||||
# variable is specified and the option accepts only one argument, then the
|
|
||||||
# option takes precedence.
|
|
||||||
#
|
|
||||||
# https://github.com/alpinelinux/alpine-make-vm-image
|
|
||||||
#---help---
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
# Some distros (e.g. Arch Linux) does not have /bin or /sbin in PATH.
|
|
||||||
PATH="$PATH:/usr/sbin:/usr/bin:/sbin:/bin"
|
|
||||||
|
|
||||||
readonly PROGNAME='alpine-make-vm-image'
|
|
||||||
readonly VERSION='0.11.1'
|
|
||||||
readonly VIRTUAL_PKG=".make-$PROGNAME"
|
|
||||||
# An opaque string used to detect changes in resolv.conf.
|
|
||||||
readonly RESOLVCONF_MARK="### created by $PROGNAME ###"
|
|
||||||
|
|
||||||
# Alpine APK keys for verification of packages for x86_64.
|
|
||||||
readonly ALPINE_KEYS='
|
|
||||||
alpine-devel@lists.alpinelinux.org-4a6a0840.rsa.pub:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1yHJxQgsHQREclQu4Ohe\nqxTxd1tHcNnvnQTu/UrTky8wWvgXT+jpveroeWWnzmsYlDI93eLI2ORakxb3gA2O\nQ0Ry4ws8vhaxLQGC74uQR5+/yYrLuTKydFzuPaS1dK19qJPXB8GMdmFOijnXX4SA\njixuHLe1WW7kZVtjL7nufvpXkWBGjsfrvskdNA/5MfxAeBbqPgaq0QMEfxMAn6/R\nL5kNepi/Vr4S39Xvf2DzWkTLEK8pcnjNkt9/aafhWqFVW7m3HCAII6h/qlQNQKSo\nGuH34Q8GsFG30izUENV9avY7hSLq7nggsvknlNBZtFUcmGoQrtx3FmyYsIC8/R+B\nywIDAQAB
|
|
||||||
alpine-devel@lists.alpinelinux.org-5261cecb.rsa.pub:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwlzMkl7b5PBdfMzGdCT0\ncGloRr5xGgVmsdq5EtJvFkFAiN8Ac9MCFy/vAFmS8/7ZaGOXoCDWbYVLTLOO2qtX\nyHRl+7fJVh2N6qrDDFPmdgCi8NaE+3rITWXGrrQ1spJ0B6HIzTDNEjRKnD4xyg4j\ng01FMcJTU6E+V2JBY45CKN9dWr1JDM/nei/Pf0byBJlMp/mSSfjodykmz4Oe13xB\nCa1WTwgFykKYthoLGYrmo+LKIGpMoeEbY1kuUe04UiDe47l6Oggwnl+8XD1MeRWY\nsWgj8sF4dTcSfCMavK4zHRFFQbGp/YFJ/Ww6U9lA3Vq0wyEI6MCMQnoSMFwrbgZw\nwwIDAQAB
|
|
||||||
alpine-devel@lists.alpinelinux.org-6165ee59.rsa.pub:MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAutQkua2CAig4VFSJ7v54\nALyu/J1WB3oni7qwCZD3veURw7HxpNAj9hR+S5N/pNeZgubQvJWyaPuQDm7PTs1+\ntFGiYNfAsiibX6Rv0wci3M+z2XEVAeR9Vzg6v4qoofDyoTbovn2LztaNEjTkB+oK\ntlvpNhg1zhou0jDVYFniEXvzjckxswHVb8cT0OMTKHALyLPrPOJzVtM9C1ew2Nnc\n3848xLiApMu3NBk0JqfcS3Bo5Y2b1FRVBvdt+2gFoKZix1MnZdAEZ8xQzL/a0YS5\nHd0wj5+EEKHfOd3A75uPa/WQmA+o0cBFfrzm69QDcSJSwGpzWrD1ScH3AK8nWvoj\nv7e9gukK/9yl1b4fQQ00vttwJPSgm9EnfPHLAtgXkRloI27H6/PuLoNvSAMQwuCD\nhQRlyGLPBETKkHeodfLoULjhDi1K2gKJTMhtbnUcAA7nEphkMhPWkBpgFdrH+5z4\nLxy+3ek0cqcI7K68EtrffU8jtUj9LFTUC8dERaIBs7NgQ/LfDbDfGh9g6qVj1hZl\nk9aaIPTm/xsi8v3u+0qaq7KzIBc9s59JOoA8TlpOaYdVgSQhHHLBaahOuAigH+VI\nisbC9vmqsThF2QdDtQt37keuqoda2E6sL7PUvIyVXDRfwX7uMDjlzTxHTymvq2Ck\nhtBqojBnThmjJQFgZXocHG8CAwEAAQ==
|
|
||||||
'
|
|
||||||
|
|
||||||
: ${APK_TOOLS_URI:="https://gitlab.alpinelinux.org/api/v4/projects/5/packages/generic/v2.12.10/x86_64/apk.static"}
|
|
||||||
: ${APK_TOOLS_SHA256:="d7506bb11327b337960910daffed75aa289d8bb350feab624c52965be82ceae8"}
|
|
||||||
|
|
||||||
: ${APK:="apk"}
|
|
||||||
: ${APK_OPTS:="--no-progress"}
|
|
||||||
|
|
||||||
|
|
||||||
# For compatibility with systems that does not have "realpath" command.
|
|
||||||
if ! command -v realpath >/dev/null; then
|
|
||||||
alias realpath='readlink -f'
|
|
||||||
fi
|
|
||||||
|
|
||||||
die() {
|
|
||||||
printf '\033[1;31mERROR:\033[0m %s\n' "$@" >&2 # bold red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
einfo() {
|
|
||||||
printf '\n\033[1;36m> %s\033[0m\n' "$@" >&2 # bold cyan
|
|
||||||
}
|
|
||||||
|
|
||||||
# Prints help and exists with the specified status.
|
|
||||||
help() {
|
|
||||||
sed -En '/^#---help---/,/^#---help---/p' "$0" | sed -E 's/^# ?//; 1d;$d;'
|
|
||||||
exit ${1:-0}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Cleans the host system. This function is executed before exiting the script.
|
|
||||||
cleanup() {
|
|
||||||
set +eu
|
|
||||||
trap '' EXIT HUP INT TERM # unset trap to avoid loop
|
|
||||||
|
|
||||||
cd /
|
|
||||||
if [ -d "$temp_dir" ]; then
|
|
||||||
rm -Rf "$temp_dir"
|
|
||||||
fi
|
|
||||||
if [ "$mount_dir" ]; then
|
|
||||||
umount_recursively "$mount_dir" \
|
|
||||||
|| die "Failed to unmount $mount_dir; unmount it and disconnect $nbd_dev manually"
|
|
||||||
rm -Rf "$mount_dir"
|
|
||||||
fi
|
|
||||||
if [ "$nbd_dev" ]; then
|
|
||||||
qemu-nbd --disconnect "$nbd_dev" \
|
|
||||||
|| die "Failed to disconnect $nbd_dev; disconnect it manually"
|
|
||||||
fi
|
|
||||||
if [ "$INSTALL_HOST_PKGS" = yes ]; then
|
|
||||||
_apk del $VIRTUAL_PKG
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
_apk() {
|
|
||||||
"$APK" $APK_OPTS "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Attaches the specified image as a NBD block device and prints its path.
|
|
||||||
attach_image() {
|
|
||||||
local image="$1"
|
|
||||||
local format="${2:-}"
|
|
||||||
local nbd_dev
|
|
||||||
|
|
||||||
nbd_dev=$(get_available_nbd) || {
|
|
||||||
modprobe nbd max_part=16
|
|
||||||
sleep 1
|
|
||||||
nbd_dev=$(get_available_nbd)
|
|
||||||
} || die 'No available nbd device found!'
|
|
||||||
|
|
||||||
qemu-nbd --connect="$nbd_dev" --cache=writeback \
|
|
||||||
${format:+--format=$format} "$image" \
|
|
||||||
&& echo "$nbd_dev"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Prints UUID of filesystem on the specified block device.
|
|
||||||
blk_uuid() {
|
|
||||||
local dev="$1"
|
|
||||||
blkid "$dev" | sed -En 's/.*UUID="([^"]+)".*/\1/p'
|
|
||||||
}
|
|
||||||
|
|
||||||
# Writes Alpine APK keys embedded in this script into directory $1.
|
|
||||||
dump_alpine_keys() {
|
|
||||||
local dest_dir="$1"
|
|
||||||
local content file line
|
|
||||||
|
|
||||||
mkdir -p "$dest_dir"
|
|
||||||
for line in $ALPINE_KEYS; do
|
|
||||||
file=${line%%:*}
|
|
||||||
content=${line#*:}
|
|
||||||
|
|
||||||
printf -- "-----BEGIN PUBLIC KEY-----\n$content\n-----END PUBLIC KEY-----\n" \
|
|
||||||
> "$dest_dir/$file"
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# Prints path of available nbdX device, or returns 1 if not any.
|
|
||||||
get_available_nbd() {
|
|
||||||
local dev; for dev in $(find /dev -maxdepth 2 -name 'nbd[0-9]*'); do
|
|
||||||
if [ "$(blockdev --getsize64 "$dev")" -eq 0 ]; then
|
|
||||||
echo "$dev"; return 0
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Prints name of the package needed for creating the specified filesystem.
|
|
||||||
fs_progs_pkg() {
|
|
||||||
local fs="$1" # filesystem name
|
|
||||||
|
|
||||||
case "$fs" in
|
|
||||||
ext4) echo 'e2fsprogs';;
|
|
||||||
btrfs) echo 'btrfs-progs';;
|
|
||||||
xfs) echo 'xfsprogs';;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# Tests if the specified command exists on the system.
|
|
||||||
has_cmd() {
|
|
||||||
command -v "$1" >/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
# Binds the directory $1 at the mountpoint $2 and sets propagation to private.
|
|
||||||
mount_bind() {
|
|
||||||
mkdir -p "$2"
|
|
||||||
mount --bind "$1" "$2"
|
|
||||||
mount --make-private "$2"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Prepares chroot at the specified path.
|
|
||||||
prepare_chroot() {
|
|
||||||
local dest="$1"
|
|
||||||
|
|
||||||
mkdir -p "$dest"/proc
|
|
||||||
mount -t proc none "$dest"/proc
|
|
||||||
mount_bind /dev "$dest"/dev
|
|
||||||
mount_bind /sys "$dest"/sys
|
|
||||||
|
|
||||||
install -D -m 644 /etc/resolv.conf "$dest"/etc/resolv.conf
|
|
||||||
echo "$RESOLVCONF_MARK" >> "$dest"/etc/resolv.conf
|
|
||||||
}
|
|
||||||
|
|
||||||
# Adds specified services to the runlevel. Current working directory must be
|
|
||||||
# root of the image.
|
|
||||||
rc_add() {
|
|
||||||
local runlevel="$1"; shift # runlevel name
|
|
||||||
local services="$*" # names of services
|
|
||||||
|
|
||||||
local svc; for svc in $services; do
|
|
||||||
mkdir -p etc/runlevels/$runlevel
|
|
||||||
ln -s /etc/init.d/$svc etc/runlevels/$runlevel/$svc
|
|
||||||
echo " * service $svc added to runlevel $runlevel"
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# Ensures that the specified device node exists.
|
|
||||||
settle_dev_node() {
|
|
||||||
local dev="$1"
|
|
||||||
|
|
||||||
[ -e "$dev" ] && return 0
|
|
||||||
|
|
||||||
sleep 1 # give hotplug handler some time to kick in
|
|
||||||
[ -e "$dev" ] && return 0
|
|
||||||
|
|
||||||
if has_cmd udevadm; then
|
|
||||||
udevadm settle --exit-if-exists="$dev"
|
|
||||||
elif has_cmd mdev; then
|
|
||||||
mdev -s
|
|
||||||
fi
|
|
||||||
[ -e "$dev" ] && return 0
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Installs and configures extlinux.
|
|
||||||
setup_extlinux() {
|
|
||||||
local mnt="$1" # path of directory where is root device currently mounted
|
|
||||||
local root_dev="$2" # root device
|
|
||||||
local modules="$3" # modules which should be loaded before pivot_root
|
|
||||||
local kernel_flavor="$4" # name of default kernel to boot
|
|
||||||
local serial_port="$5" # serial port number for serial console
|
|
||||||
local default_kernel="$kernel_flavor"
|
|
||||||
local kernel_opts=''
|
|
||||||
|
|
||||||
[ -z "$serial_port" ] || kernel_opts="console=$serial_port"
|
|
||||||
|
|
||||||
if [ "$kernel_flavor" = 'virt' ]; then
|
|
||||||
_apk search --root . --exact --quiet linux-lts | grep -q . \
|
|
||||||
&& default_kernel='lts' \
|
|
||||||
|| default_kernel='vanilla'
|
|
||||||
fi
|
|
||||||
|
|
||||||
sed -Ei \
|
|
||||||
-e "s|^[# ]*(root)=.*|\1=$root_dev|" \
|
|
||||||
-e "s|^[# ]*(default_kernel_opts)=.*|\1=\"$kernel_opts\"|" \
|
|
||||||
-e "s|^[# ]*(modules)=.*|\1=\"$modules\"|" \
|
|
||||||
-e "s|^[# ]*(default)=.*|\1=$default_kernel|" \
|
|
||||||
-e "s|^[# ]*(serial_port)=.*|\1=$serial_port|" \
|
|
||||||
"$mnt"/etc/update-extlinux.conf
|
|
||||||
|
|
||||||
chroot "$mnt" extlinux --install /boot
|
|
||||||
chroot "$mnt" update-extlinux --warn-only 2>&1 \
|
|
||||||
| { grep -Fv 'extlinux: cannot open device /dev' ||:; } >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
# Configures mkinitfs.
|
|
||||||
setup_mkinitfs() {
|
|
||||||
local mnt="$1" # path of directory where is root device currently mounted
|
|
||||||
local features="$2" # list of mkinitfs features
|
|
||||||
|
|
||||||
features=$(printf '%s\n' $features | sort | uniq | xargs)
|
|
||||||
|
|
||||||
sed -Ei "s|^[# ]*(features)=.*|\1=\"$features\"|" \
|
|
||||||
"$mnt"/etc/mkinitfs/mkinitfs.conf
|
|
||||||
}
|
|
||||||
|
|
||||||
# Unmounts all filesystem under the specified directory tree.
|
|
||||||
umount_recursively() {
|
|
||||||
local mount_point="$1"
|
|
||||||
test -n "$mount_point" || return 1
|
|
||||||
|
|
||||||
cat /proc/mounts \
|
|
||||||
| cut -d ' ' -f 2 \
|
|
||||||
| grep "^$mount_point" \
|
|
||||||
| sort -r \
|
|
||||||
| xargs umount -rn
|
|
||||||
}
|
|
||||||
|
|
||||||
# Downloads the specified file using wget and checks checksum.
|
|
||||||
wgets() (
|
|
||||||
local url="$1"
|
|
||||||
local sha256="$2"
|
|
||||||
local dest="${3:-.}"
|
|
||||||
|
|
||||||
cd "$dest" \
|
|
||||||
&& wget -T 10 --no-verbose "$url" \
|
|
||||||
&& echo "$sha256 ${url##*/}" | sha256sum -c
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
#============================= M a i n ==============================#
|
|
||||||
|
|
||||||
opts=$(getopt -n $PROGNAME -o b:cCf:hi:k:m:p:Pr:s:S:tv \
|
|
||||||
-l branch:,fs-skel-chown:,fs-skel-dir:,image-format:,image-size:,initfs-features:,kernel-flavor:,keys-dir:,mirror-uri:,no-cleanup,packages:,partition,repositories-file:,rootfs:,script-chroot,serial-console,help,version \
|
|
||||||
-- "$@") || help 1 >&2
|
|
||||||
|
|
||||||
eval set -- "$opts"
|
|
||||||
while [ $# -gt 0 ]; do
|
|
||||||
n=2
|
|
||||||
case "$1" in
|
|
||||||
-b | --branch) ALPINE_BRANCH="$2";;
|
|
||||||
-S | --fs-skel-dir) FS_SKEL_DIR="$(realpath "$2")";;
|
|
||||||
--fs-skel-chown) FS_SKEL_CHOWN="$2";;
|
|
||||||
-f | --image-format) IMAGE_FORMAT="$2";;
|
|
||||||
-s | --image-size) IMAGE_SIZE="$2";;
|
|
||||||
-i | --initfs-features) INITFS_FEATURES="${INITFS_FEATURES:-} $2";;
|
|
||||||
-k | --kernel-flavor) KERNEL_FLAVOR="$2";;
|
|
||||||
--keys-dir) KEYS_DIR="$(realpath "$2")";;
|
|
||||||
-m | --mirror-uri) ALPINE_MIRROR="$2";;
|
|
||||||
-C | --no-cleanup) CLEANUP='no'; n=1;;
|
|
||||||
-p | --packages) PACKAGES="${PACKAGES:-} $2";;
|
|
||||||
-P | --partition) PARTITION='yes'; n=1;;
|
|
||||||
-r | --repositories-file) REPOS_FILE="$(realpath "$2")";;
|
|
||||||
--rootfs) ROOTFS="$2";;
|
|
||||||
-t | --serial-console) SERIAL_CONSOLE='yes'; n=1;;
|
|
||||||
-c | --script-chroot) SCRIPT_CHROOT='yes'; n=1;;
|
|
||||||
-h | --help) help 0;;
|
|
||||||
-V | --version) echo "$PROGNAME $VERSION"; exit 0;;
|
|
||||||
--) shift; break;;
|
|
||||||
esac
|
|
||||||
shift $n
|
|
||||||
done
|
|
||||||
|
|
||||||
: ${ALPINE_BRANCH:="latest-stable"}
|
|
||||||
: ${ALPINE_MIRROR:="http://dl-cdn.alpinelinux.org/alpine"}
|
|
||||||
: ${CLEANUP:="yes"}
|
|
||||||
: ${FS_SKEL_CHOWN:=}
|
|
||||||
: ${FS_SKEL_DIR:=}
|
|
||||||
: ${IMAGE_FORMAT:=}
|
|
||||||
: ${IMAGE_SIZE:="2G"}
|
|
||||||
: ${INITFS_FEATURES:="scsi virtio"}
|
|
||||||
: ${KERNEL_FLAVOR:="virt"}
|
|
||||||
: ${KEYS_DIR:="/etc/apk/keys"}
|
|
||||||
: ${PACKAGES:=}
|
|
||||||
: ${PARTITION:="no"}
|
|
||||||
: ${REPOS_FILE:=}
|
|
||||||
: ${ROOTFS:="ext4"}
|
|
||||||
: ${SCRIPT_CHROOT:="no"}
|
|
||||||
: ${SERIAL_CONSOLE:="no"}
|
|
||||||
|
|
||||||
case "$ALPINE_BRANCH" in
|
|
||||||
[0-9]*) ALPINE_BRANCH="v$ALPINE_BRANCH";;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if [ -f /etc/alpine-release ]; then
|
|
||||||
: ${INSTALL_HOST_PKGS:="yes"}
|
|
||||||
else
|
|
||||||
: ${INSTALL_HOST_PKGS:="no"}
|
|
||||||
fi
|
|
||||||
|
|
||||||
SERIAL_PORT=
|
|
||||||
[ "$SERIAL_CONSOLE" = 'no' ] || SERIAL_PORT='ttyS0'
|
|
||||||
|
|
||||||
[ $# -ne 0 ] || help 1 >&2
|
|
||||||
|
|
||||||
IMAGE_FILE="$1"; shift
|
|
||||||
SCRIPT=
|
|
||||||
[ $# -eq 0 ] || { SCRIPT=$(realpath "$1"); shift; }
|
|
||||||
|
|
||||||
[ "$CLEANUP" = no ] || trap cleanup EXIT HUP INT TERM
|
|
||||||
|
|
||||||
#-----------------------------------------------------------------------
|
|
||||||
if [ "$INSTALL_HOST_PKGS" = yes ]; then
|
|
||||||
einfo 'Installing needed packages on host system'
|
|
||||||
|
|
||||||
# We need load btrfs module to avoid the error message:
|
|
||||||
# 'failed to open /dev/btrfs-control'
|
|
||||||
if ! grep -q -w "$ROOTFS" /proc/filesystems; then
|
|
||||||
modprobe $ROOTFS
|
|
||||||
fi
|
|
||||||
_apk add -t $VIRTUAL_PKG qemu-img $(fs_progs_pkg "$ROOTFS") rsync sfdisk
|
|
||||||
fi
|
|
||||||
|
|
||||||
#-----------------------------------------------------------------------
|
|
||||||
temp_dir=''
|
|
||||||
if ! command -v "$APK" >/dev/null; then
|
|
||||||
einfo "$APK not found, downloading static apk-tools"
|
|
||||||
|
|
||||||
temp_dir="$(mktemp -d /tmp/$PROGNAME.XXXXXX)"
|
|
||||||
wgets "$APK_TOOLS_URI" "$APK_TOOLS_SHA256" "$temp_dir"
|
|
||||||
APK="$temp_dir/apk.static"
|
|
||||||
chmod +x "$APK"
|
|
||||||
fi
|
|
||||||
|
|
||||||
#-----------------------------------------------------------------------
|
|
||||||
if [ ! -f "$IMAGE_FILE" ]; then
|
|
||||||
einfo "Creating $IMAGE_FORMAT image of size $IMAGE_SIZE"
|
|
||||||
qemu-img create ${IMAGE_FORMAT:+-f $IMAGE_FORMAT} "$IMAGE_FILE" "$IMAGE_SIZE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
#-----------------------------------------------------------------------
|
|
||||||
einfo "Attaching image $IMAGE_FILE as a NBD device"
|
|
||||||
|
|
||||||
nbd_dev=$(attach_image "$IMAGE_FILE" "$IMAGE_FORMAT")
|
|
||||||
|
|
||||||
|
|
||||||
#-----------------------------------------------------------------------
|
|
||||||
if [ "$PARTITION" = yes ]; then
|
|
||||||
einfo 'Creating GPT partition table'
|
|
||||||
|
|
||||||
printf '%s\n' \
|
|
||||||
'label: gpt' \
|
|
||||||
'name=system,type=L,bootable,attrs=LegacyBIOSBootable' \
|
|
||||||
| sfdisk "$nbd_dev"
|
|
||||||
root_dev="${nbd_dev}p1"
|
|
||||||
# This is needed when running in a container.
|
|
||||||
settle_dev_node "$root_dev" || die "system didn't create $root_dev node"
|
|
||||||
else
|
|
||||||
root_dev="$nbd_dev"
|
|
||||||
fi
|
|
||||||
|
|
||||||
#-----------------------------------------------------------------------
|
|
||||||
einfo "Formatting image to $ROOTFS"
|
|
||||||
|
|
||||||
# syslinux 6.0.3 cannot boot from ext4 w/ 64bit feature enabled.
|
|
||||||
# -E nodiscard / -K - do not attempt to discard blocks at mkfs time (it's
|
|
||||||
# useless for NBD image and prints confusing error).
|
|
||||||
[ "$ROOTFS" = ext4 ] && mkfs_args='-O ^64bit -E nodiscard' || mkfs_args='-K'
|
|
||||||
|
|
||||||
mkfs.$ROOTFS -L root $mkfs_args "$root_dev"
|
|
||||||
|
|
||||||
root_uuid=$(blk_uuid "$root_dev")
|
|
||||||
mount_dir=$(mktemp -d /tmp/$PROGNAME.XXXXXX)
|
|
||||||
|
|
||||||
#-----------------------------------------------------------------------
|
|
||||||
einfo "Mounting image at $mount_dir"
|
|
||||||
|
|
||||||
mount "$root_dev" "$mount_dir"
|
|
||||||
|
|
||||||
#-----------------------------------------------------------------------
|
|
||||||
einfo 'Installing base system'
|
|
||||||
|
|
||||||
cd "$mount_dir"
|
|
||||||
|
|
||||||
mkdir -p etc/apk/keys
|
|
||||||
if [ "$REPOS_FILE" ]; then
|
|
||||||
install -m 644 "$REPOS_FILE" etc/apk/repositories
|
|
||||||
else
|
|
||||||
cat > etc/apk/repositories <<-EOF
|
|
||||||
$ALPINE_MIRROR/$ALPINE_BRANCH/main
|
|
||||||
$ALPINE_MIRROR/$ALPINE_BRANCH/community
|
|
||||||
EOF
|
|
||||||
fi
|
|
||||||
if [ -d "$KEYS_DIR" ]; then
|
|
||||||
cp "$KEYS_DIR"/* etc/apk/keys/
|
|
||||||
else
|
|
||||||
dump_alpine_keys etc/apk/keys/
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Use APK cache if available.
|
|
||||||
if [ -L /etc/apk/cache ]; then
|
|
||||||
ln -s "$(realpath /etc/apk/cache)" etc/apk/cache
|
|
||||||
fi
|
|
||||||
|
|
||||||
_apk add --root . --update-cache --initdb alpine-base
|
|
||||||
prepare_chroot .
|
|
||||||
|
|
||||||
#-----------------------------------------------------------------------
|
|
||||||
einfo "Installing and configuring mkinitfs"
|
|
||||||
|
|
||||||
_apk add --root . mkinitfs
|
|
||||||
setup_mkinitfs . "base $ROOTFS $INITFS_FEATURES"
|
|
||||||
|
|
||||||
#-----------------------------------------------------------------------
|
|
||||||
einfo "Installing kernel linux-$KERNEL_FLAVOR"
|
|
||||||
|
|
||||||
if [ "$KERNEL_FLAVOR" = 'virt' ]; then
|
|
||||||
_apk add --root . linux-$KERNEL_FLAVOR
|
|
||||||
else
|
|
||||||
# Avoid installing *all* linux-firmware-* packages (see #21).
|
|
||||||
_apk add --root . linux-$KERNEL_FLAVOR linux-firmware-none
|
|
||||||
fi
|
|
||||||
|
|
||||||
#-----------------------------------------------------------------------
|
|
||||||
einfo 'Setting up extlinux bootloader'
|
|
||||||
|
|
||||||
_apk add --root . --no-scripts syslinux
|
|
||||||
setup_extlinux . "UUID=$root_uuid" "$ROOTFS" "$KERNEL_FLAVOR" "$SERIAL_PORT"
|
|
||||||
|
|
||||||
if [ "$PARTITION" = yes ]; then
|
|
||||||
dd bs=440 count=1 conv=notrunc if=usr/share/syslinux/gptmbr.bin of="$nbd_dev"
|
|
||||||
sync
|
|
||||||
fi
|
|
||||||
|
|
||||||
cat > etc/fstab <<-EOF
|
|
||||||
# <fs> <mountpoint> <type> <opts> <dump/pass>
|
|
||||||
UUID=$root_uuid / $ROOTFS noatime 0 1
|
|
||||||
EOF
|
|
||||||
|
|
||||||
#-----------------------------------------------------------------------
|
|
||||||
einfo 'Configuring system'
|
|
||||||
|
|
||||||
# We just prepare this config, but don't start the networking service.
|
|
||||||
cat > etc/network/interfaces <<-EOF
|
|
||||||
iface lo inet loopback
|
|
||||||
|
|
||||||
iface eth0 inet dhcp
|
|
||||||
|
|
||||||
post-up /etc/network/if-post-up.d/*
|
|
||||||
post-down /etc/network/if-post-down.d/*
|
|
||||||
EOF
|
|
||||||
|
|
||||||
if [ "$SERIAL_PORT" ]; then
|
|
||||||
echo "$SERIAL_PORT" >> etc/securetty
|
|
||||||
sed -Ei "s|^[# ]*($SERIAL_PORT:.*)|\1|" etc/inittab
|
|
||||||
fi
|
|
||||||
|
|
||||||
#-----------------------------------------------------------------------
|
|
||||||
einfo 'Enabling base system services'
|
|
||||||
|
|
||||||
rc_add sysinit devfs dmesg mdev hwdrivers
|
|
||||||
[ -e etc/init.d/cgroups ] && rc_add sysinit cgroups ||: # since v3.8
|
|
||||||
|
|
||||||
rc_add boot modules hwclock swap hostname sysctl bootmisc syslog
|
|
||||||
rc_add shutdown killprocs savecache mount-ro
|
|
||||||
|
|
||||||
#-----------------------------------------------------------------------
|
|
||||||
if [ "$PACKAGES" ]; then
|
|
||||||
einfo 'Installing additional packages'
|
|
||||||
_apk add --root . $PACKAGES
|
|
||||||
fi
|
|
||||||
|
|
||||||
#-----------------------------------------------------------------------
|
|
||||||
if [ -L /etc/apk/cache ]; then
|
|
||||||
rm etc/apk/cache >/dev/null 2>&1
|
|
||||||
fi
|
|
||||||
|
|
||||||
#-----------------------------------------------------------------------
|
|
||||||
if [ "$FS_SKEL_DIR" ]; then
|
|
||||||
einfo "Copying content of $FS_SKEL_DIR into image"
|
|
||||||
|
|
||||||
[ "$FS_SKEL_CHOWN" ] \
|
|
||||||
&& rsync_opts="--chown $FS_SKEL_CHOWN" \
|
|
||||||
|| rsync_opts='--numeric-ids'
|
|
||||||
rsync --archive --info=NAME2 --whole-file $rsync_opts "$FS_SKEL_DIR"/ . >&2
|
|
||||||
|
|
||||||
# rsync may modify perms of the rootfs dir itself, so make sure it's correct.
|
|
||||||
install -d -m 0755 -o root -g root .
|
|
||||||
fi
|
|
||||||
|
|
||||||
#-----------------------------------------------------------------------
|
|
||||||
if [ "$SCRIPT" ]; then
|
|
||||||
script_name="${SCRIPT##*/}"
|
|
||||||
|
|
||||||
if [ "$SCRIPT_CHROOT" = 'no' ]; then
|
|
||||||
einfo "Executing script: $script_name $*"
|
|
||||||
"$SCRIPT" "$@" || die 'Script failed'
|
|
||||||
else
|
|
||||||
einfo "Executing script in chroot: $script_name $*"
|
|
||||||
mount_bind "${SCRIPT%/*}" mnt/
|
|
||||||
chroot . /bin/sh -c "cd /mnt && ./$script_name \"\$@\"" -- "$@" \
|
|
||||||
|| die 'Script failed'
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
#-----------------------------------------------------------------------
|
|
||||||
if grep -qw "$RESOLVCONF_MARK" etc/resolv.conf 2>/dev/null; then
|
|
||||||
cat > etc/resolv.conf <<-EOF
|
|
||||||
# Default nameservers, replace them with your own.
|
|
||||||
nameserver 1.1.1.1
|
|
||||||
nameserver 2606:4700:4700::1111
|
|
||||||
EOF
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -Rf var/cache/apk/* ||:
|
|
||||||
|
|
||||||
einfo 'Completed'
|
|
||||||
|
|
||||||
cd - >/dev/null
|
|
||||||
ls -lh "$IMAGE_FILE"
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
# wget https://raw.githubusercontent.com/alpinelinux/alpine-make-vm-image/v0.11.1/alpine-make-vm-image \
|
|
||||||
# && echo '0d5d3e375cb676d6eb5c1a52109a3a0a8e4cd7ac alpine-make-vm-image' | sha1sum -c \
|
|
||||||
# || exit 1
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
sudo bash alpine-make-vm-image -t \
|
|
||||||
--image-format qcow2 \
|
|
||||||
--repositories-file img/repositories \
|
|
||||||
--packages "$(cat img/packages)" \
|
|
||||||
--script-chroot \
|
|
||||||
alpine.qcow2 -- ./img/configure.sh
|
|
||||||
|
|
||||||
sudo rmmod nbd
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
echo "PasswordAuthentication no" >> /etc/ssh/sshd_config
|
|
||||||
|
|
||||||
addgroup -g 1000 linsk
|
|
||||||
adduser -G linsk linsk -S -u 1000
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
openssh
|
|
||||||
lvm2
|
|
||||||
util-linux
|
|
||||||
cryptsetup
|
|
||||||
samba
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
http://dl-cdn.alpinelinux.org/alpine/latest-stable/main
|
|
||||||
200
cmd/imgbuilder/builder/build.go
Normal file
200
cmd/imgbuilder/builder/build.go
Normal file
|
|
@ -0,0 +1,200 @@
|
||||||
|
package builder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"github.com/AlexSSD7/linsk/vm"
|
||||||
|
"github.com/alessio/shellescape"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BuildContext struct {
|
||||||
|
logger *slog.Logger
|
||||||
|
|
||||||
|
vi *vm.VM
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBuildContext(logger *slog.Logger, baseISOPath string, outPath string, showVMDisplay bool) (*BuildContext, error) {
|
||||||
|
baseISOPath = filepath.Clean(baseISOPath)
|
||||||
|
outPath = filepath.Clean(outPath)
|
||||||
|
|
||||||
|
_, err := os.Stat(outPath)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return nil, errors.Wrap(err, "stat output file")
|
||||||
|
}
|
||||||
|
|
||||||
|
// File doesn't exist. Continuing.
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("output file already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = createQEMUImg(outPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "create temporary qemu image")
|
||||||
|
}
|
||||||
|
|
||||||
|
vi, err := vm.NewVM(logger.With("subcaller", "vm"), vm.VMConfig{
|
||||||
|
CdromImagePath: baseISOPath,
|
||||||
|
Drives: []vm.DriveConfig{{
|
||||||
|
Path: outPath,
|
||||||
|
}},
|
||||||
|
UnrestrictedNetworking: true,
|
||||||
|
ShowDisplay: showVMDisplay,
|
||||||
|
InstallBaseUtilities: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "create vm instance")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &BuildContext{
|
||||||
|
logger: logger,
|
||||||
|
|
||||||
|
vi: vi,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createQEMUImg(outPath string) error {
|
||||||
|
outPath = filepath.Clean(outPath)
|
||||||
|
|
||||||
|
err := exec.Command("qemu-img", "create", "-f", "qcow2", outPath, "1G").Run()
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "run qemu-img create cmd")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *BuildContext) BuildWithInterruptHandler() error {
|
||||||
|
defer func() {
|
||||||
|
err := bc.vi.Cancel()
|
||||||
|
if err != nil {
|
||||||
|
bc.logger.Error("Failed to cancel VM context", "error", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
runErrCh := make(chan error, 1)
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
ctx, ctxCancel := context.WithCancel(context.Background())
|
||||||
|
defer ctxCancel()
|
||||||
|
|
||||||
|
interrupt := make(chan os.Signal, 2)
|
||||||
|
signal.Notify(interrupt, syscall.SIGTERM, syscall.SIGINT)
|
||||||
|
defer signal.Reset()
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
err := bc.vi.Run()
|
||||||
|
ctxCancel()
|
||||||
|
runErrCh <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for i := 0; ; i++ {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case sig := <-interrupt:
|
||||||
|
lg := slog.With("signal", sig)
|
||||||
|
|
||||||
|
if i == 0 {
|
||||||
|
lg.Warn("Caught interrupt, safely shutting down")
|
||||||
|
} else if i < 10 {
|
||||||
|
lg.Warn("Caught subsequent interrupt, please interrupt n more times to panic", "n", 10-i)
|
||||||
|
} else {
|
||||||
|
panic("force interrupt")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := bc.vi.Cancel()
|
||||||
|
if err != nil {
|
||||||
|
lg.Warn("Failed to cancel VM context", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case err := <-runErrCh:
|
||||||
|
if err == nil {
|
||||||
|
return fmt.Errorf("operation canceled by user")
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.Wrap(err, "start vm")
|
||||||
|
case <-bc.vi.SSHUpNotifyChan():
|
||||||
|
sc, err := bc.vi.DialSSH()
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "dial vm ssh")
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() { _ = sc.Close() }()
|
||||||
|
|
||||||
|
bc.logger.Info("Installation in progress")
|
||||||
|
|
||||||
|
err = runAlpineSetupCmd(sc, []string{"openssh", "lvm2", "util-linux", "cryptsetup", "samba"})
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "run alpine setup cmd")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = bc.vi.Cancel()
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "cancel vm context")
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-runErrCh:
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "run vm")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runAlpineSetupCmd(sc *ssh.Client, pkgs []string) error {
|
||||||
|
sess, err := sc.NewSession()
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "new session")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Timeout for this command.
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
_ = sess.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
cmd := "ifconfig eth0 up && ifconfig lo up && udhcpc && true > /etc/apk/repositories && setup-apkrepos -1 && printf 'y' | setup-disk -m sys /dev/vda"
|
||||||
|
|
||||||
|
if len(pkgs) != 0 {
|
||||||
|
pkgsQuoted := make([]string, len(pkgs))
|
||||||
|
for i, rawPkg := range pkgs {
|
||||||
|
pkgsQuoted[i] = shellescape.Quote(rawPkg)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd += " && mount /dev/vda3 /mnt && chroot /mnt apk add " + strings.Join(pkgsQuoted, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = sess.Run(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "run setup cmd")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
51
cmd/imgbuilder/main.go
Normal file
51
cmd/imgbuilder/main.go
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/AlexSSD7/linsk/cmd/imgbuilder/builder"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rootCmd = &cobra.Command{
|
||||||
|
Use: "imgbuilder",
|
||||||
|
// TODO: Fill this
|
||||||
|
// Short: "",
|
||||||
|
// Long: ``,
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
baseISOPath := filepath.Clean(args[0])
|
||||||
|
outImagePath := filepath.Clean(args[1])
|
||||||
|
|
||||||
|
bc, err := builder.NewBuildContext(slog.With("caller", "build-context"), baseISOPath, outImagePath, vmDebugFlag)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to create a new build context", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = bc.BuildWithInterruptHandler()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to build an image", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("Success")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var vmDebugFlag bool
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, nil)))
|
||||||
|
|
||||||
|
rootCmd.PersistentFlags().BoolVar(&vmDebugFlag, "vmdebug", false, "Enable VM debug mode. This will open an accessible VM monitor. You can log in with root user and no password.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
err := rootCmd.Execute()
|
||||||
|
if err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,7 +17,7 @@ var lsCmd = &cobra.Command{
|
||||||
// TODO: Fill this
|
// TODO: Fill this
|
||||||
// Short: "",
|
// Short: "",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
os.Exit(runVM(args[0], func(ctx context.Context, i *vm.VM, fm *vm.FileManager) int {
|
os.Exit(runVM(args[0], func(ctx context.Context, i *vm.VM, fm *vm.FileManager) int {
|
||||||
lsblkOut, err := fm.Lsblk()
|
lsblkOut, err := fm.Lsblk()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -28,8 +28,6 @@ var lsCmd = &cobra.Command{
|
||||||
fmt.Print(string(lsblkOut))
|
fmt.Print(string(lsblkOut))
|
||||||
return 0
|
return 0
|
||||||
}, nil, false))
|
}, nil, false))
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ var runCmd = &cobra.Command{
|
||||||
// TODO: Fill this
|
// TODO: Fill this
|
||||||
// Short: "",
|
// Short: "",
|
||||||
Args: cobra.ExactArgs(3),
|
Args: cobra.ExactArgs(3),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
vmMountDevName := args[1]
|
vmMountDevName := args[1]
|
||||||
fsType := args[2]
|
fsType := args[2]
|
||||||
|
|
||||||
|
|
@ -64,8 +64,6 @@ var runCmd = &cobra.Command{
|
||||||
HostPort: networkSharePort,
|
HostPort: networkSharePort,
|
||||||
VMPort: 445,
|
VMPort: 445,
|
||||||
}}, false))
|
}}, false))
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ var shellCmd = &cobra.Command{
|
||||||
// TODO: Fill this
|
// TODO: Fill this
|
||||||
// Short: "",
|
// Short: "",
|
||||||
Args: cobra.RangeArgs(0, 1),
|
Args: cobra.RangeArgs(0, 1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
var passthroughArg string
|
var passthroughArg string
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
passthroughArg = args[0]
|
passthroughArg = args[0]
|
||||||
|
|
@ -121,8 +121,6 @@ var shellCmd = &cobra.Command{
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}, forwardPortRules, unrestrictedNetworkingFlag))
|
}, forwardPortRules, unrestrictedNetworkingFlag))
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,8 +52,8 @@ func runVM(passthroughArg string, fn func(context.Context, *vm.VM, *vm.FileManag
|
||||||
USBDevices: passthroughConfig,
|
USBDevices: passthroughConfig,
|
||||||
ExtraPortForwardingRules: forwardPortsRules,
|
ExtraPortForwardingRules: forwardPortsRules,
|
||||||
|
|
||||||
DebugUnrestrictedNetworking: unrestrictedNetworking,
|
UnrestrictedNetworking: unrestrictedNetworking,
|
||||||
DebugShowDisplay: vmDebugFlag,
|
ShowDisplay: vmDebugFlag,
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Alpine image should be downloaded from somewhere.
|
// TODO: Alpine image should be downloaded from somewhere.
|
||||||
|
|
|
||||||
2
go.mod
2
go.mod
|
|
@ -3,8 +3,10 @@ module github.com/AlexSSD7/linsk
|
||||||
go 1.21
|
go 1.21
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
|
||||||
github.com/alessio/shellescape v1.4.2
|
github.com/alessio/shellescape v1.4.2
|
||||||
github.com/bramvdbogaerde/go-scp v1.2.1
|
github.com/bramvdbogaerde/go-scp v1.2.1
|
||||||
|
github.com/google/uuid v1.3.1
|
||||||
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5
|
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/sethvargo/go-password v0.2.0
|
github.com/sethvargo/go-password v0.2.0
|
||||||
|
|
|
||||||
4
go.sum
4
go.sum
|
|
@ -1,3 +1,5 @@
|
||||||
|
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
|
||||||
|
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
|
||||||
github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0=
|
github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0=
|
||||||
github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
|
github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
|
||||||
github.com/bramvdbogaerde/go-scp v1.2.1 h1:BKTqrqXiQYovrDlfuVFaEGz0r4Ou6EED8L7jCXw6Buw=
|
github.com/bramvdbogaerde/go-scp v1.2.1 h1:BKTqrqXiQYovrDlfuVFaEGz0r4Ou6EED8L7jCXw6Buw=
|
||||||
|
|
@ -5,6 +7,8 @@ github.com/bramvdbogaerde/go-scp v1.2.1/go.mod h1:s4ZldBoRAOgUg8IrRP2Urmq5qqd2yP
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
|
||||||
|
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI=
|
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI=
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,16 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/acarl005/stripansi"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ClearUnprintableChars(s string) string {
|
func ClearUnprintableChars(s string, allowNewlines bool) string {
|
||||||
|
// This will remove ANSI color codes.
|
||||||
|
s = stripansi.Strip(s)
|
||||||
|
|
||||||
return strings.Map(func(r rune) rune {
|
return strings.Map(func(r rune) rune {
|
||||||
if unicode.IsPrint(r) {
|
if unicode.IsPrint(r) || (allowNewlines && r == '\n') {
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
return -1
|
return -1
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ func wrapErrWithLog(err error, msg, log string) error {
|
||||||
func getLogErrMsg(s string) string {
|
func getLogErrMsg(s string) string {
|
||||||
logToInclude := strings.ReplaceAll(s, "\n", "\\n")
|
logToInclude := strings.ReplaceAll(s, "\n", "\\n")
|
||||||
logToInclude = strings.TrimSuffix(logToInclude, "\\n")
|
logToInclude = strings.TrimSuffix(logToInclude, "\\n")
|
||||||
logToInclude = utils.ClearUnprintableChars(logToInclude)
|
logToInclude = utils.ClearUnprintableChars(logToInclude, false)
|
||||||
|
|
||||||
origLogLen := len(logToInclude)
|
origLogLen := len(logToInclude)
|
||||||
const maxLogLen = 256
|
const maxLogLen = 256
|
||||||
|
|
|
||||||
12
vm/ssh.go
12
vm/ssh.go
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AlexSSD7/linsk/utils"
|
||||||
"github.com/alessio/shellescape"
|
"github.com/alessio/shellescape"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
|
|
@ -99,14 +100,19 @@ func (vm *VM) sshSetup() (ssh.Signer, error) {
|
||||||
return nil, errors.Wrap(err, "generate ssh key")
|
return nil, errors.Wrap(err, "generate ssh key")
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := `set -ex; do_setup () { sh -c "set -ex; ifconfig eth0 up; ifconfig lo up; udhcpc; mkdir -p ~/.ssh; echo ` + shellescape.Quote(string(sshPublicKey)) + ` > ~/.ssh/authorized_keys; rc-update add sshd; rc-service sshd start"; echo "SERIAL"" ""STATUS: $?"; }; do_setup` + "\n"
|
installSSHDCmd := ""
|
||||||
|
if vm.installSSH {
|
||||||
|
installSSHDCmd = "apk add openssh; "
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := `do_setup () { sh -c "set -ex; setup-alpine -q; ` + installSSHDCmd + `mkdir -p ~/.ssh; echo ` + shellescape.Quote(string(sshPublicKey)) + ` > ~/.ssh/authorized_keys; rc-update add sshd; rc-service sshd start"; echo "SERIAL"" ""STATUS: $?"; }; do_setup` + "\n"
|
||||||
|
|
||||||
err = vm.writeSerial([]byte(cmd))
|
err = vm.writeSerial([]byte(cmd))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "write ssh setup serial command")
|
return nil, errors.Wrap(err, "write ssh setup serial command")
|
||||||
}
|
}
|
||||||
|
|
||||||
deadline := time.Now().Add(time.Second * 5)
|
deadline := time.Now().Add(time.Second * 30)
|
||||||
|
|
||||||
stdOutErrBuf := bytes.NewBuffer(nil)
|
stdOutErrBuf := bytes.NewBuffer(nil)
|
||||||
|
|
||||||
|
|
@ -118,7 +124,7 @@ func (vm *VM) sshSetup() (ssh.Signer, error) {
|
||||||
return nil, fmt.Errorf("setup command timed out %v", getLogErrMsg(stdOutErrBuf.String()))
|
return nil, fmt.Errorf("setup command timed out %v", getLogErrMsg(stdOutErrBuf.String()))
|
||||||
case data := <-vm.serialStdoutCh:
|
case data := <-vm.serialStdoutCh:
|
||||||
prefix := []byte("SERIAL STATUS: ")
|
prefix := []byte("SERIAL STATUS: ")
|
||||||
stdOutErrBuf.Write(data)
|
stdOutErrBuf.WriteString(utils.ClearUnprintableChars(string(data), true))
|
||||||
if bytes.HasPrefix(data, prefix) {
|
if bytes.HasPrefix(data, prefix) {
|
||||||
if len(data) == len(prefix) {
|
if len(data) == len(prefix) {
|
||||||
return nil, fmt.Errorf("setup command status code did not show up")
|
return nil, fmt.Errorf("setup command status code did not show up")
|
||||||
|
|
|
||||||
43
vm/vm.go
43
vm/vm.go
|
|
@ -37,6 +37,7 @@ type VM struct {
|
||||||
sshMappedPort uint16
|
sshMappedPort uint16
|
||||||
sshConf *ssh.ClientConfig
|
sshConf *ssh.ClientConfig
|
||||||
sshReadyCh chan struct{}
|
sshReadyCh chan struct{}
|
||||||
|
installSSH bool
|
||||||
|
|
||||||
serialRead *io.PipeReader
|
serialRead *io.PipeReader
|
||||||
serialReader *bufio.Reader
|
serialReader *bufio.Reader
|
||||||
|
|
@ -51,15 +52,22 @@ type VM struct {
|
||||||
canceled uint32
|
canceled uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DriveConfig struct {
|
||||||
|
Path string
|
||||||
|
SnapshotMode bool
|
||||||
|
}
|
||||||
|
|
||||||
type VMConfig struct {
|
type VMConfig struct {
|
||||||
CdromImagePath string
|
CdromImagePath string
|
||||||
|
|
||||||
USBDevices []USBDevicePassthroughConfig
|
USBDevices []USBDevicePassthroughConfig
|
||||||
ExtraPortForwardingRules []PortForwardingRule
|
ExtraPortForwardingRules []PortForwardingRule
|
||||||
|
Drives []DriveConfig
|
||||||
|
|
||||||
// Debug-related options.
|
// Mostly debug-related options.
|
||||||
DebugUnrestrictedNetworking bool
|
UnrestrictedNetworking bool
|
||||||
DebugShowDisplay bool
|
ShowDisplay bool
|
||||||
|
InstallBaseUtilities bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewVM(logger *slog.Logger, cfg VMConfig) (*VM, error) {
|
func NewVM(logger *slog.Logger, cfg VMConfig) (*VM, error) {
|
||||||
|
|
@ -81,7 +89,7 @@ func NewVM(logger *slog.Logger, cfg VMConfig) (*VM, error) {
|
||||||
|
|
||||||
netdevOpts := "user,id=net0,hostfwd=tcp:127.0.0.1:" + fmt.Sprint(sshPort) + "-:22"
|
netdevOpts := "user,id=net0,hostfwd=tcp:127.0.0.1:" + fmt.Sprint(sshPort) + "-:22"
|
||||||
|
|
||||||
if !cfg.DebugUnrestrictedNetworking {
|
if !cfg.UnrestrictedNetworking {
|
||||||
netdevOpts += ",restrict=on"
|
netdevOpts += ",restrict=on"
|
||||||
} else {
|
} else {
|
||||||
logger.Warn("Running with unsafe unrestricted networking")
|
logger.Warn("Running with unsafe unrestricted networking")
|
||||||
|
|
@ -98,9 +106,7 @@ func NewVM(logger *slog.Logger, cfg VMConfig) (*VM, error) {
|
||||||
|
|
||||||
cmdArgs = append(cmdArgs, "-device", "e1000,netdev=net0", "-netdev", netdevOpts)
|
cmdArgs = append(cmdArgs, "-device", "e1000,netdev=net0", "-netdev", netdevOpts)
|
||||||
|
|
||||||
cmdArgs = append(cmdArgs, "-drive", "file="+shellescape.Quote(cdromImagePath)+",format=qcow2,if=virtio", "-snapshot")
|
if !cfg.ShowDisplay {
|
||||||
|
|
||||||
if !cfg.DebugShowDisplay {
|
|
||||||
cmdArgs = append(cmdArgs, "-display", "none")
|
cmdArgs = append(cmdArgs, "-display", "none")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -112,6 +118,28 @@ func NewVM(logger *slog.Logger, cfg VMConfig) (*VM, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for i, extraDrive := range cfg.Drives {
|
||||||
|
_, err = os.Stat(extraDrive.Path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "stat extra drive #%v path", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
driveArgs := "file=" + shellescape.Quote(extraDrive.Path) + ",format=qcow2,if=virtio"
|
||||||
|
if extraDrive.SnapshotMode {
|
||||||
|
driveArgs += ",snapshot"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdArgs = append(cmdArgs, "-drive", driveArgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cdromImagePath != "" {
|
||||||
|
cmdArgs = append(cmdArgs, "-boot", "d", "-cdrom", cdromImagePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.InstallBaseUtilities && !cfg.UnrestrictedNetworking {
|
||||||
|
return nil, fmt.Errorf("cannot install base utilities with unrestricted networking disabled")
|
||||||
|
}
|
||||||
|
|
||||||
sysRead, userWrite := io.Pipe()
|
sysRead, userWrite := io.Pipe()
|
||||||
userRead, sysWrite := io.Pipe()
|
userRead, sysWrite := io.Pipe()
|
||||||
|
|
||||||
|
|
@ -141,6 +169,7 @@ func NewVM(logger *slog.Logger, cfg VMConfig) (*VM, error) {
|
||||||
|
|
||||||
sshMappedPort: uint16(sshPort),
|
sshMappedPort: uint16(sshPort),
|
||||||
sshReadyCh: make(chan struct{}),
|
sshReadyCh: make(chan struct{}),
|
||||||
|
installSSH: cfg.InstallBaseUtilities,
|
||||||
|
|
||||||
serialRead: userRead,
|
serialRead: userRead,
|
||||||
serialReader: userReader,
|
serialReader: userReader,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue