improve fs detection - factor out common functions

This commit is contained in:
bill-auger 2020-02-08 18:25:10 -05:00
parent 8f102e6742
commit 4872be99ac
5 changed files with 522 additions and 382 deletions

53
README

@ -26,6 +26,8 @@ the script will emit a warning and ask for confirmation to proceed.
The creation can be influenced by providing one or more of the following
options to pvmbootstrap:
-b <base-set> -- Select one of the pre-defined package-sets described below
(default: 'standard')
-H <hook> -- Enable a hook to customize the created image. This can be
the path to a custom script, or one of the predefined hooks
described below. The VM will boot the newly created image,
@ -39,16 +41,21 @@ options to pvmbootstrap:
-O -- Bootstrap an openrc system instead of a systemd one
-p <package> -- Specify additional packages to be installed in the VM image.
This option can be specified multiple times.
-s <img_size> -- Set the size (in GB) of the VM image (minimum: 1, default: 64)
-s <root_size> -- Set the size (in MB) of the root partition (default: 32000).
If this is 0 (or less than the <base-set> requires),
the VM image will be the smallest size possible,
fit to the <base-set>; and any -p <package> will be ignored.
-S <swap_size> -- Set the size (in MB) of the swap partition (default: 0)
The creation hooks currently supported are:
Pre-defined package-sets:
minimal: base
standard: base parabola-base
devel: base parabola-base base-devel
'ethernet-dhcp':
This hook will setup ethernet in the VM by enabling systemd-resolved and
openresolv properly, as well as creating and enabling a systemd-networkd
configuration.
Pre-defined hooks:
ethernet-dhcp: This hook will setup ethernet in the VM by enabling
systemd-resolved and openresolv properly, as well as creating
and enabling a systemd-networkd configuration. (systemd only)
--------------------
@ -59,10 +66,23 @@ To boot a created virtual machine, run:
$> pvmboot [options] <path_to_image> [qemu-args ...]
The script will attempt to determine the architecture of the provided virtual
machine image, and set the qemu executable and sane default flags for the qemu
invocation automatically, including kvm acceleration, if available for the
target architecture.
The script will attempt to determine the architecture and partition layout
of the provided virtual machine image, and set the qemu executable and sane
default flags for the qemu invocation automatically; and will enable KVM
acceleration, if available for the target architecture.
The pvmbootstrap script creates a boot partition formatted with either the
vfat or ext2 filesystems, and a root partition formatted with the ext4
filesystems. If the specified image was not created using pvmbootstrap, the
partition detection will fail unless the image coforms to this expected schema.
The first vfat or ext2 filesystem detected, will be considered as the boot
partition; and the first ext4 filesystem detected, will be considered as the
root partition.
The default kernel installed by pvmbootstrap is 'linux-libre'.
The -k option ca be used to specify an alternate kernel to boot.
$> pvmboot -k linux-libre-lts ./an.img
Additionally, the script will evaluate the DISPLAY environment variable to
determine whether a graphical desktop environment is available, and will start
@ -71,12 +91,7 @@ unsetting DISPLAY before executing the script:
$> DISPLAY= pvmboot ./an.img
The default kernel installed by pvmbootstrap is 'linux-libre'.
The -k option ca be used to specify an alternate kernel to boot.
$> pvmboot -k linux-libre-lts ./an.img
When the VM will boot into graphical mode, the serial console can be redirected
If qemu boots into graphical mode, the serial console can be redirected
to the host console by passing the -r option.
$> pvmboot -r ./an.img
@ -104,5 +119,5 @@ pvmbootstrap always creates a /boot partition.
for example, to generate the parabola armv7h release tarball:
$> img_filename=parabola-systemd-cli-armv7h-tarball-$(date +%Y.%m).img
$> pvmbootstrap -s 1 -H ethernet-dhcp $img_filename armv7h
$> pvm2tarball $img_filename
$> pvmbootstrap -b minimal -H ethernet-dhcp -s 0 -S0 $img_file armv7h
$> pvm2tarball $img_file

264
src/pvm-common.sh.inc Normal file

