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 2 GB - 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