Automating Ubuntu VM Creation with libvirt (KVM/QEMU) and Cloud-Init
What This Script Does ## Prerequisites Ever needed to spin up multiple Ubuntu VMs quickly for testing, development, or learning? In this post, I'll walk you through a bash script that automates the entire process using libvirt/KVM and cloud-init. Before running this script, make sure you have: If you want to use another version of Python, you can find here and update the area into the runcmd that install the python When the machine start you need to wait the cloud-init to configure everything, you can see the logs with sudo tail -f /var/log/cloud-init-output.log The network that I used was this Thanks for read until there :) Templates let you quickly answer FAQs or store snippets for re-use. Into the virt-manager you can use ctrl + alt + t to open the terminal and ctrl + alt + b for the chromium Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse COMMAND_BLOCK:
#!/bin/bash usage() { cat <<EOF
Usage: $(basename "$0") [OPTIONS] Launch Ubuntu VMs using libvirt/KVM with cloud-init configuration. OPTIONS: -n, --num-vms NUM Number of VMs to create (default: 1, max: 5) -m, --memory GB Memory per VM in GB (default: 1, max: 8) -c, --cpus NUM Number of CPUs per VM (default: 1, max: 2) -d, --disk-size GB Disk size in GB (default: 10, min: 10) -g, --github USER GitHub username for SSH key import (required) -h, --help Show this help message and exit EXAMPLES: $(basename "$0") -g myuser $(basename "$0") -n 3 -m 2 -c 2 -d 20 -g myuser $(basename "$0") --num-vms 5 --memory 4 --github myuser NOTES: - VMs will be assigned IPs in the range 192.168.1.100-150 - The script will automatically find available network octets - Ubuntu Noble cloud image will be downloaded if not present
EOF exit 0
} NUM_VMS=1
MEMORY=1
CPUS=1
DISK_SIZE=10
GITHUB_USER="" while [[ $# -gt 0 ]]; do case $1 in -n|--num-vms) NUM_VMS="$2" shift 2 ;; -m|--memory) MEMORY="$2" shift 2 ;; -c|--cpus) CPUS="$2" shift 2 ;; -d|--disk-size) DISK_SIZE="$2" shift 2 ;; -g|--github) GITHUB_USER="$2" shift 2 ;; -h|--help) usage ;; *) echo "Error: Unknown option $1" echo "Use -h or --help for usage information." exit 1 ;; esac
done if [ -z "$GITHUB_USER" ]; then echo "Error: GitHub username is required for SSH key import." echo "Use -g or --github to specify your GitHub username." echo "Use -h or --help for usage information." exit 1
fi BASE_IMG="noble-server-cloudimg-amd64.img"
IMG_URL="https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img"
IMG_DIR="/var/lib/libvirt/images"
SUBNET="192.168.122"
GATEWAY="$SUBNET.1"
DHCP_START=2
DHCP_END=254 if [ $NUM_VMS -gt $((DHCP_END - DHCP_START)) ]; then echo "Error: Max 5 VMs to stay within $DHCP_START-$DHCP_END range." exit 1
fi if [ $MEMORY -gt 8 ]; then echo "Error: Max memory is 8GB" exit 1
fi if [ $CPUS -gt 2 ]; then echo "Error: Max CPUs is 2" exit 1
fi if [ $DISK_SIZE -lt 10 ]; then echo "Error: Disk size must be at least 10GB to accommodate base image." exit 1
fi if [ ! -f "$IMG_DIR/$BASE_IMG" ]; then echo "Downloading Ubuntu Noble cloud image..." wget -O "$IMG_DIR/$BASE_IMG" "$IMG_URL" if [ $? -ne 0 ]; then echo "Error: Failed to download the base image from $IMG_URL" exit 1 fi echo "Base image downloaded successfully."
fi is_octet_available() { local octet=$1 local ip="$SUBNET.$octet" for vm in $(virsh list --all --name 2>/dev/null); do if [ -n "$vm" ]; then if [ "$vm" = "server-$octet" ]; then return 1 fi fi done if ping -c 1 -W 1 "$ip" &>/dev/null; then return 1 fi return 0
} AVAILABLE_OCTETS=()
for octet in $(seq $DHCP_START $DHCP_END); do if is_octet_available $octet; then AVAILABLE_OCTETS+=($octet) fi if [ ${#AVAILABLE_OCTETS[@]} -ge $NUM_VMS ]; then break fi
done if [ ${#AVAILABLE_OCTETS[@]} -lt $NUM_VMS ]; then echo "Error: Not enough available network addresses. Found ${#AVAILABLE_OCTETS[@]}, need $NUM_VMS." echo "Available range is $SUBNET.$DHCP_START-$DHCP_END. Some addresses may already be in use." exit 1
fi echo "Found ${#AVAILABLE_OCTETS[@]} available network addresses." for i in $(seq 0 $(($NUM_VMS - 1))); do OCTET=${AVAILABLE_OCTETS[$i]} HOSTNAME="server-$OCTET" IMG="$IMG_DIR/vm-$OCTET.qcow2" SEED="$IMG_DIR/seed-$OCTET.iso" USER_DATA="user-data-$OCTET.yaml" NETWORK_DATA="network-data-$OCTET.yaml" qemu-img create -f qcow2 -F qcow2 -b "$IMG_DIR/$BASE_IMG" "$IMG" ${DISK_SIZE}G cat <<EOF > "$USER_DATA"
#cloud-config
hostname: $HOSTNAME package_update: true
package_upgrade: true users: - name: ubuntu shell: /bin/bash groups: [users, sudo] sudo: "ALL=(ALL) NOPASSWD:ALL" lock_passwd: false passwd: 123456789 ssh_import_id: - gh:$GITHUB_USER packages: - apt-transport-https - xorg - xterm - openbox - chromium-browser - spice-vdagent runcmd: - mkdir -p /home/ubuntu/.config/openbox - wget https://raw.githubusercontent.com/Mikachu/openbox/refs/heads/master/data/rc.xml -P /home/ubuntu/.config/openbox - sed -i '/<\/keyboard>/i \ <keybind key="C-A-t">\n <action name="Execute">\n <command>xterm</command>\n </action>\n </keybind>' /home/ubuntu/.config/openbox/rc.xml - sed -i '/<\/keyboard>/i \ <keybind key="C-A-b">\n <action name="Execute">\n <command>chromium</command>\n </action>\n </keybind>' /home/ubuntu/.config/openbox/rc.xml - sudo apt-get install -y make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev liblzma-dev - cd /tmp/ - wget https://www.python.org/ftp/python/3.13.7/Python-3.13.7.tgz - tar xzf Python-3.13.7.tgz - cd Python-3.13.7 - sudo ./configure --enable-optimizations - sudo make -j\$(nproc) - sudo make altinstall - sudo ln -s /usr/local/bin/python3.13 /usr/local/bin/python - sudo ln -s /usr/local/bin/pip3.13 /usr/local/bin/pip - echo 'if [[ -z \$DISPLAY ]] && [[ \$(tty) = /dev/tty1 ]]; then exec startx; fi' | sudo tee -a /home/ubuntu/.profile - echo "alias displays='ps e | grep -Po " DISPLAY=[\\.0-9A-Za-z:]* " | sort -u'" >> /home/ubuntu/.bashrc - sudo groupadd -g 3001 docker - sudo usermod -aG docker ubuntu - curl -fsSL https://get.docker.com -o get-docker.sh - sudo sh get-docker.sh - sudo reboot
EOF cat <<EOF > "$NETWORK_DATA"
#cloud-config
network: version: 2 ethernets: interface0: match: name: "en*s0" dhcp4: false addresses: [$SUBNET.$OCTET/24] routes: - to: default via: $GATEWAY nameservers: addresses: [8.8.8.8,8.8.4.4]
EOF echo "$(cat $USER_DATA)" echo "$(cat $NETWORK_DATA)" cloud-localds --network-config "$NETWORK_DATA" "$SEED" "$USER_DATA" rm "$USER_DATA" rm "$NETWORK_DATA" virt-install \ --name "$HOSTNAME" \ --memory $(( $MEMORY * 1024 )) \ --vcpus $CPUS \ --disk path="$IMG",bus=virtio,format=qcow2 \ --disk path="$SEED",bus=virtio,format=raw \ --network default \ --os-variant ubuntu24.04 \ --graphics spice,listen=0.0.0.0 \ --channel spicevmc \ --video virtio \ --import \ --noautoconsole echo "Launched VM $((i + 1)): $HOSTNAME with IP $SUBNET.$OCTET (disk size: ${DISK_SIZE}GB)"
done echo "All $NUM_VMS VMs launched. Use 'virsh list --all' to see VMs, 'virsh start/stop/destroy <name>', 'virsh console <name>' for access. Clean up images/ISOs when done." COMMAND_BLOCK:
#!/bin/bash usage() { cat <<EOF
Usage: $(basename "$0") [OPTIONS] Launch Ubuntu VMs using libvirt/KVM with cloud-init configuration. OPTIONS: -n, --num-vms NUM Number of VMs to create (default: 1, max: 5) -m, --memory GB Memory per VM in GB (default: 1, max: 8) -c, --cpus NUM Number of CPUs per VM (default: 1, max: 2) -d, --disk-size GB Disk size in GB (default: 10, min: 10) -g, --github USER GitHub username for SSH key import (required) -h, --help Show this help message and exit EXAMPLES: $(basename "$0") -g myuser $(basename "$0") -n 3 -m 2 -c 2 -d 20 -g myuser $(basename "$0") --num-vms 5 --memory 4 --github myuser NOTES: - VMs will be assigned IPs in the range 192.168.1.100-150 - The script will automatically find available network octets - Ubuntu Noble cloud image will be downloaded if not present
EOF exit 0
} NUM_VMS=1
MEMORY=1
CPUS=1
DISK_SIZE=10
GITHUB_USER="" while [[ $# -gt 0 ]]; do case $1 in -n|--num-vms) NUM_VMS="$2" shift 2 ;; -m|--memory) MEMORY="$2" shift 2 ;; -c|--cpus) CPUS="$2" shift 2 ;; -d|--disk-size) DISK_SIZE="$2" shift 2 ;; -g|--github) GITHUB_USER="$2" shift 2 ;; -h|--help) usage ;; *) echo "Error: Unknown option $1" echo "Use -h or --help for usage information." exit 1 ;; esac
done if [ -z "$GITHUB_USER" ]; then echo "Error: GitHub username is required for SSH key import." echo "Use -g or --github to specify your GitHub username." echo "Use -h or --help for usage information." exit 1
fi BASE_IMG="noble-server-cloudimg-amd64.img"
IMG_URL="https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img"
IMG_DIR="/var/lib/libvirt/images"
SUBNET="192.168.122"
GATEWAY="$SUBNET.1"
DHCP_START=2
DHCP_END=254 if [ $NUM_VMS -gt $((DHCP_END - DHCP_START)) ]; then echo "Error: Max 5 VMs to stay within $DHCP_START-$DHCP_END range." exit 1
fi if [ $MEMORY -gt 8 ]; then echo "Error: Max memory is 8GB" exit 1
fi if [ $CPUS -gt 2 ]; then echo "Error: Max CPUs is 2" exit 1
fi if [ $DISK_SIZE -lt 10 ]; then echo "Error: Disk size must be at least 10GB to accommodate base image." exit 1
fi if [ ! -f "$IMG_DIR/$BASE_IMG" ]; then echo "Downloading Ubuntu Noble cloud image..." wget -O "$IMG_DIR/$BASE_IMG" "$IMG_URL" if [ $? -ne 0 ]; then echo "Error: Failed to download the base image from $IMG_URL" exit 1 fi echo "Base image downloaded successfully."
fi is_octet_available() { local octet=$1 local ip="$SUBNET.$octet" for vm in $(virsh list --all --name 2>/dev/null); do if [ -n "$vm" ]; then if [ "$vm" = "server-$octet" ]; then return 1 fi fi done if ping -c 1 -W 1 "$ip" &>/dev/null; then return 1 fi return 0
} AVAILABLE_OCTETS=()
for octet in $(seq $DHCP_START $DHCP_END); do if is_octet_available $octet; then AVAILABLE_OCTETS+=($octet) fi if [ ${#AVAILABLE_OCTETS[@]} -ge $NUM_VMS ]; then break fi
done if [ ${#AVAILABLE_OCTETS[@]} -lt $NUM_VMS ]; then echo "Error: Not enough available network addresses. Found ${#AVAILABLE_OCTETS[@]}, need $NUM_VMS." echo "Available range is $SUBNET.$DHCP_START-$DHCP_END. Some addresses may already be in use." exit 1
fi echo "Found ${#AVAILABLE_OCTETS[@]} available network addresses." for i in $(seq 0 $(($NUM_VMS - 1))); do OCTET=${AVAILABLE_OCTETS[$i]} HOSTNAME="server-$OCTET" IMG="$IMG_DIR/vm-$OCTET.qcow2" SEED="$IMG_DIR/seed-$OCTET.iso" USER_DATA="user-data-$OCTET.yaml" NETWORK_DATA="network-data-$OCTET.yaml" qemu-img create -f qcow2 -F qcow2 -b "$IMG_DIR/$BASE_IMG" "$IMG" ${DISK_SIZE}G cat <<EOF > "$USER_DATA"
#cloud-config
hostname: $HOSTNAME package_update: true
package_upgrade: true users: - name: ubuntu shell: /bin/bash groups: [users, sudo] sudo: "ALL=(ALL) NOPASSWD:ALL" lock_passwd: false passwd: 123456789 ssh_import_id: - gh:$GITHUB_USER packages: - apt-transport-https - xorg - xterm - openbox - chromium-browser - spice-vdagent runcmd: - mkdir -p /home/ubuntu/.config/openbox - wget https://raw.githubusercontent.com/Mikachu/openbox/refs/heads/master/data/rc.xml -P /home/ubuntu/.config/openbox - sed -i '/<\/keyboard>/i \ <keybind key="C-A-t">\n <action name="Execute">\n <command>xterm</command>\n </action>\n </keybind>' /home/ubuntu/.config/openbox/rc.xml - sed -i '/<\/keyboard>/i \ <keybind key="C-A-b">\n <action name="Execute">\n <command>chromium</command>\n </action>\n </keybind>' /home/ubuntu/.config/openbox/rc.xml - sudo apt-get install -y make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev liblzma-dev - cd /tmp/ - wget https://www.python.org/ftp/python/3.13.7/Python-3.13.7.tgz - tar xzf Python-3.13.7.tgz - cd Python-3.13.7 - sudo ./configure --enable-optimizations - sudo make -j\$(nproc) - sudo make altinstall - sudo ln -s /usr/local/bin/python3.13 /usr/local/bin/python - sudo ln -s /usr/local/bin/pip3.13 /usr/local/bin/pip - echo 'if [[ -z \$DISPLAY ]] && [[ \$(tty) = /dev/tty1 ]]; then exec startx; fi' | sudo tee -a /home/ubuntu/.profile - echo "alias displays='ps e | grep -Po " DISPLAY=[\\.0-9A-Za-z:]* " | sort -u'" >> /home/ubuntu/.bashrc - sudo groupadd -g 3001 docker - sudo usermod -aG docker ubuntu - curl -fsSL https://get.docker.com -o get-docker.sh - sudo sh get-docker.sh - sudo reboot
EOF cat <<EOF > "$NETWORK_DATA"
#cloud-config
network: version: 2 ethernets: interface0: match: name: "en*s0" dhcp4: false addresses: [$SUBNET.$OCTET/24] routes: - to: default via: $GATEWAY nameservers: addresses: [8.8.8.8,8.8.4.4]
EOF echo "$(cat $USER_DATA)" echo "$(cat $NETWORK_DATA)" cloud-localds --network-config "$NETWORK_DATA" "$SEED" "$USER_DATA" rm "$USER_DATA" rm "$NETWORK_DATA" virt-install \ --name "$HOSTNAME" \ --memory $(( $MEMORY * 1024 )) \ --vcpus $CPUS \ --disk path="$IMG",bus=virtio,format=qcow2 \ --disk path="$SEED",bus=virtio,format=raw \ --network default \ --os-variant ubuntu24.04 \ --graphics spice,listen=0.0.0.0 \ --channel spicevmc \ --video virtio \ --import \ --noautoconsole echo "Launched VM $((i + 1)): $HOSTNAME with IP $SUBNET.$OCTET (disk size: ${DISK_SIZE}GB)"
done echo "All $NUM_VMS VMs launched. Use 'virsh list --all' to see VMs, 'virsh start/stop/destroy <name>', 'virsh console <name>' for access. Clean up images/ISOs when done." COMMAND_BLOCK:
#!/bin/bash usage() { cat <<EOF
Usage: $(basename "$0") [OPTIONS] Launch Ubuntu VMs using libvirt/KVM with cloud-init configuration. OPTIONS: -n, --num-vms NUM Number of VMs to create (default: 1, max: 5) -m, --memory GB Memory per VM in GB (default: 1, max: 8) -c, --cpus NUM Number of CPUs per VM (default: 1, max: 2) -d, --disk-size GB Disk size in GB (default: 10, min: 10) -g, --github USER GitHub username for SSH key import (required) -h, --help Show this help message and exit EXAMPLES: $(basename "$0") -g myuser $(basename "$0") -n 3 -m 2 -c 2 -d 20 -g myuser $(basename "$0") --num-vms 5 --memory 4 --github myuser NOTES: - VMs will be assigned IPs in the range 192.168.1.100-150 - The script will automatically find available network octets - Ubuntu Noble cloud image will be downloaded if not present
EOF exit 0
} NUM_VMS=1
MEMORY=1
CPUS=1
DISK_SIZE=10
GITHUB_USER="" while [[ $# -gt 0 ]]; do case $1 in -n|--num-vms) NUM_VMS="$2" shift 2 ;; -m|--memory) MEMORY="$2" shift 2 ;; -c|--cpus) CPUS="$2" shift 2 ;; -d|--disk-size) DISK_SIZE="$2" shift 2 ;; -g|--github) GITHUB_USER="$2" shift 2 ;; -h|--help) usage ;; *) echo "Error: Unknown option $1" echo "Use -h or --help for usage information." exit 1 ;; esac
done if [ -z "$GITHUB_USER" ]; then echo "Error: GitHub username is required for SSH key import." echo "Use -g or --github to specify your GitHub username." echo "Use -h or --help for usage information." exit 1
fi BASE_IMG="noble-server-cloudimg-amd64.img"
IMG_URL="https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img"
IMG_DIR="/var/lib/libvirt/images"
SUBNET="192.168.122"
GATEWAY="$SUBNET.1"
DHCP_START=2
DHCP_END=254 if [ $NUM_VMS -gt $((DHCP_END - DHCP_START)) ]; then echo "Error: Max 5 VMs to stay within $DHCP_START-$DHCP_END range." exit 1
fi if [ $MEMORY -gt 8 ]; then echo "Error: Max memory is 8GB" exit 1
fi if [ $CPUS -gt 2 ]; then echo "Error: Max CPUs is 2" exit 1
fi if [ $DISK_SIZE -lt 10 ]; then echo "Error: Disk size must be at least 10GB to accommodate base image." exit 1
fi if [ ! -f "$IMG_DIR/$BASE_IMG" ]; then echo "Downloading Ubuntu Noble cloud image..." wget -O "$IMG_DIR/$BASE_IMG" "$IMG_URL" if [ $? -ne 0 ]; then echo "Error: Failed to download the base image from $IMG_URL" exit 1 fi echo "Base image downloaded successfully."
fi is_octet_available() { local octet=$1 local ip="$SUBNET.$octet" for vm in $(virsh list --all --name 2>/dev/null); do if [ -n "$vm" ]; then if [ "$vm" = "server-$octet" ]; then return 1 fi fi done if ping -c 1 -W 1 "$ip" &>/dev/null; then return 1 fi return 0
} AVAILABLE_OCTETS=()
for octet in $(seq $DHCP_START $DHCP_END); do if is_octet_available $octet; then AVAILABLE_OCTETS+=($octet) fi if [ ${#AVAILABLE_OCTETS[@]} -ge $NUM_VMS ]; then break fi
done if [ ${#AVAILABLE_OCTETS[@]} -lt $NUM_VMS ]; then echo "Error: Not enough available network addresses. Found ${#AVAILABLE_OCTETS[@]}, need $NUM_VMS." echo "Available range is $SUBNET.$DHCP_START-$DHCP_END. Some addresses may already be in use." exit 1
fi echo "Found ${#AVAILABLE_OCTETS[@]} available network addresses." for i in $(seq 0 $(($NUM_VMS - 1))); do OCTET=${AVAILABLE_OCTETS[$i]} HOSTNAME="server-$OCTET" IMG="$IMG_DIR/vm-$OCTET.qcow2" SEED="$IMG_DIR/seed-$OCTET.iso" USER_DATA="user-data-$OCTET.yaml" NETWORK_DATA="network-data-$OCTET.yaml" qemu-img create -f qcow2 -F qcow2 -b "$IMG_DIR/$BASE_IMG" "$IMG" ${DISK_SIZE}G cat <<EOF > "$USER_DATA"
#cloud-config
hostname: $HOSTNAME package_update: true
package_upgrade: true users: - name: ubuntu shell: /bin/bash groups: [users, sudo] sudo: "ALL=(ALL) NOPASSWD:ALL" lock_passwd: false passwd: 123456789 ssh_import_id: - gh:$GITHUB_USER packages: - apt-transport-https - xorg - xterm - openbox - chromium-browser - spice-vdagent runcmd: - mkdir -p /home/ubuntu/.config/openbox - wget https://raw.githubusercontent.com/Mikachu/openbox/refs/heads/master/data/rc.xml -P /home/ubuntu/.config/openbox - sed -i '/<\/keyboard>/i \ <keybind key="C-A-t">\n <action name="Execute">\n <command>xterm</command>\n </action>\n </keybind>' /home/ubuntu/.config/openbox/rc.xml - sed -i '/<\/keyboard>/i \ <keybind key="C-A-b">\n <action name="Execute">\n <command>chromium</command>\n </action>\n </keybind>' /home/ubuntu/.config/openbox/rc.xml - sudo apt-get install -y make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev liblzma-dev - cd /tmp/ - wget https://www.python.org/ftp/python/3.13.7/Python-3.13.7.tgz - tar xzf Python-3.13.7.tgz - cd Python-3.13.7 - sudo ./configure --enable-optimizations - sudo make -j\$(nproc) - sudo make altinstall - sudo ln -s /usr/local/bin/python3.13 /usr/local/bin/python - sudo ln -s /usr/local/bin/pip3.13 /usr/local/bin/pip - echo 'if [[ -z \$DISPLAY ]] && [[ \$(tty) = /dev/tty1 ]]; then exec startx; fi' | sudo tee -a /home/ubuntu/.profile - echo "alias displays='ps e | grep -Po " DISPLAY=[\\.0-9A-Za-z:]* " | sort -u'" >> /home/ubuntu/.bashrc - sudo groupadd -g 3001 docker - sudo usermod -aG docker ubuntu - curl -fsSL https://get.docker.com -o get-docker.sh - sudo sh get-docker.sh - sudo reboot
EOF cat <<EOF > "$NETWORK_DATA"
#cloud-config
network: version: 2 ethernets: interface0: match: name: "en*s0" dhcp4: false addresses: [$SUBNET.$OCTET/24] routes: - to: default via: $GATEWAY nameservers: addresses: [8.8.8.8,8.8.4.4]
EOF echo "$(cat $USER_DATA)" echo "$(cat $NETWORK_DATA)" cloud-localds --network-config "$NETWORK_DATA" "$SEED" "$USER_DATA" rm "$USER_DATA" rm "$NETWORK_DATA" virt-install \ --name "$HOSTNAME" \ --memory $(( $MEMORY * 1024 )) \ --vcpus $CPUS \ --disk path="$IMG",bus=virtio,format=qcow2 \ --disk path="$SEED",bus=virtio,format=raw \ --network default \ --os-variant ubuntu24.04 \ --graphics spice,listen=0.0.0.0 \ --channel spicevmc \ --video virtio \ --import \ --noautoconsole echo "Launched VM $((i + 1)): $HOSTNAME with IP $SUBNET.$OCTET (disk size: ${DISK_SIZE}GB)"
done echo "All $NUM_VMS VMs launched. Use 'virsh list --all' to see VMs, 'virsh start/stop/destroy <name>', 'virsh console <name>' for access. Clean up images/ISOs when done." CODE_BLOCK:
<network> <name>default</name> <uuid>2dba4e25-611d-44db-9b54-ce38bd7c5a62</uuid> <forward mode="nat"> <nat> <port start="1024" end="65535"/> </nat> </forward> <bridge name="virbr0" stp="on" delay="0"/> <mac address="ad:98:81:e2:9a:74"/> <dns> <forwarder addr="8.8.8.8"/> <forwarder addr="8.8.4.4"/> </dns> <ip address="192.168.122.1" netmask="255.255.255.0"> <dhcp> <range start="192.168.122.2" end="192.168.122.254"/> </dhcp> </ip>
</network> CODE_BLOCK:
<network> <name>default</name> <uuid>2dba4e25-611d-44db-9b54-ce38bd7c5a62</uuid> <forward mode="nat"> <nat> <port start="1024" end="65535"/> </nat> </forward> <bridge name="virbr0" stp="on" delay="0"/> <mac address="ad:98:81:e2:9a:74"/> <dns> <forwarder addr="8.8.8.8"/> <forwarder addr="8.8.4.4"/> </dns> <ip address="192.168.122.1" netmask="255.255.255.0"> <dhcp> <range start="192.168.122.2" end="192.168.122.254"/> </dhcp> </ip>
</network> CODE_BLOCK:
<network> <name>default</name> <uuid>2dba4e25-611d-44db-9b54-ce38bd7c5a62</uuid> <forward mode="nat"> <nat> <port start="1024" end="65535"/> </nat> </forward> <bridge name="virbr0" stp="on" delay="0"/> <mac address="ad:98:81:e2:9a:74"/> <dns> <forwarder addr="8.8.8.8"/> <forwarder addr="8.8.4.4"/> </dns> <ip address="192.168.122.1" netmask="255.255.255.0"> <dhcp> <range start="192.168.122.2" end="192.168.122.254"/> </dhcp> </ip>
</network> - Downloads the official Ubuntu Noble cloud image automatically
- Creates multiple VMs with customizable resources (memory, CPUs, disk)
- Configures static IPs in the 192.168.122.2-254 range
- Sets up SSH access via your GitHub public keys
- Pre-installs useful packages including Docker, Python 3.13.7, and a minimal desktop environment (Openbox) - A Linux host with KVM/QEMU installed
- libvirt and virt-install packages
- cloud-localds (from cloud-image-utils package)
- A bridge network interface (br0) configured
- wget for downloading the cloud image - Email [email protected]
- Location Brazil
- Education UNINTER
- Work Software Engineer
- Joined Dec 7, 2019