#!/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