Pages

Automating VM Deployment on ESXi with OVF Templates Using CSV Input on RHEL

Deploying multiple VMs manually on an ESXi host can be time-consuming, especially when you need to configure hostnames, network interfaces, and DNS settings. In this tutorial, we’ll walk through an automated process using a CSV-driven script, ovftool, and sshpass on RHEL Linux. This allows parallel deployment of VMs with customized network configurations.

Prerequisites:
Before we start, make sure you have the following:
  • RHEL/CentOS 8+ machine with network access to the ESXi host.
  • ESXi credentials with permissions to deploy VMs.
  • OVA template for the VM.
  • sshpass and ovftool installed on your RHEL system.
Step 1: Installing sshpass on RHEL
sshpass allows automated SSH login using a password (necessary for our script).
Enable EPEL repository (if not already installed)
# sudo dnf install -y epel-release
Install sshpass
# sudo dnf install -y sshpass
Verify installation
# sshpass -V

Step 2: Installing ovftool on RHEL
ovftool is VMware's OVF deployment utility.
Download VMware-ovftool for Linux from VMware’s official site "https://developer.broadcom.com/tools/open-virtualization-format-ovf-tool/latest"
Extract and install:
# dnf install libnsl libxcrypt-compat -y
unzip VMware-ovftool-5.0.0-24781994-lin.x86_64.zip -d /opt/
chmod +x /opt/ovftool/ovftool /opt/ovftool/ovftool.bin
ln -s /opt/ovftool/ovftool /usr/local/bin/ovftool

Verify installation:
# ovftool --version

Step 2: Passwordless Authentication RHEL server to ESXI8 Server 

Generate the Key on RHEL
Log in to your RHEL server and generate the 4096-bit RSA key.
# ssh-keygen -t rsa -b 4096
Press Enter to save to the default location (/root/.ssh/id_rsa).
Enter a passphrase for extra security.
Display and copy the key:
# cat ~/.ssh/id_rsa.pub
Highlight and copy the entire string starting with ssh-rsa.
To fix Error :Unable to negotiate with 192.168.10.103 port 22: no matching host key type found. Their offer: ssh-rsa,ssh-dss
# vi ~/.ssh/config
Host *
    HostKeyAlgorithms +ssh-rsa
    PubkeyAcceptedAlgorithms +ssh-rsa
To Fix Error: ssh_dispatch_run_fatal: Connection to 192.168.10.103 port 22: error in libcrypto
system that uses crypto-policies, you can set it to LEGACY to restore compatibility with older hardware:
# update-crypto-policies --set LEGACY

Prepare the ESXi 8 Server
Enable SSH: Log into the ESXi Host Client (Web UI) -> Manage -> Services -> Start TSM-SSH.
Login via SSH using your root password.
Install the Key on ESXi
In ESXi 8, the default location for the root user's authorized keys is /etc/ssh/keys-root/authorized_keys.
Open the file:
# vi /etc/ssh/keys-root/authorized_keys
Paste the key: Press i for Insert mode, paste your key, then press Esc and type :wq! to save and exit.
Set Strict Permissions: ESXi will ignore the key if the permissions are too open.
# chmod 600 /etc/ssh/keys-root/authorized_keys

Test the Connection from RHEL
Go back to your RHEL terminal and attempt to connect.
# ssh root@<ESXi_IP_Address>

Note: If it still asks for a password, check the ESXi /etc/ssh/sshd_config file to ensure PubkeyAuthentication yes and AuthorizedKeysFile points to /etc/ssh/keys-root/authorized_keys.

Step 4: Prepare Your CSV Input
Your VM deployment details are stored in a CSV file. Example:

