Introduction
What is the worst programming language you have ever worked with? Mine is Nant, especially if you try to use it for creating very sophisticated software staging and testing workflow scripts. Using Nant for Windows automation instead of PowerShell is insane. For all these people out there that don’t know enough about PowerShell and need to be weaned off Nant, here is an example that might convince you switching.

Figure 1: Picking the right technology for maximum productivity. Circuit board –> Nant –> PowerShell
This blog post features a PowerShell script that installs software on a clean virtual machine and sends an email when it is complete. It uses PowerShell Remoting to extract a zip file on the remote machine and to install the extracted MSI file.
Sequence
Here is the sequence of steps that the script is orchestrating.
- Revert VMWare Image
- Start VMWare Image
- Copy zip file into a share on the VMWare image
- Unzip file using PowerShell Remoting
- Launch MSI install via PowerShell Remoting
- Run tests (Tests are not included in this example, but can be easily scripted of course)
- Send email notification once some test is finished
Scripts
Batch file to feed parameters to batch file that calls PowerShell script
run.bat 9.200.34456.0 20110409 \\bs\bs\9\9.200.34456.0
Batch file that calls PowerShell script
@echo %1
@echo %2
@echo %3
PowerShell -command "E:\SmokeTesting\Smoke-Test.ps1 %1 %3" -NoExit
PAUSE
Smoke test PowerShell script
Param([string] $BuildNumber="9.200.34456.0", [string] $BuildFolder="\\bs\bs\9\9.200.34456.0")
Set-StrictMode -Version "Latest"
$DebugPreference = "Continue"
Write-Debug $BuildFolder
Write-Debug $BuildNumber
$ScriptFileFolder = Split-Path -Path $($MyInvocation.MyCommand.Definition) -Parent
Write-Debug $ScriptFileFolder
function Load-Configuration([string] $ConfigurationFileName)
{
$ConfigFilePath = Join-Path -Path $ScriptFileFolder -ChildPath $ConfigurationFileName
Write-Debug $ConfigFilePath
if(Test-Path -Path $ConfigFilePath)
{
. $ConfigFilePath
}
else
{
Throw "Configuration file `"$ConfigFilePath`" not found!"
}
}
function Revert-SnapShot([string] $VMPath, [string] $SnapShotName)
{
$QuotedVMPath = "`"{0}`"" -f $VMPath
$Result = Start-Process -FilePath $Config.VMRunUtiltyPath -ArgumentList "-T", "ws", "revertToSnapShot", $QuotedVMPath, $SnapShotName -Wait -PassThru -NoNewWindow
Write-Debug ($Result.HasExited)
Write-Debug ($Result.ExitCode)
}
function Start-VM([string] $VMPath)
{
$QuotedVMPath = "`"{0}`"" -f $VMPath
$Result = Start-Process -FilePath $Config.VMRunUtiltyPath -ArgumentList "-T", "ws", "start", $QuotedVMPath -Wait -PassThru -NoNewWindow
Write-Debug ($Result.HasExited)
Write-Debug ($Result.ExitCode)
[System.Threading.Thread]::Sleep($Config.VMSecondsToWaitAfterImageStart * 1000)
}
function Copy-BuildZipFile([string] $BuildFolder, [string] $Target)
{
$PackagesPath = Join-Path -Path $BuildFolder -ChildPath $Config.BuildImageRelativePath
$ZipFile = Get-ChildItem -Path $PackagesPath -Filter $Config.BuildZipFileNameFilter
Copy-Item -Path $ZipFile.FullName -Destination $Target -Force
}
function Get-DecryptedString($EncryptedString)
{
$Ptr = [System.Runtime.InteropServices.Marshal]::SecureStringToCoTaskMemUnicode($EncryptedString)
$Result = [System.Runtime.InteropServices.Marshal]::PtrToStringUni($Ptr)
[System.Runtime.InteropServices.Marshal]::ZeroFreeCoTaskMemUnicode($Ptr)
$Result
}
function Send-SuccessMail([string] $BuildNumber)
{
Send-MailMessage -Body $Config.EmailBody -From $Config.EmailFrom -SmtpServer $Config.EmailSMTPServer -Subject ($Config.EmailSubject -f $BuildNumber) -To $Config.EmailTo
}
function Create-SecretPasswordFile([string] $PasswordFilePath)
{
$Password = Read-Host "Enter your password" -AsSecureString
$EncryptedPassword = ConvertFrom-SecureString $Password
$EncryptedPassword | Out-File -FilePath $PasswordFilePath
}
function Create-PSRemotingSession()
{
#The password key is only usable by the domain user who created it.
#Use the Create-SecretPasswordFile function to create one for your login
$SecurePassWord = ConvertTo-SecureString $Config.PSRemotingCredentialPassword
$DomainUser = "{0}\{1}" -f $Config.PSRemotingCredentialDomain, $Config.PSRemotingCredentialUserName
Write-Debug $DomainUser
$Cred = New-Object -TypeName "System.Management.Automation.PSCredential" -ArgumentList $DomainUser, $SecurePassWord
$Session = New-PSSession -ComputerName $Config.VMComputerName -credential $Cred
return $Session
}
function Unzip-Remote($Session)
{
$UnzipScriptString = @'
$ShellApplication = new-object -com "shell.application"
$PathWithFilter = "{0}*.zip"
$ZipFileName = Get-Item -Path $PathWithFilter
$ZipPackage = $ShellApplication.NameSpace($ZipFileName.FullName)
$DestinationFolder = $ShellApplication.NameSpace("{1}")
$DestinationFolder.CopyHere($ZipPackage.Items())
'@
$UnzipScriptString = $UnzipScriptString -f $Config.StagingPathOnVM, $Config.ExtractZipToPathOnVM
$UnzipScript = [scriptblock]::Create($UnzipScriptString)
Write-Debug $UnzipScriptString
$Job = Invoke-Command -Session $Session -Scriptblock $UnzipScript -AsJob
$Null = Wait-Job -Job $Job
}
function Install-MSIRemote($Session)
{
$InstallScriptString = @'
Start-Process -FilePath "msiexec" -ArgumentList "/qn", "/i", "{0}", "SQLSERVERNAME={1}", "SQLAUTHMODE={2}", "SQLUSERNAME={3}", "SQLPASSWORD={4}" -NoNewWindow -PassThru -RedirectStandardError "C:\Landing\stderrorlog.txt" -RedirectStandardOutput "C:\Landing\stdoutlog.txt" -Wait
'@
#The password key is only usable by the domain user who created it.
#Use the Create-SecretPasswordFile function to create one for your login
$InstallScriptString = $InstallScriptString -f $Config.ExtractZipMSIPath, $Config.MSISQLServerName, $Config.MSISQLSQLAuthMode, $Config.MSISQLUserName, (Get-DecryptedString (ConvertTo-SecureString $Config.MSISQLPassword))
$InstallScript = [scriptblock]::Create($InstallScriptString)
Write-Debug $InstallScriptString
$job = Invoke-Command -Session $Session -Scriptblock $InstallScript -AsJob
$Null = Wait-Job -Job $Job
}
Write-Debug $(Get-Date)
$Config = Load-Configuration -ConfigurationFileName "bobster_cloud59.ps1"
Revert-SnapShot -VMPath $Config.VMImagePathOnHost -SnapShotName $Config.VMSnapShotName
Start-VM -VMPath $Config.VMImagePathOnHost
Copy-BuildZipFile -PublishedBuildDir $BuildFolder -Target $Config.StagingShareOnVM
$Session = Create-PSRemotingSession
Unzip-Remote -Session $Session
Install-MSIRemote -Session $Session
Remove-PSSession -Session $Session
Send-SuccessMail -BuildNumber $BuildNumber
Write-Debug $(Get-Date)
Configuration file
Write-Debug "Loading Config File"
$Config = @{}
$Config.VMImagePathOnHost="I:\Images\CT2008R2\CT2008R2.vmx"
$Config.VMSnapShotName="SmokeTestReady"
$Config.VMComputerName="cloud59"
$Config.VMRunUtiltyPath="C:\Program Files (x86)\VMware\VMware Workstation\vmrun"
$Config.VMSecondsToWaitAfterImageStart=60
$Config.StagingShareOnVM="\\" + $Config.VMComputerName + "\Landing"
$Config.StagingPathOnVM="C:\Landing\"
$Config.MSISQLServerName="."
$Config.MSISQLSQLAuthMode="SQLServerAuth"
$Config.MSISQLUserName="sa"
#The password key is only usable by the domain user who created it.
#Use the Create-SecretPasswordFile function to create one for your login
$Config.MSISQLPassword="...00002000000000003660000c00000001000000044fd0a..."
$Config.ExtractZipToPathOnVM= Join-Path -Path $Config.StagingPathOnVM -ChildPath "Temp"
$Config.ExtractZipMSIPath = Join-Path -Path $Config.ExtractZipToPathOnVM -ChildPath "Client\Setup.msi"
$Config.PSRemotingCredentialDomain="sky"
$Config.PSRemotingCredentialUserName="bobster"
#The password key is only usable by the domain user who created it.
#Use the Create-SecretPasswordFile function to create one for your login
$Config.PSRemotingCredentialPassword="...00002000000000003660000c00330001000000044fd0a..."
$Config.BuildImageRelativePath="web\download"
$Config.BuildZipFileNameFilter="SkyCli*.zip"
$Config.EmailBody="The latest build has been installed on {0}" -f $Config.VMComputerName
$Config.EmailSubject="Build {0} is ready for test {1}" -f "{0}", $Config.VMComputerName
$Config.EmailSMTPServer="smtp.skysmtp.ptm"
$Config.EmailFrom="king_of_the_lab@sky.me"
$Config.EmailTo="devster@sky.me"
return $Config
Smoke test setup instructions
In case you are interested in setting up a smoke test system yourself using VMWare Workstation and PowerShell use the following setup steps:
Install PowerShell
This only applies to older versions of Windows. If you are using Windows 7 or Windows 2008 Server R2 skip this step.
Prepare PowerShell
- Set-ExecutionPolicy "remotesigned"
- Enable-PSRemoting
On VM
- Create folder C:\Landing\Package
- Create share \\testvm\Landing and give full control to sky\bobster
On Host
- Create a folder e.g. E:\SmokeTesting and copy smoke test files
- Rename configuration file and set configuration parameters
- Change Smoke-Test script file to pick up new configuration file
- Make sure to use the latest version of VMWare Workstation 7.1.2 or greater
- Change passwords encodings in configuration file using the Create-SecretPasswordFile function
Download
The script files can be downloaded here: Smoke-Test.zip
Ausblick
This script is be basic framework for running the actual smoke tests checks. To put some more spice to the mix, I am thinking about hooking up PSUnit to run the smoke test verifications and use a VM in the cloud (SkyTap cloud) instead. Stay tuned!