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