#vm_name,hostname,primary_ip,primary_gateway,primary_dns,secondary_ip,secondary_gateway,secondary_dns
#vm_name,hostname,primary_ip,primary_gateway,primary_dns,secondary_ip,secondary_gateway,secondary_dns
INDRXLTST11,indrxltst11.ppc.com,192.168.10.50,192.168.10.1,192.168.10.100,192.168.20.50,192.168.20.1,192.168.20.100
INDRXLTST12,indrxltst12.ppc.com,192.168.10.51,192.168.10.1,192.168.10.100,192.168.20.51,192.168.20.1,192.168.20.100
INDRXLTST13,indrxltst13.ppc.com,192.168.10.52,192.168.10.1,192.168.10.100,192.168.20.52,192.168.20.1,192.168.20.100

Save this file as vm-deploy.csv.

Step 5: Deploying VMs with the Script
We use a Bash script to read the CSV file and deploy each VM using ovftool. The script also configures network settings and hostnames on the VM using sshpass for password-based SSH.

Key features:
  • Deploy VMs from an OVA template.
  • Set hostname, primary, and secondary NIC IPs.
  • Reboot the VM and wait for SSH availability.
  • Run post-deployment checks (hostname, IP, disk, and network).
Script: deploy-configure-vms.sh
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
#!/bin/bash
# Author: adminCtrlX
set -e

ESXI_IP="192.168.10.101"
ESXI_USER="root"
ESXI_PASS="Welcome@123"
OVA_PATH="/ppcdata/servers-template/redhat10-template.ova"
DATASTORE="STG-DC01-MNG"
PRIMARY_NETWORK="Private Network"
SECONDARY_NETWORK="Local Network"
VM_ROOT_PASS="root123"

# Max concurrent deployments
MAX_PARALLEL=3

# Check if sshpass is installed
if ! command -v sshpass &>/dev/null; then
    echo "sshpass is required but not installed. Install it first."
    exit 1
fi

# =========================================
# Read CSV file
# =========================================
read -p "Enter CSV file path: " CSV_FILE
[[ ! -f "$CSV_FILE" ]] && { echo "CSV file not found!"; exit 1; }

