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