Перейти к содержанию

Скрипт автоматизации создания виртуальных машин в KVM/QEMU

Решил набросать небольшой скрипт для упрощения создания виртуальных машин при помощи Debootstrap. Привожу здесь исходный код.

#!/bin/bash
#
# Create a virtual machine based on debootstrap.
#
# Usage:
# create-vm.sh 
#  VM_NAME      : virtual machine name
#  DISK_SIZE_GB : [1-200] in gigabytes
#  BLK_DEVICE   : /dev/nbdX should be used
#  VM_IP_ADDR   : virtual machine IP-address (this script dont check an IP existance)
#  VM_NETMASK   : netmask
#  VM_GATEWAY   : gateway
#  RAM_KB       : [1024-4096] in megabytes
#  VNC_PORT     : [5900-5999]
#  BRIDGE       : virtual bridge ID (e.g. br0)
#

EXIT_SUCCESS=0
EXIT_ERROR=1

MSG_USAGE='usage: '"$0"' VM_NAME DISK_SIZE_GB BLK_DEVICE VM_IP_ADDR VM_NETMASK VM_GATEWAY RAM_KB VNC_PORT BRIDGE'

str_eandu() {
  CLEARED="`expr $1 : '[.]*\([a-zA-Z0-9.-]*\)'`"
  if [ "$CLEARED" != $1 ]; then
    return "$EXIT_ERROR"
  fi
  return "$EXIT_SUCCESS"
}

ip_check() {
  MIN=1
  MAX=253
  if [ "$#" -eq 3 ]; then
    MIN="$2"
    MAX="$3"
  fi
  IP_ADDR=$(echo $1 | tr '.' ' ')
  for OCTET in $IP_ADDR; do
    if ! [[ "$OCTET" =~ ^-?[0-9]+$ ]]; then
      exit_error "$VM_IP_ADDR"': '"$OCTET"' is NOT an integer'
    else
      if [ "$OCTET" -lt "$MIN" ] || [ "$OCTET" -gt "$MAX" ]; then
        exit_error "$VM_IP_ADDR"': '"$OCTET is bad"
      fi
    fi
  done
}

integer_check() {
  if ! [[ "$1" =~ ^-?[0-9]+$ ]]; then
    exit_error "$1"' is NOT an integer'
  fi
}

exit_error() {
  >&2 echo "$1"
  exit "$EXIT_ERROR"
}

if [ "$#" == 0 ] || [ "$#" -lt 9 ]; then
  exit_error "$MSG_USAGE"
fi

IMG_TYPE="qcow2"
VM_ROOT="/hdd"

VM_NAME="$1"

DISK_SIZE_MIN=8
DISK_SIZE_MAX=200
DISK_SIZE_GB="$2"
DISK_FULL_FILENAME="${VM_ROOT}/${1}.${IMG_TYPE}"

BLK_DEVICE="$3"

VM_IP_ADDR="$4"
VM_NETMASK="$5"
VM_GATEWAY="$6"

RAM_MIN=1024
RAM_MAX=4096
RAM_KB="$7"

let SWAP_SIZE=RAM_KB/2
EFI_PART_SIZE=10
let SWAP_SIZE_OFFSET=SWAP_SIZE+EFI_PART_SIZE

VNC_PORT_MIN=5900
VNC_PORT_MAX=5999
VNC_PORT="$8"

BRIDGE="$9"

#
# proxy settings
#
# use: export http_proxy="http://username:password@proxy-ip:proxy-port"
VM_PROXY="# no proxy"
if [ ! "$http_proxy" == "" ]; then
  VM_PROXY='Acquire::http::Proxy \"'"$http_proxy"'\";'
fi

#
# kernel module load
#
NBD=$(lsmod | grep nbd)
if [ -z "$NBD" ]; then
  modprobe nbd
fi

#
# VM name check
#
if ! str_eandu "$VM_NAME" -eq 0; then
  exit_error 'VM name '"$VM_NAME"' is not valid'
fi

#
# VM existance check
#
IS_ANY_VM_EXISTS=$(virsh list --name)
if [ ! "$IS_ANY_VM_EXISTS" == "" ]; then
  IS_VM_EXISTS=$(virsh list --name | grep -e ^${VM_NAME}$)
  if [ ! "$IS_VM_EXISTS" == "" ]; then
    exit_error 'VM '"$VM_NAME"' is exists'
  fi
fi

#
# VM image file check
#
if [ -f "$DISK_FULL_FILENAME" ]; then
  exit_error "$DISK_FULL_FILENAME"' is already exists'
fi
integer_check "$DISK_SIZE_GB"
if [ "$DISK_SIZE_GB" -lt "$DISK_SIZE_MIN" ] || [ "$DISK_SIZE_GB" -gt "$DISK_SIZE_MAX" ]; then
  exit_error "$DISK_SIZE_GB"' is invalid value'
fi
#
# value "$DISK_SIZE_GB"G or "${DISK_SIZE_GB}G" should be used later
#

#
# block device check
#
if [[ $(file "$BLK_DEVICE") != *"block special"* ]]; then
  exit_error "$BLK_DEVICE"' is NOT a block device'
fi
DEVICE_NAME=$(basename ${BLK_DEVICE} | grep ^nbd)
if [ "$DEVICE_NAME" == "" ]; then
  exit_error "$BLK_DEVICE"' is not supported (should be /dev/nbd)'
fi

#
# VM IP-address check
#
# Warning! There is no IP-address existance checking here
#
ip_check "$VM_IP_ADDR"
ip_check "$VM_NETMASK" 0 255
ip_check "$VM_GATEWAY"

#
# RAM size check
#
integer_check "$RAM_KB"
if [ "$RAM_KB" -lt "$RAM_MIN" ] || [ "$RAM_KB" -gt "$RAM_MAX" ]; then
  exit_error "$RAM_KB"' is invalid value'
fi

#
# VM VNC port check
#
integer_check "$VNC_PORT"
if [ "$VNC_PORT" -lt "$VNC_PORT_MIN" ] || [ "$VNC_PORT" -gt "$VNC_PORT_MAX" ]; then
  exit_error "$VNC_PORT"' is invalid value'
fi
virsh list --name --all | while read VM ; do
  if [ ! "$VM" == "" ]; then
    RESULT=$(virsh vncdisplay ${VM} 2>/dev/null)
    if [ ! "$RESULT" == "" ]; then
      CUR_PORT=$(echo ${RESULT} | awk -F ':' '{print $2}')
      let FULL_PORT=CUR_PORT+5900
      if [ "$VNC_PORT" == "$FULL_PORT" ]; then
        exit_error "$VNC_PORT is already used"
      fi
    fi
  fi
done

#
# bridge check
#
if [ "$(ip a | grep ' '${BRIDGE}:)" == "" ]; then
  exit_error "Bridge $BRIDGE check error"
fi

#
# additional check
#
if [ "$?" -ne 0 ]; then
  exit_error "Some errors before image creating"
fi

#
# start VM image creating
#

qemu-img create -f "$IMG_TYPE" "$DISK_FULL_FILENAME" "$DISK_SIZE_GB"G
if [ "$?" -eq 1 ]; then
  exit_error "$DISK_FULL_FILENAME creating error"
fi

qemu-nbd -c "$BLK_DEVICE" "$DISK_FULL_FILENAME" 2>/dev/null
if [ "$?" -eq 1 ]; then
  rm -rf "$DISK_FULL_FILENAME"
  exit_error "Device $BLK_DEVICE is already used"
fi

# Set partition table to GPT (UEFI)
parted "$BLK_DEVICE" --script mktable gpt
if [ "$?" -ne 0 ]; then
  qemu-nbd --disconnect "$BLK_DEVICE"
  rm -rf "$DISK_FULL_FILENAME"
  exit_error "Parted: GPT creating error"
fi

# Create EFI partition
parted "$BLK_DEVICE" --script mkpart EFI fat16 1MiB "$EFI_PART_SIZE"MiB
if [ "$?" -ne 0 ]; then
  qemu-nbd --disconnect "$BLK_DEVICE"
  rm -rf "$DISK_FULL_FILENAME"
  exit_error "Parted: EFI part creating error"
fi
parted "$BLK_DEVICE" --script set 1 msftdata on

# Create SWAP partition
parted "$BLK_DEVICE" --script mkpart linux-swap "$EFI_PART_SIZE"MiB "$SWAP_SIZE_OFFSET"MiB
if [ "$?" -ne 0 ]; then
  qemu-nbd --disconnect "$BLK_DEVICE"
  rm -rf "$DISK_FULL_FILENAME"
  exit_error "Parted: SWAP part creating error"
fi

# Create OS partition
parted "$BLK_DEVICE" --script mkpart LINUX ext4 "$SWAP_SIZE_OFFSET"MiB 100%
if [ "$?" -ne 0 ]; then
  qemu-nbd --disconnect "$BLK_DEVICE"
  rm -rf "$DISK_FULL_FILENAME"
  exit_error "Parted: EXT4 (rootfs) part creating error"
fi

# Format partitions
mkfs.vfat -n EFI "$BLK_DEVICE"p1
if [ "$?" -ne 0 ]; then
  qemu-nbd --disconnect "$BLK_DEVICE"
  rm -rf "$DISK_FULL_FILENAME"
  exit_error "EFI format error"