@ -0,0 +1,264 @@
# readonly DATA_IMG=./pvmdata.img # optional large qemu disk
readonly THIS_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
readonly UNPRIVILEGED_ERR_MSG="This program must be run as a regular user"
readonly MOUNTS_ERR_MSG="some PVM mountpoints are mounted - possibly orphans from a previous run - unmount them first"
readonly MOUNTVARS_ERR_MSG="FIXME: pvm_setup_loopdev() was called in an improper state - one of [ \$bootdir , \$workdir , \$loopdev ] is already set"
# shellcheck source=/usr/lib/libretools/messages.sh
source "$(librelib messages)"
pvm_get_pvmboot_cmd()
{
local intree_cmd=$THIS_DIR/pvmboot.sh
local installed_cmd='pvmboot'
[[ -f "$intree_cmd" ]] && echo "$intree_cmd" && return "$EXIT_SUCCESS"
type -p $installed_cmd &>/dev/null && echo "$installed_cmd" && return "$EXIT_SUCCESS"
error "can not find pvmboot" && return "$EXIT_FAILURE"
}
pvm_get_hook() # ( hook_name )
{
local hook_name=$1
local intree_dir=$THIS_DIR/hooks
local installed_dir=/usr/lib/parabola-vmbootstrap
local hook_filename=hook-$hook_name.sh
local locations=( "$intree_dir/$hook_filename" "$installed_dir/$hook_filename" "$hook_name" '' )
local location
for location in "${locations[@]}" ; do [[ -f "$location" ]] && break ; done ;
[[ "$location" ]] && echo "$location" || warning "no such hook: '%s'" "$hook_name"
[[ "$location" ]] && return "$EXIT_SUCCESS" || return "$EXIT_FAILURE"
}
pvm_find_part_n() # ( imagefile fs_types ) , sets: $part_n
{
local imagefile="$1" ; shift ;
local fs_types="$@"
# try locating the partition by filesystem type
for fs_type in $fs_types
do local part_data=$(parted "$imagefile" print 2> /dev/null | grep $fs_type | head -n 1)
part_n=$( echo $part_data | cut -d ' ' -f 1 )
! [[ "$part_n" =~ ^[0-9]+$ ]] && part_n='' || break
done
[[ "$part_n" =~ ^[0-9]+$ ]] && return "$EXIT_SUCCESS" || return "$EXIT_FAILURE"
}
pvm_find_boot_part_n() # ( imagefile ) , sets: boot_part_n
{
local imagefile="$1"
local part_n
pvm_find_part_n "$imagefile" fat32 ext2 || return "$EXIT_FAILURE"
boot_part_n=$part_n
return "$EXIT_SUCCESS"
}
pvm_find_root_part_n() # ( imagefile ) , sets: root_part_n
{
local imagefile="$1"
local part_n
pvm_find_part_n "$imagefile" ext4 || return "$EXIT_FAILURE"
root_part_n=$part_n
return "$EXIT_SUCCESS"
}
pvm_check_unprivileged() # exits on failure
{
[[ "$(id -u)" -eq 0 ]] && error "$UNPRIVILEGED_ERR_MSG" && exit "$EXIT_NOPERMISSION"
}
pvm_native_arch() # ( arch )
{
local arch=$1
local native_arch=$( [[ "$arch" =~ arm.* ]] && echo 'armv7l' || echo "$arch" )
setarch "$native_arch" /bin/true 2>/dev/null && return "$EXIT_SUCCESS" || \
return "$EXIT_FAILURE"
}
pvm_check_file_exists_writable() # (file_path [ is_error_if_not_exists ])
{
local file_path="$1"
local is_error_if_not_exists=$( [[ "$2" == 'true' ]] && echo 1 || echo 0 )
if [[ -e "$file_path" ]]
then if [[ -w "$file_path" ]]
then return "$EXIT_SUCCESS"
else error "file exists but is not writable: '%s'" "$file_path"
return "$EXIT_FAILURE"
fi
elif (( ! $is_error_if_not_exists ))
then return "$EXIT_SUCCESS"
else error "no such file: %s" "$file_path"
return "$EXIT_FAILURE"
fi
}
pvm_check_file_exists_and_writable() # (file_path)
{
local file_path="$1"
pvm_check_file_exists_writable $file_path true && return "$EXIT_SUCCESS" || \
return "$EXIT_FAILURE"
}
pvm_check_file_not_exists_or_writable() # (file_path)
{
local file_path="$1"
pvm_check_file_exists_writable $file_path && return "$EXIT_SUCCESS" || \
return "$EXIT_FAILURE"
}
pvm_prompt_clobber_file() # (file_path)
{
local file_path="$1"
if pvm_check_file_not_exists_or_writable "$file_path"
then if [[ -e "$file_path" ]]
then warning "file exists: '%s'\nContinue? [y/N]" "$file_path"
read -p " " -n 1 -r ; echo ;
[[ $REPLY =~ ^[Yy]$ ]] || return "$EXIT_FAILURE"
rm -f "$file_path" || return "$EXIT_FAILURE"
fi
return "$EXIT_SUCCESS"
else return "$EXIT_FAILURE"
fi
}
pvm_setup_loopdev() # assumes: $imagefile , sets: $bootdir $workdir $loopdev , traps: INT TERM EXIT
{
if file "$imagefile" | grep -Eq ': (data|DOS/MBR )'; then
if [[ -z "${bootdir}${workdir}${loopdev}" ]]; then
pvm_check_no_mounts && msg "creating loopback devices" || return "$EXIT_FAILURE"
else
error "$MOUNTVARS_ERR_MSG"
return "$EXIT_FAILURE"
fi
else
error "not a raw qemu image: '%s'" "$imagefile"
return "$EXIT_FAILURE"
fi
trap 'pvm_cleanup' INT TERM EXIT
# setup the loopback device
bootdir="$(mktemp -d -t pvm-bootfs-XXXXXXXXXX)" || return "$EXIT_FAILURE"
workdir="$(mktemp -d -t pvm-rootfs-XXXXXXXXXX)" || return "$EXIT_FAILURE"
loopdev="$(sudo losetup -fLP --show "$imagefile")" || return "$EXIT_FAILURE"
return "$EXIT_SUCCESS"
}
pvm_mount() # assumes: $imagefile $loopdev $bootdir $workdir
{
pvm_setup_loopdev || return "$EXIT_FAILURE" # sets: $bootdir $workdir $loopdev
# find boot and root filesystem partitions
local boot_part_n root_part_n
pvm_find_boot_part_n "$imagefile" || return "$EXIT_FAILURE" # sets: $boot_part_n
pvm_find_root_part_n "$imagefile" || return "$EXIT_FAILURE" # sets: $root_part_n
# mount boot and root filesystems
msg "mounting image filesystems"
sudo mount "$loopdev"p$boot_part_n "$bootdir" || return "$EXIT_FAILURE"
sudo mount "$loopdev"p$root_part_n "$workdir" || return "$EXIT_FAILURE"
return "$EXIT_SUCCESS"
}
pvm_umount() # unsets: $bootdir $workdir
{
[[ "${bootdir}${workdir}${loopdev}" ]] && msg "un-mounting image filesystems"
(sudo umount "$workdir"/boot && rmdir "$workdir") 2> /dev/null
(sudo umount "$bootdir" && rmdir "$bootdir") 2> /dev/null
(sudo umount "$workdir" && rmdir "$workdir") 2> /dev/null
unset bootdir
unset workdir
}
pvm_cleanup() # unsets: $loopdev , untraps: INT TERM EXIT
{
trap - INT TERM EXIT
pvm_umount
sudo losetup -d "$loopdev"
pvm_check_no_mounts || return "$EXIT_FAILURE"
unset loopdev
return "$EXIT_SUCCESS"
}
pvm_check_no_mounts() # assumes: $imagefile
{
local n_pvm_mounts=$( mount | grep /tmp/pvm | wc --lines )
local n_loop_devs=$( sudo losetup --associated $imagefile | wc --lines )
local are_any_mounts=$( (( $n_pvm_mounts + $n_loop_devs )) && echo 1 || echo 0 )
(( $are_any_mounts )) && error "$MOUNTS_ERR_MSG" && return "$EXIT_FAILURE" || \
return "$EXIT_SUCCESS"
}
pvm_probe_arch() # assumes: $bootdir $workdir $imagefile , sets: $arch
{
msg "detecting CPU architecture for image"
local kernel=$(find "$bootdir" -maxdepth 1 -type f -iname '*vmlinu*' | head -n1)
local guest_arch
if [ -n "$kernel" ]; then
msg2 "found kernel binary: %s" "$kernel"
else
warning "%s: unable to find kernel binary" "$imagefile"
return "$EXIT_FAILURE"
fi
guest_arch="$(readelf -h "$workdir"/bin/true 2>/dev/null | \
grep Machine | sed 's|[^:]*:\s*\([^:,]*\).*|\1|')"
case "$guest_arch" in
ARM ) arch=armv7h ;;
i386|i386:*|*\ 80386) arch=i686 ;;
PowerPC64 ) arch=ppc64le ;;
RISC-V ) arch=riscv64 ;;
x86_64|*\ X86-64 ) arch=x86_64 ;;
* ) arch='' ;;
esac
if [[ "$arch" ]]; then
msg2 "detected guest \`/bin/true\` arch: '%s'=>'%s'" "$guest_arch" "$arch"
return "$EXIT_SUCCESS"
else
error "image arch is unknown: '%s'" "$guest_arch"
return "$EXIT_FAILURE"
fi
}
pvm_boot() # ( imagefile qemu_args )
{
local imagefile="$1" ; shift ;
local qemu_args=(-no-reboot $@)
local pvmboot_cmd=$(pvm_get_pvmboot_cmd)
local was_error=$?
[[ "$pvmboot_cmd" ]] || return $EXIT_FAILURE
DISPLAY='' "$pvmboot_cmd" "$imagefile" "${qemu_args[@]}" ; was_error=$? ;
(( ! $was_error )) && return $EXIT_SUCCESS || return $EXIT_FAILURE
}