# Function to deploy/configure a single VM
deploy_vm() {
    local VM_NAME="$1"
    local HOSTNAME="$2"
    local P_IP="$3"
    local P_GW="$4"
    local P_DNS="$5"
    local S_IP="$6"
    local S_GW="$7"
    local S_DNS="$8"

    local LOGFILE="deploy_${VM_NAME}.log"
    echo "==== Deploying $VM_NAME ====" | tee -a "$LOGFILE"

    # Deploy OVA if VM doesn't exist
    VMID=$(sshpass -p "$ESXI_PASS" ssh -o StrictHostKeyChecking=no ${ESXI_USER}@${ESXI_IP} \
           "vim-cmd vmsvc/getallvms" | awk -v vm="$VM_NAME" '$0 ~ vm {print $1}')
    if [[ -z "$VMID" ]]; then
        echo "Deploying OVA template..." | tee -a "$LOGFILE"
        ovftool --acceptAllEulas --skipManifestCheck \
            --name="$VM_NAME" \
            --datastore="$DATASTORE" \
            --network="$PRIMARY_NETWORK" \
            "$OVA_PATH" \
            "vi://${ESXI_USER}:${ESXI_PASS}@${ESXI_IP}/" &>> "$LOGFILE"

        sleep 10
        VMID=$(sshpass -p "$ESXI_PASS" ssh -o StrictHostKeyChecking=no ${ESXI_USER}@${ESXI_IP} \
               "vim-cmd vmsvc/getallvms" | awk -v vm="$VM_NAME" '$0 ~ vm {print $1}')
    fi

    [[ -z "$VMID" ]] && { echo "ERROR: VM deployment failed for $VM_NAME" | tee -a "$LOGFILE"; return; }
    echo "VMID: $VMID" | tee -a "$LOGFILE"

    # Power on VM
    STATE=$(sshpass -p "$ESXI_PASS" ssh -o StrictHostKeyChecking=no ${ESXI_USER}@${ESXI_IP} \
            "vim-cmd vmsvc/power.getstate $VMID" | tail -n1)
    if [[ "$STATE" != "Powered on" ]]; then
        echo "Powering on VM..." | tee -a "$LOGFILE"
        sshpass -p "$ESXI_PASS" ssh -o StrictHostKeyChecking=no ${ESXI_USER}@${ESXI_IP} \
            "vim-cmd vmsvc/power.on $VMID" &>> "$LOGFILE"
    else
        echo "VM already powered on." | tee -a "$LOGFILE"
    fi

    # Wait for initial VMware Tools IP (optional logging)
    VM_IP=""
    for i in {1..30}; do
        VM_IP=$(sshpass -p "$ESXI_PASS" ssh -o StrictHostKeyChecking=no ${ESXI_USER}@${ESXI_IP} \
                 "vim-cmd vmsvc/get.guest $VMID" | awk -F\" '/ipAddress/ {print $2; exit}')
        [[ -n "$VM_IP" && "$VM_IP" != "0.0.0.0" ]] && break
        sleep 5
    done
    [[ -n "$VM_IP" ]] && echo "VM initially reports IP: $VM_IP" | tee -a "$LOGFILE"

    # Configure hostname & network
    sshpass -p "$VM_ROOT_PASS" ssh -o StrictHostKeyChecking=no root@$VM_IP <<EOF &>> "$LOGFILE"
set -e

HN_SHORT=\$(echo "$HOSTNAME" | cut -d. -f1)
hostnamectl set-hostname "\$HN_SHORT"

cat > /etc/hosts <<EOL
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6

$P_IP   $HOSTNAME \$HN_SHORT
EOL

# Primary NIC (ens192)
CON1=\$(nmcli -t -f NAME,DEVICE con show | grep ens192 | cut -d: -f1)
nmcli con mod "\$CON1" ipv4.addresses $P_IP/24 ipv4.gateway $P_GW ipv4.dns $P_DNS ipv4.method manual

# Secondary NIC (ens224)
CON2=\$(nmcli -t -f NAME,DEVICE con show | grep ens224 | cut -d: -f1)
nmcli con mod "\$CON2" ipv4.addresses $S_IP/24 ipv4.gateway $S_GW ipv4.dns $S_DNS ipv4.method manual

reboot
EOF

    echo "Waiting for VM $VM_NAME to come back..." | tee -a "$LOGFILE"

    # Wait for SSH on primary IP
    for i in {1..60}; do
        if nc -z -w5 "$P_IP" 22 &>/dev/null; then
            echo "SSH available on $P_IP" | tee -a "$LOGFILE"
            break
        fi
        echo "SSH not ready yet... retry $i/60" | tee -a "$LOGFILE"
        sleep 10
    done
    if ! nc -z -w5 "$P_IP" 22 &>/dev/null; then
        echo "ERROR: SSH never became available on $P_IP" | tee -a "$LOGFILE"
        return
    fi

    # Run post-checks
    sshpass -p "$VM_ROOT_PASS" ssh -o StrictHostKeyChecking=no root@$P_IP <<'POSTEOF' &>> "$LOGFILE"
set -e
echo "====== Post-deployment checks ======"
echo "Hostname: $(hostname)"
echo "IP Addresses:"; ip addr show
echo "Disk Usage:"; df -h
echo "Network connectivity test:"; ping -c 2 $P_IP || echo "Ping failed"
echo "Services status (example: sshd):"; systemctl status sshd | head -20
echo "Post-deployment checks completed successfully."
POSTEOF

    echo "==== VM $VM_NAME deployment complete! ====" | tee -a "$LOGFILE"
}

# =========================================
# Read CSV and deploy VMs in parallel
# =========================================
PIDS=()
while IFS=, read -r VM_NAME HOSTNAME P_IP P_GW P_DNS S_IP S_GW S_DNS; do
    [[ -z "$VM_NAME" || "$VM_NAME" =~ ^# ]] && continue

    deploy_vm "$VM_NAME" "$HOSTNAME" "$P_IP" "$P_GW" "$P_DNS" "$S_IP" "$S_GW" "$S_DNS" &

    PIDS+=($!)

    # Limit parallel jobs
    while [[ $(jobs -r -p | wc -l) -ge $MAX_PARALLEL ]]; do
        sleep 5
    done

done < "$CSV_FILE"

# Wait for all background jobs to finish
wait

echo "All VMs processed successfully ..............................................."

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Step 6: Running the Deployment
[root@inddcpppz01 scripts]# chmod +x deploy-configure-vms.sh
[root@inddcpppz01 scripts]# ./deploy-configure-vms.sh
[root@inddcpppz01 scripts]# ./esxi-deploy-configure.sh
Enter CSV file path: /root/scripts/deploy-configure-vms.csv
==== Deploying INDRXLTST11 ====
==== Deploying INDRXLTST12 ====
==== Deploying INDRXLTST13 ====
Deploying OVA template...
Deploying OVA template...
Deploying OVA template...
VMID: 4
VMID: 5
VMID: 6
Powering on VM...
Powering on VM...
Powering on VM...
VM initially reports IP: 192.168.10.38
VM initially reports IP: 192.168.10.48
VM initially reports IP: 192.168.10.47
Waiting for VM INDRXLTST13 to come back...
Waiting for VM INDRXLTST12 to come back...
Waiting for VM INDRXLTST11 to come back...
SSH not ready yet... retry 1/60
SSH not ready yet... retry 1/60
SSH not ready yet... retry 1/60
SSH not ready yet... retry 2/60
SSH not ready yet... retry 2/60
SSH not ready yet... retry 2/60
SSH available on 192.168.10.51
SSH available on 192.168.10.50
SSH available on 192.168.10.52
==== VM INDRXLTST12 deployment complete! ====
==== VM INDRXLTST13 deployment complete! ====
==== VM INDRXLTST11 deployment complete! ====
All VMs processed successfully ...............................................
[root@inddcpppz01 scripts]#

The script will:
  • Check if VM exists; if not, deploy using OVA.
  • Power on the VM.
  • Configure hostnames and network interfaces.
  • Wait for SSH to become available.
  • Run post-deployment checks.
Step 7: Example Deployment Log
After deployment, logs like deploy_INDRXLTST11.log are generated:

[root@inddcpppz01 scripts]# cat deploy_INDRXLTST11.log
==== Deploying INDRXLTST11 ====
Deploying OVA template...
Opening OVA source: /ppcdata/servers-template/redhat10-template.ova
Opening VI target: vi://root@192.168.10.101:443/
Deploying to VI: vi://root@192.168.10.101:443/
Transfer Completed
The manifest does not validate
Warning:
 - The manifest is present but user flag causing to skip it
Completed successfully
VMID: 53
Powering on VM...
Powering on VM:
VM initially reports IP: 192.168.10.39
Pseudo-terminal will not be allocated because stdin is not a terminal.
*********************************************************
*    !!!! WELCOME TO PPC.COM TEST LAB SERVER'S !!!!     *
* This server is meant for testing Linux commands and   *
* Tools. If you are not associated with ppc.com and     *
* Not authorized. Please dis-connect immediately.       *
*********************************************************
Waiting for VM INDRXLTST11 to come back...
SSH not ready yet... retry 1/60
SSH not ready yet... retry 2/60
SSH available on 192.168.10.50
Pseudo-terminal will not be allocated because stdin is not a terminal.
*********************************************************
*    !!!! WELCOME TO PPC.COM TEST LAB SERVER'S !!!!     *
* This server is meant for testing Linux commands and   *
* Tools. If you are not associated with ppc.com and     *
* Not authorized. Please dis-connect immediately.       *
*********************************************************
====== Post-deployment checks ======
Hostname: indrxltst11
IP Addresses:
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host noprefixroute
       valid_lft forever preferred_lft forever
2: ens192: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 00:0c:29:ca:5b:29 brd ff:ff:ff:ff:ff:ff
    altname enp11s0
    altname enx000c29ca5b29
    inet 192.168.10.50/24 brd 192.168.10.255 scope global noprefixroute ens192
       valid_lft forever preferred_lft forever
    inet6 fe80::20c:29ff:feca:5b29/64 scope link noprefixroute
       valid_lft forever preferred_lft forever
3: ens224: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 00:0c:29:ca:5b:33 brd ff:ff:ff:ff:ff:ff
    altname enp19s0
    altname enx000c29ca5b33
    inet 192.168.20.50/24 brd 192.168.20.255 scope global noprefixroute ens224
       valid_lft forever preferred_lft forever
    inet6 fe80::20c:29ff:feca:5b33/64 scope link noprefixroute
       valid_lft forever preferred_lft forever
Disk Usage:
Filesystem               Size  Used Avail Use% Mounted on
/dev/mapper/rootvg-root   17G  2.7G   15G  16% /
devtmpfs                 4.0M     0  4.0M   0% /dev
tmpfs                    478M     0  478M   0% /dev/shm
tmpfs                    192M  3.8M  188M   2% /run
tmpfs                    1.0M     0  1.0M   0% /run/credentials/systemd-journald.service
/dev/sda2                960M  279M  682M  29% /boot
tmpfs                    1.0M     0  1.0M   0% /run/credentials/getty@tty1.service
tmpfs                     96M  4.0K   96M   1% /run/user/0
Network connectivity test:
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=128 time=51.7 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=128 time=51.8 ms

--- 8.8.8.8 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 51.660/51.710/51.761/0.050 ms
Services status (example: sshd):
● sshd.service - OpenSSH server daemon
     Loaded: loaded (/usr/lib/systemd/system/sshd.service; enabled; preset: enabled)
     Active: active (running) since Fri 2026-02-06 21:13:27 IST; 5s ago
 Invocation: 1c42fed46327449098fb26c3255a7190
       Docs: man:sshd(8)
             man:sshd_config(5)
   Main PID: 1003 (sshd)
      Tasks: 1 (limit: 5893)
     Memory: 7.6M (peak: 24.1M)
        CPU: 127ms
     CGroup: /system.slice/sshd.service
             └─1003 "sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups"

Feb 06 21:13:27 indrxltst11 systemd[1]: Starting sshd.service - OpenSSH server daemon...
Feb 06 21:13:27 indrxltst11 (sshd)[1003]: sshd.service: Referenced but unset environment variable evaluates to an empty string: OPTIONS
Feb 06 21:13:27 indrxltst11 sshd[1003]: Server listening on 0.0.0.0 port 22.
Feb 06 21:13:27 indrxltst11 systemd[1]: Started sshd.service - OpenSSH server daemon.
Feb 06 21:13:27 indrxltst11 sshd[1003]: Server listening on :: port 22.
Feb 06 21:13:30 indrxltst11 sshd-session[1330]: Accepted password for root from 192.168.10.104 port 40404 ssh2
Feb 06 21:13:31 indrxltst11 sshd-session[1330]: pam_unix(sshd:session): session opened for user root(uid=0) by root(uid=0)
Post-deployment checks completed successfully.
==== VM INDRXLTST11 deployment complete! ====
[root@inddcpppz01 scripts]# 

Step 8: Summary
By combining ovftool, sshpass, and a CSV-driven Bash script, you can:
  • Rapidly deploy multiple VMs in parallel.
  • Automatically configure hostnames and network settings.
  • Perform initial health checks post-deployment.
  • Maintain logs for auditing and troubleshooting.
This approach is perfect for labs, test environments, and repetitive deployment scenarios.

No comments:

Post a Comment