Skip to content

Instantly share code, notes, and snippets.

@jp-hoehmann
Created August 9, 2025 18:26
Show Gist options
  • Save jp-hoehmann/f7a13fa83409853204d1998ff05ac103 to your computer and use it in GitHub Desktop.
Save jp-hoehmann/f7a13fa83409853204d1998ff05ac103 to your computer and use it in GitHub Desktop.
Backup Scripts for ESXi
#!/bin/env sh
#
# /vmfs/volumes/datastore3/backup/backup.sh
#
# Backup virtual machines.
#
# This will create a backup of my virtual machines. It has
# very little error handling, since no error handling will
# ever be fool proof enough for me to trust it, so I will
# always have to check the work of this script manually
# anyways.
#
# This script will back up all virtual machines from one
# datastore passed on the command line. As part of the
# backup process any snapshots of all backed up machines
# will be removed. This script will also copy disks
# belonging to the given virtual machines, if they are
# stored on other datastores, as long as the directory the
# disks are stored in is named after the virtual machine.
#
#
# Constants
#
# Datastores eligible as sources.
#
# All datastores that data may be copied from must match
# this. The datastore provided on the command line must
# be eligible as a source. Disk images residing on stores
# other than the one passed on the command line will only
# be cloned if the datastore they reside on is eligible as
# a source. This is done to prevent copying of existing
# backups (or clones of backups) into the new backup.
# The value for this is a regex, which is anchored
# automatically and will need to match the entire name of
# the datastore.
#
srcs="datastore[12]"
# First datastore to backup to
#
# All virtual machines from the source will first be copied to
# this datastore.
#
dst='datastore3'
# Machines to ignore
#
# If this is set, machines whose names match this regex will
# not be backed up and their snapshots will be left intact.
# This regex is anchored automatically and will need to
# match the entire machine name.
#
ign='Nobox'
#
# Functions
#
# Get a list of VMs to back up.
#
# Given a datastore, this will list all the VMs on that
# datastore, that are not set to be ignored.
#
get_vms() {
vim-cmd vmsvc/getallvms \
| awk -F '[[:space:]][[:space:]]+' '$2 !~ /^('"${ign}"')$/ && $3 ~ /^\['"${1}"'\]\s/'
}
# Get the IDs of the VMs to back up.
#
# Given a datastore, this will list the IDs of all VMs on
# that datastore, that are not set to be ignored.
#
get_vmids() {
get_vms "${1}" \
| awk -F '[[:space:]][[:space:]]+' '{ print $1; }'
}
# Get the names of the VMs to back up.
#
# Given a datastore, this will list the names of all VMs
# on that datastore, that are not set to be ignored.
#
get_vmnames() {
get_vms "${1}" \
| awk -F '[[:space:]][[:space:]]+' '{ print $2; }'
}
# Get directories to back up.
#
# Given a datastore, this will return the path to all
# directories whose name matches that of one of the
# machines that are to be backed up from that datastore,
# relative to /vmfs/volumes. This will return the
# directories of the machines that should be backed up,
# along with directories on other datastores that may or
# may not contain additional disks (which may or may not
# need to be backed up), as well as directories in the
# actual backup, containing existing copies of the
# machines that will be backed up.
#
# The directories are all collected, then emitted
# alphabetically all at once. This is to avoid a nasty
# race condition if a script uses the output of this
# function to delete the very directories this function
# is listing.
#
get_vmdirs() {
local pwd=`pwd`
cd /vmfs/volumes/
get_vmnames "${1}" \
| awk '{ print gensub(/([][*?])/,"\\\\\\1", "g"); }' \
| while read -r; do find datastore*/. -type d -name "${REPLY}"; done \
| sort
cd "${pwd}"
}
# Delete all snapshots of the VMs included in the backup.
#
# Given a datastore, this will delete the snapshots of
# all the VMs on that datastore, that are not set to be
# ignored.
#
remove_snapshots() {
for i in `get_vmids "${1}"`
do vim-cmd vmsvc/snapshot.removeall "${i}"
done
}
# Print statistics.
#
# This will print a list all files contained in the backup
# that have a file type $2 with a modification time $3,
# labeled $1. Files whose names contain '*-flat*, or
# '*-s0*' will not be listed, no matter what. Only
# filenames will be listed, the path and file ending is
# stripped.
#
print_stats() {
local pwd=`pwd`
cd "/vmfs/volumes/${dst}/backup/"
for i in datastore*
do
cd "${i}"
echo -en "[${i}] ${1}"
find . -name "*.${2}" -not -name '*-flat*' -not -name '*-s0' -mtime "${3}" -exec basename {} \; \
| awk '{ sub("\\\.'"${2}"'$", ""); print; }' ORS=', ' \
| head -c -2
echo
cd ..
done
cd "${pwd}"
}
# Print a really striking error message.
#
err() {
cat << 'EOF'
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ @
@ !!! ERROR !!! THE OPERATION HAS FAILED !!! ERROR !!! @
@ @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
EOF
false
}
#
# Logging
#
exec >> "`dirname $0`/log/backup_`date +%Y-%m`.log"
exec 2>&1
echo
cat << EOF
#
# MARK: Starting backup of ${1} at `date`
#
EOF
#
# Traps
#
trap '[ $? -eq 0 ] || err' exit
#
# Options
#
set -euo pipefail
#
# Main routine
#
cd /vmfs/volumes/
# Check for conflicting PID-files.
for i in backup clone
do
if [ -f "/var/run/${i}.pid" ]
then
echo "Found existing PID-file for ${i}!"
echo "The previous ${i} has either failed, or is still running."
exit 1
fi
done
# Create PID-file.
echo $$ > /var/run/backup.pid
# Make sure the source datastore exists and is eligible for backup.
if [ ! -d "${1}" ]
then
echo "Datastore not found!"
echo "Please ensure the specified datastore '${1}' does exist."
exit 1
fi
if [ "x`echo "${1}" | awk '/^('"${srcs}"')$/'`" != "x${1}" ]
then
echo "Datastore not eligible!"
echo "The datastore '${1}' must match against the regex of allowed data sources."
exit 1
fi
# Ensure backup directory exists
[ -d "${dst}/backup/${1}" ] || mkdir "${dst}/backup/${1}"
# Remove existing snapshots
echo
echo 'Removing snapshots...'
remove_snapshots "${1}"
# Remove old backups
echo
echo 'Preparing backup...'
get_vmdirs "${1}" \
| awk '/^('"${srcs}"')\/\.\//' \
| while read -r
do
if [ -d "${dst}/backup/${REPLY}/" ]
then
echo "Removing old backup: ${dst}/backup/${REPLY}"
rm -r "${dst}/backup/${REPLY}/"
fi
echo "Preparing new backup: ${dst}/backup/${REPLY}"
mkdir -p "${dst}/backup/${REPLY}/" || :
if ! [ -d "${dst}/backup/${REPLY}/" ]
then
echo "Could not create directory ${dst}/backup/${REPLY}!"
exit 1
fi
done
# Copy VM configuration files
echo
echo 'Copying configuration files...'
get_vmdirs "${1}" \
| awk '/^'"${1}"'\/\.\//' \
| while read -r
do
echo "Copying configuration: ${REPLY}"
cd "${REPLY}"
cp *.nvram *.vmsd *.vmx "/vmfs/volumes/${dst}/backup/${REPLY}/"
cd /vmfs/volumes
done
# Get disks to copy
echo
echo 'Finding disks to clone...'
vmdks=`
get_vmdirs "${1}" \
| awk '/^('"${srcs}"')\/\.\//' \
| while read -r; do find "${REPLY}" -name '*.vmdk' ! -name '*-flat*' ! -name '*-s0*'; done
`
echo 'Found disks:'
echo "${vmdks}"
# Take snapshots
echo
echo 'Taking snapshots...'
for i in `get_vmids "${1}"`
do vim-cmd vmsvc/snapshot.create $i tmp 'Temporary snapshot created by backup script' 0 0
done
# Copy disks
echo
echo 'Cloning disks...'
echo "${vmdks}" \
| while read -r
do
echo "Cloning: ${REPLY}"
vmkfstools -i "${REPLY}" "${dst}/backup/${REPLY}" -d thin > /dev/null
done
# Remove snapshots
echo
echo 'Removing temporary snapshots...'
remove_snapshots "${1}"
# If we arrive here, the backup has succeded. Print some
# nice, reassuring stats.
cd "${dst}/backup"
echo
echo "#"
echo "# Backup successfully completed at `date`"
echo "#"
print_stats 'Curr. VMs: ' 'vmx' '-30'
print_stats 'Curr. imgs: ' 'vmdk' '-30'
print_stats 'Old VMs: ' 'vmx' '+30'
print_stats 'Old imgs: ' 'vmdk' '+30'
# All done, remove PID-file.
rm /var/run/backup.pid
#!/bin/env sh
#
# /vmfs/volumes/datastore3/backup/clone.sh
#
# Clone backup
#
# This will create a clone of the backup of my virtual machines. It has very
# little error handling, since no error handling will ever be fool proof enough
# for me to trust it, so I will always have to check the work of this script
# manually anyways.
#
# This script will create a copy of everything (except for hidden files and
# directories) on the backup datastore, making sure to sparsely clone vmdks
# instead of copying them.
#
#
# Constants
#
# Datastore to backup from
#
# Any virtual machines residing in this data store will be
# backed up.
#
src='datastore3'
# Datastores to try for cloning.
#
# All virtual machines from the source will first be copied to
# the first available datastore from this list.
#
dsts='datastore4 datastore5'
# Machines to snapshot
#
# All machines that have disks connected to them that will be cloned, need to be
# snapshoted first, so the ids of the affected machines need to be provided
# here. All machines provided here will have their existing snapshots removed
# in the process of cloning.
#
vmids='33' # valo.media MacPro
# Delete all snapshots of the VMs included in the backup.
#
remove_snapshots() {
for i in $vmids
do vim-cmd vmsvc/snapshot.removeall "${i}"
done
}
# Print a really striking error message.
#
err() {
cat << 'EOF'
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ @
@ !!! ERROR !!! THE OPERATION HAS FAILED !!! ERROR !!! @
@ @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
EOF
false
}
#
# Logging
#
log="/vmfs/volumes/${src}/backup/log/clone_`date +%Y-%m`.log"
exec >> "${log}"
exec 2>&1
echo
cat << EOF
#
# MARK: Starting clone of backup at `date`
#
EOF
#
# Traps
#
trap '[ $? -eq 0 ] || err' exit
#
# Options
#
set -euo pipefail
#
# Main routine
#
cd /vmfs/volumes/
# Check for conflicting PID-files.
for i in backup clone
do
if [ -f "/var/run/${i}.pid" ]
then
echo
echo "Found existing PID-file for ${i}!"
echo "The previous ${i} has either failed, or is still running."
exit 1
fi
done
# Create PID-file.
echo $$ > /var/run/clone.pid
# Find target datastore
echo
echo 'Choosing target datastore...'
for i in ${dsts}
do
echo "Trying target datastore: ${i}"
if [ -d $i ]
then
echo "Found available datastore: ${i}"
dst=$i
break
fi
done
if [ "x${dst}" = "x" ]
then
echo "No suitable datastore for cloning found!"
exit 1
fi
# Remove old backup.
echo
echo 'Removing existing clone...'
cd "${dst}"
if [ "x`echo *`" = "x*" ]
then
echo "Nothing to remove"
else
for i in *
do
echo "Removing: ${i}"
rm -r "${i}"
done
fi
cd ..
# Remove existing snapshots.
echo
echo 'Removing snapshots...'
remove_snapshots
# Get file listings.
echo
echo 'Finding contents...'
cd "${src}"
dirs=`find . -type d ! -name '.*'`
files=`find . -type f ! -name '*.vmdk' ! -name '.*'`
disks=`find . -name '*.vmdk' ! -name '*-flat*' ! -name '*-s0*' ! -name '.*'`
cd ..
echo 'Found directories:'
echo "${dirs}"
echo 'Found files:'
echo "${files}"
echo 'Found disks:'
echo "${disks}"
# Take temporary snapshots.
echo
echo 'Taking snapshots...'
for i in ${vmids}
do vim-cmd vmsvc/snapshot.create $i tmp 'Temporary snapshot created by clone script' 0 0
done
# Create directories.
echo
echo 'Creating directories...'
echo "${dirs}" \
| while read -r
do
echo "Creating: ${REPLY}"
mkdir "${dst}/${REPLY}"
done
# Copy files.
echo
echo 'Copying files...'
echo "${files}" \
| while read -r
do
echo "Copying: ${REPLY}"
cp "${src}/${REPLY}" "${dst}/${REPLY}"
done
# Copy disks.
echo
echo 'Cloning disks...'
echo "${disks}" \
| while read -r
do
echo "Cloning: ${REPLY}"
vmkfstools -i "${src}/${REPLY}" "${dst}/${REPLY}" -d thin > /dev/null
done
# Remove temporary snapshots
echo
echo 'Removing snapshots...'
remove_snapshots
# If we arrive here, the clone has succeded. Print some
# nice, reassuring stats.
echo
echo "#"
echo "# Clone successfully completed at `date`"
echo "#"
df -h "${src}" "${dst}"
cd "${dst}"
du -sh backup/datastore*
du -sch *
# All done, remove PID-file.
rm /var/run/clone.pid
# Copy log-file to the destination as well.
cp "${log}" backup/log/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment