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