diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9016f3f --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*.iso +*.img +*.cow +*.cow2 +*.log +mnt diff --git a/README.md b/README.md index fb99dfa..1d22359 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,73 @@ # qemu-arch-linux-automation -Scripts and playbooks to automate the installation, configuration and management of Arch Linux VMs in qemu + +Scripts and playbooks to automate the installation, configuration and +management of Arch Linux VMs in qemu. + +## Dependencies + +- `qemu` +- `curl` +- `expect` + +## Installation + +- Add the path to the `src` directory to your `PATH` environment variable +- Create a symbolic link to `src/` directory to your `PATH` environment variable + +## Usage + +### Install an Arch Linux virtual machine + +```text +Usage: qemu-arch install [-o ] [-a ] [-s ] + [-m ] [-h ] [-P ] [-u ] + [-p ] [-z ] [-l ] [-M ] + +-o Path of the output disk image (default: ./arch.img) +-a Target architecture (default: x86_64) +-s Disk size (default: 8G) +-m RAM size in KB (default: 2048) +-h VM hostname (default: qemu) +-P Root password. If not specified it will be prompted +-u Username for the main non-root user +-p Password for the non-root user. If not specified + it will be prompted +-z System timezone (default: UTC) +-l System locale (default: en_US.UTF-8) +-M Arch Linux download mirror URL + (default: http://mirror.cj2.nl/archlinux/iso/latest/) + Consult https://archlinux.org/download/ for a + full list of the available download mirrors. +``` + +If you want to install an extra list of packages besides the default ones, then +specify them in a file named `PKGLIST` in the same directory as the disk image file. + +If you want to run a custom post-installation script after the core system has been +installed, then create a custom script named `post-install.sh` in the same directory +as the disk image file. + +#### Notes + +The keyring population process may currently (as of March 2022) take a long time. +This is a [known issue](https://www.reddit.com/r/archlinux/comments/rbjbcr/pacman_keyring_update_taking_too_long/). + +As a workaround, if you want to speed up the OS installation process, you can +temporarily disable pacman keyring checks upon package installation by +uncommenting the relevant lines in `src/helpers/install.sh` (function: +`install_os`). + +### Resizing an existing image + +```bash +qemu-img resize "$imgfile" +10G +``` + +### Create a COW (Copy-On-Write) image on top of a disk image + +```bash +qemu-img create -o backing_file="$imgfile",backing_fmt=raw -f qcow2 img1.cow +``` + +This is particularly useful if you want to have a "base" image and several customized +images built on it. diff --git a/src/actions/install.sh b/src/actions/install.sh new file mode 100755 index 0000000..753fef7 --- /dev/null +++ b/src/actions/install.sh @@ -0,0 +1,109 @@ +export default_imgfile=arch.img +export default_architecture=x86_64 +export default_disk_size=8G +export default_memory=2048 +export default_hostname=qemu +export default_root_password=root +export default_username=user +export default_user_password=password +export default_timezone=UTC +export default_locale=en_US.UTF-8 +export default_img_download_page='http://mirror.cj2.nl/archlinux/iso/latest/' +export isofile=archlinux-latest.iso + +imgfile= +architecture= +disk_size= +memory= +hostname= +root_password= +username= +user_password= +timezone= +locale="$default_locale" +img_download_page="$default_img_download_page" + +function usage() { + echo "Install an Arch Linux system on a QEMU disk image" + echo + echo "Usage: $(basename "$0") install [-o ] [-a ] [-s ]" + echo -e "\t[-m ] [-h ] [-P ] [-u ]" + echo -e "\t[-p ] [-z ] [-l ] [-M ]" + echo + echo -e "-o \t\tPath of the output disk image (default: ./arch.img)" + echo -e "-a \t\tTarget architecture (default: x86_64)" + echo -e "-s \t\t\tDisk size (default: 8G)" + echo -e "-m \t\t\tRAM size in KB (default: 2048)" + echo -e "-h \t\t\tVM hostname (default: qemu)" + echo -e "-P \t\tRoot password. If not specified it will be prompted" + echo -e "-u \t\tUsername for the main non-root user" + echo -e "-p \tPassword for the non-root user. If not specified it will be prompted" + echo -e "-z \t\t\tSystem timezone (default: UTC)" + echo -e "-l \t\t\tSystem locale (default: en_US.UTF-8)" + echo -e "-M \t\tArch Linux download mirror URL (default: http://mirror.cj2.nl/archlinux/iso/latest/)" + echo -e "\t\t\t\tConsult https://archlinux.org/download/ for a full list of the available download mirrors." + echo + echo "If you want to install an extra list of packages besides the default ones, then" + echo "specify them in a file named PKGLIST in the same directory as the disk image file." + echo + echo "If you want to run a custom post-installation script after the core system has been" + echo "installed, then create a custom script named post-install.sh in the same directory as" + echo "the disk image file". + exit 1 +} + + +optstring=':o:a:s:m:h:P:u:p:z:l:M:' +[[ "$1" == '--help' ]] && usage + +while getopts ${optstring} arg; do + case ${arg} in + o) imgfile="${OPTARG}";; + a) architecture="${OPTARG}";; + s) disk_size="${OPTARG}";; + m) memory="${OPTARG}";; + h) hostname="${OPTARG}";; + P) root_password="${OPTARG}";; + u) username="${OPTARG}";; + p) user_password="${OPTARG}";; + z) timezone="${OPTARG}";; + l) locale="${OPTARG}";; + M) img_download_page="${OPTARG}";; + ?) + echo "Invalid option: -${OPTARG}" >&2 + usage;; + esac +done + +[ -z "$imgfile" ] && read -p "Output disk image file [$default_imgfile]: " imgfile +[ -z "$architecture" ] && read -p "Architecture [$default_architecture]: " architecture +[ -z "$disk_size" ] && read -p "Disk size [$default_disk_size]: " disk_size +[ -z "$memory" ] && read -p "Memory in KB [$default_memory]: " memory +[ -z "$hostname" ] && read -p "Hostname [$default_hostname]: " hostname +[ -z "$root_password" ] && read -sp "Root password [$default_root_password]: " root_password && echo +[ -z "$username" ] && read -p "Non-admin username [$default_username]: " username +[ -z "$user_password" ] && read -sp "Non-admin user password [$default_user_password]: " user_password && echo +[ -z "$timezone" ] && read -p "Timezone [$default_timezone]: " timezone +[ -z "$locale" ] && read -p "Locale [$default_locale]: " locale + +for var in imgfile \ + architecture \ + disk_size \ + memory \ + hostname \ + root_password \ + username \ + user_password \ + timezone \ + locale + do + default_var=default_$var + [ -z "${!var}" ] && declare ${var}=${!default_var} + export $var + done + +source "$srcdir/helpers/install.sh" +download_latest_arch_iso +create_disk_image +install_os + diff --git a/src/helpers/common.sh b/src/helpers/common.sh new file mode 100644 index 0000000..5b49080 --- /dev/null +++ b/src/helpers/common.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +export GREEN='\033[0;32m' +export RED='\033[0;31m' +export WHITE='\033[0;37m' +export NORMAL='\033[0m' + diff --git a/src/helpers/install.sh b/src/helpers/install.sh new file mode 100644 index 0000000..eefbf6b --- /dev/null +++ b/src/helpers/install.sh @@ -0,0 +1,285 @@ +function download_latest_arch_iso() { + latest_iso=$( + curl -s "$img_download_page" | + grep -e '' | + head -1 | + sed -r -e 's/^.*.*/\1/' + ) + + if [ ! -f "$latest_iso" ]; then + echo -e "${GREEN}Downloading the latest Arch Linux ISO image${NORMAL}" + rm -f archlinux*.iso + curl -o "${latest_iso}" "${img_download_page}/${latest_iso}" + ln -sf "$latest_iso" "$isofile" + else + echo -e "${GREEN}Latest Arch Linux image already downloaded${NORMAL}" + fi +} + +function create_disk_image() { + # Create a backing COW image + # qemu-img create -o backing_file="$imgfile",backing_fmt=raw -f qcow2 img1.cow + + if [ ! -f "$imgfile" ]; then + echo -e "${GREEN}Creating base disk image${NORMAL}" + qemu-img create -f raw "$imgfile" $disk_size + else + echo -e "${RED}The base disk image file $imgfile already exists.${NORMAL}" + echo -e "${RED}Remove it and re-run this script if you intend to run a new fresh installation.${NORMAL}" + fi +} + +function _get_ssh_key_name() { + keyfiles=$(cat < "$logfile" + expect <> /mnt/etc/fstab\r" + expect \$prompt + + # chroot to the newly installed system + send -- "arch-chroot /mnt\r" + expect \$chroot_prompt + + # Set the timezone and sync the clock + send -- "ln -sf /usr/share/zoneinfo/$timezone /etc/localtime\r" + expect \$chroot_prompt + send -- "hwclock --systohc\r" + expect \$chroot_prompt + + # Configure localization + send -- "echo $locale UTF-8 >> /etc/locale.gen\r" + expect \$chroot_prompt + send -- "locale-gen\r" + expect \$chroot_prompt + send -- "echo LANG=en_US.UTF-8 > /etc/locale.conf\r" + expect \$chroot_prompt + + # Set the hostname + send -- "echo $hostname > /etc/hostname\r" + expect \$chroot_prompt + + # Generate /etc/hosts + send -- "echo -e '127.0.0.1 localhost\\n::1 localhost' >> /etc/hosts\r" + expect \$chroot_prompt + + # Generate the initpcio + send -- "mkinitcpio -P\r" + expect \$chroot_prompt + + # Update the keyring + # This may currently currently take a long time + # see https://www.reddit.com/r/archlinux/comments/rbjbcr/pacman_keyring_update_taking_too_long/ + send -- "pacman-key --init\r" + expect \$chroot_prompt + send -- "pacman-key --populate archlinux\r" + expect \$chroot_prompt + send -- "pacman-key --refresh-keys\r" + expect \$chroot_prompt + + # As a workaround, you can temporarily disable signature check on pacman.conf + #send -- "cp /etc/pacman.conf /etc/pacman.conf.orig\r" + #expect \$chroot_prompt + #send -- "sed -i /etc/pacman.conf -r -e 's/^(SigLevel\\\\s*=\\\\s*).*$/\\\\1 Never/g'\r" + #expect \$chroot_prompt + + # Install extra packages + send -- {echo "$packages" | pacman -S --noconfirm -} + send -- "\r" + expect \$chroot_prompt + + # Install syslinux + send -- "pacman -S --noconfirm syslinux\r" + expect \$chroot_prompt + send -- "syslinux-install_update -i -a -m\r" + #expect \$chroot_prompt + #send -- "dd bs=440 count=1 conv=notrunc if=/usr/lib/syslinux/bios/mbr.bin of=/dev/sda\r" + expect \$chroot_prompt + send -- "sed -i /boot/syslinux/syslinux.cfg -e '1i SERIAL 0 115200'\r" + expect \$chroot_prompt + send -- "sed -i /boot/syslinux/syslinux.cfg -e 's|APPEND root=/dev/sda3|APPEND console=tty0 console=ttyS0,115200 root=/dev/sda1|g'\r" + expect \$chroot_prompt + + # Restore the original pacman.conf + send -- "mv /etc/pacman.conf.orig /etc/pacman.conf\r" + expect \$chroot_prompt + + # Set the root password + send -- "passwd\r" + expect "New password: " + send -- "$root_password\r" + expect "Retype new password: " + send -- "$root_password\r" + expect \$chroot_prompt + + # Create a non-admin user + send -- "useradd -d /home/$username -m $username\r" + expect \$chroot_prompt + send -- "passwd $username\r" + expect "New password: " + send -- "$user_password\r" + expect "Retype new password: " + send -- "$user_password\r" + expect \$chroot_prompt + + # Enable the dhcpcd service + send -- "ln -sf /usr/lib/systemd/system/dhcpcd.service /etc/systemd/system/multi-user.target.wants/dhcpcd.service\r" + expect \$chroot_prompt + + # Enable and configure the SSH daemon + send -- "ln -sf /usr/lib/systemd/system/sshd.service /etc/systemd/system/multi-user.target.wants/sshd.service\r" + expect \$chroot_prompt + send -- "cat <<_EOF_ >> /etc/ssh/sshd_config +RSAAuthentication yes +PubkeyAuthentication yes +_EOF_ +" + + expect \$chroot_prompt + + # Copy the user SSH key + send -- "mkdir -p /home/$username/.ssh\r" + expect \$chroot_prompt + send -- {echo "$ssh_pubkey" >> "/home/$username/.ssh/authorized_keys"} + send -- "\r" + expect \$chroot_prompt + send -- {echo "$ssh_privkey" > "/home/$username/.ssh/$(basename $ssh_keyfile)"} + send -- "\r" + expect \$chroot_prompt + send -- {chmod 0600 "/home/$username/.ssh/$(basename $ssh_keyfile)"} + send -- "\r" + expect \$chroot_prompt + send -- {echo "$ssh_pubkey" > "/home/$username/.ssh/$(basename $ssh_keyfile).pub"} + send -- "\r" + expect \$chroot_prompt + send -- {chown -R $username "/home/$username/.ssh"} + send -- "\r" + expect \$chroot_prompt + + # Run any post-install scripts + send -- {$post_install} + send -- "\r" + expect \$chroot_prompt + + # Clear the pacman cache + send -- "rm -rf /var/cache/pacman/pkg/*\r" + expect \$chroot_prompt + + # Exit and shutdown + send -- "exit\r" + expect \$prompt + send -- "umount /mnt\r" + expect \$prompt + send -- "shutdown -h now\r" + expect eof + + system {echo -e "\n${GREEN}Arch Linux system installed under ${imgfile}${NORMAL}"} +EOF + + echo "--- Log closed at $(date)" >> "$logfile" +} + diff --git a/src/qemu-arch b/src/qemu-arch new file mode 100755 index 0000000..1d664dd --- /dev/null +++ b/src/qemu-arch @@ -0,0 +1,31 @@ +#!/bin/bash + +function usage() { + actions=$(find "$srcdir/actions" -maxdepth 1 -name '*.sh' | xargs basename | cut -d. -f1) + cat < + +Run $(basename "$0") --help for more details. +Available actions: + +$actions +EOF + + exit 1 +} + +if [ -L "$0" ]; then + export srcdir="$(dirname "$(readlink -f "$0")")" +else + export srcdir="$(cd "$(dirname "$0")" && pwd)" +fi + +source "$srcdir/helpers/common.sh" +action=$1 +shift + +case "$action" in + install) source "$srcdir/actions/install.sh";; + *) usage;; +esac +