├── .github └── FUNDING.yml ├── Change NIC PCI slot number.ps1 ├── ESXi cloner.ps1 ├── Get VMware or Hyper-V powered on vm details.ps1 ├── LICENSE ├── Power state change running VMs.ps1 ├── README.md ├── Run script in VM.ps1 ├── Set VMware guest info.ps1 ├── VMware GUI.ps1 └── mstsc sizer.ps1 /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: guyrleech 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: guyrleech 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /Change NIC PCI slot number.ps1: -------------------------------------------------------------------------------- 1 |  2 | <# 3 | .SYNOPSIS 4 | 5 | Change the PCI slot number for the network adapters in specified virtual machines 6 | 7 | .DESCRIPTION 8 | 9 | Citrix Provisioning Services (PVS) target devices can fail to boot with a BSoD which may be due to the virtual NIC being in a different PCI slot number from the machine it was created on 10 | 11 | .PARAMETER vcenter 12 | 13 | The VMware Virtual Center to connect to 14 | 15 | .PARAMETER slotNumber 16 | 17 | The PCI slot number to set the vNICS to 18 | 19 | .PARAMETER nicType 20 | 21 | The type of the NIC to pick the slot number from in the source VM (use when it has multiple NICs) 22 | 23 | .PARAMETER network 24 | 25 | The name of the network that the NIC to pick the slot number from in the source VM is connected to (use when it has multiple NICs) 26 | 27 | .PARAMETER vms 28 | 29 | The name/pattern of the VMs to change 30 | 31 | .PARAMETER port 32 | 33 | The vCenter port to connect on if not the default 34 | 35 | .PARAMETER protocol 36 | 37 | The vCenter protocol to use if not the default 38 | 39 | .PARAMETER allLinked 40 | 41 | Connect to all linked vCenters 42 | 43 | .PARAMETER force 44 | 45 | Suppresses all user interface prompts during vCenter connection 46 | 47 | .PARAMETER fromVM 48 | 49 | The name/pattern of the VM to get the NIC PCI slot number from to apply to the VMs to change 50 | 51 | .EXAMPLE 52 | 53 | & '.\Change NIC PCI slot number.ps1' -fromVM GLXAPVSMASTS19 -vms GLXA19PVS40* -vcenter grl-vcenter04.guyrleech.local 54 | 55 | Change the PCI port number in VMs matching GLXA19PVS40* to the PCI port number in the VM GLXAPVSMASTS19 using the VMware vCenter server grl-vcenter04.guyrleech.local 56 | 57 | .EXAMPLE 58 | 59 | & '.\Change NIC PCI slot number.ps1' -slotNumber 256 -vms GLXA19PVS40* -vcenter grl-vcenter04.guyrleech.local 60 | 61 | Change the PCI port number in VMs matching GLXA19PVS40* to 256 using the VMware vCenter server grl-vcenter04.guyrleech.local 62 | 63 | .EXAMPLE 64 | 65 | & '.\Change NIC PCI slot number.ps1' -slotNumber 256 -vms GLXA19PVS40* -vcenter grl-vcenter04.guyrleech.local -network "Internal Network" 66 | 67 | Change the PCI port number on the NIC connected to the "Internal Network" network in VMs matching GLXA19PVS40* to 256 using the VMware vCenter server grl-vcenter04.guyrleech.local 68 | Use this when the VMs have multiple NICs 69 | 70 | .NOTES 71 | 72 | VMs must be powered off. 73 | 74 | Requires VMware PowerCLI. 75 | 76 | Modification History: 77 | 78 | 04/07/2021 @guyrleech Initial public release 79 | 05/07/2021 @guyrleech Deal with VMs to change having multiple NICs or no slot number (never booted). 80 | Added -poweron 81 | 09/12/2021 @guyrleech Added -credential 82 | #> 83 | 84 | <# 85 | Copyright © 2021 Guy Leech 86 | 87 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, 88 | including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 89 | 90 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 91 | 92 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 93 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 94 | #> 95 | 96 | [CmdletBinding(SupportsShouldProcess=$True,ConfirmImpact='High')] 97 | 98 | Param 99 | ( 100 | [string]$vcenter , 101 | [int]$slotNumber , 102 | [string]$fromVM , 103 | [ValidateSet('e1000','e1000e','vmxnet2','vmxnet3','flexible','enhancedvmxnet','SriovEthernetCard','Vmxnet3Vrdma')] 104 | [string]$nicType , 105 | [string]$network , 106 | [string]$vms , 107 | [switch]$powerOn , 108 | [pscredential]$credential , 109 | [int]$port , 110 | [ValidateSet('http','https')] 111 | [string]$protocol , 112 | [switch]$allLinked , 113 | [switch]$force 114 | ) 115 | 116 | if( $PSBoundParameters.ContainsKey( 'slotNumber' ) -and $PSBoundParameters[ 'fromVM' ] ) 117 | { 118 | Throw "Only one of -slotNumber and -fromVM is allowed" 119 | } 120 | 121 | Import-Module -Name VMware.VimAutomation.Core -Verbose:$false 122 | 123 | [hashtable]$vcenterParameters = @{ 'Server' = $vcenter ; 'AllLinked' = $allLinked ; 'Force' = $force } 124 | 125 | if( $PSBoundParameters[ 'credential' ] ) 126 | { 127 | $vcenterParameters.Add( 'credential' , $credential ) 128 | } 129 | 130 | if( $PSBoundParameters[ 'port' ] ) 131 | { 132 | $vcenterParameters.Add( 'port' , $port ) 133 | } 134 | 135 | if( $PSBoundParameters[ 'protocol' ] ) 136 | { 137 | $vcenterParameters.Add( 'protocol' , $protocol ) 138 | } 139 | 140 | if( ! ( $viconnection = Connect-VIServer @vcenterParameters ) ) 141 | { 142 | Throw "Unable to connect to $vcenter" 143 | } 144 | 145 | if( ! $PSBoundParameters[ 'slotNumber' ] ) 146 | { 147 | if( ! ( $sourceVM = Get-VM -Name $fromVM ) ) 148 | { 149 | Throw "Unable to find source VM $fromVM" 150 | } 151 | if( $sourceVM -is [array] ) 152 | { 153 | Throw "Found $($sourceVM.Count) VMs matching $fromVM" 154 | } 155 | if( ! ( $nic = Get-NetworkAdapter -VM $sourceVM ) ) 156 | { 157 | Throw "VM $($sourceVM.Name) has no NICs" 158 | } 159 | if( $nic -is [array] -and $nic.Count -gt 1 ) 160 | { 161 | [array]$slots = @( $nic | Select-Object -ExpandProperty ExtensionData | Select-Object -ExpandProperty SlotInfo | Group-Object -Property PciSlotNumber ) 162 | if( $slots.Count -gt 1 ) 163 | { 164 | ## More than 1 NIC and different slots so need to filter on nic type and/or network name 165 | [array]$filtered = $nic 166 | if( $PSBoundParameters[ 'nicType' ] ) 167 | { 168 | $filtered = @( $filtered.Where( { $_.Type -eq $nicType } )) 169 | } 170 | if( $PSBoundParameters[ 'network' ] ) 171 | { 172 | $filtered = @( $filtered.Where( { $_.NetworkName -eq $network } )) 173 | } 174 | $slots = @( $filtered | Select-Object -ExpandProperty ExtensionData | Select-Object -ExpandProperty SlotInfo | Group-Object -Property PciSlotNumber ) 175 | if( $slots.Count -ne 1 ) 176 | { 177 | Throw "Unable to refine NIC list to find a single slot number" 178 | } 179 | } 180 | 181 | if( ! $PSBoundParameters[ 'nicType' ] ) 182 | { 183 | ## find the NIC for which we have the slot number so we can get its type as destination VMs probably have multiple NICs too 184 | if( $thisNic = $nic.Where( { $_.ExtensionData.SlotInfo.PciSlotNumber -eq $slots[0].Group.PciSlotNumber -and ( [string]::IsNullOrEmpty( $nicType ) -or $_.Type -eq $nicType ) -and ( [string]::IsNullOrEmpty( $network ) -or $_.NetworkName -eq $network ) } ) ) 185 | { 186 | $nicType = $thisNic.Type 187 | Write-Verbose -Message "$($nic.Count) NICs found so setting type to $nicType" 188 | } 189 | else 190 | { 191 | Write-Warning -Message "Failed to find filtered NIC in $($sourceVM.Name) for slot $($slots[0].Name)" 192 | } 193 | if( $thisNIC -and ! $PSBoundParameters[ 'network' ] ) 194 | { 195 | $network = $thisNIC.NetworkName 196 | Write-Verbose -Message "$($nic.Count) NICs found so setting network name to `"$network`"" 197 | } 198 | } 199 | 200 | $slotNumber = $slots[0].Name 201 | } 202 | else 203 | { 204 | $slotNumber = $nic.ExtensionData.SlotInfo.PciSlotNumber 205 | if( ! $PSBoundParameters[ 'nicType' ] ) 206 | { 207 | $nicType = $nic.type 208 | } 209 | if( ! $PSBoundParameters[ 'network' ] ) 210 | { 211 | $network = $nic.NetworkName 212 | } 213 | } 214 | 215 | Write-Verbose -Message "Slot number is $slotNumber in $($sourceVM.Name)" 216 | } 217 | 218 | [array]$vmsToChange = @( Get-VM -Name $vms | Sort-Object -Property Name ) 219 | 220 | if( ! $vmsToChange -or ! $vmsToChange.Count ) 221 | { 222 | Throw "No VMs found matching $vms" 223 | } 224 | 225 | ForEach( $vmToChange in $vmsToChange ) 226 | { 227 | if( $nic = Get-NetworkAdapter -VM $vmToChange | Where-Object { ( [string]::IsNullOrEmpty( $network ) -or $_.NetworkName -eq $network ) -and ( [string]::IsNullOrEmpty( $nicType ) -or $_.Type -eq $nicType ) } ) 228 | { 229 | if( $nic -is [array] ) 230 | { 231 | Write-Warning -Message "Unable to change nic for $($vmToChange.Name) as there are $($nic.Count) - use -nictype and/or -network to be more specific" 232 | } 233 | else 234 | { 235 | [int]$existingSlotNumber = -1 236 | 237 | try 238 | { 239 | $existingSlotNumber = $nic.ExtensionData.SlotInfo.PciSlotNumber 240 | } 241 | catch 242 | { 243 | Write-Warning -Message "Unable to get existing slot number for nic in $($vmToChange.Name)" 244 | } 245 | 246 | if( $existingSlotNumber -ne $slotNumber ) 247 | { 248 | if( $vmToChange.PowerState -ne 'PoweredOff' ) 249 | { 250 | Write-Warning -Message "Cannot change $($vmToChange.Name) from $existingSlotNumber to $slotNumber because VM is not powered off, it is $($vmToChange.PowerState)" 251 | } 252 | elseif( $PSCmdlet.ShouldProcess( $vmToChange.Name , "Change $($nic.Name) ($($nic.Type)) on `"$($nic.NetworkName)`" PCI slot number from $existingSlotNumber to $slotNumber" )) 253 | { 254 | $spec = New-Object VMware.Vim.VirtualMachineConfigSpec 255 | $device = New-Object VMware.Vim.VirtualDeviceConfigSpec 256 | 257 | $device.Operation = [VMware.Vim.VirtualDeviceConfigSpecOperation]::edit 258 | $device.Device = $nic.ExtensionData 259 | ## if never booted then may not have slot number 260 | if( $null -eq $device.Device.SlotInfo ) 261 | { 262 | $device.Device.SlotInfo = New-Object -TypeName VMware.Vim.VirtualDevicePciBusSlotInfo 263 | Write-Warning -Message "$($vmToChange.Name) did not have slot info so added" 264 | } 265 | 266 | $device.Device.SlotInfo.PciSlotNumber = $slotNumber 267 | 268 | $spec.deviceChange = @( $device ) 269 | 270 | $vmToChange.ExtensionData.ReconfigVM($spec) 271 | if( ! $? ) 272 | { 273 | Write-Error "Problem changing slot from $existingSlotNumber to $slotNumber in $($vmToChange.Name)" 274 | } 275 | elseif( $powerOn ) 276 | { 277 | Start-VM -VM $vmToChange 278 | } 279 | } 280 | } 281 | else 282 | { 283 | Write-Warning "NIC in $($vmToChange.Name) is already in slot $slotNumber" 284 | } 285 | } 286 | } 287 | else 288 | { 289 | [string]$message = "No NIC found in $($vmToChange.Name)" 290 | if( $network ) 291 | { 292 | $message += " on network $network" 293 | } 294 | Write-Warning -Message $message 295 | } 296 | } 297 | 298 | # SIG # Begin signature block 299 | # MIIZsAYJKoZIhvcNAQcCoIIZoTCCGZ0CAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB 300 | # gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR 301 | # AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUJ8T9vrdWO07o63eW3itopnTz 302 | # 4yWgghS+MIIE/jCCA+agAwIBAgIQDUJK4L46iP9gQCHOFADw3TANBgkqhkiG9w0B 303 | # AQsFADByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD 304 | # VQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEyIEFz 305 | # c3VyZWQgSUQgVGltZXN0YW1waW5nIENBMB4XDTIxMDEwMTAwMDAwMFoXDTMxMDEw 306 | # NjAwMDAwMFowSDELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMu 307 | # MSAwHgYDVQQDExdEaWdpQ2VydCBUaW1lc3RhbXAgMjAyMTCCASIwDQYJKoZIhvcN 308 | # AQEBBQADggEPADCCAQoCggEBAMLmYYRnxYr1DQikRcpja1HXOhFCvQp1dU2UtAxQ 309 | # tSYQ/h3Ib5FrDJbnGlxI70Tlv5thzRWRYlq4/2cLnGP9NmqB+in43Stwhd4CGPN4 310 | # bbx9+cdtCT2+anaH6Yq9+IRdHnbJ5MZ2djpT0dHTWjaPxqPhLxs6t2HWc+xObTOK 311 | # fF1FLUuxUOZBOjdWhtyTI433UCXoZObd048vV7WHIOsOjizVI9r0TXhG4wODMSlK 312 | # XAwxikqMiMX3MFr5FK8VX2xDSQn9JiNT9o1j6BqrW7EdMMKbaYK02/xWVLwfoYer 313 | # vnpbCiAvSwnJlaeNsvrWY4tOpXIc7p96AXP4Gdb+DUmEvQECAwEAAaOCAbgwggG0 314 | # MA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsG 315 | # AQUFBwMIMEEGA1UdIAQ6MDgwNgYJYIZIAYb9bAcBMCkwJwYIKwYBBQUHAgEWG2h0 316 | # dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAfBgNVHSMEGDAWgBT0tuEgHf4prtLk 317 | # YaWyoiWyyBc1bjAdBgNVHQ4EFgQUNkSGjqS6sGa+vCgtHUQ23eNqerwwcQYDVR0f 318 | # BGowaDAyoDCgLoYsaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL3NoYTItYXNzdXJl 319 | # ZC10cy5jcmwwMqAwoC6GLGh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9zaGEyLWFz 320 | # c3VyZWQtdHMuY3JsMIGFBggrBgEFBQcBAQR5MHcwJAYIKwYBBQUHMAGGGGh0dHA6 321 | # Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBPBggrBgEFBQcwAoZDaHR0cDovL2NhY2VydHMu 322 | # ZGlnaWNlcnQuY29tL0RpZ2lDZXJ0U0hBMkFzc3VyZWRJRFRpbWVzdGFtcGluZ0NB 323 | # LmNydDANBgkqhkiG9w0BAQsFAAOCAQEASBzctemaI7znGucgDo5nRv1CclF0CiNH 324 | # o6uS0iXEcFm+FKDlJ4GlTRQVGQd58NEEw4bZO73+RAJmTe1ppA/2uHDPYuj1UUp4 325 | # eTZ6J7fz51Kfk6ftQ55757TdQSKJ+4eiRgNO/PT+t2R3Y18jUmmDgvoaU+2QzI2h 326 | # F3MN9PNlOXBL85zWenvaDLw9MtAby/Vh/HUIAHa8gQ74wOFcz8QRcucbZEnYIpp1 327 | # FUL1LTI4gdr0YKK6tFL7XOBhJCVPst/JKahzQ1HavWPWH1ub9y4bTxMd90oNcX6X 328 | # t/Q/hOvB46NJofrOp79Wz7pZdmGJX36ntI5nePk2mOHLKNpbh6aKLzCCBTAwggQY 329 | # oAMCAQICEAQJGBtf1btmdVNDtW+VUAgwDQYJKoZIhvcNAQELBQAwZTELMAkGA1UE 330 | # BhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2lj 331 | # ZXJ0LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQgQXNzdXJlZCBJRCBSb290IENBMB4X 332 | # DTEzMTAyMjEyMDAwMFoXDTI4MTAyMjEyMDAwMFowcjELMAkGA1UEBhMCVVMxFTAT 333 | # BgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEx 334 | # MC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVkIElEIENvZGUgU2lnbmluZyBD 335 | # QTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPjTsxx/DhGvZ3cH0wsx 336 | # SRnP0PtFmbE620T1f+Wondsy13Hqdp0FLreP+pJDwKX5idQ3Gde2qvCchqXYJawO 337 | # eSg6funRZ9PG+yknx9N7I5TkkSOWkHeC+aGEI2YSVDNQdLEoJrskacLCUvIUZ4qJ 338 | # RdQtoaPpiCwgla4cSocI3wz14k1gGL6qxLKucDFmM3E+rHCiq85/6XzLkqHlOzEc 339 | # z+ryCuRXu0q16XTmK/5sy350OTYNkO/ktU6kqepqCquE86xnTrXE94zRICUj6whk 340 | # PlKWwfIPEvTFjg/BougsUfdzvL2FsWKDc0GCB+Q4i2pzINAPZHM8np+mM6n9Gd8l 341 | # k9ECAwEAAaOCAc0wggHJMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQD 342 | # AgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMDMHkGCCsGAQUFBwEBBG0wazAkBggrBgEF 343 | # BQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEMGCCsGAQUFBzAChjdodHRw 344 | # Oi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0Eu 345 | # Y3J0MIGBBgNVHR8EejB4MDqgOKA2hjRodHRwOi8vY3JsNC5kaWdpY2VydC5jb20v 346 | # RGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3JsMDqgOKA2hjRodHRwOi8vY3JsMy5k 347 | # aWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3JsME8GA1UdIARI 348 | # MEYwOAYKYIZIAYb9bAACBDAqMCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdp 349 | # Y2VydC5jb20vQ1BTMAoGCGCGSAGG/WwDMB0GA1UdDgQWBBRaxLl7KgqjpepxA8Bg 350 | # +S32ZXUOWDAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzANBgkqhkiG 351 | # 9w0BAQsFAAOCAQEAPuwNWiSz8yLRFcgsfCUpdqgdXRwtOhrE7zBh134LYP3DPQ/E 352 | # r4v97yrfIFU3sOH20ZJ1D1G0bqWOWuJeJIFOEKTuP3GOYw4TS63XX0R58zYUBor3 353 | # nEZOXP+QsRsHDpEV+7qvtVHCjSSuJMbHJyqhKSgaOnEoAjwukaPAJRHinBRHoXpo 354 | # aK+bp1wgXNlxsQyPu6j4xRJon89Ay0BEpRPw5mQMJQhCMrI2iiQC/i9yfhzXSUWW 355 | # 6Fkd6fp0ZGuy62ZD2rOwjNXpDd32ASDOmTFjPQgaGLOBm0/GkxAG/AeB+ova+YJJ 356 | # 92JuoVP6EpQYhS6SkepobEQysmah5xikmmRR7zCCBTEwggQZoAMCAQICEAqhJdbW 357 | # Mht+QeQF2jaXwhUwDQYJKoZIhvcNAQELBQAwZTELMAkGA1UEBhMCVVMxFTATBgNV 358 | # BAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEkMCIG 359 | # A1UEAxMbRGlnaUNlcnQgQXNzdXJlZCBJRCBSb290IENBMB4XDTE2MDEwNzEyMDAw 360 | # MFoXDTMxMDEwNzEyMDAwMFowcjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lD 361 | # ZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTExMC8GA1UEAxMoRGln 362 | # aUNlcnQgU0hBMiBBc3N1cmVkIElEIFRpbWVzdGFtcGluZyBDQTCCASIwDQYJKoZI 363 | # hvcNAQEBBQADggEPADCCAQoCggEBAL3QMu5LzY9/3am6gpnFOVQoV7YjSsQOB0Uz 364 | # URB90Pl9TWh+57ag9I2ziOSXv2MhkJi/E7xX08PhfgjWahQAOPcuHjvuzKb2Mln+ 365 | # X2U/4Jvr40ZHBhpVfgsnfsCi9aDg3iI/Dv9+lfvzo7oiPhisEeTwmQNtO4V8CdPu 366 | # XciaC1TjqAlxa+DPIhAPdc9xck4Krd9AOly3UeGheRTGTSQjMF287DxgaqwvB8z9 367 | # 8OpH2YhQXv1mblZhJymJhFHmgudGUP2UKiyn5HU+upgPhH+fMRTWrdXyZMt7HgXQ 368 | # hBlyF/EXBu89zdZN7wZC/aJTKk+FHcQdPK/P2qwQ9d2srOlW/5MCAwEAAaOCAc4w 369 | # ggHKMB0GA1UdDgQWBBT0tuEgHf4prtLkYaWyoiWyyBc1bjAfBgNVHSMEGDAWgBRF 370 | # 66Kv9JLLgjEtUYunpyGd823IDzASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB 371 | # /wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcDCDB5BggrBgEFBQcBAQRtMGswJAYI 372 | # KwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEFBQcwAoY3 373 | # aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9v 374 | # dENBLmNydDCBgQYDVR0fBHoweDA6oDigNoY0aHR0cDovL2NybDQuZGlnaWNlcnQu 375 | # Y29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDA6oDigNoY0aHR0cDovL2Ny 376 | # bDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDBQBgNV 377 | # HSAESTBHMDgGCmCGSAGG/WwAAgQwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cu 378 | # ZGlnaWNlcnQuY29tL0NQUzALBglghkgBhv1sBwEwDQYJKoZIhvcNAQELBQADggEB 379 | # AHGVEulRh1Zpze/d2nyqY3qzeM8GN0CE70uEv8rPAwL9xafDDiBCLK938ysfDCFa 380 | # KrcFNB1qrpn4J6JmvwmqYN92pDqTD/iy0dh8GWLoXoIlHsS6HHssIeLWWywUNUME 381 | # aLLbdQLgcseY1jxk5R9IEBhfiThhTWJGJIdjjJFSLK8pieV4H9YLFKWA1xJHcLN1 382 | # 1ZOFk362kmf7U2GJqPVrlsD0WGkNfMgBsbkodbeZY4UijGHKeZR+WfyMD+NvtQEm 383 | # tmyl7odRIeRYYJu6DC0rbaLEfrvEJStHAgh8Sa4TtuF8QkIoxhhWz0E0tmZdtnR7 384 | # 9VYzIi8iNrJLokqV2PWmjlIwggVPMIIEN6ADAgECAhAE/eOq2921q55B9NnVIXVO 385 | # MA0GCSqGSIb3DQEBCwUAMHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy 386 | # dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAvBgNVBAMTKERpZ2lD 387 | # ZXJ0IFNIQTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25pbmcgQ0EwHhcNMjAwNzIwMDAw 388 | # MDAwWhcNMjMwNzI1MTIwMDAwWjCBizELMAkGA1UEBhMCR0IxEjAQBgNVBAcTCVdh 389 | # a2VmaWVsZDEmMCQGA1UEChMdU2VjdXJlIFBsYXRmb3JtIFNvbHV0aW9ucyBMdGQx 390 | # GDAWBgNVBAsTD1NjcmlwdGluZ0hlYXZlbjEmMCQGA1UEAxMdU2VjdXJlIFBsYXRm 391 | # b3JtIFNvbHV0aW9ucyBMdGQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB 392 | # AQCvbSdd1oAAu9rTtdnKSlGWKPF8g+RNRAUDFCBdNbYbklzVhB8hiMh48LqhoP7d 393 | # lzZY3YmuxztuPlB7k2PhAccd/eOikvKDyNeXsSa3WaXLNSu3KChDVekEFee/vR29 394 | # mJuujp1eYrz8zfvDmkQCP/r34Bgzsg4XPYKtMitCO/CMQtI6Rnaj7P6Kp9rH1nVO 395 | # /zb7KD2IMedTFlaFqIReT0EVG/1ZizOpNdBMSG/x+ZQjZplfjyyjiYmE0a7tWnVM 396 | # Z4KKTUb3n1CTuwWHfK9G6CNjQghcFe4D4tFPTTKOSAx7xegN1oGgifnLdmtDtsJU 397 | # OOhOtyf9Kp8e+EQQyPVrV/TNAgMBAAGjggHFMIIBwTAfBgNVHSMEGDAWgBRaxLl7 398 | # KgqjpepxA8Bg+S32ZXUOWDAdBgNVHQ4EFgQUTXqi+WoiTm5fYlDLqiDQ4I+uyckw 399 | # DgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMHcGA1UdHwRwMG4w 400 | # NaAzoDGGL2h0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9zaGEyLWFzc3VyZWQtY3Mt 401 | # ZzEuY3JsMDWgM6Axhi9odHRwOi8vY3JsNC5kaWdpY2VydC5jb20vc2hhMi1hc3N1 402 | # cmVkLWNzLWcxLmNybDBMBgNVHSAERTBDMDcGCWCGSAGG/WwDATAqMCgGCCsGAQUF 403 | # BwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5jb20vQ1BTMAgGBmeBDAEEATCBhAYI 404 | # KwYBBQUHAQEEeDB2MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5j 405 | # b20wTgYIKwYBBQUHMAKGQmh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdp 406 | # Q2VydFNIQTJBc3N1cmVkSURDb2RlU2lnbmluZ0NBLmNydDAMBgNVHRMBAf8EAjAA 407 | # MA0GCSqGSIb3DQEBCwUAA4IBAQBT3M71SlOQ8vwM2txshp/XDvfoKBYHkpFCyanW 408 | # aFdsYQJQIKk4LOVgUJJ6LAf0xPSN7dZpjFaoilQy8Ajyd0U9UOnlEX4gk2J+z5i4 409 | # sFxK/W2KU1j6R9rY5LbScWtsV+X1BtHihpzPywGGE5eth5Q5TixMdI9CN3eWnKGF 410 | # kY13cI69zZyyTnkkb+HaFHZ8r6binvOyzMr69+oRf0Bv/uBgyBKjrmGEUxJZy+00 411 | # 7fbmYDEclgnWT1cRROarzbxmZ8R7Iyor0WU3nKRgkxan+8rzDhzpZdtgIFdYvjeO 412 | # c/IpPi2mI6NY4jqDXwkx1TEIbjUdrCmEfjhAfMTU094L7VSNMYIEXDCCBFgCAQEw 413 | # gYYwcjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UE 414 | # CxMQd3d3LmRpZ2ljZXJ0LmNvbTExMC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1 415 | # cmVkIElEIENvZGUgU2lnbmluZyBDQQIQBP3jqtvdtaueQfTZ1SF1TjAJBgUrDgMC 416 | # GgUAoHgwGAYKKwYBBAGCNwIBDDEKMAigAoAAoQKAADAZBgkqhkiG9w0BCQMxDAYK 417 | # KwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAjBgkqhkiG 418 | # 9w0BCQQxFgQUDbYn/VvMWhiPGNIcFtochC5jrdwwDQYJKoZIhvcNAQEBBQAEggEA 419 | # kHzsaDOzWlqst2Dml6T3RQdgl5sbcgx9POwMvt3R1IgUEND0aJpNQqK6Alb35aMr 420 | # zZWPJYo0qdvt4pDGE36J8sw2DaPJ+w9mlFZYYh/Vjksdv1tl/0dHs3FFsrJ1NBcP 421 | # BCyWGTl5qD0pB2L8DSUp8CzHsjZScEYOqD1CskRJu/PvS95bF4YQaswNLPogtZMC 422 | # OYMQFEMHY1OrqKytSHxDkKVG9JxNMUIsxY9RbkFkRk8zAH3y0pXGXHZwbg7y11wh 423 | # CLv8XXfNE7sgIQqmn9+ZgVzrHYppMhLN/Q6julCo4vFiJ/eE0MswCndXWBYZeJGh 424 | # UrMnT7RfVQmakNrQoCmI+KGCAjAwggIsBgkqhkiG9w0BCQYxggIdMIICGQIBATCB 425 | # hjByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQL 426 | # ExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEyIEFzc3Vy 427 | # ZWQgSUQgVGltZXN0YW1waW5nIENBAhANQkrgvjqI/2BAIc4UAPDdMA0GCWCGSAFl 428 | # AwQCAQUAoGkwGAYJKoZIhvcNAQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUx 429 | # DxcNMjExMjA5MTYzNDQ2WjAvBgkqhkiG9w0BCQQxIgQg/j3TrX9wk0nUu+cs10Tt 430 | # rFjBj8m+KHxl44asEmhSjW4wDQYJKoZIhvcNAQEBBQAEggEAk9TvRz9rQXMGO17h 431 | # D0JeirmYW5Fs5U5SwZ9OcoJzXx/82ZrDuEQAzKVwb/GvKlzBRofhbj2OSMZhNY8V 432 | # Ouqs/D4rt6ucxmlsWZZr5o/0tR+LG9r4Jk8CTE+uABjuZFpTuyO2hj6oounJiOfh 433 | # x2091iyTZ4f0KGMNva99bc23nOyOW3gFnKRzfx2As4MlShtZvqkGFogRQNOec525 434 | # TD8h918RCc6xzHqR51OSoyNcOHplaFNMfddWB1zc4xTjxa/K8CjylOkdMC80AAsI 435 | # jXMFW1RF5LTHAqBp80taW/28OhS6f8YHTSBMTThJSeumeI1w+xuLSZ7O8+0SdTBN 436 | # U0VL0A== 437 | # SIG # End signature block 438 | -------------------------------------------------------------------------------- /ESXi cloner.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | Clone one or more VMware ESXi VMs from a 'template' VM 3 | 4 | @guyrleech 2018 5 | 6 | THIS SCRIPT COMES WITH ABSOLUTELY NO WARRANTY. USE AT YOUR OWN RISK. THE AUTHOR CANNOT BE HELD RESPONSIBLE FOR ANY UNDESIRABLE SCRIPT BEHAVIOURS. 7 | 8 | Modification History: 9 | 10 | 04/11/18 GRL Added -linkedClone, -username, -password and -waitToExit parameters 11 | Added extra verifications on VM name in GUI 12 | Made GUI window normal if start minimised 13 | Hooked -datastore parameter into the GUI 14 | Changed creation of some devices to use enumeration of existing devices 15 | Fixed path bug for copied disks 16 | 09/11/18 GRL Added support for E1000 and VMxnet2 NICs 17 | Added time report in verbose mode 18 | 20/12/18 GRL Added option to protect parent disk in linked clone from deletion 19 | Validation checks VM name(s) does not exist already 20 | Option to start clone numbering from >1 21 | 16/09/19 GRL Fixed issue with duplicate MAC addresses 22 | #> 23 | 24 | <# 25 | .SYNOPSIS 26 | 27 | Clone one ore more VMware ESXi VMs from a 'template' VM 28 | 29 | .DESCRIPTION 30 | 31 | Since ESXi without vCenter does not have templates and therefore a cloning mechanism built in, this script mimics this by allowing existing VMs to 32 | be treated as templates and this script will create one or more new VMs with the same specification as the chosen template and copy its hard disks. 33 | 34 | .PARAMETER esxihost 35 | 36 | The name or IP address of the ESXi host to use. Credentials will be prompted for if saved ones are not available 37 | 38 | .PARAMETER templateName 39 | 40 | The exact name or regular expression matching the template VM to use. Must only match one VM 41 | 42 | .PARAMETER dataStore 43 | 44 | The datastore to create the copied hard disks in 45 | 46 | .PARAMETER vmName 47 | 48 | The name of the VM to create. If creating more than one, it must contain %d which will be replaced by the number of the clone 49 | 50 | .PARAMETER snapshot 51 | 52 | The name of the snapshot to use when creating a linked clone. Specifying this automatically enables the linked clone disk feature 53 | 54 | .PARAMETER noGUI 55 | 56 | Do not show the user interface and go straight to cloning the VM using the supplied command line parameters 57 | 58 | .PARAMETER linkedClone 59 | 60 | Ticks the box in the GUI to enable linked clones 61 | 62 | .PARAMETER count 63 | 64 | The number of clones to create 65 | 66 | .PARAMETER startFrom 67 | 68 | The number to start the cloning naming from. 69 | 70 | .PARAMETER notes 71 | 72 | Notes to assign to the created VM(s) 73 | 74 | .PARAMETER protect 75 | 76 | Edit the parent disk(s) of linked clones to protect them such that they are not deleted when the linked clone is deleted 77 | 78 | .PARAMETER powerOn 79 | 80 | Power on the VM(s) once creation is complete 81 | 82 | .PARAMETER noConnect 83 | 84 | Do not automatically connect to ESXi 85 | 86 | .PARAMETER disconnect 87 | 88 | Disconnect from ESXi before exit 89 | 90 | .PARAMETER numCpus 91 | 92 | Override the number of CPUs defined in the template with the number specified here 93 | 94 | .PARAMETER numCores 95 | 96 | Override the number of cores per CPU defined in the template with the number specified here 97 | 98 | .PARAMETER MB 99 | 100 | Override the allocated memory defined in the template with the number specified here in MB 101 | 102 | .PARAMETER waitToExit 103 | 104 | Requires the key to be pressed before the script exits 105 | 106 | .PARAMETER maxVmdkDescriptorSize 107 | 108 | If the vmdk file exceeds this size then the script will not attempt to edt it because it is probably a binary file and not a text descriptor 109 | 110 | .PARAMETER configRegKey 111 | 112 | The registry key used to store the ESXi host and template name to save having to enter them every time 113 | 114 | .EXAMPLE 115 | 116 | & '.\ESXi cloner.ps1' 117 | 118 | Run the user interface which will require various fields to be completed and when "OK" is clicked, create VMs as per these fields 119 | 120 | .EXAMPLE 121 | 122 | & '.\ESXi cloner.ps1' -noGUI -templateName 'Server 2016 Sysprep' -dataStore 'Datastore1' -vmName 'GRL-2016-%d' -count 4 -notes "Guy's for product testing" -snapshot 'Linked Clone' -poweron 123 | 124 | Do not show a user interface and go straight to creating four VMs from the given template name in the datastore1 datastore, creating linked clone (delta) disks from the 125 | snapshot if the "Server 2016 Sysprep" VM called "Linked Clone". Power each one on once its creation is complete. 126 | 127 | .NOTES 128 | 129 | Credentials for ESXi can be stored by running "Connect-VIServer -Server -User -Password -SaveCredentials 130 | 131 | Username and password can also be specified via %esxiusername% and %esxipassword% environment variables respectively 132 | 133 | #> 134 | 135 | [CmdletBinding()] 136 | 137 | Param 138 | ( 139 | [string]$esxihost , 140 | [string]$templateName , 141 | [string]$dataStore , 142 | [string]$vmName , 143 | [string]$snapshot , 144 | [switch]$noGui , 145 | [int]$count = 1 , 146 | [int]$startFrom = 1 , 147 | [string]$notes , 148 | [switch]$powerOn , 149 | [switch]$noConnect , 150 | [switch]$disconnect , 151 | [switch]$linkedClone , 152 | [string]$username , 153 | [string]$password , 154 | [switch]$protect , 155 | ## Parameters to override what comes from template 156 | [int]$numCpus , 157 | [int]$numCores , 158 | [int]$MB , 159 | [switch]$waitToExit , 160 | ## it is advised not to use the following parameters 161 | [int]$maxVmdkDescriptorSize = 10KB , 162 | [string]$configRegKey = 'HKCU:\Software\Guy Leech\ESXi Cloner' 163 | ) 164 | 165 | ## Adding so we can make it app modal as well as system 166 | Add-Type @' 167 | using System; 168 | using System.Runtime.InteropServices; 169 | 170 | namespace PInvoke.Win32 171 | { 172 | public static class Windows 173 | { 174 | [DllImport("user32.dll")] 175 | public static extern int MessageBox(int hWnd, String text, String caption, uint type); 176 | } 177 | } 178 | '@ 179 | 180 | #region Functions 181 | Function Get-Templates 182 | { 183 | Param 184 | ( 185 | $GUIobject , 186 | $pattern 187 | ) 188 | $_.Handled = $true 189 | [hashtable]$params = @{} 190 | if( $pattern ) 191 | { 192 | $params.Add( 'Name' , "*$pattern*" ) 193 | } 194 | [string[]]$templates = @( Get-VM @params | Where-Object { $_.PowerState -eq 'PoweredOff' } | Sort Name | Select -ExpandProperty Name) 195 | if( ! $templates -or ! $templates.Count ) 196 | { 197 | $null = Display-MessageBox -window $GUIobject -text "Failed to get any powered off templates matching `"$pattern`"" -caption 'Unable to Clone' -buttons OK -icon Error 198 | return 199 | } 200 | ## Stuff in the template list 201 | $WPFcomboTemplate.Items.Clear() 202 | $WPFcomboTemplate.IsEnabled = $true 203 | $templates | ForEach-Object { $WPFcomboTemplate.items.add( $_ ) } 204 | } 205 | 206 | Function Connect-Hypervisor( $GUIobject , $servers , [bool]$pregui, [ref]$vServer , [string]$username , [string]$password ) 207 | { 208 | [hashtable]$connectParams = @{} 209 | 210 | if( ! [string]::IsNullOrEmpty( $username ) ) 211 | { 212 | $connectParams.Add( 'User' , $username ) 213 | } 214 | elseif( ! [string]::IsNullOrEmpty( $env:esxiusername ) ) 215 | { 216 | $connectParams.Add( 'User' , $env:esxiusername ) 217 | } 218 | if( ! [string]::IsNullOrEmpty( $password ) ) 219 | { 220 | $connectParams.Add( 'Password' , $password ) 221 | } 222 | elseif( ! [string]::IsNullOrEmpty( $env:esxipassword ) ) 223 | { 224 | $connectParams.Add( 'Password' , $env:esxipassword ) 225 | } 226 | 227 | $vServer.Value = Connect-VIServer -Server $servers -ErrorAction Continue @connectParams 228 | 229 | if( ! $vServer.Value ) 230 | { 231 | $null = Display-MessageBox -window $GUIobject -text "Failed to connect to $($servers -join ' , ')" -caption 'Unable to Connect' -buttons OK -icon Error 232 | } 233 | elseif( ! $pregui ) 234 | { 235 | $_.Handled = $true 236 | $WPFbtnFetch.IsEnabled = $true 237 | $WPFcomboDatastore.Items.Clear() 238 | $WPFcomboDatastore.IsEnabled = $true 239 | $WPFcomboTemplate.Items.Clear() 240 | Get-Datastore | Select -ExpandProperty Name | ForEach-Object { $WPFcomboDatastore.items.add( $_ ) } 241 | if( $WPFcomboDatastore.items.Count -eq 1 ) 242 | { 243 | $WPFcomboDatastore.SelectedIndex = 0 244 | } 245 | elseif( ! [string]::IsNullOrEmpty( $dataStore ) ) 246 | { 247 | $WPFcomboDatastore.SelectedValue = $dataStore 248 | } 249 | } 250 | } 251 | 252 | Function PopulateFrom-Template( $guiobject , $templateName ) 253 | { 254 | if( ! [string]::IsNullOrEmpty( $templateName ) ) 255 | { 256 | $vm = Get-VM -Name $templateName 257 | 258 | if( $vm ) 259 | { 260 | $WPFtxtCoresPerCpu.Text = $vm.CoresPerSocket 261 | $WPFtxtCPUs.Text = $vm.NumCpu 262 | $WPFtxtMemory.Text = $vm.MemoryMB 263 | ## select MB in the drop down unit comboMemoryUnits 264 | $wpfcomboMemoryUnits.SelectedItem = $WPFcomboMemoryUnits.Items.GetItemAt(0) 265 | $wpfcomboMemoryUnits.BringIntoView() 266 | if( $WPFchkLinkedClone.IsChecked ) 267 | { 268 | $WPFcomboSnapshot.Items.Clear() 269 | $WPFcomboSnapshot.IsEnabled = $true 270 | Get-Snapshot -VM $VM | Select -ExpandProperty Name | ForEach-Object { $WPFcomboSnapshot.items.add( $_ ) } 271 | } 272 | } 273 | else 274 | { 275 | $null = Display-MessageBox -window $guiobject -text "Failed to retrieve template `"$templateName`"" -caption 'Unable to Clone' -buttons OK -icon Error 276 | } 277 | } 278 | } 279 | 280 | Function Validate-Fields( $guiobject ) 281 | { 282 | $_.Handled = $true 283 | 284 | if( [string]::IsNullOrEmpty( $WPFtxtVMName.Text ) ) 285 | { 286 | $null = Display-MessageBox -window $guiobject -text 'No VM Name Specified' -caption 'Unable to Clone' -buttons OK -icon Error 287 | return $false 288 | } 289 | if( ! $WPFcomboTemplate.SelectedItem ) 290 | { 291 | $null = Display-MessageBox -window $guiobject -text 'No Template VM Selected' -caption 'Unable to Clone' -buttons OK -icon Error 292 | return $false 293 | } 294 | if( ! $WPFcomboDatastore.SelectedItem ) 295 | { 296 | $null = Display-MessageBox -window $guiobject -text 'No Datastore Selected' -caption 'Unable to Clone' -buttons OK -icon Error 297 | return $false 298 | } 299 | if( $WPFchkLinkedClone.IsChecked -and ! $WPFcomboSnapshot.SelectedItem ) 300 | { 301 | $null = Display-MessageBox -window $guiobject -text 'No Snapshot Selected for Linked Clone' -caption 'Unable to Clone' -buttons OK -icon Error 302 | return $false 303 | } 304 | $result = $null 305 | [int]$clonesStartFrom = -1 306 | if( ! [int]::TryParse( $WPFtxtCloneStart.Text , [ref]$clonesStartFrom ) -or $clonesStartFrom -lt 0 ) 307 | { 308 | $null = Display-MessageBox -window $guiobject -text 'Specified clone numbering start value is invalid' -caption 'Unable to Clone' -buttons OK -icon Error 309 | return $false 310 | } 311 | if( ! [int]::TryParse( $WPFtxtNumberClones.Text , [ref]$result ) -or ! $result ) 312 | { 313 | $null = Display-MessageBox -window $guiobject -text 'Specified number of clones is invalid' -caption 'Unable to Clone' -buttons OK -icon Error 314 | return $false 315 | } 316 | if( $result -gt 1 -and $WPFtxtVMName.Text -notmatch '%d' ) 317 | { 318 | $null = Display-MessageBox -window $guiobject -text 'Must specify %d replacement pattern in the VM name when creating more than one clone' -caption 'Unable to Clone' -buttons OK -icon Error 319 | return $false 320 | } 321 | 322 | if( $result -eq 1 -and $WPFtxtVMName.Text -match '%' ) 323 | { 324 | $null = Display-MessageBox -window $guiobject -text 'Illegal character ''%'' in VM name - did you mean to make more than one clone ?' -caption 'Unable to Clone' -buttons OK -icon Error 325 | return $false 326 | } 327 | if( $WPFtxtVMName.Text -match '[\$\*\\/]' ) 328 | { 329 | $null = Display-MessageBox -window $guiobject -text 'Illegal character(s) in VM name' -caption 'Unable to Clone' -buttons OK -icon Error 330 | return $false 331 | } 332 | if( ( $result -eq 1 -or $WPFtxtVMName.Text -notmatch '%d' ) -and ( Get-VM -Name $WPFtxtVMName.Text -ErrorAction SilentlyContinue | Where-Object { $_.Name -eq $WPFtxtVMName.Text } ) ) ## must check exact name match 333 | { 334 | $null = Display-MessageBox -window $guiobject -text "VM `"$($WPFtxtVMName.Text)`" already exists" -caption 'Unable to Clone' -buttons OK -icon Error 335 | return $false 336 | } 337 | if( $result -gt 1 ) 338 | { 339 | [string[]]$existing = $null 340 | $clonesStartFrom..($result + $clonesStartFrom - 1) | ForEach-Object ` 341 | { 342 | [string]$thisVMName = $WPFtxtVMName.Text -replace '%d' , $_ 343 | $existing += @( Get-VM -Name $thisVMName -ErrorAction SilentlyContinue | Where-Object { $_.Name -eq $thisVMName } | Select -ExpandProperty Name ) 344 | } 345 | if( $existing -and $existing.Count ) 346 | { 347 | $null = Display-MessageBox -window $guiobject -text "VMs `"$($existing -join '","')`" already exist" -caption 'Unable to Clone' -buttons OK -icon Error 348 | return $false 349 | } 350 | } 351 | if( ! [int]::TryParse( $WPFtxtMemory.Text , [ref]$result ) -or ! $result ) 352 | { 353 | $null = Display-MessageBox -window $guiobject -text 'Specified memory value is invalid' -caption 'Unable to Clone' -buttons OK -icon Error 354 | return $false 355 | } 356 | if( ! [int]::TryParse( $WPFtxtCPUs.Text , [ref]$result ) -or ! $result ) 357 | { 358 | $null = Display-MessageBox -window $guiobject -text 'Specified number of CPUs is invalid' -caption 'Unable to Clone' -buttons OK -icon Error 359 | return $false 360 | } 361 | if( ! [int]::TryParse( $WPFtxtCoresPerCpu.Text , [ref]$result ) -or ! $result ) 362 | { 363 | $null = Display-MessageBox -window $guiobject -text 'Specified number of cores is invalid' -caption 'Unable to Clone' -buttons OK -icon Error 364 | return $false 365 | } 366 | return $true 367 | } 368 | 369 | Function Display-MessageBox( $window , $text , $caption , [System.Windows.MessageBoxButton]$buttons , [System.Windows.MessageBoxImage]$icon ) 370 | { 371 | if( $window -and $window.PSObject.Properties[ 'handle' ] -and $window.Handle ) 372 | { 373 | [int]$modified = switch( $buttons ) 374 | { 375 | 'OK' { [System.Windows.MessageBoxButton]::OK } 376 | 'OKCancel' { [System.Windows.MessageBoxButton]::OKCancel } 377 | 'YesNo' { [System.Windows.MessageBoxButton]::YesNo } 378 | 'YesNoCancel' { [System.Windows.MessageBoxButton]::YesNo } 379 | } 380 | [int]$choice = [PInvoke.Win32.Windows]::MessageBox( $Window.handle , $text , $caption , ( ( $icon -as [int] ) -bor $modified ) ) ## makes it app modal so UI blocks 381 | switch( $choice ) 382 | { 383 | ([MessageBoxReturns]::IDYES -as [int]) { 'Yes' } 384 | ([MessageBoxReturns]::IDNO -as [int]) { 'No' } 385 | ([MessageBoxReturns]::IDOK -as [int]) { 'Ok' } 386 | ([MessageBoxReturns]::IDABORT -as [int]) { 'Abort' } 387 | ([MessageBoxReturns]::IDCANCEL -as [int]) { 'Cancel' } 388 | ([MessageBoxReturns]::IDCONTINUE -as [int]) { 'Continue' } 389 | ([MessageBoxReturns]::IDIGNORE -as [int]) { 'Ignore' } 390 | ([MessageBoxReturns]::IDRETRY -as [int]) { 'Retry' } 391 | ([MessageBoxReturns]::IDTRYAGAIN -as [int]) { 'TryAgain' } 392 | } 393 | } 394 | else 395 | { 396 | [Windows.MessageBox]::Show( $text , $caption , $buttons , $icon ) 397 | } 398 | } 399 | 400 | ## Based on code from http://www.lucd.info/2010/03/31/uml-diagram-your-vm-vdisks-and-snapshots/ 401 | Function Get-SnapshotDisk{ 402 | param($vmName , $snapshotname) 403 | 404 | Function Get-SnapHash{ 405 | param($parent) 406 | Process { 407 | $snapHash[$_.Snapshot.Value] = @($_.Name,$parent) 408 | if($_.ChildSnapshotList){ 409 | $newparent = $_ 410 | $_.ChildSnapShotList | Get-SnapHash $newparent 411 | } 412 | } 413 | } 414 | 415 | $snapHash = @{} 416 | Get-View -ViewType VirtualMachine -Filter @{'Name' = $vmName} | ForEach-Object { 417 | $vm = $_ 418 | if($vm.Snapshot){ 419 | $vm.Snapshot.RootSnapshotList | Get-SnapHash $vm 420 | } 421 | else{ 422 | return 423 | } 424 | $firstHD = $true 425 | $_.Config.Hardware.Device | Where-Object {$_.DeviceInfo.Label -match '^Hard disk' -and $_.Backing.DiskMode -notmatch 'independent' } | ForEach-Object { 426 | $hd = $_ 427 | $hdNr = $hd.DeviceInfo.Label.Split('\s')[-1] 428 | $exDisk = $vm.LayoutEx.Disk | Where-Object {$_.Key -eq $hd.Key} 429 | $diskFiles = @() 430 | $exDisk.Chain | ForEach-Object {$_.FileKey} | ForEach-Object { $diskFiles += $_ } 431 | 432 | $snapHash.GetEnumerator() | ForEach-Object { 433 | $key = $_.Key 434 | $value = $_.Value 435 | if( [string]::IsNullOrEmpty( $snapshotname ) -or $snapshotname -eq $value[0] ) { 436 | $vm.LayoutEx.Snapshot | Where-Object {$_.Key.Value -eq $key} | ForEach-Object { 437 | $vmsnId = $_.DataKey 438 | $_.Disk | Where-Object{$_.Key -eq $hd.Key} | ForEach-Object { 439 | if($diskFiles -notcontains $_.Chain[-1].FileKey[0] -and $diskFiles -notcontains $_.Chain[-1].FileKey[1]){ 440 | $chain = $_.Chain[-1] 441 | } 442 | else{ 443 | $preSnapFiles = $_.Chain | ForEach-Object {$_.FileKey} | ForEach-Object {$_} 444 | $vm.layoutEx.Disk | Where-Object {$_.Key -eq $hd.Key} | ForEach-Object { 445 | foreach($chain in $_.Chain){ 446 | if($preSnapFiles -notcontains $chain.FileKey[0] -and $preSnapFiles -notcontains $chain.FileKey[1]){ 447 | break 448 | } 449 | } 450 | } 451 | } 452 | $chain.FileKey | ForEach-Object { 453 | if( ! $vm.LayoutEx.File[$_].Size ){ 454 | $vm.LayoutEx.File[$_].Name 455 | } 456 | } 457 | } 458 | } 459 | } 460 | } 461 | $firstHD = $false 462 | } 463 | } 464 | } 465 | 466 | Function Add-NewDisk 467 | { 468 | [CmdletBinding()] 469 | 470 | Param 471 | ( 472 | $VM , 473 | $sourceDisk , 474 | $newDisk 475 | ) 476 | 477 | $sourceController = $sourceDisk | Get-ScsiController 478 | if( ! $sourceController ) 479 | { 480 | Write-Warning "Source disk $($sourceDisk.Name) was not attached to a SCSI controller so creating new one" 481 | [hashtable]$diskParams = @{ 482 | 'VM' = $VM 483 | 'DiskPath' = $newDisk 484 | 'ErrorAction' = 'Continue' 485 | } 486 | New-HardDisk @diskParams | New-ScsiController -Type Default -BusSharingMode NoSharing 487 | } 488 | else 489 | { 490 | $dsName = $newDisk.Split(']')[0].TrimStart('[') 491 | $ds = Get-Datastore -Name $dsName 492 | $spec = New-Object VMware.Vim.VirtualMachineConfigSpec 493 | $spec.deviceChange = @() 494 | $spec.deviceChange += New-Object VMware.Vim.VirtualDeviceConfigSpec 495 | $spec.deviceChange[0].device = New-Object VMware.Vim.VirtualDisk 496 | $spec.deviceChange[0].device.backing = New-Object VMware.Vim.VirtualDiskFlatVer2BackingInfo 497 | $spec.deviceChange[0].device.backing.datastore = $ds.ExtensionData.MoRef 498 | $spec.deviceChange[0].device.backing.fileName = $newDisk 499 | $spec.deviceChange[0].device.backing.diskMode = switch( $sourceDisk.Persistence ) 500 | { 501 | 'IndependentNonPersistent' { 'independent_nonpersistent' } 502 | 'IndependentPersistent' { 'independent_persistent' } 503 | default { $_ } 504 | } 505 | $spec.deviceChange[0].device.unitnumber = -1 506 | $spec.deviceChange[0].device.controllerKey = $scsiControllers[ $sourceController.ExtensionData.Key ] 507 | $spec.deviceChange[0].operation = 'add' 508 | $VM.ExtensionData.ReconfigVM($spec) 509 | 510 | Get-HardDisk -VM $VM | Where-Object { $_.Filename -eq $newDisk } 511 | } 512 | } 513 | 514 | Function New-ClonedDisk 515 | { 516 | [CmdletBinding()] 517 | Param 518 | ( 519 | $cloneVM , 520 | $sourceDisk , 521 | [string]$destinationDatastore , 522 | [string]$parent , 523 | [int]$diskCount 524 | ) 525 | [string]$baseDiskName = [io.path]::GetFileNameWithoutExtension( ( Split-Path $sourceDisk.FileName -Leaf ) ) 526 | Write-Verbose "Cloning `"$baseDiskName`" , format $($sourceDisk.StorageFormat) to [$destinationDatastore] $($cloneVM.Name)" 527 | [hashtable]$copyParams = @{} 528 | if( $sourceDisk.PSObject.properties[ 'DestinationStorageFormat' ] ) 529 | { 530 | $copyParams.Add( 'DestinationStorageFormat' , $sourceDisk.StorageFormat ) 531 | } 532 | $clonedDisk = $sourceDisk | Copy-HardDisk -DestinationPath "[$destinationDatastore] $($cloneVM.Name)" @copyParams 533 | if( $clonedDisk ) 534 | { 535 | [string]$diskToAdd = $null 536 | [string]$diskProviderPath = $null 537 | [string]$qualifier = $null 538 | ## Now rename the disk files for this disk 539 | Get-ChildItem -Path (Join-Path $destinationFolder ($baseDiskName + '*.vmdk')) | ForEach-Object ` 540 | { 541 | [string]$restOfName = $_.Name -replace "^$baseDiskName(.*)\.vmdk$" , '$1' 542 | [string]$newDiskName = $cloneVM.Name 543 | if( $sourceDisks.Count -gt 1 ) 544 | { 545 | $qualifier = ".disk$diskCount" 546 | $newDiskName += $qualifier 547 | } 548 | 549 | $newDiskName += "$restOfName.vmdk" 550 | Rename-Item -Path $_.FullName -NewName $newDiskName -ErrorAction Stop ## PassThru doesn't work 551 | if( ! $diskToAdd -and ! $restOfName ) 552 | { 553 | $diskToAdd = "{0}/{1}" -f $_.FolderPath , $newDiskName ## can't use Join-Path as that uses a backslash 554 | $diskProviderPath = Join-Path -Path (Split-Path $_.FullName -Parent) -ChildPath $newDiskName 555 | } 556 | } 557 | if( $diskToAdd -and $diskProviderPath ) 558 | { 559 | ## Now we have to edit the file to point its extents at the renamed file. Can't edit in situ so have to copy to local file system, change and copy back 560 | [int]$fileLength = Get-ChildItem -Path $diskProviderPath | Select -ExpandProperty Length 561 | if( $fileLength -lt $maxVmdkDescriptorSize ) 562 | { 563 | [string]$tempDisk = Join-Path $env:temp ( $baseDiskName + '.' + $pid + '.vmdk' ) 564 | if( Test-Path -Path $tempDisk -ErrorAction SilentlyContinue ) 565 | { 566 | Remove-Item -Path $tempDisk -Force -ErrorAction Stop 567 | } 568 | Copy-DatastoreItem -Item $diskProviderPath -Destination $tempDisk 569 | $existingContent = Get-Content -Path $tempDisk 570 | $newContent = $existingContent | ForEach-Object ` 571 | { 572 | if( $_ -match "^(.*) `"$baseDiskName(.*)\.vmdk\`"$" ) 573 | { 574 | "$($matches[1]) `"$($cloneVM.Name)$qualifier$($matches[2]).vmdk`"" 575 | } 576 | else 577 | { 578 | $_ 579 | } 580 | } 581 | if( $existingContent -eq $newContent ) 582 | { 583 | Write-Warning "Unexpectedly, no changes were made to renamed disk $diskToAdd" 584 | } 585 | ## Need to output without CR/LF that Windows normally does with text files. It's ASCII not UTF8 despite what the "encoding" says in the file 586 | $streamWriter = New-Object System.IO.StreamWriter( $tempDisk , $false , [System.Text.Encoding]::ASCII ) 587 | $newcontent | ForEach-Object { $streamwriter.Write( ( $_ + "`n" ) ) } 588 | $streamWriter.Close() 589 | 590 | Copy-DatastoreItem -Destination $diskProviderPath -Item $tempDisk -Force -ErrorAction Continue 591 | Remove-Item -Path $tempDisk -Force 592 | $result = Add-NewDisk -VM $cloneVM -sourceDisk $sourceDisk -newDisk $diskToAdd 593 | if( ! $result ) 594 | { 595 | Throw "Failed to add cloned disk `"$diskToAdd`" to cloned VM" 596 | } 597 | } 598 | else 599 | { 600 | Write-Error "`"$diskProviderPath`" is large ($($fileLength/1KB)KB) which means it probably isn't a text descriptor file that can be changed" 601 | } 602 | } 603 | else 604 | { 605 | Write-Error "No disk path to add to new VM for disk $baseDiskName" 606 | } 607 | } 608 | else 609 | { 610 | Throw "Failed to clone disk $($sourceDisk.FileName)" 611 | } 612 | } 613 | #endregion Functions 614 | 615 | Remove-Module -Name Hyper-V -ErrorAction SilentlyContinue ## lest it clashes as there is some overlap in cmdlet names 616 | [string]$oldVerbosity = $VerbosePreference 617 | $VerbosePreference = 'SilentlyContinue' 618 | Import-Module -Name VMware.PowerCLI -ErrorAction Stop 619 | $VerbosePreference = $oldVerbosity 620 | 621 | if( Test-Path -Path $configRegKey -PathType Container -ErrorAction SilentlyContinue ) 622 | { 623 | if( [string]::IsNullOrEmpty( $esxihost ) ) 624 | { 625 | $esxihost = Get-ItemProperty -Path $configRegKey -Name 'ESXi Host' -ErrorAction SilentlyContinue | select -ExpandProperty 'ESXi Host' 626 | } 627 | if( [string]::IsNullOrEmpty( $templateName ) ) 628 | { 629 | $templateName = Get-ItemProperty -Path $configRegKey -Name 'Template Pattern' -ErrorAction SilentlyContinue | select -ExpandProperty 'Template Pattern' 630 | } 631 | } 632 | 633 | $vServer = $null 634 | 635 | if( ! $noConnect -and ! [string]::IsNullOrEmpty( $esxihost ) ) 636 | { 637 | Connect-Hypervisor -GUIobject $null -servers $esxihost -pregui $true -vServer ([ref]$vServer) -username $username -password $password 638 | } 639 | 640 | #region XAML&Modules 641 | 642 | [string]$mainwindowXAML = @' 643 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 |