#!/bin/bash
# region header
# vim: set tabstop=4 shiftwidth=4 expandtab:
# vim: foldmethod=marker foldmarker=region,endregion:
# region dependencies:
# VMware Player - Runs a virtual machine.
# VMware Workstation - Runs, edits, creates virtual machines for business
# usage.
# VirtualBox ver. 4+ - Plays, creates, edits virtual machines.
# bash ver. 4+ - A sh-compatible command line interpreter.
# cat - Concatenate and prints file contents.
# echo - Prints chars.
# grep - Prints lines which matches a given regular expression
# pattern.
# sed - Stream-editor for filtering and transformation text.
# mktemp - Creates temporary files and folders.
# dirname - Extracts the last element from a given file path.
# mv - moves and renames file objects.
# rm - Removes files and folders.
# cp - Copies files and folders.
# sleep - Delays program calls.
# readlink - Print resolved symbolic links or canonical file names.
# endregion
# Abbreviation for "createLinkedClone".
__NAME__='vmchooser-clc'
# endregion
# Provides the main module scope.
function vmchooser-clc() {
# region configuration
# region private properties
# region command line arguments
local _VERBOSE='no'
# NOTE: You should write upper case first letters to support valid
# camel case method names to handle given hypervisor.
local _SUPPORTED_HYPERVISOR=('VMware' 'VirtualBox')
# NOTE: This value will be determined automatically. If no hypervisor
# could be detected this value will be used as default.
# The first value from supported Machines is taken as default.
local _HYPERVISOR="$_SUPPORTED_HYPERVISOR"
local _PERSISTENT_SHORT_DESCRIPTION_SUFFIX=' --persistent--'
local _PERSISTENT_CONFIG_TARGET=''
local _BASIC_IMAGE_CONFIGURATION_FILE_PATH=''
local _TARGET_PATH=''
local _VIRTUAL_BOX_SNAPSHOT_NAME='persistentUserSnapshot'
# endregion
local _STANDARD_OUTPUT=/dev/null
local _ERROR_OUTPUT=/dev/null
local _VIRTUAL_BOX_SNAPSHOT_UUID_FILE_PATH='/tmp/clcVirtualBoxSnapshotUUID'
# endregion
# endregion
# region functions
# region command line interface
# Prints a description about how to use this program.
function clcPrintUsageMessage() {
cat << EOF
$__NAME__ Generates a linked clone from given machine description file in
given target location.
EOF
return $?
}
# Prints a description about how to use this program by providing examples.
function clcPrintUsageExamples() {
cat << EOF
# Getting a help message.
>>> $0 --help
# Creating a linked clone.
>>> $0 /path/to/config.xml ~/.persistentLinkedClones/
# Creating a linked clone configuration file.
>>> $0 /path/to/config.xml ~/.persistentLinkedClones/ -c
# Creating a linked clone in verbose mode.
>>> $0 /path/to/config.xml ~/.persistentLinkedClones/ --verbose
# Creating a linked clone in verbose mode with debugging output.
>>> $0 /path/to/config.xml ~/.persistentLinkedClones/ --verbose --debug
# Creating a linked clone in verbose mode with debugging output.
>>> $0 /path/to/config.xml ~/.persistentLinkedClones/ -v -d
EOF
return $?
}
# Prints descriptions about each available command line option.
function clcPrintCommandLineOptionDescriptions() {
# NOTE; All letters are used for short options.
cat << EOF
-h --help Shows this help message.
-v --verbose Tells you what is going on (default: "$_VERBOSE").
-d --debug Gives you any output from all tools which are used
(default: "$_DEBUG").
-c --create-persistent-config PERSISTENT_IMAGE_FILE_PATH If set an xml file
for persistent openslx boot will be created referencing to given
persistent image path (default: "$_PERSISTENT_CONFIG_TARGET").
-v --virtualbox-snapshot-name NAME Provide a name for newly created
snapshots. (default: "$_VIRTUAL_BOX_SNAPSHOT_NAME").
-u --virtualbox-snapshot-uuid-file-path PATH Provide a file path where to
save the newly generated snapshot uuid (default: \
"$_VIRTUAL_BOX_SNAPSHOT_UUID_FILE_PATH").
EOF
return $?
}
# Provides a help message for this module.
function clcPrintHelpMessage() {
echo -e \
"\nUsage: $0 BASIC_IMAGE_CONFIGURATION_FILE_PATH TARGET_PATH [options]\n" && \
clcPrintUsageMessage "$@" && \
echo -e '\nExamples:\n' && \
clcPrintUsageExamples "$@" && \
echo -e '\nOption descriptions:\n' && \
clcPrintCommandLineOptionDescriptions "$@" && \
echo && \
return $?
}
# Provides the command line interface and interactive questions.
function clcCommandLineInterface() {
while true; do
case "$1" in
-h|--help)
shift
clcPrintHelpMessage "$0"
exit 0
;;
-v|--verbose)
shift
_VERBOSE='yes'
;;
-d|--debug)
shift
_DEBUG='yes'
_STANDARD_OUTPUT=/dev/stdout
_ERROR_OUTPUT=/dev/stderr
;;
-c|--create-persistent-config)
shift
_PERSISTENT_CONFIG_TARGET="$1"
shift
;;
-v|--virtualbox-snapshot-name)
shift
_VIRTUAL_BOX_SNAPSHOT_NAME="$1"
shift
;;
-u|--virtualbox-snapshot-uuid-file-path)
shift
_VIRTUAL_BOX_SNAPSHOT_UUID_FILE_PATH="$1"
shift
;;
'')
shift
break 2
;;
*)
if [[ ! "$_BASIC_IMAGE_CONFIGURATION_FILE_PATH" ]]; then
_BASIC_IMAGE_CONFIGURATION_FILE_PATH="$1"
elif [[ ! "$_TARGET_PATH" ]]; then
_TARGET_PATH="$1"
else
clcLog 'critical' \
"Given argument: \"$1\" is not available." '\n'
clcPrintHelpMessage "$0"
return 1
fi
shift
;;
esac
done
if [[ ! "$_BASIC_IMAGE_CONFIGURATION_FILE_PATH" ]] || \
[[ ! "$_TARGET_PATH" ]]; then
clcLog 'critical' \
"You have to provide a basic image configuration file and a destination path."
clcPrintHelpMessage "$0"
return 1
fi
local supportedVirtualMachine
for supportedVirtualMachine in ${_SUPPORTED_HYPERVISOR[*]}; do
if [[ "$(clcGetXMLValue 'virtualMachine' | \
grep --ignore-case "$supportedVirtualMachine")" ]]; then
_HYPERVISOR="$supportedVirtualMachine"
clcLog 'debug' "Detected \"$_HYPERVISOR\" as hypervisor."
break
fi
done
clcLog 'info' "Using \"$_HYPERVISOR\" as hypervisor." && \
return $?
}
# Grabs a value from currently loaded xml file.
function clcGetXMLValue() {
grep --ignore-case --only-matching "<$1 param=.*" \
"$_BASIC_IMAGE_CONFIGURATION_FILE_PATH" | awk -F '"' '{ print $2 }'
return $?
}
# Handles logging messages. Returns non zero and exit on log level error to
# support chaining the message into toolchain.
function clcLog() {
local loggingType='info'
local message="$1"
if [ "$2" ]; then
loggingType="$1"
message="$2"
fi
if [ "$_VERBOSE" == 'yes' ] || [ "$loggingType" == 'error' ] || \
[ "$loggingType" == 'critical' ]; then
if [ "$3" ]; then
echo -e -n "$3"
fi
echo -e "${loggingType}: $message"
fi
if [ "$loggingType" == 'error' ]; then
exit 1
fi
}
# endregion
# region tools
# Returns the minimal vmx vmware configuration file content to create a
# snapshot.
function clcGetTemporaryVMXContent() {
cat << EOF
.encoding = "UTF-8"
config.version = "8"
virtualHW.version = "7"
ide0:0.present = "TRUE"
ide0:0.fileName = "$1"
displayName = ""
EOF
return $?
}
# This functions escapes every special meaning character for a sed
# replacement.
#
# Examples:
#
# >>> sed "s/myInputString/$(clcValidateSEDReplacement '\hans/peter&klaus')/g"
function clcValidateSEDReplacement() {
## bash
## sed --expression 's/\\/\\\\/g' --expression 's/\//\\\//g' \
## --expression 's/&/\\\&/g' <<< "$1"
sed -e 's/\\/\\\\/g' -e 's/\//\\\//g' -e 's/&/\\\&/g' <<< "$1"
##
return $?
}
# endregion
# region main tasks
# Creates a snapshot from VMware generated virtual machine.
function clcCreateVMwareSnapshot() {
local temporaryConfigurationPath="$(mktemp --directory)/" \
1>"$_STANDARD_OUTPUT" 2>"$_ERROR_OUTPUT" && \
local temporaryConfigurationFilePath="$(mktemp --suffix '.vmx')" \
1>"$_STANDARD_OUTPUT" 2>"$_ERROR_OUTPUT" && \
clcLog "Generate a temporary virtual machine configuration file \"$temporaryConfigurationFilePath\"." && \
clcGetTemporaryVMXContent "$(dirname \
"$_BASIC_IMAGE_CONFIGURATION_FILE_PATH")/$(clcGetXMLValue \
'image_name')" 1>"$temporaryConfigurationFilePath" \
2>"$_ERROR_OUTPUT" && \
mv "$temporaryConfigurationFilePath" "$temporaryConfigurationPath" \
1>"$_STANDARD_OUTPUT" 2>"$_ERROR_OUTPUT" && \
clcLog "Needed temporary files generated in \"$temporaryConfigurationPath\" generated." && \
vmrun snapshot "$temporaryConfigurationPath"*.vmx \
persistentUserSnapshot 1>"$_STANDARD_OUTPUT" \
2>"$_ERROR_OUTPUT" && \
mv "$temporaryConfigurationPath"*.vmdk "$_TARGET_PATH" \
1>"$_STANDARD_OUTPUT" 2>"$_ERROR_OUTPUT"
local result=$?
if [[ "$_DEBUG" == 'no' ]]; then
## bash rm --recursive "$temporaryConfigurationPath" \
rm -r "$temporaryConfigurationPath" \
1>"$_STANDARD_OUTPUT" 2>"$_ERROR_OUTPUT"
fi
return $result
}
# Creates a snapshot from virtualBox generated virtual machine.
function clcCreateVirtualBoxSnapshot() {
local temporaryConfigurationPath="$(mktemp --directory)/" \
1>"$_STANDARD_OUTPUT" 2>"$_ERROR_OUTPUT" && \
## bash
## (cp --recursive ~/.VirtualBox/ \
## "${temporaryConfigurationPath}virtualBoxBackup" \
## 1>"$_STANDARD_OUTPUT" 2>"$_ERROR_OUTPUT" || \
## mkdir --parents \
## "${temporaryConfigurationPath}virtualBoxBackup" \
## 1>"$_STANDARD_OUTPUT" 2>"$_ERROR_OUTPUT") && \
(cp -r ~/.VirtualBox/ \
"${temporaryConfigurationPath}virtualBoxBackup" \
1>"$_STANDARD_OUTPUT" 2>"$_ERROR_OUTPUT" || \
mkdir -p "${temporaryConfigurationPath}virtualBoxBackup" \
1>"$_STANDARD_OUTPUT" 2>"$_ERROR_OUTPUT") && \
##
# VBoxManage version to create snapshots.
clcLog 'Create a temporary virtual machine.' && \
# NOTE: Virtualbox needs 5 at leas 5 seconds to register that it has
# a new configuration directory.
sleep 5 1>"$_STANDARD_OUTPUT" 2>"$_ERROR_OUTPUT" && \
VBoxManage createvm --name tmp --basefolder \
"$temporaryConfigurationPath" --register 1>"$_STANDARD_OUTPUT" \
2>"$_ERROR_OUTPUT" && \
clcLog 'Create a temporary virtual ide controller.' && \
VBoxManage storagectl tmp --name tmp --add ide 1>"$_STANDARD_OUTPUT" \
2>"$_ERROR_OUTPUT" && \
clcLog 'Attach given virtual disk image to temporary virtual machine.' && \
VBoxManage storageattach tmp --storagectl tmp --port 0 --device 0 \
--type hdd --medium "$(dirname \
"$_BASIC_IMAGE_CONFIGURATION_FILE_PATH")/$(clcGetXMLValue \
'image_name')" \
1>"$_STANDARD_OUTPUT" 2>"$_ERROR_OUTPUT" && \
clcLog "Needed temporary files generated in \"$temporaryConfigurationPath\" generated." && \
clcLog "Take a snapshot with name or uuid \"$_VIRTUAL_BOX_SNAPSHOT_NAME\"." && \
VBoxManage snapshot tmp take "$_VIRTUAL_BOX_SNAPSHOT_NAME" \
1>"$_STANDARD_OUTPUT" 2>"$_ERROR_OUTPUT" && \
## bash
## local virtualBoxSnapshotUUID="$(basename \
## "${temporaryConfigurationPath}tmp/Snapshots/"*.vdi | sed \
## --regexp-extended 's/\{(.+)\}\..+/\1/g')" \
## 1>"$_STANDARD_OUTPUT" 2>"$_ERROR_OUTPUT" && \
local virtualBoxSnapshotUUID="$(basename \
"${temporaryConfigurationPath}tmp/Snapshots/"*.vdi | sed \
-r 's/\{(.+)\}\..+/\1/g')" 1>"$_STANDARD_OUTPUT" \
2>"$_ERROR_OUTPUT" && \
##
echo "$virtualBoxSnapshotUUID" \
1>"$_VIRTUAL_BOX_SNAPSHOT_UUID_FILE_PATH" 2>"$_ERROR_OUTPUT" && \
clcLog "Created snapshot uuid is \"$virtualBoxSnapshotUUID\" (uuid saved to \"$_VIRTUAL_BOX_SNAPSHOT_UUID_FILE_PATH\")." && \
clcLog "Copy result to given target path \"$_TARGET_PATH\"." && \
cp "${temporaryConfigurationPath}tmp/Snapshots/"*.vdi "$_TARGET_PATH" \
1>"$_STANDARD_OUTPUT" 2>"$_ERROR_OUTPUT" && \
# NOTE: Isn't possible because virtualbox tries to consolidate with
# readonly basic image.
#VBoxManage snapshot tmp delete "$_VIRTUAL_BOX_SNAPSHOT_NAME" \
# 1>"$_STANDARD_OUTPUT" 2>"$_ERROR_OUTPUT" && \
# NOTE: Isn't possible because virtual box has child images registered.
#VBoxManage closemedium disk "$(dirname \
# "$_BASIC_IMAGE_CONFIGURATION_FILE_PATH")/$(clcGetXMLValue \
# 'image_name')" 1>"$_STANDARD_OUTPUT" 2>"$_ERROR_OUTPUT" && \
clcLog 'Detach given virtual disk image from temporary created virtual machine.' && \
VBoxManage storageattach tmp --storagectl tmp --port 0 --device 0 \
--medium none 1>"$_STANDARD_OUTPUT" 2>"$_ERROR_OUTPUT" && \
clcLog 'Remove virtual dummy harddisk.' && \
VBoxManage storagectl tmp --name tmp --remove \
1>"$_STANDARD_OUTPUT" 2>"$_ERROR_OUTPUT" && \
clcLog 'Unregister temporary created virtual machine.' && \
VBoxManage unregistervm tmp 1>"$_STANDARD_OUTPUT" \
2>"$_ERROR_OUTPUT" && \
clcLog 'Restore virtualbox home directory.' && \
## bash
## rm --recursive --force ~/.VirtualBox/ 1>"$_STANDARD_OUTPUT" \
## 2>"$_ERROR_OUTPUT" && \
## cp --recursive "${temporaryConfigurationPath}virtualBoxBackup/" \
## ~/.VirtualBox/ 1>"$_STANDARD_OUTPUT" 2>"$_ERROR_OUTPUT" && \
## rm --recursive --force "$temporaryConfigurationPath" && \
## 1>"$_STANDARD_OUTPUT" 2>"$_ERROR_OUTPUT" && \
rm -rf ~/.VirtualBox/ 1>"$_STANDARD_OUTPUT" \
2>"$_ERROR_OUTPUT" && \
cp -r "${temporaryConfigurationPath}virtualBoxBackup/" \
~/.VirtualBox/ 1>"$_STANDARD_OUTPUT" 2>"$_ERROR_OUTPUT" && \
rm -rf "$temporaryConfigurationPath" && \
1>"$_STANDARD_OUTPUT" 2>"$_ERROR_OUTPUT" && \
##
# NOTE: Virtualbox needs at least 5 seconds to register that it has
# a new configuration directory.
sleep 5 1>"$_STANDARD_OUTPUT" 2>"$_ERROR_OUTPUT"
local result=$?
if [[ "$_DEBUG" == 'no' ]]; then
## bash rm --recursive "$temporaryConfigurationPath" \
rm -r "$temporaryConfigurationPath" \
1>"$_STANDARD_OUTPUT" 2>"$_ERROR_OUTPUT" || \
return $?
fi
return $result
}
# Creates a persistent version of given configuration file.
function clcCreatePersistentConfig() {
clcLog "Create a persistent configuration file version from \"$_BASIC_IMAGE_CONFIGURATION_FILE_PATH\" in \"$_TARGET_PATH\"." && \
cp "$_BASIC_IMAGE_CONFIGURATION_FILE_PATH" "$_TARGET_PATH" && \
1>"$_STANDARD_OUTPUT" 2>"$_ERROR_OUTPUT" && \
clcLog 'Edit short description.' && \
## bash sed --in-place --regexp-extended \
sed -i -r \
"s/(< *short_description[^>]*param=\"[^\"]*)(\")/\\1$_PERSISTENT_SHORT_DESCRIPTION_SUFFIX\\2/g" \
"$_TARGET_PATH" 1>"$_STANDARD_OUTPUT" 2>"$_ERROR_OUTPUT" && \
clcLog 'Append "original_xml_file_path" tag.' && \
## bash
## sed --in-place --regexp-extended \
## "s/^([ \\t]*)(< *persistent[^>]+param=\"[^\"]+\"[^>]*>)/\\1\\2\\n\\1/g" \
## "$_TARGET_PATH" 1>"$_STANDARD_OUTPUT" 2>"$_ERROR_OUTPUT" && \
sed -i -r \
"s/^([ \\t]*)(< *persistent[^>]+param=\"[^\"]+\"[^>]*>)/\\1\\2\\n\\1/g" \
"$_TARGET_PATH" 1>"$_STANDARD_OUTPUT" 2>"$_ERROR_OUTPUT" && \
##
clcLog 'Edit image file path.' && \
## bash sed --in-place --regexp-extended \
sed -i -r \
"s/(< *image_name[^>]*param=\")[^\"]*(\")/\\1$(clcValidateSEDReplacement \
"$_PERSISTENT_CONFIG_TARGET")\\2/g" "$_TARGET_PATH" \
1>"$_STANDARD_OUTPUT" 2>"$_ERROR_OUTPUT" && \
clcLog 'Convert configuration file from windows style line endings to unix line endings.' && \
## bash
## sed --in-place --regexp-extended 's/\r//g' "$_TARGET_PATH" \
## 1>"$_STANDARD_OUTPUT" 2>"$_ERROR_OUTPUT" && \
sed -i -r 's/\r//g' "$_TARGET_PATH" 1>"$_STANDARD_OUTPUT" \
2>"$_ERROR_OUTPUT" && \
##
return $?
}
# endregion
# endregion
# region controller
clcCommandLineInterface "$@" || return $?
if [[ "$_PERSISTENT_CONFIG_TARGET" ]]; then
clcCreatePersistentConfig || \
clcLog 'error' 'Creating persistent configuration file failed.'
else
"clcCreate${_HYPERVISOR}Snapshot" || \
clcLog 'error' 'Creating Linked Clone failed.'
fi
clcLog 'Program has successfully finished.' && \
return $?
# endregion
}
# region footer
if [[ "$0" == *"${__NAME__}" ]]; then
"$__NAME__" "$@"
fi
# endregion