plaintext, LF
  1#!/usr/bin/env bash
  2# create-vm.sh -- Interactive KVM/libvirt VM creator with cloud-init
  3set -euo pipefail
  4
  5IMGDIR="/var/lib/libvirt/images"
  6
  7# -- Preflight ----------------------------------------------------------------
  8for cmd in virsh virt-install qemu-img xorriso curl; do
  9  if ! command -v "$cmd" &>/dev/null; then
 10    echo "ERROR: '$cmd' is required but not installed." >&2
 11    exit 1
 12  fi
 13done
 14
 15if ! systemctl is-active --quiet libvirtd; then
 16  echo "ERROR: libvirtd is not running. Start it with: systemctl start libvirtd" >&2
 17  exit 1
 18fi
 19
 20# -- 1. VM Name ---------------------------------------------------------------
 21echo ""
 22echo "=== KVM VM Creator ==="
 23echo ""
 24while true; do
 25  read -rp "VM name: " VMNAME
 26  VMNAME="${VMNAME// /-}"
 27  if [[ -z "$VMNAME" ]]; then
 28    echo "  Name cannot be empty."
 29  elif virsh dominfo "$VMNAME" &>/dev/null 2>&1; then
 30    echo "  '${VMNAME}' is already in use -- pick another name."
 31  else
 32    break
 33  fi
 34done
 35
 36# -- 2. Distro ----------------------------------------------------------------
 37echo ""
 38echo "Select distro:"
 39select DISTRO in "AlmaLinux" "Debian" "Ubuntu"; do
 40  [[ -n "$DISTRO" ]] && break
 41done
 42
 43# -- 3. Version ---------------------------------------------------------------
 44echo ""
 45echo "Select version:"
 46case $DISTRO in
 47  AlmaLinux)
 48    select VERSION in "8" "9"; do
 49      [[ -n "$VERSION" ]] && break
 50    done
 51    case $VERSION in
 52      8)
 53        OS_VARIANT="almalinux8"
 54        BASE_URL="https://repo.almalinux.org/almalinux/8/cloud/x86_64/images"
 55        IMGFILE=$(curl -q -LSsf "${BASE_URL}/" \
 56          | grep -oE 'AlmaLinux-8-GenericCloud-[0-9.]+-[0-9]+\.x86_64\.qcow2' \
 57          | sort -V | tail -1)
 58        ;;
 59      9)
 60        OS_VARIANT="almalinux9"
 61        BASE_URL="https://repo.almalinux.org/almalinux/9/cloud/x86_64/images"
 62        IMGFILE=$(curl -q -LSsf "${BASE_URL}/" \
 63          | grep -oE 'AlmaLinux-9-GenericCloud-[0-9.]+-[0-9]+\.x86_64\.qcow2' \
 64          | sort -V | tail -1)
 65        ;;
 66    esac
 67    IMG_URL="${BASE_URL}/${IMGFILE}"
 68    ;;
 69
 70  Debian)
 71    select VERSION in "11 (Bullseye)" "12 (Bookworm)"; do
 72      [[ -n "$VERSION" ]] && break
 73    done
 74    case $VERSION in
 75      "11"*)
 76        OS_VARIANT="debian11"
 77        IMGFILE="debian-11-genericcloud-amd64.qcow2"
 78        IMG_URL="https://cloud.debian.org/images/cloud/bullseye/latest/${IMGFILE}"
 79        ;;
 80      "12"*)
 81        OS_VARIANT="debian12"
 82        IMGFILE="debian-12-genericcloud-amd64.qcow2"
 83        IMG_URL="https://cloud.debian.org/images/cloud/bookworm/latest/${IMGFILE}"
 84        ;;
 85    esac
 86    ;;
 87
 88  Ubuntu)
 89    select VERSION in "20.04 (Focal)" "22.04 (Jammy)" "24.04 (Noble)"; do
 90      [[ -n "$VERSION" ]] && break
 91    done
 92    case $VERSION in
 93      "20.04"*)
 94        OS_VARIANT="ubuntu20.04"
 95        IMGFILE="focal-server-cloudimg-amd64.img"
 96        IMG_URL="https://cloud-images.ubuntu.com/focal/current/${IMGFILE}"
 97        ;;
 98      "22.04"*)
 99        OS_VARIANT="ubuntu22.04"
