Deploying a Hyper-V VM using Octopus – Part 2: Deploying the VM
Welcome to part two of the series in deploying a Hyper-V VM using Octopus Deploy. In this post, I'll be taking the template VHD from part one and deploying it to a Hyper-V host using Octopus Deploy. In later parts I'll install different services, such as IIS or SQL Server, join the machine to the domain and then install an Octopus tentacle on it so that it is immediately ready to deploy applications to.
Full disclosure, I had intended to get this post out last weekend, but I got completely distracted with creating a CI process for a new project we're working on; I can't say too much but we're building a .net core app in Docker containers using TeamCity with Linux build agents, pushing that to the Azure Container Registry and from there using Octopus to deploy the container to the Web App for Containers service to run it. And breathe. Very exciting.
The Process
First of all, this is much more of a code oriented post. The PowerShell script that I'm referring to in this post is freely available here: https://github.com/yakcam/BlogScripts/blob/master/CreateVm.ps1
You should have the following setup before going any further:
- Octopus tentacle installed, running and registered on a Hyper-V host that you'll be deploying this machine to
- The VHD file copied to a location that the Hyper-V service and Octopus tentacle have access to
If you don't have the template VHD file, have a look at part one here: Deploying a Hyper-V VM using Octopus – Part 1: The Template
In my setup, I've got the tentacle installed on my laptop and my laptop is part of its own environment. It's got a 'Hyper-V Host' role against it and the environment is part of the 'Machine Deployment' lifecycle that I'll be using in the process I'm creating.
The process that I have set up consists of just a single step, that runs against the 'Hyper-V Host' role:
That step is just a PowerShell script step and the script is what is linked above. If you look at the script you'll see the following variables:
[code language="powershell"]
$name = $OctopusParameters["Process.MachineName"];
$switchName = $OctopusParameters["Process.SwitchName"];
$sourceTemplateVhd = $OctopusParameters["Process.SourceTemplateVhdPath"];
$destFolder = $OctopusParameters["Process.DestinationVmFolder"] + $name + '\';
$destPath = $destFolder + $name + '.vhdx';
[/code]
Octopus will pass all variables into the script within the $OctopusParameters object and above you can see those being stored for later and accessed.
There are a couple more variables defined against the project in Octopus as follows:
The ones that are actually in use for this script are as follows:
- Process.DestinationVmFolder - The base folder of where the VM will be created. Refers to a location on or accessible to the Hyper-V host.
- Process.MachineName - The name given to the new VM and also what the machine is renamed to as part of the process
- Process.NewLocalAdminPass - The password that the 'Administrator' is given as part of the process
- Process.SourceTemplateVhdPath - The path of the base VHD file created in part one of this series. This must refer to a location that is accessible to the Hyper-V host.
- Process.SwitchName - The name of the Hyper-V switch that the VM will be connected to
- Process.TemplateLocaAdminPass - The password of the local administrator for the template that was set in part one of this series.
At this point, you should have all you need to add this into Octopus and get deploying! If you want to know more about what's going on within the script, then please read on.
The Script
Functions
Near the top of the script, you can see two (not amazingly written, I'm still learning PowerShell!) functions. the first of these is BlockUntilGuestRunning:
[code language="powershell"]
function BlockUntilGuestRunning($name) {
Write-Host "Checking status of '$name'...";
$VM = Get-VMIntegrationService -VMName $name -Name Heartbeat;
while ($VM.PrimaryStatusDescription -ne "OK")
{
$VM = Get-VMIntegrationService -VMName $name -Name Heartbeat;
write-host "The VM is not on";
Start-Sleep 5;
}
Write-Host "The VM is running.";
}
[/code]
This function will keep looping until Hyper-V reports that the guest OS is running inside the VM. It writes out basic status information to try and give an idea that it hasn't frozen or something untoward hasn't happened.
The second function waits until the VM has a valid, routable IPv4 address that can be used to connect to it:
[code language="powershell"]
function BlockUntilHasIpAddress($name) {
$ip = (Get-VM -Name $name | Select-Object -ExpandProperty NetworkAdapters).IPAddresses[0];
while (!$ip) {
$ip = (Get-VM -Name $name | Select-Object -ExpandProperty NetworkAdapters).IPAddresses[0];
}
while ($ip -eq "" -or ($ip.StartsWith("169.")) -or ($ip.StartsWith("fe80"))) {
$ip = (Get-VM -Name $name | Select-Object -ExpandProperty NetworkAdapters).IPAddresses[0];
Write-Host "'$ip' is not a valid ipv4 address...";
Start-Sleep 5;
}
Write-Host "The VM has the ipv4 address '$ip'.";
}
[/code]
VM Creation
[code language="powershell"]
if (-Not (Test-Path $destFolder))
{
mkdir $destFolder -Force > $null;
}
Write-Host 'Copying base vhd...';
Copy-Item `
-Path "$sourceTemplateVhd" `
-Destination $destPath > $null;
Write-Host 'Creating VM...';
New-VM `
-Name $name `
-MemoryStartupBytes 1GB `
-BootDevice VHD `
-VHDPath $destPath `
-Path $destFolder `
-Generation 2 `
-Switch "$switchName" > $null;
[/code]
Change Local Admin Password
[code language="powershell"]
Write-Host 'Starting VM...';
Start-VM -Name $name;
Write-Host 'Waiting for VM to start...';
BlockUntilGuestRunning $name;
BlockUntilHasIpAddress $name;
$ip = (Get-VM -Name $name | Select-Object -ExpandProperty NetworkAdapters).IPAddresses[0];
Write-Host "VM up and has IP: $ip";
Write-Host 'Adding IP to list of allowed RM clients...';
Set-Item WSMan:\localhost\Client\TrustedHosts -Value $ip -Force;
$password = ConvertTo-SecureString $OctopusParameters["Process.TemplateLocalAdminPass"] -AsPlainText -Force;
$cred= New-Object System.Management.Automation.PSCredential ("Administrator", $password );
Write-Host 'Connecting to remote machine...';
$s = New-PSSession -ComputerName $ip -Credential $cred;
Write-Host "Changing local admin password...";
$newAdminPassword = ConvertTo-SecureString $OctopusParameters["Process.NewLocalAdminPass"] -AsPlainText -Force;
Invoke-Command -Session $s -ScriptBlock { param($newAdminPassword) Set-LocalUser -Name "Administrator" -Password $newAdminPassword; } -ArgumentList $newAdminPassword;
[/code]
One of the most important things to do after deploying our template VHd to a new VM is to change the local admin password. In the script above we start the VM, wait for it to come up and get an IPv4 address that we can use to connect to it remotely. We have to set the IP as a trusted one, note that this does require local admin/elevated permissions. We take the previous and new password provided through the variables, connect to the machine and change the password of the 'Administrator' account on the machine.
Renaming the Machine and Attaching a New Disk
[code language="powershell"]
Write-Host 'Renaming remote machine...';
Invoke-Command -Session $s -Script { param($newName) Rename-Computer -NewName $newName -Restart; } -Args $name;
Disconnect-PSSession -Session $s > $null;
Remove-PSSession -Session $s > $null;
Start-Sleep 5;
Write-Host 'Waiting for VM to restart...';
BlockUntilGuestRunning $name;
BlockUntilHasIpAddress $name;
Write-Host "Creating Octopus (D) drive...";
$diskPath = $destFolder + $name + "_D_Octopus.vhdx";
Stop-Service -Name ShellHWDetection > $null;
New-VHD -Path "$diskPath" -SizeBytes 20GB `
| Mount-VHD -Passthru `
| Initialize-Disk -Passthru `
| New-Partition -AssignDriveLetter -UseMaximumSize `
| Format-Volume -FileSystem NTFS -NewFileSystemLabel "Octopus" -Confirm:$false -Force > $null;
Dismount-VHD -Path "$diskPath" > $null;
Start-Service -Name ShellHWDetection > $null;
Write-Host "Adding drive to VM...";
Add-VMHardDiskDrive -VMName "$name" -Path "$diskPath" > $null;
[/code]
Now we rename the machine and instruct it to reboot. We wait for it to come back up and then create a new VHD to install the Octopus tentacle on later. You may have noticed we stop and then start the 'ShellHWDetection' around creating the disk, this is to stop the irritating popup about having to format a disk when it is momentarily connected to the machine whilst it's being initialised and formatted. Lastly, that VHD is unmounted and connected to the VM.
Bring the New Drive Online
[code language="powershell"]
Write-Host 'Connecting to remote machine...';
$s2 = New-PSSession -ComputerName $ip -Credential $cred;
Write-Host "Bringing drive online...";
Invoke-Command -Session $s2 -ScriptBlock { Set-Disk -Number 1 -IsOffline $false; Set-Disk -Number 1 -IsReadOnly $false; Set-Volume -DriveLetter "D" -NewFileSystemLabel "Octopus"; };
Write-Host 'Complete.';
[/code]
Finished!
We now have a base VHD and we can use Octopus Deploy (or anything that can run a PowerShell script) to deploy a new VM using that VHD.
As ever if you have any questions or find any issues (something that doesn't run, something you think could be improved) please let me know!