Skip to content

Commit f39afd5

Browse files
committed
Added contrib script for Hyperv auto-provisioning
1 parent 04f9d5c commit f39afd5

File tree

1 file changed

+378
-0
lines changed

1 file changed

+378
-0
lines changed

Contrib/New-NSHyperVInstance.ps1

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

0 commit comments

Comments
 (0)