100        IMGFILE="jammy-server-cloudimg-amd64.img"
101        IMG_URL="https://cloud-images.ubuntu.com/jammy/current/${IMGFILE}"
102        ;;
103      "24.04"*)
104        OS_VARIANT="ubuntu24.04"
105        IMGFILE="noble-server-cloudimg-amd64.img"
106        IMG_URL="https://cloud-images.ubuntu.com/noble/current/${IMGFILE}"
107        ;;
108    esac
109    ;;
110esac
111
112# -- 4. VM Specs (vCPU + RAM) -------------------------------------------------
113echo ""
114echo "What specs should the VM have?"
115select SPECS in \
116  "Micro    (1 vCPU,  512MB RAM)" \
117  "Tiny     (1 vCPU,  1GB   RAM)" \
118  "Small    (2 vCPU,  2GB   RAM)" \
119  "Standard (2 vCPU,  4GB   RAM)" \
120  "Medium   (4 vCPU,  4GB   RAM)" \
121  "Large    (4 vCPU,  8GB   RAM)" \
122  "XLarge   (8 vCPU,  8GB   RAM)" \
123  "2XLarge  (8 vCPU,  16GB  RAM)" \
124  "4XLarge  (16 vCPU, 32GB  RAM)" \
125  "Custom"; do
126  case $SPECS in
127    "Micro"*)    VCPUS=1;  RAM=512;   break ;;
128    "Tiny"*)     VCPUS=1;  RAM=1024;  break ;;
129    "Small"*)    VCPUS=2;  RAM=2048;  break ;;
130    "Standard"*) VCPUS=2;  RAM=4096;  break ;;
131    "Medium"*)   VCPUS=4;  RAM=4096;  break ;;
132    "Large"*)    VCPUS=4;  RAM=8192;  break ;;
133    "XLarge"*)   VCPUS=8;  RAM=8192;  break ;;
134    "2XLarge"*)  VCPUS=8;  RAM=16384; break ;;
135    "4XLarge"*)  VCPUS=16; RAM=32768; break ;;
136    "Custom")
137      read -rp "  vCPUs: "    VCPUS
138      read -rp "  RAM (MB): " RAM
139      break ;;
140  esac
141done
142
143# -- 5. Disk Size (always GB) -------------------------------------------------
144echo ""
145echo "Select disk size:"
146select DISKOPT in \
147  "10"   \
148  "20"   \
149  "40"   \
150  "60"   \
151  "80"   \
152  "100"  \
153  "200"  \
154  "500"  \
155  "1000" \
156  "Custom"; do
157  case $DISKOPT in
158    "Custom")
159      read -rp "  Disk size (GB): " DISK
160      DISK="${DISK//[^0-9]/}"
161      break ;;
162    *)
163      DISK="$DISKOPT"
164      break ;;
165  esac
166done
167
168# -- 6. Networking ------------------------------------------------------------
169echo ""
170echo "Which network setup?"
171select NET in \
172  "NAT (default)" \
173  "Bridge (br0)" \
174  "Isolated (no internet)"; do
175  case $NET in
176    "NAT"*)      NETWORK="network=default,model=virtio";  break ;;
177    "Bridge"*)   NETWORK="bridge=br0,model=virtio";       break ;;
178    "Isolated"*) NETWORK="network=isolated,model=virtio"; break ;;
179  esac
180done
181
182# -- Summary + confirm --------------------------------------------------------
183echo ""
184echo "+----------------------------------------------+"
185printf "| %-44s |\n" "VM Summary"
186echo "+----------------------------------------------+"
187printf "| %-14s %-29s |\n" "Name:"      "$VMNAME"
188printf "| %-14s %-29s |\n" "Distro:"    "$DISTRO $VERSION"
189printf "| %-14s %-29s |\n" "vCPUs:"     "$VCPUS"
190printf "| %-14s %-29s |\n" "RAM:"       "${RAM}MB"
191printf "| %-14s %-29s |\n" "Disk:"      "${DISK}GB"
192printf "| %-14s %-29s |\n" "Network:"   "$NET"
193echo "+----------------------------------------------+"
194read -rp "Proceed? [y/N] " CONFIRM
195[[ "${CONFIRM,,}" != "y" ]] && echo "Aborted." && exit 0
196
197# -- Download base image (skip if cached) ------------------------------------
198BASE_IMG="${IMGDIR}/${IMGFILE}"
199if [[ -f "$BASE_IMG" ]]; then
200  echo "Using cached image: ${BASE_IMG}"
201else
202  echo "Downloading ${IMGFILE}..."
203  curl -q -LSsf -o "$BASE_IMG" "$IMG_URL"
204  echo "Done: $(du -h "$BASE_IMG" | cut -f1)"
205fi
206
207# -- Create VM disk (thin overlay) -------------------------------------------
208VM_IMG="${IMGDIR}/${VMNAME}.qcow2"
209qemu-img create -f qcow2 -b "$BASE_IMG" -F qcow2 "$VM_IMG" "${DISK}G"
210
211# -- Cloud-init seed ISO ------------------------------------------------------
212SEED_ISO="${IMGDIR}/${VMNAME}-seed.iso"
213TMPD="$(mktemp -d -t "${VMNAME}-XXXXXX")"
214trap 'rm -rf "$TMPD"' EXIT
215
216cat > "${TMPD}/meta-data" <<EOF
217instance-id: ${VMNAME}
218local-hostname: ${VMNAME}
219EOF
220
221cat > "${TMPD}/user-data" <<'EOF'
222#cloud-config
223chpasswd:
224  list: |
225    root:almalinux
226  expire: false
227ssh_pwauth: true
228disable_root: false
229runcmd:
230  - growpart /dev/vda 1 || growpart /dev/vda 4 || true
231  - xfs_growfs / || resize2fs /dev/vda1 || true
232EOF
233
234xorriso -as mkisofs \
235  -output "$SEED_ISO" \
236  -volid cidata \
237  -joliet -rock \
238  "${TMPD}/user-data" "${TMPD}/meta-data" 2>/dev/null
239
240# -- Create and start VM ------------------------------------------------------
241echo ""
242echo "Creating VM '${VMNAME}'..."
243virt-install \
244  --name        "$VMNAME" \
245  --memory      "$RAM" \
246  --vcpus       "$VCPUS" \
247  --disk        "${VM_IMG},device=disk,bus=virtio" \
248  --disk        "${SEED_ISO},device=cdrom" \
249  --os-variant  "$OS_VARIANT" \
250  --network     "$NETWORK" \
251  --graphics    none \
252  --console     pty,target_type=serial \
253  --import \
254  --noautoconsole \
255  --boot        hd
256
257# -- Wait for IP --------------------------------------------------------------
258echo "Waiting for IP address..."
259IP=""
260for i in $(seq 1 30); do
261  IP=$(virsh domifaddr "$VMNAME" 2>/dev/null | awk '/ipv4/{print $4}' | cut -d/ -f1)
262  [[ -n "$IP" ]] && break
263  sleep 2
264done
265
266echo ""
267echo "+----------------------------------------------+"
268printf "| VM '%-40s' |\n" "${VMNAME} is running"
269echo "+----------------------------------------------+"
270if [[ -n "$IP" ]]; then
271  printf "| %-14s %-29s |\n" "IP:"       "$IP"
272  printf "| %-14s %-29s |\n" "SSH:"      "ssh root@${IP}"
273  printf "| %-14s %-29s |\n" "Password:" "almalinux"
274else
275  printf "| %-14s %-29s |\n" "IP:" "(run: virsh domifaddr ${VMNAME})"
276fi
277printf "| %-14s %-29s |\n" "Console:"  "virsh console ${VMNAME}"
278echo "+----------------------------------------------+"
279printf "| %-14s %-29s |\n" "Start:"    "virsh start ${VMNAME}"
280printf "| %-14s %-29s |\n" "Stop:"     "virsh shutdown ${VMNAME}"
281printf "| %-14s %-29s |\n" "Force off:" "virsh destroy ${VMNAME}"
282printf "| %-14s %-29s |\n" "Delete:"   "virsh undefine ${VMNAME} --remove-all-storage"
283echo "+----------------------------------------------+"

Created: Sun, 17 May 2026 18:36:44 +0000

Expires: Never