host/AzureRecoveryUtil/Scripts/win32/StartupScript.ps1 (515 lines of code) (raw):
##+----------------------------------------------------------------------------------+
## Copyright(c) Microsoft Corp. 2015
##+----------------------------------------------------------------------------------+
## File : StartupScript.ps1
##
## Description : Start-up script invoked by Azure guest agent when the custom-script
## extension is set for hydration-vm as part of recovery work-flow.
##
## History : 1-6-2015 (Venu Sivanadham) - Created
##+----------------------------------------------------------------------------------+
Param
(
[Parameter(Mandatory=$false,Position=1)]
[string]$hostid="",
[Parameter(Mandatory=$false,Position=2)]
[string]$Scenario="recovery",
[Parameter(Mandatory=$false)]
[int]$MaxRetryCount = 2,
[Parameter(Mandatory=$false)]
[int]$MaxProcessWaitTimeSec = 900,
[Parameter(Mandatory=$false)]
[int]$MaxWaitForKillTimeSec = 120,
[Parameter(Mandatory=$false)]
[string]$HydrationConfigSettings="emptyconfig"
)
#
# Global Variables declaration
#
$global:Working_Dir = "$env:SystemDrive\AzureRecovery"
$global:Log_File = "$Working_Dir\AzureRecoveryUtil.log"
#
# Global variables/constants related to config files and executables
#
$global:HostInfoFile_Prefix = "hostinfo"
$global:HostInfoFile = ""
$global:AzureRecoveryInfoFile_Prefix = "azurerecovery"
$global:AzureRecoveryInfoFile = ""
$global:AzureRecoveryToolsZipFile = "AzureRecoveryTools.zip"
$global:AzureRecoveryUtil = "AzureRecoveryUtil.exe"
#
# Status global variables used for updating execution status
#
[string]$global:Exec_Status = "Processing"
[string]$global:Exec_Task_Desc = "Preparing Environment"
[int]$global:Exec_Error = 0
[string]$global:Exec_ErrorMsg = "Preparing the environment for pre-recovery steps execution"
[int]$global:Exec_Progress = 0
[int]$global:retCode = 0
# Operation Names
[string]$global:Scenario_Migration = "migration"
[string]$global:Scenario_Migration_Test = "migrationtest"
[string]$global:Scenario_Recovery = "recovery"
[string]$global:Scenario_Recovery_Test = "recoverytest"
[string]$global:Scenario_GenConversion = "genconversion"
[string]$global:Scenario_GenConversion_Test = "genconversiontest"
#
# Trace Log helper functions
#
function WriteToLog ( [string]$msg )
{
$log_dir = $(Split-Path -Path "$Log_File" -Parent)
if( $(Test-Path -Path $log_dir -PathType Container) )
{
$msg | Out-File -Encoding ascii -Width 2048 -Append -Force -FilePath "$Log_File"
}
# Write log to standard console as well.
Write-Host "$msg"
}
function Trace_Error ( [string]$msg )
{
$trace_time = $(Get-Date -UFormat "%a %b %d %Y %T")
WriteToLog "$trace_time : $PID : Error : $msg"
}
function Trace ( [string]$msg )
{
$trace_time = $(Get-Date -UFormat "%a %b %d %Y %T")
WriteToLog "$trace_time : $PID : $msg"
}
function IsMigration ()
{
return $Scenario -ieq $global:Scenario_Migration -Or $Scenario -ieq $global:Scenario_Migration_Test;
}
function IsGenConversion ()
{
return $Scenario -ieq $global:Scenario_GenConversion -Or $Scenario -ieq $global:Scenario_GenConversion_Test;
}
function IsTestScenario()
{
return $Scenario -like "*test";
}
#
# Verify the list of files Azure Guest agent should download and keep in Startup Script current directory
#
function Verify-Downloaded-Files ( [string]$hostid )
{
#
# Prepare the downloaded files list and verify them
#
$Files = $("")
if( IsMigration )
{
#
# In case of migration there won't be hostinfo xml file.
#
$Files = "$PWD\$global:AzureRecoveryToolsZipFile", "$PWD\$global:AzureRecoveryInfoFile_Prefix-$hostid.conf"
}
elseif( IsGenConversion )
{
#
# In case of genconversion there won't be hostinfo xml file.
#
$Files = "$PWD\$global:AzureRecoveryToolsZipFile", "$PWD\$global:AzureRecoveryInfoFile_Prefix-$hostid.conf"
}
else
{
$Files = "$PWD\$global:AzureRecoveryToolsZipFile", "$PWD\$global:AzureRecoveryInfoFile_Prefix-$hostid.conf", "$PWD\$global:HostInfoFile_Prefix-$hostid.xml"
}
foreach ( $file in $Files )
{
Trace "Verifying the file $file"
if ( !$(Test-Path "$file" -PathType Leaf) )
{
Write-Error "Error verifying file $file"
return $false
}
}
Trace "All the files are verified"
return $true
}
#
# Create directory, if it already exist then delete it and then create a new.
#
function Force-Create-Directory ( [string]$dir_name )
{
#
# Check wheather dir exists
#
if ( $(Test-Path -Path "$dir_name" -PathType Container) )
{
Write-Host "Directory $dir_name already exist. Deleting it ..."
Remove-Item $dir_name -Recurse -Force
}
#
# Create the directory
#
Write-Host "Creating the directory $dir_name ..."
New-Item -Path "$dir_name" -ItemType Directory -Force
return $?
}
#
# Copy config files to working directory
#
function Copy-Files-To-WorkingDir ( $files )
{
Trace "Copying files to working directory ..."
foreach ( $file in $files )
{
Trace "$file --> $Working_Dir"
Copy-Item -Path "$file" -Destination "$Working_Dir" -Force
if( !$? )
{
return $false
}
}
Trace "Successfuly copied all the files"
return $true
}
#
# Extract Executables to working directory
#
function Extract-Executables-To-Dir ( [string]$dir_name )
{
$zipfile = "$PWD\$global:AzureRecoveryToolsZipFile"
Trace "Extracting $zipfile to $dir_name"
$copyflags = [int](
4 + # No progress dlg
16 + # Yes to all
512 + # No conformation for create dir
1024) # No error dlg
$shell = New-Object -ComObject shell.application
$ziped_files = $shell.NameSpace($zipfile)
if ( $ziped_files -eq $null )
{
Trace_Error "Could not open the zip file $zipfile"
return $false
}
foreach ( $file in $ziped_files.items() )
{
$shell.NameSpace($dir_name).copyhere($file, $copyflags)
if( !$? )
{
Trace_Error "Error extracting files from $zipfile"
return $false
}
}
Trace "Successfully extracted the files from $zipfile"
return $true
}
#
# Invoke the Recovery tool to update status
#
function Update-Status-To-Blob ()
{
$StatusCmdAgrs = @("--operation" ,"updatestatus",
"--recoveryinfofile",$global:AzureRecoveryInfoFile,
"--status" ,$global:Exec_Status,
"--errorcode" ,$global:Exec_Error,
"--progress" ,$global:Exec_Progress,
"--taskdescription" ,$global:Exec_Task_Desc,
"--errormsg" ,$global:Exec_ErrorMsg
)
Trace "$global:Working_Dir\$global:AzureRecoveryUtil $StatusCmdAgrs"
&"$global:Working_Dir\$global:AzureRecoveryUtil" $StatusCmdAgrs
return $?
}
#
# Invoke the Recovery tool to update status to a local file.
#
function Update-Status-To-TestFile ()
{
$StatusCmdAgrs = @("--operation" ,"statusupdatetest",
"--recoveryinfofile",$global:AzureRecoveryInfoFile,
"--status" ,$global:Exec_Status,
"--errorcode" ,$global:Exec_Error,
"--progress" ,$global:Exec_Progress,
"--taskdescription" ,$global:Exec_Task_Desc,
"--errormsg" ,$global:Exec_ErrorMsg
)
Trace "$global:Working_Dir\$global:AzureRecoveryUtil $StatusCmdAgrs"
&"$global:Working_Dir\$global:AzureRecoveryUtil" $StatusCmdAgrs
return $?
}
#
# Invke the Recovery tool to upload the log
#
function Upload-Execution-Log ()
{
$UploadCmdArgs = @("--operation" ,"uploadlog",
"--recoveryinfofile",$global:AzureRecoveryInfoFile,
"--logfile" ,$global:Log_File
)
Trace "$global:Working_Dir\$global:AzureRecoveryUtil $UploadCmdArgs"
&"$global:Working_Dir\$global:AzureRecoveryUtil" $UploadCmdArgs
return $?
}
#
# Prepare Evnvironment to run the azure pre-recovery steps
#
function Prepare-Environment ( [string]$hostid )
{
Write-Host "Preparing Environment ..."
if ( !$(Force-Create-Directory "$Working_Dir") )
{
Write-Error "Error creating working directory $global:Working_Dir"
return $false
}
#
# Start trace messages from here as the working directory is ready.
#
if ( !$(Verify-Downloaded-Files $hostid) )
{
Write-Error "Error verifying script dependent files. They might be missing"
return $false
}
$copy_files = $("")
if ( IsMigration )
{
$copy_files = $("$PWD\$global:AzureRecoveryInfoFile_Prefix-$hostid.conf")
}
elseif ( IsGenConversion )
{
$copy_files = $("$PWD\$global:AzureRecoveryInfoFile_Prefix-$hostid.conf")
}
else
{
$copy_files = $("$PWD\$global:AzureRecoveryInfoFile_Prefix-$hostid.conf", "$PWD\$global:HostInfoFile_Prefix-$hostid.xml")
}
if ( !$( Copy-Files-To-WorkingDir $copy_files ) )
{
Write-Error "Could not copy config files to working directory"
return $false
}
#
# Update config file-paths golbal variables
#
$global:AzureRecoveryInfoFile = "$global:Working_Dir\$global:AzureRecoveryInfoFile_Prefix-$hostid.conf"
$global:HostInfoFile = "$global:Working_Dir\$global:HostInfoFile_Prefix-$hostid.xml"
#
# Extract the executables to working directory from zip file
#
if ( !$(Extract-Executables-To-Dir "$global:Working_Dir") )
{
Write-Error "Error extracting executables to working directory"
return $false
}
#
# Move to working directory
#
Trace "Changing directory to $global:Working_Dir"
cd $global:Working_Dir
if( !$? )
{
Write-Error "Can change directory to $global:Working_Dir"
return $false
}
return $true
}
#
# Invoke the Recovery tool to execute pre-recovery steps.
# Caller need to check $global:retCode value for the process exit code.
# If there is any exception in executing the command then $global:retCode set to 1.
#
function Execute-Recovery-Steps ()
{
Trace "Starting Pre-Recovery execution steps"
$RecCmdArgs = $("")
if ( IsTestScenario )
{
if ( IsMigration )
{
$RecCmdArgs = @("--operation" , "migrationtest",
"--recoveryinfofile", $global:AzureRecoveryInfoFile,
"--workingdir" , $global:Working_Dir,
"--hydrationconfigsettings" , hydrationConfigSettings
)
}
elseif ( IsGenConversion )
{
$RecCmdArgs = @("--operation" , "genconversiontest",
"--recoveryinfofile", $global:AzureRecoveryInfoFile,
"--workingdir" , $global:Working_Dir,
"--hydrationconfigsettings" , hydrationConfigSettings
)
}
else
{
$RecCmdArgs = @("--operation" , "recoverytest",
"--recoveryinfofile", $global:AzureRecoveryInfoFile,
"--hostinfofile" , $global:HostInfoFile,
"--workingdir" , $global:Working_Dir,
"--hydrationconfigsettings" , hydrationConfigSettings
)
}
}
else
{
if ( IsMigration )
{
$RecCmdArgs = @("--operation" , "migration",
"--recoveryinfofile", $global:AzureRecoveryInfoFile,
"--workingdir" , $global:Working_Dir,
"--hydrationconfigsettings" , hydrationConfigSettings
)
}
elseif ( IsGenConversion )
{
$RecCmdArgs = @("--operation" , "genconversion",
"--recoveryinfofile", $global:AzureRecoveryInfoFile,
"--workingdir" , $global:Working_Dir,
"--hydrationconfigsettings" , hydrationConfigSettings
)
}
else
{
$RecCmdArgs = @("--operation" , "recovery",
"--recoveryinfofile", $global:AzureRecoveryInfoFile,
"--hostinfofile" , $global:HostInfoFile,
"--workingdir" , $global:Working_Dir,
"--hydrationconfigsettings" , hydrationConfigSettings
)
}
}
Trace "$global:Working_Dir\$global:AzureRecoveryUtil $RecCmdArgs"
$global:retCode = 1
try
{
for( $retry = 1; ; $retry++)
{
$recProc = Start-Process -FilePath $global:Working_Dir\$global:AzureRecoveryUtil -ArgumentList $RecCmdArgs -PassThru
# Wait for the process to exit.
if ( !$recProc.WaitForExit($MaxProcessWaitTimeSec * 1000) )
{
Trace "Recovery command did not complete with-in time limit. Killing the process $($recProc.Id) ..."
# Try to kill the process
try
{
$recProc.Kill();
Trace "Issued kill signal to the process $($recProc.Id)"
}
Catch [System.ComponentModel.Win32Exception]
{
#Process might be terminating by the time of Kill call. Wait for the process to terminate
Trace "Kill has thown Win32Exception. Exception Details: `n$_"
}
Catch [System.InvalidOperationException]
{
Trace "Process might have exited by the time of kill. Exception Details: `n$_"
if( $recProc.HasExited )
{
$global:retCode = $recProc.ExitCode
break
}
}
Catch
{
Trace "Unkown exception for the Kill. Exception Details: `n$_"
}
# Wait for the process to exit
$maxKillWait = 0
while ( !$recProc.HasExited )
{
if ( ++$maxKillWait -ge $MaxWaitForKillTimeSec )
{
Trace "Process $($recProc.Id) is not getting killed... Failing the operation."
return
}
# Sleep for a second and then check again.
Start-Sleep -Seconds 1
}
Trace "Process $($recProc.Id) has exited"
#upload the available log.
if ( !$( IsTestScenario ) )
{
Upload-Execution-Log
}
if ( $retry -lt $MaxRetryCount )
{
Trace "Retrying the operation ..."
}
else
{
Trace "Reached maximun retry attempts. Failing the operation."
break
}
}
else
{
$global:retCode = $recProc.ExitCode
Trace "Recovery command exited with exit-code: $global:retCode"
break
}
}
}
Catch
{
Trace "Error executing the recovery command. Error details: `n$_"
}
return
}
function Main ( )
{
#
# hostid validation
#
if ( !$hostid -or !$Scenario)
{
Write-Error "Argument error: host-id or scenario is missing"
Write-Host "Usage : StartupScript.ps1 <host-id> <recovery/migration/genconversion/recoverytest/migrationtest/genconversiontest> [-MaxRetryCount <value>] [-MaxProcessWaitTimeSec <value>] [-MaxWaitForKillTimeSec <value>]"
exit 1
}
#
# Prepare Environment
#
if( !$(Prepare-Environment $hostid) )
{
Write-Error "Error Preparing Environment."
exit 1
}
#
# Set Execution status golbal variables
#
$global:Exec_Task_Desc = "Initiating $Scenario steps"
$global:Exec_ErrorMsg = "Environment was prepared successfully"
$global:Exec_Progress = 20
#
# Update status
#
if(!$(IsTestScenario))
{
if ( Update-Status-To-Blob )
{
Trace "Successfuly updated prepare-environment status"
}
else
{
Trace_Error "Error updating prepare-environment status"
Write-Error "Status update failed"
}
}
#
# Start Recovery/Migration
#
Execute-Recovery-Steps
if( $global:retCode -ne 0 )
{
Trace_Error "Recovery steps execution failed."
$global:Exec_ErrorMsg = "Recovery tool exited unexpectedly"
$global:Exec_Status = "Failed"
$global:Exec_Error = 1
if( IsTestScenario)
{
if ( !$(Update-Status-To-TestFile) )
{
Trace_Error "Status update on local file failed."
}
}
else
{
if ( !$(Update-Status-To-Blob) ) { exit 1 }
if ( !$(Upload-Execution-Log) ) { exit 1 }
}
}
}
### Startup script Entry Point ###
Main