Skip to content

Commit bacf672

Browse files
authored
Merge pull request #74 from dbroeglin/exp/hyper-auto-provision
A script to auto-provision HyperV NetScaler instances
2 parents cd722de + f2bad8d commit bacf672

File tree

1 file changed

+388
-0
lines changed

1 file changed

+388
-0
lines changed

Contrib/New-NSHyperVInstance.ps1

Lines changed: 388 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,388 @@
1+
<#
2+
.SYNOPSIS
3+
Generates a new Hyper-V Netscaler instance from a Netscaler VPX package.
4+
5+
.PARAMETER Package
6+
Location of the VPX package to use.
7+
8+
.PARAMETER Path
9+
Location where the virtual machine will be created.
10+
11+
.PARAMETER VMName
12+
Name of the created VM.
13+
14+
.PARAMETER SwitchName
15+
Name of the switch the network adapter of the created instance will
16+
be connected to.
17+
18+
.PARAMETER MacAddress
19+
MAC address to set for the VM network interface.
20+
Defaults to: "00155D7E3100"
21+
22+
.PARAMETER Force
23+
If the VM is already present destroy it and create a new one.
24+
25+
.PARAMETER NSIP
26+
NSIP to auto-provision the instane with.
27+
28+
.PARAMETER Netmask
29+
Netmask to auto-provision the instane with.
30+
31+
.PARAMETER DefaultGateway
32+
Default gateway to auto-provision the instane with
33+
34+
.EXAMPLE
35+
New-NSHyperVInstance.ps1 -Verbose -Package C:\temp\NSVPX-HyperV-11.1-50.10_nc.zip `
36+
-VMName NSVPX-11-1 `
37+
-SwitchName Labnet `
38+
-NSIP 10.0.0.30 -DefaultGateway 10.0.0.254 `
39+
-Path C:\temp\NSVPX-11-1 `
40+
-Force
41+
42+
Create a new NetScaler Hyper-V VM named 'NSVPX-11-1' in directory 'c:\temp\NSVPX-11-1'
43+
from the given VPX package.
44+
Auto-provision it with NSIP 10.0.0.30/24 default gateway 10.0.0.254 on switch
45+
'Labnet'. If the VM already exists, remove it first.
46+
47+
.NOTES
48+
Copyright 2017 Dominique Broeglin¨
49+
50+
MIT License
51+
Permission is hereby granted, free of charge, to any person obtaining a copy
52+
of this software and associated documentation files (the ""Software""), to deal
53+
in the Software without restriction, including without limitation the rights
54+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
55+
copies of the Software, and to permit persons to whom the Software is
56+
furnished to do so, subject to the following conditions:
57+
The above copyright notice and this permission notice shall be included in all
58+
copies or substantial portions of the Software.
59+
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
60+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
61+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
62+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
63+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
64+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
65+
SOFTWARE.
66+
67+
We reuse the New-ISOFile function available here:
68+
https://gallery.technet.microsoft.com/scriptcenter/New-ISOFile-function-a8deeffd
69+
70+
.LINK
71+
TODO
72+
#>
73+
[CmdletBinding()]
74+
Param (
75+
[Parameter(Mandatory)]
76+
[String]$Package,
77+
78+
[Parameter(Mandatory)]
79+
# This is a safeguard to prevent deleting our whole disk...
80+
[ValidateScript({$_.Length -ge 4})]
81+
[String]$Path,
82+
83+
[Parameter(Mandatory)]
84+
[String]$VMName,
85+
86+
[Parameter(Mandatory)]
87+
[String]$SwitchName,
88+
89+
[Parameter(Mandatory)]
90+
[ValidateScript({$_ -as [ipaddress]})]
91+
[String]$NSIP,
92+
93+
[ValidateScript({$_ -as [ipaddress]})]
94+
[String]$Netmask = "255.255.255.0",
95+
96+
[Parameter(Mandatory)]
97+
[ValidateScript({$_ -as [ipaddress]})]
98+
[String]$DefaultGateway,
99+
100+
[ValidatePattern("[0-9A-F]{8}")]
101+
[String]$MacAddress = "00155D7E3100",
102+
103+
[Switch]$Force
104+
)
105+
$ErrorActionPreference = "Stop"
106+
107+
function New-TemporaryDirectory {
108+
$parent = [System.IO.Path]::GetTempPath()
109+
[string] $name = [System.Guid]::NewGuid()
110+
New-Item -ItemType Directory -Path (Join-Path $parent $name)
111+
}
112+
113+
# Source: https://gallery.technet.microsoft.com/scriptcenter/New-ISOFile-function-a8deeffd
114+
function New-IsoFile
115+
{
116+
<#
117+
.Synopsis
118+
Creates a new .iso file
119+
.Description
120+
The New-IsoFile cmdlet creates a new .iso file containing content from chosen folders
121+
.Example
122+
New-IsoFile "c:\tools","c:Downloads\utils"
123+
This command creates a .iso file in $env:temp folder (default location) that contains c:\tools and c:\downloads\utils folders. The folders themselves are included at the root of the .iso image.
124+
.Example
125+
New-IsoFile -FromClipboard -Verbose
126+
Before running this command, select and copy (Ctrl-C) files/folders in Explorer first.
127+
.Example
128+
dir c:\WinPE | New-IsoFile -Path c:\temp\WinPE.iso -BootFile "${env:ProgramFiles(x86)}\Windows Kits\10\Assessment and Deployment Kit\Deployment Tools\amd64\Oscdimg\efisys.bin" -Media DVDPLUSR -Title "WinPE"
129+
This command creates a bootable .iso file containing the content from c:\WinPE folder, but the folder itself isn't included. Boot file etfsboot.com can be found in Windows ADK. Refer to IMAPI_MEDIA_PHYSICAL_TYPE enumeration for possible media types: http://msdn.microsoft.com/en-us/library/windows/desktop/aa366217(v=vs.85).aspx
130+
.Notes
131+
NAME: New-IsoFile
132+
AUTHOR: Chris Wu
133+
LASTEDIT: 03/23/2016 14:46:50
134+
#>
135+
136+
[CmdletBinding(DefaultParameterSetName='Source')]Param(
137+
[parameter(Position=1,Mandatory=$true,ValueFromPipeline=$true, ParameterSetName='Source')]$Source,
138+
[parameter(Position=2)][string]$Path = "$env:temp\$((Get-Date).ToString('yyyyMMdd-HHmmss.ffff')).iso",
139+
[ValidateScript({Test-Path -LiteralPath $_ -PathType Leaf})][string]$BootFile = $null,
140+
[ValidateSet('CDR','CDRW','DVDRAM','DVDPLUSR','DVDPLUSRW','DVDPLUSR_DUALLAYER','DVDDASHR','DVDDASHRW','DVDDASHR_DUALLAYER','DISK','DVDPLUSRW_DUALLAYER','BDR','BDRE')][string] $Media = 'DVDPLUSRW_DUALLAYER',
141+
[string]$Title = (Get-Date).ToString("yyyyMMdd-HHmmss.ffff"),
142+
[switch]$Force,
143+
[parameter(ParameterSetName='Clipboard')][switch]$FromClipboard
144+
)
145+
146+
Begin {
147+
($cp = new-object System.CodeDom.Compiler.CompilerParameters).CompilerOptions = '/unsafe'
148+
if (!('ISOFile' -as [type])) {
149+
Add-Type -CompilerParameters $cp -TypeDefinition @'
150+
public class ISOFile
151+
{
152+
public unsafe static void Create(string Path, object Stream, int BlockSize, int TotalBlocks)
153+
{
154+
int bytes = 0;
155+
byte[] buf = new byte[BlockSize];
156+
var ptr = (System.IntPtr)(&bytes);
157+
var o = System.IO.File.OpenWrite(Path);
158+
var i = Stream as System.Runtime.InteropServices.ComTypes.IStream;
159+
160+
if (o != null) {
161+
while (TotalBlocks-- > 0) {
162+
i.Read(buf, BlockSize, ptr); o.Write(buf, 0, bytes);
163+
}
164+
o.Flush(); o.Close();
165+
}
166+
}
167+
}
168+
'@
169+
}
170+
171+
if ($BootFile) {
172+
if('BDR','BDRE' -contains $Media) { Write-Warning "Bootable image doesn't seem to work with media type $Media" }
173+
($Stream = New-Object -ComObject ADODB.Stream -Property @{Type=1}).Open() # adFileTypeBinary
174+
$Stream.LoadFromFile((Get-Item -LiteralPath $BootFile).Fullname)
175+
($Boot = New-Object -ComObject IMAPI2FS.BootOptions).AssignBootImage($Stream)
176+
}
177+
178+
$MediaType = @('UNKNOWN','CDROM','CDR','CDRW','DVDROM','DVDRAM','DVDPLUSR','DVDPLUSRW','DVDPLUSR_DUALLAYER','DVDDASHR','DVDDASHRW','DVDDASHR_DUALLAYER','DISK','DVDPLUSRW_DUALLAYER','HDDVDROM','HDDVDR','HDDVDRAM','BDROM','BDR','BDRE')
179+
180+
Write-Verbose -Message "Selected media type is $Media with value $($MediaType.IndexOf($Media))"
181+
($Image = New-Object -com IMAPI2FS.MsftFileSystemImage -Property @{VolumeName=$Title}).ChooseImageDefaultsForMediaType($MediaType.IndexOf($Media))
182+
183+
if (!($Target = New-Item -Path $Path -ItemType File -Force:$Force -ErrorAction SilentlyContinue)) { Write-Error -Message "Cannot create file $Path. Use -Force parameter to overwrite if the target file already exists."; break }
184+
}
185+
186+
Process {
187+
if($FromClipboard) {
188+
if($PSVersionTable.PSVersion.Major -lt 5) { Write-Error -Message 'The -FromClipboard parameter is only supported on PowerShell v5 or higher'; break }
189+
$Source = Get-Clipboard -Format FileDropList
190+
}
191+
192+
foreach($item in $Source) {
193+
if($item -isnot [System.IO.FileInfo] -and $item -isnot [System.IO.DirectoryInfo]) {
194+
$item = Get-Item -LiteralPath $item
195+
}
196+
197+
if($item) {
198+
Write-Verbose -Message "Adding item to the target image: $($item.FullName)"
199+
try { $Image.Root.AddTree($item.FullName, $true) } catch { Write-Error -Message ($_.Exception.Message.Trim() + ' Try a different media type.') }
200+
}
201+
}
202+
}
203+
204+
End {
205+
if ($Boot) { $Image.BootImageOptions=$Boot }
206+
$Result = $Image.CreateResultImage()
207+
[ISOFile]::Create($Target.FullName,$Result.ImageStream,$Result.BlockSize,$Result.TotalBlocks)
208+
Write-Verbose -Message "Target image ($($Target.FullName)) has been created"
209+
$Target
210+
}
211+
}
212+
213+
function Write-UserData {
214+
Param(
215+
[String]$NSIP,
216+
[String]$Netmask,
217+
[String]$DefaultGateway,
218+
[String]$DestinationPath
219+
)
220+
221+
[xml]$userdata = @"
222+
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
223+
<Environment xmlns:oe="http://schemas.dmtf.org/ovf/environment/1"
224+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
225+
oe:id=""
226+
xmlns="http://schemas.dmtf.org/ovf/environment/1">
227+
<PlatformSection>
228+
<Kind>HYPER-V</Kind>
229+
<Version>2013.1</Version>
230+
<Vendor>CISCO</Vendor>
231+
<Locale>en</Locale>
232+
</PlatformSection>
233+
<PropertySection>
234+
<Property oe:key="com.citrix.netscaler.ovf.version" oe:value="1.0"/>
235+
<Property oe:key="com.citrix.netscaler.platform" oe:value="NS1000V"/>
236+
<Property oe:key="com.citrix.netscaler.orch_env" oe:value="cisco-orch-env"/>
237+
<Property oe:key="com.citrix.netscaler.mgmt.ip" oe:value=""/>
238+
<Property oe:key="com.citrix.netscaler.mgmt.netmask" oe:value=""/>
239+
<Property oe:key="com.citrix.netscaler.mgmt.gateway" oe:value=""/>
240+
</PropertySection>
241+
</Environment>
242+
"@
243+
244+
$userdata.Environment.PropertySection.Property | ForEach-Object {
245+
$Property = $_
246+
switch ($Property.key) {
247+
"com.citrix.netscaler.mgmt.ip" { $Property.value = $NSIP }
248+
"com.citrix.netscaler.mgmt.netmask" { $Property.value = $Netmask }
249+
"com.citrix.netscaler.mgmt.gateway" { $Property.value = $DefaultGateway }
250+
}
251+
}
252+
253+
$userdata.save($DestinationPath)
254+
}
255+
256+
function Wait-NS {
257+
Param(
258+
$ip = $NSIP,
259+
$WaitTimeout = 120,
260+
[ScriptBlock]$AfterBlock
261+
)
262+
$ip = $nsip
263+
$canWait = $true
264+
$WaitTimeout = 180
265+
$ping = New-Object -TypeName System.Net.NetworkInformation.Ping
266+
if ($True) {
267+
$waitStart = Get-Date
268+
Write-Verbose -Message 'Trying to ping until unreachable to ensure reboot process'
269+
while ($canWait -and $($ping.Send($ip, 2000)).Status -eq [System.Net.NetworkInformation.IPStatus]::Success) {
270+
if ($($(Get-Date) - $waitStart).TotalSeconds -gt $WaitTimeout) {
271+
$canWait = $false
272+
break
273+
} else {
274+
Write-Verbose -Message 'Still reachable. Pinging again...'
275+
Start-Sleep -Seconds 2
276+
}
277+
}
278+
279+
if ($canWait) {
280+
Write-Verbose -Message 'Trying to reach Nitro REST API to test connectivity...'
281+
while ($canWait) {
282+
$connectTestError = $null
283+
$response = $null
284+
try {
285+
$params = @{
286+
Uri = "http://$ip/nitro/v1/config"
287+
Method = 'GET'
288+
ContentType = 'application/json'
289+
ErrorVariable = 'connectTestError'
290+
}
291+
$response = Invoke-RestMethod @params
292+
}
293+
catch {
294+
if ($connectTestError) {
295+
if ($connectTestError.InnerException.Message -eq 'The remote server returned an error: (401) Unauthorized.') {
296+
break
297+
} elseif ($($(Get-Date) - $waitStart).TotalSeconds -gt $WaitTimeout) {
298+
$canWait = $false
299+
break
300+
} else {
301+
Write-Verbose -Message 'Nitro REST API is not responding. Trying again...'
302+
Start-Sleep -Seconds 5
303+
}
304+
}
305+
}
306+
if ($response) {
307+
break
308+
}
309+
}
310+
}
311+
312+
if ($canWait) {
313+
Write-Verbose -Message 'NetScaler appliance is back online.'
314+
& $AfterBlock
315+
} else {
316+
throw 'Timeout expired. Unable to determine if NetScaler appliance is back online.'
317+
}
318+
}
319+
320+
}
321+
322+
if($Force -and (Get-VM -Name $VMName -ErrorAction SilentlyContinue)) {
323+
Write-Verbose "Removing existing VM '$VMName'..."
324+
Remove-VM -Name $VMName -Force
325+
}
326+
327+
$TempDir = New-TemporaryDirectory
328+
Write-Verbose "Expanding package '$Package' into '$TempDir'..."
329+
330+
try {
331+
Expand-Archive -Path $Package -DestinationPath $TempDir
332+
$Vhd = Get-ChildItem -Recurse -Path $TempDir -Include Dynamic.vhd
333+
334+
if (-not($Vhd)) {
335+
Write-Error "Unable to find Dynamic.vhd file in the expanded archive"
336+
return
337+
}
338+
339+
if (Test-Path $Path) {
340+
Write-Warning "Path '$Path' already exists!"
341+
342+
if ($Force) {
343+
Write-Verbose "Removing '$Path'..."
344+
Remove-Item -Recurse $Path
345+
} else {
346+
Write-Error "Exiting. If you want to replace the existing VM use -Force."
347+
}
348+
}
349+
350+
New-Item -ItemType Directory -Path $Path > $Null
351+
352+
$Vhdx = Join-Path $Path "$VMName.vhdx"
353+
Write-Verbose "Converting VHD to '$Vhdx'..."
354+
Convert-VHD -Path $Vhd -DestinationPath $Vhdx -VHDType Dynamic
355+
356+
Write-Verbose "Importing disk $Vhd..."
357+
New-VM -Name $VMName -MemoryStartupBytes 2GB -VHDPath $Vhdx
358+
Set-VMProcessor -VMName $VMName -Count 2
359+
360+
Write-Verbose "Setting MAC address to '$MacAddress'..."
361+
Set-VMNetworkAdapter -VMName $VMName -StaticMacAddress $MacAddress
362+
Connect-VMNetworkAdapter -VMName $VMName -SwitchName $SwitchName
363+
364+
$UserDataFile = Join-Path $TempDir "userdata"
365+
$UserDataISOFile = Join-Path $TempDir "userdata.iso"
366+
Write-Verbose "Creating userdata ISO..."
367+
Write-UserData -NSIP $NSIP -Netmask $Netmask -DefaultGateway $DefaultGateway `
368+
-DestinationPath $UserDataFile
369+
New-IsoFile -Media CDR -Source $UserDataFile -Path $UserDataISOFile
370+
371+
Set-VMDvdDrive -VMName $VMName -Path $UserDataISOFile
372+
373+
Start-VM -Name $VMName
374+
375+
Wait-Ns -AfterBlock {
376+
Get-VMDvdDrive -VMName $VMName | ForEach-Object {
377+
$_ | Set-VMDvdDrive -Path $Null
378+
}
379+
}
380+
} finally {
381+
# This is a safeguard to prevent deleting our whole disk...
382+
if ($TempDir.Fullname.Length -ge 4) {
383+
Remove-Item -Recurse $TempDir -Force
384+
} else {
385+
# Prevent full disk wipe out
386+
Write-Error "Refusing to delete directory '$TempDir' (too short)"
387+
}
388+
}

0 commit comments

Comments
 (0)