Created
August 9, 2025 18:26
-
-
Save jp-hoehmann/f7a13fa83409853204d1998ff05ac103 to your computer and use it in GitHub Desktop.
Backup Scripts for ESXi
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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