fi
mkswap "$BLK_DEVICE"p2
if [ "$?" -ne 0 ]; then
  qemu-nbd --disconnect "$BLK_DEVICE"
  rm -rf "$DISK_FULL_FILENAME"
  exit_error "SWAP format error"
fi
mkfs.ext4 -L LINUX "$BLK_DEVICE"p3
if [ "$?" -ne 0 ]; then
  qemu-nbd --disconnect "$BLK_DEVICE"
  rm -rf "$DISK_FULL_FILENAME"
  exit_error "EXT4 (rootfs) format error"
fi

# Mount OS partition
ROOTFS="/tmp/installing-rootfs"
mkdir -p "$ROOTFS"
if [ "$?" -ne 0 ]; then
  qemu-nbd --disconnect "$BLK_DEVICE"
  rm -rf "$ROOTFS"
  rm -rf "$DISK_FULL_FILENAME"
  exit_error "$ROOTFS creating error"
fi
mount "$BLK_DEVICE"p3 "$ROOTFS"
if [ "$?" -ne 0 ]; then
  qemu-nbd --disconnect "$BLK_DEVICE"
  rm -rf "$ROOTFS"
  rm -rf "$DISK_FULL_FILENAME"
  exit_error "$ROOTFS mount error"
fi

# Debootstrap system
debootstrap --no-check-gpg --arch=amd64 --include="openssh-server,sudo" stable "$ROOTFS" http://httpredir.debian.org/debian/
if [ "$?" -ne 0 ]; then
  umount "$ROOTFS"
  qemu-nbd --disconnect "$BLK_DEVICE"
  rm -rf "$ROOTFS"
  rm -rf "$DISK_FULL_FILENAME"
  exit_error "Debootstrap failed"
fi

# Mount EFI partition
mkdir -p "$ROOTFS"/boot/efi
if [ "$?" -ne 0 ]; then
  umount "$ROOTFS"
  qemu-nbd --disconnect "$BLK_DEVICE"
  rm -rf "$ROOTFS"
  rm -rf "$DISK_FULL_FILENAME"
  exit_error "$ROOTFS/boot/efi creating error"
fi
mount "$BLK_DEVICE"p1 "$ROOTFS"/boot/efi
if [ "$?" -ne 0 ]; then
  umount "$ROOTFS"
  qemu-nbd --disconnect "$BLK_DEVICE"
  rm -rf "$ROOTFS"
  rm -rf "$ROOTFS"/boot/efi
  rm -rf "$DISK_FULL_FILENAME"
  exit_error "$ROOTFS/boot/efi mount error"
fi

# Get ready for chroot
mount --bind /dev "$ROOTFS"/dev
if [ "$?" -ne 0 ]; then
  umount "$ROOTFS"/boot/efi
  umount "$ROOTFS"
  qemu-nbd --disconnect "$BLK_DEVICE"
  rm -rf "$ROOTFS"
  rm -rf "$ROOTFS"/boot/efi
  rm -rf "$DISK_FULL_FILENAME"
  exit_error "$ROOTFS/dev mount error"
fi
mount -t devpts /dev/pts "$ROOTFS"/dev/pts
if [ "$?" -ne 0 ]; then
  umount "$ROOTFS"/dev
  umount "$ROOTFS"/boot/efi
  umount "$ROOTFS"
  qemu-nbd --disconnect "$BLK_DEVICE"
  rm -rf "$ROOTFS"
  rm -rf "$ROOTFS"/boot/efi
  rm -rf "$DISK_FULL_FILENAME"
  exit_error "$ROOTFS/dev/pts mount error"
fi
mount -t proc proc "$ROOTFS"/proc
if [ "$?" -ne 0 ]; then
  umount "$ROOTFS"/dev/pts
  umount "$ROOTFS"/dev
  umount "$ROOTFS"/boot/efi
  umount "$ROOTFS"
  qemu-nbd --disconnect "$BLK_DEVICE"
  rm -rf "$ROOTFS"
  rm -rf "$ROOTFS"/boot/efi
  rm -rf "$DISK_FULL_FILENAME"
  exit_error "$ROOTFS/proc mount error"
fi
mount -t sysfs sysfs "$ROOTFS"/sys
if [ "$?" -ne 0 ]; then
  umount "$ROOTFS"/proc
  umount "$ROOTFS"/dev/pts
  umount "$ROOTFS"/dev
  umount "$ROOTFS"/boot/efi
  umount "$ROOTFS"
  qemu-nbd --disconnect "$BLK_DEVICE"
  rm -rf "$ROOTFS"
  rm -rf "$ROOTFS"/boot/efi
  rm -rf "$DISK_FULL_FILENAME"
  exit_error "$ROOTFS/sys mount error"
fi
mount -t tmpfs tmpfs "$ROOTFS"/tmp
if [ "$?" -ne 0 ]; then
  umount "$ROOTFS"/sys
  umount "$ROOTFS"/proc
  umount "$ROOTFS"/dev/pts
  umount "$ROOTFS"/dev
  umount "$ROOTFS"/boot/efi
  umount "$ROOTFS"
  qemu-nbd --disconnect "$BLK_DEVICE"
  rm -rf "$ROOTFS"
  rm -rf "$ROOTFS"/boot/efi
  rm -rf "$DISK_FULL_FILENAME"
  exit_error "$ROOTFS/tmp mount error"
fi

# Entering chroot, installing Linux kernel and Grub
cat << EOF | chroot "$ROOTFS"
  set -e

  echo "$VM_NAME" > /etc/hostname
  echo 127.0.0.1 localhost $VM_NAME >> /etc/hosts

  chpasswd <<<"root:root"
  useradd -m administrator
  chpasswd <<<"administrator:administrator"
  usermod -aG sudo administrator

  export HOME=/root
  export DEBIAN_FRONTEND=noninteractive

  debconf-set-selections <<< 'grub-efi-amd64 grub2/update_nvram boolean false'

  echo 'deb http://deb.debian.org/debian/ bookworm main non-free-firmware' >> /etc/apt/sources.list
  echo 'deb http://security.debian.org/debian-security bookworm-security main non-free-firmware' >> /etc/apt/sources.list
  echo 'deb http://deb.debian.org/debian/ bookworm-updates main non-free-firmware' >> /etc/apt/sources.list

  echo "$VM_PROXY" >> /etc/apt/apt.conf

  apt update
  apt upgrade -y

  apt install -y firmware-realtek linux-image-amd64 linux-headers-amd64 grub-efi

  grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=debian --recheck --no-nvram --removable

  update-grub

  echo '/dev/sda1 /boot/efi vfat umask=0077 0 1' >> /etc/fstab
  echo '/dev/sda2 none swap sw 0 0' >> /etc/fstab
  echo '/dev/sda3 / ext4 errors=remount-ro 0 1' >> /etc/fstab

  echo 'auto enp1s0' >> /etc/network/interfaces
  echo 'iface enp1s0 inet static' >> /etc/network/interfaces
  echo 'address '"$VM_IP_ADDR" >> /etc/network/interfaces
  echo 'netmask '"$VM_NETMASK" >> /etc/network/interfaces
  echo 'gateway '"$VM_GATEWAY" >> /etc/network/interfaces

  echo 'administrator ALL=(ALL:ALL) ALL' >> /etc/sudoers
EOF

umount "$ROOTFS"/dev/pts
umount "$ROOTFS"/dev
umount "$ROOTFS"/proc
umount "$ROOTFS"/sys
umount "$ROOTFS"/tmp
umount "$ROOTFS"/boot/efi
umount "$ROOTFS"

qemu-nbd --disconnect "$BLK_DEVICE"
if [ "$?" -ne 0 ]; then
  rm -rf "$ROOTFS"
  rm -rf "$ROOTFS"/boot/efi
  rm -rf "$DISK_FULL_FILENAME"
  exit_error "$BLK_DEVICE disconnect error"
fi

# test start
#qemu-system-x86_64 -bios /usr/share/ovmf/OVMF.fd -m $RAM_KB -drive file="$DISK_FULL_FILENAME" -vnc "0.0.0.0":"$VNC_PORT" -daemonize

virt-install --name="$VM_NAME" \
  --ram="$RAM_KB" \
  --cpu host \
  --boot firmware="efi" \
  --import \
  --disk path="$DISK_FULL_FILENAME",bus=scsi,format=qcow \
  --os-variant=debian11 \
  --graphics type=vnc,port="$VNC_PORT",listen=0.0.0.0 \
  --network bridge="$BRIDGE" \
  --controller usb2 \
  --controller type=scsi,model=virtio-scsi \
  --noreboot
if [ "$?" -ne 0 ]; then
  rm -rf "$ROOTFS"
  rm -rf "$ROOTFS"/boot/efi
  rm -rf "$DISK_FULL_FILENAME"
  virsh undefine --nvram "$VM_NAME"
  exit_error "$VM_NAME creating error"
fi

virsh start $VM_NAME
if [ "$?" -ne 0 ]; then
  rm -rf "$ROOTFS"
  rm -rf "$ROOTFS"/boot/efi
  rm -rf "$DISK_FULL_FILENAME"
  virsh undefine --nvram "$VM_NAME"
  exit_error "$VM_NAME starting error"
fi

echo 'Done'

exit "$EXIT_SUCCESS"