@ -19,10 +19,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. #
###############################################################################
# shellcheck source=/usr/lib/libretools/messages.sh
source "$(librelib messages)"
usage() {
usage()
{
print "USAGE:"
print " pvm2tarball [-h] [-o <FILENAME>] <IMG>"
echo
@ -41,78 +40,16 @@ usage() {
echo " <https://git.parabola.nu/parabola-vmbootstrap.git>"
}
pvm_mount() {
if file "$imagefile" | grep -q ' DOS/MBR '; then
msg "mounting filesystems"
else
error "%s: does not seem to be a raw qemu image." "$imagefile"
return "$EXIT_FAILURE"
fi
trap 'pvm_umount' INT TERM EXIT
workdir="$(mktemp -d -t pvm-XXXXXXXXXX)" || return
loopdev="$(sudo losetup -fLP --show "$imagefile")" || return
# find the root partition
local part rootpart bootpart
for part in "$loopdev"p*; do
sudo mount "$part" "$workdir" || continue
if [ -f "$workdir"/etc/fstab ]; then
rootpart="$part"
break
fi
sudo umount "$workdir"
done
if [ -n "$rootpart" ]; then
msg "found root filesystem partition: %s" "$rootpart"
else
error "%s: unable to determine root partition." "$imagefile"
return "$EXIT_FAILURE"
fi
# find the boot partition
if (( $(find /boot/ -name initramfs-* | wc -l) > 0 )) && \
(( $(find /boot/ -name vmlinuz-* | wc -l) > 0 )); then
msg "found /boot on root filesystem partition"
else
bootpart="$(findmnt -senF "$workdir"/etc/fstab /boot | awk '{print $2}')"
if [ -n "$bootpart" ]; then
# mount and be happy
msg "found boot filesystem partition: %s" "$bootpart"
sudo mount "$bootpart" "$workdir"/boot || return "$EXIT_FAILURE"
else
error "%s: unable to determine boot filesystem partition." "$imagefile"
return "$EXIT_FAILURE"
fi
fi
}
pvm_umount() {
msg "un-mounting filesystems"
trap - INT TERM EXIT
[ -n "$workdir" ] && (sudo umount -R "$workdir"; rmdir "$workdir")
unset workdir
[ -n "$loopdev" ] && sudo losetup -d "$loopdev"
unset loopdev
}
main() {
if [ "$(id -u)" -eq 0 ]; then
error "This program must be run as a regular user"
exit "$EXIT_NOPERMISSION"
fi
main()
{
pvm_check_unprivileged # exits on failure
# parse options
local output
local outfile
while getopts 'ho:' arg; do
case "$arg" in
h) usage; return "$EXIT_SUCCESS";;
o) output="$OPTARG";;
o) outfile="$OPTARG";;
*) error "invalid argument: %s\n" "$arg"; usage >&2; exit "$EXIT_INVALIDARGUMENT";;
esac
done
@ -124,29 +61,16 @@ main() {
image_filename="$(basename "$imagefile")"
shift
# check for input file presence
if [ ! -e "$imagefile" ]; then
error "%s: file not found" "$imagefile"
exit "$EXIT_FAILURE"
fi
# determine output file
[ -n "$output" ] || output="${image_filename%.img*}.tar.gz"
[ -n "$outfile" ] || outfile="$(dirname $imagefile)/${image_filename%.img*}.tar.gz"
# check for output file presence
if [ -e "$output" ]; then
warning "%s: file exists. Continue? [y/N]" "$output"
read -p " " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit "$EXIT_FAILURE"
fi
rm -f "$output" || exit
fi
# ensure that the image file exists, prompt to clobber existing output file
pvm_check_file_exists_and_writable "$imagefile" || exit "$EXIT_FAILURE"
pvm_prompt_clobber_file "$outfile" || exit "$EXIT_FAILURE"
# mount the root filesystem
local workdir loopdev
pvm_mount || exit
local bootdir workdir loopdev
pvm_mount || exit "$EXIT_FAILURE" # assumes: $imagefile , sets: $loopdev $bootdir $workdir
# tar the root filesystem, excluding unneeded things
# HACKING:
@ -156,7 +80,7 @@ main() {
#
# `tar -tf <tarball> | sort`
msg "imploding tarball"
sudo tar -c -f "$output" -C "$workdir" -X - . << EOF
sudo tar -c -f "$outfile" -C "$workdir" -X - . << EOF
./boot/lost+found
./etc/.updated
./etc/pacman.d/gnupg
@ -169,10 +93,15 @@ main() {
EOF
# give the archive back to the user
sudo chown "$(id -u)":"$(id -g)" "$output"
sudo chown "$(id -u)":"$(id -g)" "$outfile"
# cleanup
pvm_umount
pvm_cleanup
}
main "$@"
if source /usr/lib/parabola-vmbootstrap/pvm-common.sh.inc 2> /dev/null || \
source "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"/pvm-common.sh.inc 2> /dev/null
then main "$@"
else echo "can not find pvm-common.sh.inc" && exit 1
fi

@ -19,8 +19,6 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. #
###############################################################################
# shellcheck source=/usr/lib/libretools/messages.sh
source "$(librelib messages)"
readonly DEF_KERNEL='linux-libre' # ASSERT: must be 'linux-libre', per 'parabola-base'
readonly DEF_RAM_MB=1000
@ -29,14 +27,16 @@ Kernel=$DEF_KERNEL
RedirectSerial=0
usage() {
usage()
{
print "USAGE:"
print " pvmboot [-h] [-k <kernel>] [-r] <img> [qemu-args ...]"
echo
prose "Determine the architecture of <img> and boot it using qemu. <img> is assumed
to be a valid, raw-formatted parabola virtual machine image, ideally
created using pvmbootstrap. The started instances are assigned
${DEF_RAM_MB}MB of RAM and one SMP core."
created using pvmbootstrap. If the image was not created using pvmbootstrap,
the boot partition must be vfat or ext2, and the root partition must be ext4
The machine instance is assigned ${DEF_RAM_MB}MB of RAM and one SMP core."
echo
prose "When a graphical desktop environment is available, start the machine
normally, otherwise append -nographic to the qemu options. This behavior
@ -63,138 +63,76 @@ usage() {
echo " <https://git.parabola.nu/parabola-vmbootstrap.git>"
}
pvm_mount() {
if ! file "$imagefile" | grep -q ' DOS/MBR '; then
error "%s: does not seem to be a raw qemu image." "$imagefile"
return "$EXIT_FAILURE"
fi
msg "mounting filesystems"
trap 'pvm_umount' INT TERM EXIT
workdir="$(mktemp -d -t pvm-XXXXXXXXXX)" || return "$EXIT_FAILURE"
loopdev="$(sudo losetup -fLP --show "$imagefile")" || return "$EXIT_FAILURE"
sudo mount "$loopdev"p1 "$workdir" || \
sudo mount "$loopdev"p2 "$workdir" || return "$EXIT_FAILURE"
}
pvm_umount() {
trap - INT TERM EXIT
[ -n "$workdir" ] && (sudo umount "$workdir"; rmdir "$workdir")
[ -n "$loopdev" ] && sudo losetup -d "$loopdev"
unset workdir
unset loopdev
}
pvm_probe_arch() {
local kernel
kernel=$(find "$workdir" -maxdepth 1 -type f -iname '*vmlinu*' | head -n1)
if [ -z "$kernel" ]; then
warning "%s: unable to find kernel binary" "$imagefile"
return "$EXIT_FAILURE"
fi
# attempt to get kernel arch from elf header
arch="$(readelf -h "$kernel" 2>/dev/null | grep Machine | awk '{print $2}')"
pvm_guess_qemu_cmd() # assumes: $arch , sets: $qemu_cmd
{
case "$arch" in
PowerPC64) arch=ppc64 ; return "$EXIT_SUCCESS" ;;
RISC-V ) arch=riscv64 ; return "$EXIT_SUCCESS" ;;
* ) arch="" ;;
armv7h ) qemu_cmd="qemu-system-arm" ;;
i686 ) qemu_cmd="qemu-system-i386" ;;
ppc64le) qemu_cmd="qemu-system-ppc64" ;;
riscv64) qemu_cmd="qemu-system-riscv64" ;;
x86_64 ) qemu_cmd="qemu-system-x86_64" ;;
* ) error "unknown image arch: '%s'" "$arch" ; return "$EXIT_FAILURE" ;;
esac
# attempt to get kernel arch from objdump
arch="$(objdump -f "$kernel" 2>/dev/null | grep architecture: | awk '{print $2}' | tr -d ',')"
case "$arch" in
i386 ) arch=i386 ; return "$EXIT_SUCCESS" ;;
i386:*) arch=x86_64 ; return "$EXIT_SUCCESS" ;;
* ) arch="" ;;
esac
# attempt to get kernel arch from file magic
arch="$(file "$kernel")"
case "$arch" in
*"ARM boot executable"*) arch=arm ; return "$EXIT_SUCCESS" ;;
* ) arch="" ;;
esac
# no more ideas; giving up.
}
pvm_native_arch() {
local arch
pvm_guess_qemu_args() # assumes: $qemu_args $imagefile $arch $bootdir , appends: $qemu_args
{
msg "configuring the virtual machine ($arch)"
case "$1" in
arm*) arch=armv7l ;;
* ) arch="$1" ;;
esac
qemu_args+=(-m $DEF_RAM_MB )
setarch "$arch" /bin/true 2>/dev/null || return
}
# optional large qemu disk
qemu_args+=( $( [[ -w $DATA_IMG ]] && echo "-hdb $DATA_IMG" ) )
pvm_guess_qemu_args() {
# if we're not running on X / wayland, disable graphics
if [ -z "$DISPLAY" ]; then qemu_args+=(-nographic);
elif (( ${RedirectSerial} )); then qemu_args+=(-serial "mon:stdio");
fi
# find root filesystem partition
local root_part_n
pvm_find_root_part_n "$imagefile" || return "$EXIT_FAILURE" # sets: $root_part_n
# if we're running a supported arch, enable kvm
if pvm_native_arch "$arch"; then qemu_args+=(-enable-kvm); fi
# find root filesystem partition (necessary for arches without bootloader)
local root_loopdev_n=$(echo $(parted "$imagefile" print 2> /dev/null | grep ext4) | cut -d ' ' -f 1)
local root_loopdev="$loopdev"p$root_loopdev_n
local root_vdev=/dev/vda$root_loopdev_n
if [[ -b "$root_loopdev" ]]
then
msg "found root filesystem loop device: %s" "$root_loopdev"
else
error "%s: unable to determine root filesystem loop device" "$imagefile"
return "$EXIT_FAILURE"
fi
# set arch-specific args
local kernel_console
local kernel_tty
case "$arch" in
i386|x86_64|ppc64)
qemu_args+=(-m $DEF_RAM_MB -hda "$imagefile")
# unmount the unneeded virtual drive early
pvm_umount ;;
arm)
kernel_console="console=tty0 console=ttyAMA0 "
qemu_args+=(-machine virt
-m $DEF_RAM_MB
-kernel "$workdir"/vmlinuz-${Kernel}
-initrd "$workdir"/initramfs-${Kernel}.img
-append "${kernel_console}rw root=${root_vdev}"
armv7h ) kernel_tty="console=tty0 console=ttyAMA0 " ;;
i686 ) kernel_tty=$( [[ -z "$DISPLAY" ]] && echo "console=ttyS0 " ) ;;
ppc64le) ;; # TODO:
riscv64) ;; # TODO:
x86_64 ) kernel_tty=$( [[ -z "$DISPLAY" ]] && echo "console=ttyS0 " ) ;;
esac
case "$arch" in
armv7h ) qemu_args+=(-machine virt
-kernel "$bootdir"/vmlinuz-${Kernel}
-initrd "$bootdir"/initramfs-${Kernel}.img
-append "${kernel_tty}rw root=/dev/vda$root_part_n"
-drive "if=none,file=${imagefile},format=raw,id=hd"
-device "virtio-blk-device,drive=hd"
-netdev "user,id=mynet"
-device "virtio-net-device,netdev=mynet") ;;
riscv64)
kernel_console=$( [ -z "$DISPLAY" ] && echo "console=ttyS0 " )
qemu_args+=(-machine virt
-m $DEF_RAM_MB
-kernel "$workdir"/bbl
-append "${kernel_console}rw root=/dev/vda"
-drive "file=${root_vdev},format=raw,id=hd0"
i686 ) qemu_args+=(-hda "$imagefile") ;;
ppc64le) qemu_args+=(-hda " $imagefile") ;;
riscv64) qemu_args+=(-machine virt
-kernel "$bootdir"/bbl
-append "${kernel_tty}rw root=/dev/vda"
-drive "file=/dev/vda$root_part_n,format=raw,id=hd0"
-device "virtio-blk-device,drive=hd0"
-object "rng-random,filename=/dev/urandom,id=rng0"
-device "virtio-rng-device,rng=rng0"
-netdev "user,id=usernet"
-device "virtio-net-device,netdev=usernet") ;;
*)
error "%s: unable to determine default qemu args" "$imagefile"
return "$EXIT_FAILURE" ;;
x86_64 ) qemu_args+=(-hda "$imagefile") ;;
esac
}
main() {
if [ "$(id -u)" -eq 0 ]; then
error "This program must be run as a regular user"
exit "$EXIT_NOPERMISSION"
fi
main() # ( [cli_options] imagefile qemu_args )
{
pvm_check_unprivileged # exits on failure
# parse options
while getopts 'hk:r' arg; do
@ -205,34 +143,40 @@ main() {
*) error "invalid argument: %s\n" "$arg"; usage >&2; exit "$EXIT_INVALIDARGUMENT" ;;
esac
done
local shiftlen=$(( OPTIND - 1 ))
shift $shiftlen
local imagefile="$1"
shift
local shiftlen=$(( OPTIND - 1 )) ; shift $shiftlen ;
local imagefile="$1" ; shift ;
local cli_args=$@
[ ! -n "$imagefile" ] && error "no image file specified" && exit "$EXIT_FAILURE"
[ ! -e "$imagefile" ] && error "image file not found: '%s'" "$imagefile" && exit "$EXIT_FAILURE"
[ ! -w "$imagefile" ] && error "image file not writable: %s" "$imagefile" && exit "$EXIT_FAILURE"
msg "initializing ...."
local workdir loopdev
pvm_mount || exit
local bootdir workdir loopdev
local arch
pvm_probe_arch || exit
if [ -z "$arch" ]; then
error "image arch is unknown: '%s'" "$arch"
exit "$EXIT_FAILURE"
fi
local qemu_cmd
local qemu_args=()
pvm_guess_qemu_args || exit
qemu_args+=("$@")
local was_error
pvm_mount || exit "$EXIT_FAILURE" # assumes: $imagefile , sets: $loopdev $bootdir $workdir
pvm_probe_arch || exit "$EXIT_FAILURE" # assumes: $bootdir $workdir $imagefile , sets: $arch
pvm_guess_qemu_cmd || exit "$EXIT_FAILURE" # assumes: $arch , sets: $qemu_cmd
pvm_guess_qemu_args || exit "$EXIT_FAILURE" # assumes: $qemu_args $imagefile $arch $bootdir , appends: $qemu_args
msg "booting VM ...."
(set -x; qemu-system-"$arch" "${qemu_args[@]}")
# unmount the virtual disks early, for images with a bootloader
[[ "$arch" =~ ^i686$|^x86_64$|^ppc64le$ ]] && pvm_cleanup
msg "booting the virtual machine ...."
(set -x; $qemu_cmd "${qemu_args[@]}" $cli_args) ; was_error=$? ;
# clean up the terminal, in case SeaBIOS did something weird
echo -n "[?7h"
pvm_umount
pvm_cleanup
(( ! $was_error )) && exit "$EXIT_SUCCESS" || exit "$EXIT_FAILURE"
}
main "$@"
if source /usr/lib/parabola-vmbootstrap/pvm-common.sh.inc 2> /dev/null || \
source "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"/pvm-common.sh.inc 2> /dev/null
then main "$@"
else echo "can not find pvm-common.sh.inc" && exit 1
fi

@ -20,10 +20,6 @@
###############################################################################
# shellcheck source=/usr/lib/libretools/messages.sh
source "$(librelib messages)"
# defaults
readonly PKG_SET_MIN='minimal'
readonly PKG_SET_STD='standard'
@ -34,16 +30,22 @@ readonly DEV_PKGS=('base' 'parabola-base' 'base-devel') ; readonly ROOT_MB_DEV=1
readonly DEF_PKGS=(${STD_PKGS[@]} ) ; readonly DEF_MIN_MB=$ROOT_MB_STD ;
readonly DEF_KERNEL='linux-libre' # ASSERT: must be 'linux-libre', per 'parabola-base'
readonly DEF_MIRROR=https://repo.parabola.nu
readonly DEF_ROOT_MB=64000
readonly DEF_ROOT_MB=32000
readonly DEF_BOOT_MB=100
readonly DEF_SWAP_MB=0
readonly MANDATORY_PKGS_ALL=( )
readonly MANDATORY_PKGS_armv7h=( haveged net-tools )
readonly MANDATORY_PKGS_i686=( haveged net-tools grub )
readonly MANDATORY_PKGS_ppc64le=( haveged net-tools )
readonly MANDATORY_PKGS_riscv64=( )
readonly MANDATORY_PKGS_x86_64=( haveged net-tools grub )
# misc
readonly THIS_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
readonly GUEST_CACHED_PKGS=('ca-certificates-utils')
readonly PVM_HOOKS_SUCCESS_MSG="[hooks.sh] pre-init hooks successful"
# options
BasePkgSet=$PKG_SET_STD
BasePkgSet=$DEF_PKG_SET
MinRootMb=$DEF_MIN_MB
Hooks=()
Kernels=()
@ -57,7 +59,8 @@ SwapSizeMb=$DEF_SWAP_MB
HasSwap=0
usage() {
usage()
{
print "USAGE:"
print " pvmbootstrap [-b <base-set>] [-h] [-H <hook>] [-k <kernel>] [-M <mirror>]"
print " [-O] [-p <package>] [-s <root_size>] [-S <swap_size>]"
@ -110,26 +113,25 @@ usage() {
echo " <https://git.parabola.nu/parabola-vmbootstrap.git>"
}
pvm_native_arch() {
local arch=$( [[ "$1" =~ arm.* ]] && echo 'armv7l' || echo "$1" )
pvm_bootstrap() # assumes: $arch $imagefile $loopdev $workdir , traps: INT TERM RETURN
{
# prompt to clobber if the target output file already exists
pvm_check_no_mounts || return "$EXIT_FAILURE"
mkdir -p "$(dirname "$imagefile")" || return "$EXIT_FAILURE"
pvm_prompt_clobber_file "$imagefile" || return "$EXIT_FAILURE"
setarch "$arch" /bin/true 2>/dev/null || return "$EXIT_FAILURE"
}
pvm_bootstrap() {
msg "starting creation of %s image: %s" "$arch" "$imagefile"
msg "starting build for %s image: %s" "$arch" "$imagefile"
# create the raw image file
local img_mb=$(( $BootSizeMb + $SwapSizeMb + $RootSizeMb ))
qemu-img create -f raw "$imagefile" "${img_mb}M" || return "$EXIT_FAILURE"
# prepare for cleanup
trap 'pvm_cleanup' INT TERM RETURN
trap 'pvm_bootstrap_cleanup' INT TERM RETURN
# mount the virtual disk
local workdir loopdev
workdir="$(mktemp -d -t pvm-rootfs-XXXXXXXXXX)" || return "$EXIT_FAILURE"
loopdev="$(sudo losetup -fLP --show "$imagefile")" || return "$EXIT_FAILURE"
local bootdir workdir loopdev
pvm_setup_loopdev || return "$EXIT_FAILURE" # sets: $bootdir $workdir $loopdev
sudo dd if=/dev/zero of="$loopdev" bs=1M count=8 || return "$EXIT_FAILURE"
# partition
@ -202,20 +204,19 @@ pvm_bootstrap() {
# setup qemu-user-static, if necessary
if ! pvm_native_arch "$arch"; then
# target arch can't execute natively, pacstrap is going to need help by qemu
local qemu_arch
case "$arch" in
armv7h) qemu_arch=arm ;;
* ) qemu_arch="$arch" ;;
esac
local qemu_user_static=$(sudo grep -l -F -e "interpreter /usr/bin/qemu-$qemu_arch-" \
local qemu_static=$(sudo grep -l -F -e "interpreter /usr/bin/qemu-$qemu_arch-" \
-r -- /proc/sys/fs/binfmt_misc 2>/dev/null | \
xargs -r sudo grep -xF 'enabled' )
if [[ -n "$qemu_user_static" ]]; then
msg "found qemu-user-static for %s" "$arch"
if [[ -n "$qemu_static" ]]; then
msg "found qemu-user-static for arch: '%s'" "$qemu_arch"
else
error "missing qemu-user-static for %s" "$arch"
error "missing qemu-user-static for arch: '%s'" "$qemu_arch"
return "$EXIT_FAILURE"
fi
@ -224,9 +225,8 @@ pvm_bootstrap() {
fi
# prepare pacstrap config
local pacconf repos
pacconf="$(mktemp -t pvm-pacconf-XXXXXXXXXX)" || return "$EXIT_FAILURE"
repos=(libre core extra community pcr)
local pacconf="$(mktemp -t pvm-pacconf-XXXXXXXXXX)" || return "$EXIT_FAILURE"
local repos=(libre core extra community pcr)
(( $IsNonsystemd )) && repos=('nonsystemd' ${repos[@]})
echo -e "[options]\nArchitecture = $arch" > "$pacconf"
for repo in ${repos[@]}; do echo "[$repo]" >> "$pacconf";
@ -235,14 +235,16 @@ pvm_bootstrap() {
# prepare package lists
local kernels=( ${Kernels[@]} )
local pkgs=( ${Pkgs[@]} ${Kernels[@]} ${OptPkgs[@]} )
local pkgs=( ${Pkgs[@]} ${Kernels[@]} ${OptPkgs[@]} ${MANDATORY_PKGS_ALL[@]} )
local pkgs_cached=( ${GUEST_CACHED_PKGS[@]} )
case "$arch" in
i686|x86_64) pkgs+=(grub) ;;
riscv64 ) ;;
* ) pkgs+=(haveged net-tools) ;;
armv7h ) pkgs+=( ${MANDATORY_PKGS_armv7h[@]} ) ;;
i686 ) pkgs+=( ${MANDATORY_PKGS_i686[@]} ) ;;
ppc64le) pkgs+=( ${MANDATORY_PKGS_ppc64le[@]} ) ;;
riscv64) pkgs+=( ${MANDATORY_PKGS_riscv64[@]} ) ;;
x86_64 ) pkgs+=( ${MANDATORY_PKGS_x86_64[@]} ) ;;
esac
(( $IsNonsystemd )) && && pkgs+=(libelogind)
(( $IsNonsystemd )) && [[ "$BasePkgSet" == "$PKG_SET_MIN" ]] && pkgs+=(libelogind)
(( ! $IsNonsystemd )) && [[ "${Hooks[@]}" =~ hook-ethernet-dhcp.sh ]] && pkgs+=(dhcpcd)
# minimize package lists
@ -254,6 +256,9 @@ pvm_bootstrap() {
msg "installing packages into the work chroot"
sudo pacstrap -GMc -C "$pacconf" "$workdir" "${pkgs[@]}" || return "$EXIT_FAILURE"
sudo pacstrap -GM -C "$pacconf" "$workdir" "${pkgs_cached[@]}" || return "$EXIT_FAILURE"
msg2 "creating a list of installed packages"
pacman -Sl -r "$workdir/" --config "$pacconf" | \
awk '/\[installed\]$/ {print $1 "/" $2 "-" $3}' > $(dirname $imagefile)/pkglist.txt
# create an fstab
msg "generating /etc/fstab"
@ -270,13 +275,16 @@ pvm_bootstrap() {
local hostname='parabola'
local lang='en_US.UTF-8'
msg "configuring system envoronment"
echo "/etc/hostname: " ; echo $hostname | sudo tee "$workdir"/etc/hostname ;
echo "/etc/locale.conf: " ; echo "LANG=$lang" | sudo tee "$workdir"/etc/locale.conf ;
echo -n "/etc/hostname: " ; echo $hostname | sudo tee "$workdir"/etc/hostname ;
echo -n "/etc/locale.conf: " ; echo "LANG=$lang" | sudo tee "$workdir"/etc/locale.conf ;
sudo sed -i "s/#${lang}/${lang}/" "$workdir"/etc/locale.gen
# install a boot loader
msg "installing boot loader"
case "$arch" in
armv7h)
msg2 "(armv7h has no boot loader)"
;;
i686|x86_64)
local grub_def_file="$workdir"/etc/default/grub
local grub_cfg_file=/boot/grub/grub.cfg
@ -292,26 +300,26 @@ pvm_bootstrap() {
sudo arch-chroot "$workdir" grub-install "$loopdev" || return "$EXIT_FAILURE"
sudo arch-chroot "$workdir" grub-mkconfig -o $grub_cfg_file || return "$EXIT_FAILURE"
;;
armv7h)
echo "(armv7h has no boot loader)"
ppc64le)
msg2 "(ppc64le has no boot loader)"
;;
riscv64)
# FIXME: for the time being, use fedora bbl to boot
warning "(riscv64 requires a blob - downloading it now)"
local bbl_url=https://fedorapeople.org/groups/risc-v/disk-images/bbl
sudo wget $bbl_url -O "$workdir"/boot/bbl || return "$EXIT_FAILURE"
;;
ppc64le)
# FIXME: what about ppc64le?
echo "(ppc64le has no boot loader)"
# FIXME: for the time being, use berkeley bootloader to boot
if [[ -f /usr/lib/parabola-vmbootstrap/bbl ]]; then
cp /usr/lib/parabola-vmbootstrap/bbl "$workdir"/boot/
else
error "riscv64 requires the berkeley bootloader from the 'parabola-vmbootstrap' package"
return "$EXIT_FAILURE"
fi
;;
esac
# regenerate the initcpio(s), skipping the autodetect hook
# regenerate the initcpio(s), to skip the 'autodetect' hook
for kernel in ${Kernels[@]}
do
local preset_file="$workdir"/etc/mkinitcpio.d/${kernel}.preset
local default_options="default_options=\"-S autodetect\""
msg "regenerating initcpio for kernel: '${kernel}'"
sudo cp "$preset_file"{,.backup} || return "$EXIT_FAILURE"
echo "$default_options" | sudo tee -a "$preset_file" > /dev/null || return "$EXIT_FAILURE"
@ -326,19 +334,16 @@ pvm_bootstrap() {
# push hooks into the image
msg "preparing hooks"
sudo mkdir -p "$workdir/root/hooks"
sudo mkdir -p "$workdir"/root/hooks
[ "${#Hooks[@]}" -eq 0 ] || sudo cp -v "${Hooks[@]}" "$workdir"/root/hooks/
(( $IsNonsystemd )) && sudo rm "$workdir"/root/hooks/hook-ethernet-dhcp.sh # systemd-only hook
# create a master hook script
local hooks_success_msg="[hooks.sh] pre-init hooks successful"
echo "hooks.sh:"
msg2 "hooks.sh:"
sudo tee "$workdir"/root/hooks.sh << EOF
#!/bin/bash
echo "[hooks.sh] boot successful - configuring ...."
systemctl disable preinit.service
# generate the locale
locale-gen
@ -355,19 +360,20 @@ for hook in /root/hooks/*; do
done
# clean up after yourself
systemctl disable preinit.service
rm -f /root/.bash_history
rm -rf /root/hooks
rm -f /root/hooks.sh
rm -f /usr/lib/systemd/system/preinit.service
rm -f /var/cache/pacman/pkg/*
rm -f /root/.bash_history
# report success :)
echo "$hooks_success_msg - powering off"
echo "$PVM_HOOKS_SUCCESS_MSG - powering off"
[[ -e "/usr/lib/libretools/common.sh" ]] && rm -f /usr/lib/libretools/common.sh
EOF
# create a pre-init service to run the hooks
echo "preinit.service:"
msg2 "preinit.service:"
sudo tee "$workdir"/usr/lib/systemd/system/preinit.service << 'EOF'
[Unit]
Description=Oneshot VM Preinit
@ -394,23 +400,18 @@ EOF
sudo arch-chroot "$workdir" systemctl enable preinit.service || return "$EXIT_FAILURE"
# unmount everything
pvm_cleanup
pvm_bootstrap_cleanup
}
pvm_bootstrap_preinit() # assumes: $imagefile
{
pvm_check_no_mounts || return "$EXIT_FAILURE"
# boot the machine to run the pre-init hooks
local pvmboot_cmd
local qemu_flags=(-no-reboot)
if [ -f "$THIS_DIR/pvmboot.sh" ]; then # in-tree
pvmboot_cmd=("$THIS_DIR/pvmboot.sh")
elif type -p pvmboot &>/dev/null; then # installed
pvmboot_cmd=('pvmboot')
else
error "pvmboot not available -- unable to run hooks"
return "$EXIT_FAILURE"
fi
pvmboot_cmd+=("$imagefile" "${qemu_flags[@]}")
[[ "$(pvm_get_pvmboot_cmd)" ]] && msg "booting the VM to run the pre-init hooks" || \
warning "unable to run pre-init hooks"
exec 3>&1
msg "booting the machine to run the pre-init hooks"
DISPLAY='' "${pvmboot_cmd[@]}" | tee /dev/fd/3 | grep -q -F "$hooks_success_msg"
pvm_boot "$imagefile" | tee /dev/fd/3 | grep -q -F "$PVM_HOOKS_SUCCESS_MSG"
local res=$?
exec 3>&-
! (( $res )) || error "%s: failed to complete preinit hooks" "$imagefile"
@ -418,28 +419,22 @@ EOF
return $res
}
pvm_cleanup() {
pvm_bootstrap_cleanup() # sets: $pacconf , untraps: INT TERM RETURN
{
trap - INT TERM RETURN
[ -n "${workdir}${loopdev}${pacconf}" ] && msg "cleaning up"
[[ "${workdir}${pacconf}" ]] && msg "cleaning up"
[[ -n "$workdir" ]] && sudo rm -f "$workdir"/usr/bin/qemu-*C
[[ -n "$pacconf" ]] && rm -f "$pacconf"
pvm_cleanup || return "$EXIT_FAILURE"
if [ -n "$workdir" ]; then
sudo rm -f "$workdir"/usr/bin/qemu-*C
sudo umount -R "$workdir" 2> /dev/null
rmdir "$workdir"
fi
if [ -n "$loopdev" ]; then sudo losetup -d "$loopdev"; fi;
if [ -n "$pacconf" ]; then rm -f "$pacconf"; fi;
unset workdir
unset loopdev
unset pacconf
}
main() {
if [ "$(id -u)" -eq 0 ]; then
error "This program must be run as a regular user"
exit "$EXIT_NOPERMISSION"
fi
main() # ( [cli_options] imagefile arch )
{
pvm_check_unprivileged # exits on failure
# parse options
while getopts 'b:hH:k:M:Op:s:S:' arg; do
@ -450,28 +445,19 @@ main() {
Pkgs=(${STD_PKGS[@]}) ; MinRootMb=$ROOT_MB_STD ;;
$PKG_SET_DEV) BasePkgSet=$OPTARG ; Kernels+=($DEF_KERNEL) ;
Pkgs=(${DEV_PKGS[@]}) ; MinRootMb=$ROOT_MB_DEV ;;
* ) warning "%s: invalid base set" "$OPTARG" ;;
* ) warning "invalid base set: %s" "$OPTARG" ;;
esac ;;
h) usage; return "$EXIT_SUCCESS" ;;
H) if [ -e "$THIS_DIR/hooks/hook-$OPTARG.sh" ]; then # in-tree
Hooks+=("$THIS_DIR/hooks/hook-$OPTARG.sh")
elif [ -e "/usr/lib/libretools/pvmbootstrap/hook-$OPTARG.sh" ]; then # installed
Hooks+=("/usr/lib/libretools/pvmbootstrap/hook-$OPTARG.sh")
elif [ -e "$OPTARG" ]; then
Hooks+=("$OPTARG")
else
warning "%s: hook does not exist" "$OPTARG"
fi ;;
H) Hooks+=( "$(pvm_get_hook $OPTARG)" ) ;;
k) Kernels+=($OPTARG) ;;
M) Mirror="$OPTARG" ;;
O) IsNonsystemd=0 ;; # TODO:
p) OptPkgs+=($OPTARG) ;;
s) RootSizeMb="$(sed 's|[^0-9]||g' <<<$OPTARG)" ;;
S) SwapSizeMb="$(sed 's|[^0-9]||g' <<<$OPTARG)" ;;
*) error "invalid argument: %s\n" "$arg"; usage >&2; exit "$EXIT_INVALIDARGUMENT";;
*) error "invalid option: '%s'" "$arg" ; usage >&2 ; exit "$EXIT_INVALIDARGUMENT" ;;
esac
done
local shiftlen=$(( OPTIND - 1 ))
shift $shiftlen
local imagefile="$1"
@ -484,32 +470,34 @@ main() {
RootSizeMb=$(( $RootSizeMb + (${#Kernels[@]} * 75) ))
HasSwap=$( (( $SwapSizeMb > 0 )) && echo 1 || echo 0 )
msg "making $arch image: $imagefile"
# determine if the target arch is supported
case "$arch" in
i686|x86_64|armv7h) ;;
ppc64le|riscv64 ) warning "arch %s is experimental" "$arch" ;;
* ) error "arch %s is unsupported" "$arch"
ppc64le|riscv64 ) warning "arch is experimental: %s" "$arch" ;;
* ) error "arch is unsupported: %s" "$arch"
exit "$EXIT_INVALIDARGUMENT" ;;
esac
# determine whether the target output file already exists
if [ -e "$imagefile" ]; then
warning "%s: file exists. Continue? [y/N]" "$imagefile"
read -p " " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
# create the virtual machine
if pvm_bootstrap; then
if pvm_bootstrap_preinit; then
msg "bootstrap complete for image: %s" "$imagefile"
exit "$EXIT_SUCCESS"
else
error "bootstrap complete, but preinit failed for image: %s" "$imagefile"
exit "$EXIT_FAILURE"
fi
rm -f "$imagefile" || exit
fi
# create the virtual machine
if ! pvm_bootstrap; then
else
error "bootstrap failed for image: %s" "$imagefile"
exit "$EXIT_FAILURE"
fi
msg "bootstrap complete for image: %s" "$imagefile"
}
main "$@"
if source /usr/lib/parabola-vmbootstrap/pvm-common.sh.inc 2> /dev/null || \
source "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"/pvm-common.sh.inc 2> /dev/null
then main "$@"
else echo "can not find pvm-common.sh.inc" && exit 1
fi