diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b0da791 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +state/ +images/ +ssh.conf \ No newline at end of file diff --git a/README.md b/README.md index ec7ac13..3479662 100644 --- a/README.md +++ b/README.md @@ -1 +1,53 @@ # VM Tool + +This script sets up multiple temporary VMs using QEMU. They run in the background, are on the same network as one another, and can be accessed via SSH. + +## Usage + +Ensure QEMU and `mkisofs` are installed. On Debian-like systems: + +```sh +sudo apt install qemu-system genisoimage +``` + +Download a base image: + +```sh +mkdir -p images +curl -C - -Lo images/Rocky-9-GenericCloud-Base.latest.x86_64.qcow2 https://dl.rockylinux.org/pub/rocky/9/images/x86_64/Rocky-9-GenericCloud-Base.latest.x86_64.qcow2 +``` + +Create the VMs (by default, two VMs are created): + +```sh +./vmtool.sh create +``` + +*Creating VMs will destroy any previous VMs first.* + +Once the VMs are running, access them via SSH: + +```sh +ssh -F ssh.conf node0 +ssh -F ssh.conf node1 +``` + +The VMs are on the same subnet, starting with ip `192.168.30.10` for `node0`, `192.168.30.11` for `node1`, and so on. They can reach one another: + +```console +[root@node0 ~]# ping 192.168.30.11 +PING 192.168.30.11 (192.168.30.11) 56(84) bytes of data. +64 bytes from 192.168.30.11: icmp_seq=1 ttl=64 time=0.465 ms +64 bytes from 192.168.30.11: icmp_seq=2 ttl=64 time=0.390 ms +64 bytes from 192.168.30.11: icmp_seq=3 ttl=64 time=0.684 ms +``` + +When done, you can stop and/or destroy the VMs: + +```sh +./vmtool.sh destroy +``` + +## Configuration + +Check the environment variables at the top of the script to set memory, CPU count, max disk size, number of nodes, etc. diff --git a/vmtool.sh b/vmtool.sh new file mode 100755 index 0000000..f26432f --- /dev/null +++ b/vmtool.sh @@ -0,0 +1,175 @@ +#!/bin/bash + +# shellcheck disable=SC2155 +# shellcheck disable=SC2164 +readonly script_path="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" # https://stackoverflow.com/a/4774063 + +# Location of the SSH config to be generated +SSH_CONFIG=${SSH_CONFIG:-"$script_path/ssh.conf"} + +NUM_NODES=${NUM_NODES:-"2"} +MEMORY=${MEMORY:-"2048"} +CPU=${CPU:-"2"} +DISK_MAX_SIZE=${DISK_MAX_SIZE:-"20G"} + +# Location of VM state information, including its persistent disk +STATE_DIR=${STATE_DIR:-"$script_path/state"} + +# Location of Linux cloud image +BASE_IMAGE_FILENAME=${BASE_IMAGE_FILENAME:-"$script_path/images/Rocky-9-GenericCloud-Base.latest.x86_64.qcow2"} + +# Architecture that QEMU will emulate +EMU_ARCH=${EMU_ARCH:-"x86_64"} + +# Check for prerequisites +if ! command -v mkisofs > /dev/null 2>&1; then + echo "mkisofs not found." + exit 1 +fi + +if ! command -v qemu-system-"$EMU_ARCH" > /dev/null 2>&1; then + echo "qemu-system-$EMU_ARCH not found." + exit 1 +fi + +start() { + for ((i=0;i/dev/null 2>&1 && ((attempts_remaining > 0)); + do + ((attempts_remaining--)) + if [ $attempts_remaining == 0 ]; then + echo "SSH service on node$i took too long to be ready" + exit 1 + fi + echo "Waiting for SSH service on node$i to be ready; $attempts_remaining attempts remaining" + sleep 5 + done + echo "SSH service on node$i is up" + done + + echo "To access a VM, run:" + echo + echo " ssh -F $SSH_CONFIG node#" + echo + echo "Where # is the node number you want to reach." +} + +stop() { + for ((i=0;i/dev/null 2>&1 + rm -f "$STATE_DIR"/node$i.pid + done +} + +destroy() { + stop + + rm -f "$STATE_DIR"/id_ed25519 + rm -f "$STATE_DIR"/id_ed25519.pub + rm -f "$SSH_CONFIG" + + for ((i=0;i/dev/null 2>&1 + ssh_pubkey=$(tr -d '\n' < "$STATE_DIR"/id_ed25519.pub) + + for ((i=0;i "$STATE_DIR"/user-data <<- EOF + #cloud-config + + users: + - name: root + ssh_authorized_keys: + - $ssh_pubkey + EOF + + cat > "$STATE_DIR"/meta-data <<- EOF + instance-id: node$i + local-hostname: node$i + EOF + + cat > "$STATE_DIR"/network-config <<- EOF + network: + version: 2 + ethernets: + eth0: + match: + macaddress: "52:54:00:00:00:$(printf '%02x' $i)" + dhcp4: false + addresses: + - 192.168.30.$((10 + i))/24 + EOF + + mkisofs -o "$STATE_DIR"/node$i-cloudinit.iso -V cidata -r -J "$STATE_DIR"/user-data "$STATE_DIR"/meta-data "$STATE_DIR"/network-config >/dev/null 2>&1 + + rm "$STATE_DIR"/user-data + rm "$STATE_DIR"/meta-data + rm "$STATE_DIR"/network-config + + # Create main disk, backed by read-only cloud image + qemu-img create -f qcow2 -F qcow2 -b "$BASE_IMAGE_FILENAME" "$STATE_DIR"/node$i.qcow2 20G >/dev/null 2>&1 + + # Create SSH config to be used for accessing the VM + cat >> "$SSH_CONFIG" <<- EOF + Host node$i + StrictHostKeyChecking no + UserKnownHostsFile /dev/null + IdentityFile $STATE_DIR/id_ed25519 + IdentitiesOnly yes + User root + Hostname localhost + Port $((3100 + i)) + EOF + done + + start +} + +case $1 in + stop) + shift + stop "$@" + ;; + destroy) + shift + destroy "$@" + ;; + create) + shift + create "$@" + ;; +esac