├── README.md ├── .github └── FUNDING.yml ├── LICENSE ├── Power state change running VMs.ps1 ├── Set VMware guest info.ps1 ├── Get VMware or Hyper-V powered on vm details.ps1 ├── Change NIC PCI slot number.ps1 ├── Run script in VM.ps1 └── ESXi cloner.ps1 /README.md: -------------------------------------------------------------------------------- 1 | # VMware -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Guy Leech 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Power state change running VMs.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | Pause or shutdown running VMs - designed to be run by UPS shutdown software 3 | 4 | @guyrleech 2019 5 | 6 | Modification History: 7 | 8 | 09/09/19 GRL Added manual input of password when encrypting it for later use 9 | #> 10 | 11 | <# 12 | .SYNOPSIS 13 | 14 | Pause or shutdown running VMs and the ESXi host - designed to be run by UPS shutdown software 15 | 16 | .PARAMETER viServer 17 | 18 | The ESXi server to connect to 19 | 20 | .PARAMETER username 21 | 22 | The username to use when connecting to the ESXi server 23 | 24 | .PARAMETER password 25 | 26 | The clear text password for the username specified. If none is specified and there are no saved credentials or pass thru then the contents of the environment varinble _Mval12 are used as the password if set 27 | 28 | .PARAMETER securePassword 29 | 30 | A password previously encrypted using the -encryptPassword argument 31 | 32 | .PARAMETER encryptPassword 33 | 34 | Encrypt the password specified via -password, in the environment variable _Mval12 or prompt if neither is present 35 | 36 | .PARAMETER vm 37 | 38 | The name or pattern to match for the VMs to operate on 39 | 40 | .PARAMETER selfName 41 | 42 | If the name of the VM running the script is not the same as the NetBIOS name, specify it with this argument as long as -notSelf is not specified or -vm does not match it since it must be shutdown/paused last 43 | 44 | .PARAMETER logFile 45 | 46 | The name and path of a log file to append to 47 | 48 | .PARAMETER hostShutdown 49 | 50 | Shut down the host specified via -viServer when power operations are complete 51 | 52 | .PARAMETER notSelf 53 | 54 | Do not shutdown/pause the VM running this script. If the name of the VM is not its NetBIOS name use -selfName to specify the VM name 55 | 56 | .PARAMETER shutdown 57 | 58 | Shutdown the VMs instead of pausing them. Note that if there are outstanding Windows Updates to install, shutdown can take a long time 59 | 60 | .EXAMPLE 61 | 62 | & '.\Power state change running VMs.ps1" -Verbose -viServer esxi01 -logFile c:\scripts\vm.shutdown.log -notSelf -hostShutdown 63 | 64 | Pause all VMs running on esxi01, except for the VM running this script and then shutdown the host esxi01. A log file will be written to c:\scripts\vm.shutdown.log 65 | 66 | .EXAMPLE 67 | 68 | & '.\Power state change running VMs.ps1" -encryptPassword 69 | 70 | Prompt for a password, encrypt it and output it such that it can be passed in another script invocation via the -securePassword parameter 71 | 72 | .NOTES 73 | 74 | Save credentials first with New-VICredentialStoreItem, for the account that will run the script, if pass thru won't work and not passing username and password 75 | 76 | If -notSelf is used with -hostShutdown, an automatic power action should be configured for that VM. 77 | 78 | #> 79 | 80 | [CmdletBinding()] 81 | 82 | Param 83 | ( 84 | [string]$viServer , 85 | [string]$username , 86 | [string]$password , 87 | [string]$securePassword , 88 | [switch]$encryptPassword , 89 | [string]$vm = '*' , 90 | [string]$selfName , 91 | [string]$logFile , 92 | [switch]$hostShutdown , 93 | [switch]$notSelf , 94 | [switch]$shutdown 95 | ) 96 | 97 | if( $encryptPassword ) 98 | { 99 | if( ! $PSBoundParameters[ 'password' ] -and ! ( $password = $env:_Mval12 ) ) 100 | { 101 | $enteredPassword = Read-Host -Prompt "Enter password to encrypt" -AsSecureString 102 | if( $enteredPassword -and $enteredPassword.Length ) 103 | { 104 | $enteredPassword | ConvertFrom-SecureString 105 | } 106 | else 107 | { 108 | Throw 'No password entered' 109 | } 110 | } 111 | else 112 | { 113 | ConvertTo-SecureString -AsPlainText -String $password -Force | ConvertFrom-SecureString 114 | } 115 | Exit 0 116 | } 117 | 118 | if( $PSBoundParameters[ 'LogFile' ] ) 119 | { 120 | Start-Transcript -Path $logFile -Append 121 | } 122 | 123 | Try 124 | { 125 | $oldVerbosePreference = $VerbosePreference 126 | $VerbosePreference = 'SilentlyContinue' 127 | Import-Module -Name VMware.PowerCLI -Verbose:$false 128 | $VerbosePreference = $oldVerbosePreference 129 | 130 | [hashtable]$connectParams = @{} 131 | if( $PSBoundParameters[ 'viServer' ] ) 132 | { 133 | $connectParams.Add( 'Server' , $viServer ) 134 | } 135 | if( $PSBoundParameters[ 'username' ] ) 136 | { 137 | $connectParams.Add( 'user' , $username ) 138 | 139 | if( $PSBoundParameters[ 'password' ] -or ( $password = $env:_Mval12 ) ) 140 | { 141 | $connectParams.Add( 'password' , $password ) 142 | } 143 | elseif( $PSBoundParameters[ 'securePassword' ] ) 144 | { 145 | $credential = New-Object -Typename System.Management.Automation.PSCredential -Argumentlist $username , (ConvertTo-SecureString -String $securePassword) 146 | $connectParams.Add( 'Credential' , $credential ) 147 | } 148 | } 149 | 150 | $connection = Connect-VIServer @connectParams -Force -ErrorAction Continue 151 | $securePassword = $password = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' 152 | if( ! [string]::IsNullOrEmpty( $env:_Mval12 ) ) 153 | { 154 | $env:_Mval12 = $password 155 | } 156 | 157 | if( ! $connection ) 158 | { 159 | Throw "Unable to connect to $viServer" 160 | } 161 | 162 | [int]$counter = 0 163 | $self = $null 164 | Get-VM -Name $vm | Where-Object { $_.PowerState -eq 'PoweredOn' } | ForEach-Object ` 165 | { 166 | $thisVM = $_ 167 | $counter++ 168 | Write-Verbose -Message "$(Get-Date -Format G) : $counter : $($thisVM.Name)" 169 | if( ( $PSBoundParameters[ 'selfName' ] -and $selfName -eq $thisVM.name ) -or $thisVM.name -eq $env:COMPUTERNAME ) 170 | { 171 | $self = $thisVM 172 | Write-Verbose -Message "Got self $($thisVM.Name) so deferring until the end" 173 | } 174 | else 175 | { 176 | if( $shutdown ) 177 | { 178 | Shutdown-VMGuest -VM $thisVM -Confirm:$false 179 | } 180 | else 181 | { 182 | Suspend-VM -VM $thisVM -RunAsync -Confirm:$false 183 | } 184 | } 185 | } 186 | 187 | if( ! $notSelf ) 188 | { 189 | if( $self ) 190 | { 191 | Write-Verbose -Message "$(Get-Date -Format G): Performing power action on self" 192 | 193 | if( $shutdown ) 194 | { 195 | Shutdown-VMGuest -VM $self -Confirm:$false 196 | } 197 | else 198 | { 199 | Suspend-VM -VM $self -RunAsync -Confirm:$false 200 | } 201 | } 202 | else 203 | { 204 | Write-Warning -Message "Did not find self $env:COMPUTERNAME" 205 | } 206 | } 207 | 208 | if( $hostShutdown ) 209 | { 210 | Write-Verbose -Message "$(Get-Date -Format G): Shutting down host $viServer" 211 | Stop-VMHost -VMHost $viServer -Force -RunAsync -Confirm:$false 212 | } 213 | } 214 | Catch 215 | { 216 | Throw $_ 217 | } 218 | Finally 219 | { 220 | if( $connection ) 221 | { 222 | Disconnect-VIServer -Server $connection -Force -Confirm:$false 223 | } 224 | 225 | if( $PSBoundParameters[ 'LogFile' ] ) 226 | { 227 | Stop-Transcript 228 | } 229 | } -------------------------------------------------------------------------------- /Set VMware guest info.ps1: -------------------------------------------------------------------------------- 1 | 2 | <# 3 | .SYNOPSIS 4 | 5 | Set VM guest information so it can be retrieved in VMs 6 | 7 | .DESCRIPTION 8 | 9 | The VMware properties to set are those returned from the Get-VM cmdlet 10 | 11 | .PARAMETER server 12 | 13 | The vCenter server(s) to connect to 14 | 15 | .PARAMETER properties 16 | 17 | Comma separated list of properties to set where the property name to set is before the delimiter, default is =, and the VMware property to use is after the delimiter 18 | 19 | .PARAMETER VMs 20 | 21 | Specific VM name or pattern to operate on. If not specified will operate on all powered on VMs. 22 | 23 | .PARAMETER credential 24 | 25 | Credentials to use to connect to the vCenter(s) 26 | 27 | .PARAMETER port 28 | 29 | The port to connect to on the vCenter(s) 30 | 31 | .PARAMETER protocol 32 | 33 | The protocol to use with the specified vCenter(s) 34 | 35 | .PARAMETER allLinked 36 | 37 | Connect to all vCenters linked to the specified vCenter(s) 38 | 39 | .PARAMETER guestinfo 40 | 41 | The prefix to use for the property name in the VM 42 | 43 | .PARAMETER delimiter 44 | 45 | The delimiter to specify the property name from the VMware property name 46 | 47 | .PARAMETER literal 48 | 49 | The string prefix which denotes that a VMware property name is a string literal rather than a VMware property name 50 | 51 | .PARAMETER powerState 52 | 53 | A regular expression that matches the power state of VMs to operate on 54 | 55 | .PARAMETER remove 56 | 57 | Remove the specified properties rather than set them 58 | 59 | .EXAMPLE 60 | 61 | & '.\Set VMware guest info.ps1' -server grl-vcenter04 -VMs GRL-W10* -properties host.name=VMhost,host.version=VMhost.Version,host.build=VMhost.Build,host.parent=VMhost.parent,owner='*Guy Leech' 62 | 63 | Set the specified properties for all VMs matching the pattern GRL-W10* managed by the vCenter server grl-vcenter04 64 | 65 | .NOTES 66 | 67 | Retrieve the properties set via this script in a Windows VM via vmtoolsd.exe https://www.virtuallyghetto.com/2011/01/how-to-extract-host-information-from.html 68 | 69 | @guyrleech 70 | #> 71 | 72 | [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='High')] 73 | 74 | Param 75 | ( 76 | [Parameter(Mandatory,HelpMessage='VMware vCenter to use')] 77 | [string[]]$server , 78 | [Parameter(Mandatory,HelpMessage='Properties to set name=property')] 79 | [string[]]$properties , 80 | [string]$VMs , 81 | [System.Management.Automation.PSCredential]$credential , 82 | [int]$port , 83 | [ValidateSet( 'http' , 'https' )] 84 | [string]$protocol , 85 | [switch]$allLinked , 86 | [string]$guestinfo = 'guestinfo' , 87 | [string]$delimiter = '=' , 88 | [string]$literal = '*' , 89 | [string]$powerState = 'PoweredOn' , 90 | [switch]$remove 91 | ) 92 | 93 | Function Get-VMProperty 94 | { 95 | Param 96 | ( 97 | [Parameter(Mandatory)] 98 | $VM , 99 | [Parameter(Mandatory)] 100 | [string]$Property 101 | ) 102 | 103 | ## we have dots in the property name which are taken literally so we have to get to the property 1 property at a time 104 | [string[]]$individualProperties = $Property -split '\.' 105 | 106 | if( ! $individualProperties -or $individualProperties.Count -le 1 ) 107 | { 108 | $VM.$Property.ToString() ## top level property 109 | } 110 | else 111 | { 112 | $parent = $VM 113 | ForEach( $individualProperty in $individualProperties ) 114 | { 115 | if( $parent -and $parent.PSObject.Properties[ $individualProperty ] ) 116 | { 117 | $parent = $parent.$individualProperty 118 | } 119 | else 120 | { 121 | $parent = $null 122 | } 123 | } 124 | if( $parent ) 125 | { 126 | $parent.ToString() 127 | } 128 | else 129 | { 130 | Write-Warning -Message "Failed to get property $property for VM $($VM.Name)" 131 | } 132 | } 133 | } 134 | 135 | $connection = $null 136 | 137 | Import-Module -Name VMware.VimAutomation.Core -Verbose:$false 138 | 139 | [hashtable]$connectionParameters = @{ 'Server' = $server ; 'AllLinked' = $allLinked } 140 | 141 | if( $PSBoundParameters[ 'credential' ] ) 142 | { 143 | $connectionParameters.Add( 'Credential' , $credential ) 144 | } 145 | 146 | if( $PSBoundParameters[ 'port' ] ) 147 | { 148 | $connectionParameters.Add( 'port' , $port ) 149 | } 150 | 151 | if( $PSBoundParameters[ 'protocol' ] ) 152 | { 153 | $connectionParameters.Add( 'protocol' , $protocol ) 154 | } 155 | 156 | if( ! ( $connection = Connect-VIServer @connectionParameters ) ) 157 | { 158 | Throw "Failed to connect to $server" 159 | } 160 | 161 | try 162 | { 163 | ## either get all powered on VMs or jsut a specific set 164 | [hashtable]$getvmParameters = @{ } 165 | if( $PSBoundParameters[ 'VMs' ] ) 166 | { 167 | $getvmParameters.Add( 'Name' , $VMs ) 168 | } 169 | 170 | [array]$virtualMachines = @( Get-VM @getvmParameters | Where-Object { $_.PowerState -match $powerState } ) 171 | 172 | if( ! $virtualMachines -or ! $virtualMachines.Count ) 173 | { 174 | Throw "No virtual machines retrieved" 175 | } 176 | 177 | Write-Verbose -Message "Retrieved $($virtualMachines.Count) powered on virtual machines" 178 | 179 | [int]$counter = 0 180 | 181 | ForEach( $virtualMachine in $virtualMachines ) 182 | { 183 | $counter++ 184 | Write-Verbose -Message "$counter / $($virtualMachines.Count) $($virtualMachine.Name)" 185 | if( $PSCmdlet.ShouldProcess( "$($properties.Count) properties in VM $($virtualMachine.Name)" , 'Set' ) ) 186 | { 187 | [int]$propertyCounter = 0 188 | ForEach( $property in $properties ) 189 | { 190 | $propertyCounter++ 191 | [string]$advancedSettingName,[string]$vmPropertyName = $property -split $delimiter , 2 192 | ## don't check strings as empty/null vmProperty can be set 193 | if( $remove ) 194 | { 195 | if( $advancedSetting = Get-AdvancedSetting -Entity $virtualMachine -Name $advancedSettingName ) 196 | { 197 | Remove-AdvancedSetting -AdvancedSetting $advancedSetting 198 | } 199 | } 200 | else 201 | { 202 | $vmPropertyValue = $null 203 | if( ! [string]::IsNullOrEmpty( $vmPropertyName ) ) 204 | { 205 | if( ! [string]::IsNullOrEmpty( $literal ) -and $vmPropertyName.StartsWith( $literal ) ) 206 | { 207 | $vmPropertyValue = $vmPropertyName.SubString( $literal.Length ) 208 | } 209 | else 210 | { 211 | $vmPropertyValue = Get-VMProperty -VM $virtualMachine -Property $vmPropertyName 212 | } 213 | } 214 | if( $null -ne $vmPropertyValue ) 215 | { 216 | [string]$fullPropertyName = "$guestinfo.$advancedSettingName" 217 | Write-Verbose -Message "Setting property $propertyCounter / $($properties.Count) $fullPropertyName to `"$vmPropertyValue`" in VM $($virtualMachine.Name)" 218 | if( ! ( $setting = New-AdvancedSetting -Name $fullPropertyName -Value $vmPropertyValue -Entity $virtualMachine -Confirm:$false -Force ) ) 219 | { 220 | Write-Warning -Message "Failed to set property $fullPropertyName to `"$vmPropertyValue`" in VM $($virtualMachine.Name)" 221 | } 222 | } 223 | } 224 | } 225 | } 226 | } 227 | } 228 | catch 229 | { 230 | Throw $_ 231 | } 232 | finally 233 | { 234 | $connection | Disconnect-VIServer -Confirm:$false 235 | $connection = $null 236 | } 237 | -------------------------------------------------------------------------------- /Get VMware or Hyper-V powered on vm details.ps1: -------------------------------------------------------------------------------- 1 | #requires -version 3 2 | <# 3 | Show VMware ESXi/vSphere or Hyper-V VM details in a grid view, standard output or text file that Sysinternals BGinfo can use as a custom field 4 | 5 | @guyrleech 2018 6 | #> 7 | 8 | <# 9 | .SYNOPSIS 10 | 11 | Show VMware ESXi/vSphere or Hyper-V VM details in a grid view, standard output or text file that Sysinternals BGinfo can use as a custom field 12 | 13 | .DESCRIPTION 14 | 15 | BGinfo allows custom fields but only via WMI queries, VBS scripts or text file contents so this script can create a text file with VM details in 16 | which can then be used in a custom field in a BGInfo .bgi file 17 | 18 | .PARAMETER viServers 19 | 20 | A comma separated list of ESXi servers to connect to. If not specified then Hyper-V is assumed as the hypervisor 21 | 22 | .PARAMETER hypervServer 23 | 24 | The name of a Hyper-V server to connect to. Cannot be used with -viServers. Will use the local computer if not specified 25 | 26 | .PARAMETER vmName 27 | 28 | The name or pattern of a virtual machine to include. If not specified then all VMs found will be included. 29 | 30 | .PARAMETER wait 31 | 32 | Wait for keyboard input when the script has finished. Useful if the script is being run from a shortcut so the output doesn't disappear until the enter key is pressed. 33 | 34 | .PARAMETER gridView 35 | 36 | Display the results in an on screen filterable and sortable grid view. Any rows selected when OK is clicked are put in the clipboard. If not specified output goes to standard output 37 | 38 | .PARAMETER ipv4only 39 | 40 | Do not include IPv6 addresses 41 | 42 | .PARAMETER noHeaders 43 | 44 | Do not output any headers 45 | 46 | .PARAMETER bginfoFile 47 | 48 | A non-existent text file, unless -overwrite is specified, which will have the VM details written to for use as a custom field in SysInternals BGinfo 49 | 50 | .PARAMETER overWrite 51 | 52 | Will overwrite an existing file when the -bginfoFile option is used 53 | 54 | .PARAMETER bgInfoFields 55 | 56 | The VM fields which will be placed into the file specified -bginfofile 57 | 58 | .PARAMETER fields 59 | 60 | The VM fields to display or output 61 | 62 | .PARAMETER tabStop 63 | 64 | The tab stop size used by BGinfo. Do not change unless output is misaligned 65 | 66 | .EXAMPLE 67 | 68 | '.\Get VMware or Hyper-V powered on vm details.ps1' -bginfoFile C:\temp\vmaddresses.txt -overwrite -viservers 192.168.0.69 -ipv4only -vmName 'GRL*' 69 | 70 | Retrieve details for powered on VMware VMs called GRL* from the ESXi/vCenter server at 192.168.0.69 and write the VM details to the file C:\temp\vmaddresses.txt which can then be used in a custom rule in BGinfo. 71 | 72 | .EXAMPLE 73 | 74 | '.\Get VMware or Hyper-V powered on vm details.ps1' -gridview 75 | 76 | Retrieve details for all powered on VMs from the local Hyper-V server and output the results to an on screen gridview 77 | 78 | .EXAMPLE 79 | 80 | '.\Get VMware or Hyper-V powered on vm details.ps1' -hypervServer GRL-HYPERV07 81 | 82 | Retrieve details for all powered on VMs from the Hyper-V server GRL-HYPERV07 and output the results to standard output which can then be piped into other scripts/cmdlets or a file 83 | 84 | .NOTES 85 | 86 | To apply a BGinfo .bgi file automatically, run the fulling where the .bgi file already has the custom field in where the output file from -bginfofile is specified: 87 | 88 | Bginfo.exe' 'c:\temp\your-custom-background.bgi' /timer:0 /nolicprompt 89 | 90 | Run at logon and/or as a periodically scheduled scheduled task 91 | 92 | Requires VMware PowerCLI if querying VMware machines or the Microsoft Hyper-V module if querying Hyper-V VMs 93 | 94 | BGInfo is available at https://docs.microsoft.com/en-us/sysinternals/downloads/bginfo 95 | 96 | #> 97 | 98 | [CmdletBinding()] 99 | 100 | Param 101 | ( 102 | [Parameter(Mandatory=$true, ParameterSetName = "VMware")] 103 | [string[]]$viservers , 104 | [Parameter(Mandatory=$false, ParameterSetName = "HyperV")] 105 | [string]$hypervServer , 106 | [string]$vmName , 107 | [switch]$wait , 108 | [switch]$gridView , 109 | [switch]$ipv4only , 110 | [switch]$noHeaders , 111 | [string]$bginfoFile , 112 | [switch]$overwrite , 113 | [string[]]$bginfoFields = @( 'Name' , 'IPAddresses' ) , 114 | [string[]]$fields = @( 'Name' , 'NumCPU' , 'CoresPerSocket' , 'MemoryMB' , 'IPAddresses' , 'UsedSpaceGB' , 'ProvisionedSpaceGB' ) , 115 | [int]$tabStop = 6 ## for bginfo 116 | ) 117 | 118 | if( $PSBoundParameters[ 'bginfoFile' ] -and $gridView ) 119 | { 120 | Throw '-bginfoFile and -gridView cannot be used together' 121 | } 122 | 123 | Function Get-VHDSize 124 | { 125 | [CmdletBinding()] 126 | 127 | Param 128 | ( 129 | $vhd 130 | ) 131 | 132 | [long]$usedSpaceSoFar = 0 133 | if( $vhd ) 134 | { 135 | $disk = Get-VHD -Path $vhd.Path 136 | $usedSpaceSoFar = $disk.FileSize 137 | if( $vhd.ParentPath ) 138 | { 139 | $usedSpaceSoFar += Get-VHDSize -vhd (Get-VHD -Path $vhd.ParentPath) 140 | } 141 | } 142 | $UsedSpaceSoFar 143 | } 144 | 145 | [hashtable]$getvmparams = @{} 146 | 147 | if( $PSBoundParameters[ 'vmName' ] ) 148 | { 149 | $getvmparams.Add( 'Name' , $vmName ) 150 | } 151 | 152 | [string]$addressPattern = $null 153 | 154 | if( $ipv4only ) 155 | { 156 | $addressPattern = '^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$' 157 | } 158 | 159 | $connectedServer = $null 160 | 161 | if( $PSBoundParameters[ 'viservers' ] ) 162 | { 163 | Import-Module VMware.PowerCLI -ErrorAction Stop 164 | 165 | $connectedServer = Connect-VIServer -Server $viservers -ErrorAction Stop 166 | $vms = @( Get-VM @getvmparams | Where-Object PowerState -eq 'PoweredOn'|Select *,@{n='IPAddresses';e={($_.guest.IPaddress | Where-Object { $_ -match $addressPattern } | Sort ) -join ' , '}} | Select -Property $fields ) 167 | } 168 | else 169 | { 170 | Remove-Module VMware.* -ErrorAction SilentlyContinue 171 | Import-Module Hyper-V -ErrorAction Stop 172 | if( $PSBoundParameters[ 'hypervServer' ] ) 173 | { 174 | $getvmparams.Add( 'ComputerName' , $hypervServer ) 175 | } 176 | ## if they have multiple NICs then it produces multiple entries for NIC so process per VM to get single entry 177 | $vms = @( Get-VM @getvmparams | Where-Object State -eq 'Running' | ForEach-Object ` 178 | { 179 | $VM = $_ 180 | $result = [pscustomobject]@{ 181 | 'Name' = $VM.Name 182 | 'NumCPU' = $VM.ProcessorCount 183 | 'CoresPerSocket' = 1 -as [int] 184 | 'IPAddresses' = '' 185 | 'MemoryMB' = [int]($VM.MemoryAssigned / 1MB ) 186 | 'UsedSpaceGB' = 0 -as [long] 187 | 'ProvisionedSpaceGB' = 0 -as [long] 188 | } 189 | $VM | Get-VMNetworkAdapter | Select -ExpandProperty IPAddresses -ErrorAction SilentlyContinue | Sort | ForEach-Object ` 190 | { 191 | if( $_ -match $addressPattern ) 192 | { 193 | $result.IPAddresses += ( "{0}{1}" -f $(if( $result.IPAddresses.Length ) { ' , ' } ) , $_ ) 194 | } 195 | } 196 | Get-VHD -VMid $VM.VMid | ForEach-Object ` 197 | { 198 | $result.ProvisionedSpaceGB += $_.Size 199 | ## will recurse to parent disks if snapshots 200 | $result.UsedSpaceGB = Get-VHDSize -vhd $_ 201 | } 202 | $result.UsedSpaceGB = [int]($result.UsedSpaceGB / 1GB) 203 | $result.ProvisionedSpaceGB = [int]($result.ProvisionedSpaceGB / 1GB) 204 | $result 205 | }) 206 | } 207 | 208 | if( ! $vms -or ! $vms.Count ) 209 | { 210 | Write-Warning 'Found no VMs' 211 | } 212 | elseif( $gridView ) 213 | { 214 | $selected = $vms | Out-GridView -PassThru 215 | 216 | if( $selected ) 217 | { 218 | $selected | Set-Clipboard 219 | } 220 | } 221 | else 222 | { 223 | if( $PSBoundParameters[ 'bginfoFile' ] ) 224 | { 225 | ## output data in format that bginfo can read as a text file as a custom field in a bgi file 226 | [hashtable]$outfileParams = @{ 'FilePath' = $bginfoFile ; 'NoClobber' = (!$overwrite) } 227 | ## figure out longest name so we know how many tabs to use 228 | [string]$tabs = "`t`t`t`t`t`t`t`t`t" ## overkill! 229 | [string]$firstPad = $null 230 | [int]$longestName = -1 231 | $vms | ForEach-Object { $longestName = [math]::Max( $longestName , $_.Name.Length ) } 232 | $vms | Select -Property $bginfoFields | ForEach-Object ` 233 | { 234 | "{0}{1}:`t{2}{3}" -f $firstPad , $_.Name , $tabs.Substring( 0 , ( $longestName - $_.Name.Length ) / $tabStop ) , ( $_.PSObject.Properties | Where Name -eq $bginfoFields[-1] | Select -ExpandProperty Value ) 235 | $firstPad = "`t" 236 | } | Out-File @outfileParams 237 | } 238 | else 239 | { 240 | [hashtable]$ftParams = @{ 'AutoSize' = $true ; 'HideTableHeaders' = $noHeaders } 241 | $vms | Format-Table @ftParams 242 | } 243 | } 244 | 245 | if( $wait ) 246 | { 247 | $null = Read-Host "Hit to continue" 248 | } 249 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Run script in VM.ps1: -------------------------------------------------------------------------------- 1 | #requires -version 3 2 | 3 | <# 4 | .SYNOPSIS 5 | Copy a script to a remote machine via Invoke-VMScript, as doesn't require explicitly specified credentials, and run it. 6 | 7 | .DESCRIPTION 8 | Works around an issue that limits PowerShell script length to 8K because VMware run powershell.exe via cmd.exe 9 | 10 | .PARAMETER scriptFile 11 | The full path to the PowerShell script file which will be run in the VM(s) specified. 12 | Can be local or remote to the machine running the script 13 | 14 | .PARAMETER VMs 15 | The powered on VMware VM(s) to operate on. VMware Tools must be running. 16 | 17 | .PARAMETER copyTo 18 | A share on the VM(s) to copy the script to directly rather than base64 encoding the script and copying via Invoke-VMscript (which can be slow). 19 | If the folder does not exist, it, and parents, will be created 20 | 21 | .PARAMETER scriptParameters 22 | Parameters to pass when running the script specified by -scriptFile 23 | 24 | .PARAMETER vCenter 25 | The VMware vCenter(s) to connect to 26 | 27 | .PARAMETER chunkSizeBytes 28 | The maximum amount of data to send in each part of the base64 encoded script file. 29 | Values larger than the default may fail 30 | 31 | .PARAMETER quitOnError 32 | Quit the script immediately on any errors encoutered. If not specified the script will move on to the next VM, if there is one 33 | 34 | .PARAMETER port 35 | The vCenter port to connect to. Only specify if non-standard 36 | 37 | .PARAMETER protocol 38 | The protocol to use to connect to vCenter 39 | 40 | .PARAMETER forceVcenter 41 | Suppress any prompts when connecting to vCenter 42 | 43 | .PARAMETER allLinked 44 | Connect to all vCenters linked with the ones specified by -vCennter 45 | 46 | .PARAMETER OperationTimeoutSec 47 | Timeout in seconds of the WMI/CIM operation used to get the remote shares when -copyTo specified 48 | 49 | .PARAMETER CIMprotocol 50 | The CIM protocol to use in the WMI/CIM operation used to get the remote shares when -copyTo specified 51 | 52 | .EXAMPLE 53 | . '.\Run script in VM.ps1" -scriptFile "C:\Scripts\Remvoe Ghost NICS.ps1" -VMs GLXAPVSMASTS19 -scriptParameters '-nicregex "Intel.*Gigabit" -confirm:$false' -vCenter grl-vcenter04.guyrleech.local 54 | Copy the script specified to the VM specified using Invoke-VMscript. When copied, run it with the specified parameters 55 | 56 | .EXAMPLE 57 | . '.\Run script in VM.ps1" -scriptFile "C:\Scripts\Remvoe Ghost NICS.ps1" -VMs GLXAPVSMASTS19 -scriptParameters '-nicregex "Intel.*Gigabit" -confirm:$false' -vCenter grl-vcenter04.guyrleech.local -copyTo 'D$\temp\guy' -Confirm:$false 58 | Copy the script specified to the VM via its D$ share. When copied, run it with the specified parameters 59 | 60 | .NOTES 61 | Invoke-VMscript can be slow so use the -CopyTo parameter if the account running the script has write access to C$ share or another local share in each VM 62 | 63 | Modification History: 64 | 65 | 2021/08/23 @guyrleech Initial release 66 | 2021/08/23 @guyrleech Changed Get-CimInstance to use Dcom to avoid using PS remoting by default since if PS remoting is enabled, it would be better to use that rather than this script 67 | #> 68 | 69 | <# 70 | Copyright © 2021 Guy Leech 71 | 72 | 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, 73 | 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: 74 | 75 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 76 | 77 | 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. 78 | 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. 79 | #> 80 | 81 | [CmdletBinding(SupportsShouldProcess=$True,ConfirmImpact='High')] 82 | 83 | Param 84 | ( 85 | [Parameter(Mandatory=$true,HelpMessage='Script to copy to VM and run')] 86 | [string]$scriptFile , 87 | [Parameter(Mandatory=$true,HelpMessage='The VM(s) to operate on')] 88 | [string[]]$VMs , 89 | [string]$copyTo , 90 | [string]$scriptParameters , 91 | [string[]]$vCenter , 92 | [int]$chunkSizeBytes = 8000 , 93 | [switch]$quitOnError , 94 | [int]$port , 95 | [ValidateSet('http','https')] 96 | [string]$protocol , 97 | [switch]$forceVcenter , 98 | [switch]$allLinked , 99 | [ValidateSet('dcom','wsman','default')] 100 | [string]$CIMprotocol = 'dcom' , 101 | [int]$OperationTimeoutSec = 15 102 | ) 103 | 104 | Import-Module -Name VMware.VimAutomation.Core -Verbose:$false 105 | 106 | if( $PSBoundParameters[ 'vCenter' ] ) 107 | { 108 | [hashtable]$vcenterParameters = @{ 'Server' = $vcenter ; 'AllLinked' = $allLinked ; 'Force' = $forceVcenter } 109 | 110 | if( $PSBoundParameters[ 'port' ] ) 111 | { 112 | $vcenterParameters.Add( 'port' , $port ) 113 | } 114 | 115 | if( $PSBoundParameters[ 'protocol' ] ) 116 | { 117 | $vcenterParameters.Add( 'protocol' , $protocol ) 118 | } 119 | 120 | if( ! ( $viconnection = Connect-VIServer @vcenterParameters ) ) 121 | { 122 | Throw "Unable to connect to $vcenter" 123 | } 124 | } 125 | 126 | ## read whole file and base 64 encode 127 | [byte[]]$data = $null 128 | [string]$base64encoded = $null 129 | [string]$share = $null 130 | if( -Not $PSBoundParameters[ 'copyTo' ] ) 131 | { 132 | $data = [System.IO.File]::ReadAllBytes( $scriptFile ) 133 | if( ! $data -or ! $data.Count ) 134 | { 135 | Throw "Failed to get any data from file $scriptFile" 136 | } 137 | 138 | $base64encoded = [System.Convert]::ToBase64String( $data ) 139 | Write-Verbose -Message "Base64 data length is $($base64encoded.Length)" 140 | } 141 | elseif( -Not ( Test-Path -Path $scriptFile -PathType Leaf ) ) 142 | { 143 | Throw "Unable to access script file $scriptFile" 144 | } 145 | else 146 | { 147 | [int]$skip = 0 148 | For( [int]$index = 0 ; $index -lt $copyTo.Length ; $index++ ) 149 | { 150 | if( $copyTo[$index] -eq '\' ) 151 | { 152 | $skip++ 153 | } 154 | else 155 | { 156 | break 157 | } 158 | } 159 | $share = $copyTo -split '\\' | Select-Object -First 1 -Skip $skip 160 | if( [String]::IsNullOrEmpty( $share ) ) 161 | { 162 | Throw "Failed to get share from $copyTo" 163 | } 164 | } 165 | 166 | ## Create a temporary file and copy script to it 167 | [scriptblock]$scriptBlock = [scriptblock]::Create( 168 | { 169 | if( $tempFile = New-TemporaryFile ) 170 | { 171 | [string]$destination = "$tempfile.txt" 172 | if( Move-Item -Path $tempFile -Destination $destination -PassThru ) 173 | { 174 | $destination 175 | } 176 | } 177 | }) 178 | 179 | if( ! ( $CIMsessionOption = New-CimSessionOption -Protocol $CIMprotocol ) ) 180 | { 181 | Throw "Failed to create CIM session option with protocol $CIMprotocol" 182 | } 183 | 184 | [string]$errorMessage = $null 185 | 186 | ## may have been flattened if called bu scheduled task 187 | if( $VMs.Count -eq 1 -and $VMs[0].IndexOf( ',' ) -ge 0 ) 188 | { 189 | $VMs = $VMs -split ',' 190 | } 191 | 192 | ForEach( $virtualMachine in $VMs ) 193 | { 194 | if( -Not ( $vmObjects = @( Get-VM -Name $virtualMachine ) ) ) 195 | { 196 | $errorMessage = "Failed to get vm $virtualMachine" 197 | if( $quitOnError ) 198 | { 199 | Throw $errorMessage 200 | } 201 | else 202 | { 203 | Write-Error -Message $errorMessage 204 | continue 205 | } 206 | } 207 | 208 | ## iterate in case an array, eg had * in name 209 | ForEach( $VM in $vmObjects ) 210 | { 211 | [string]$decodeScript = $null 212 | if( $VM.PowerState -ne 'PoweredOn' ) 213 | { 214 | Write-Warning -Message "Unable to operate on $($VM.Name) as power state is $($VM.powerstate)" 215 | } 216 | elseif( $PSCmdlet.ShouldProcess( $VM.Name , "Run script" ) ) 217 | { 218 | if( -Not $VM.extensiondata.guest -or $VM.extensiondata.guest.ToolsRunningStatus -ne 'guestToolsRunning' ) 219 | { 220 | Write-Warning -Message "VMware Tools appear not to be running in $($VM.Name)" 221 | } 222 | ## if we have a location to copy to, we'll try that as may be running with credentials that allow that 223 | if( $PSBoundParameters[ 'copyTo' ] ) 224 | { 225 | [string]$destination = Join-Path -Path "\\$($VM.Name)" -ChildPath $copyTo 226 | Write-Verbose -Message "Destination for script copy is $destination" 227 | $testPathError = $null 228 | ## finding that operations on non-existent shares can take upwards of 45 seconds each so check share exists if we can 229 | $CIMError = $null 230 | $errorMessage = $null 231 | 232 | Write-Verbose -Message "Trying to get file shares from $($VM.Name) to check for $share" 233 | if( ( $cimSession = New-CimSession -ComputerName $VM.Name -SessionOption $CIMsessionOption -ErrorAction SilentlyContinue ) ` 234 | -and ( [array]$shares = @( Get-CimInstance -ClassName win32_share -ComputerName $VM.Name -ErrorAction SilentlyContinue -OperationTimeoutSec $OperationTimeoutSec -Filter 'Path IS NOT NULL' -QueryDialect WQL -ErrorVariable CIMerror | Where-Object { $_.Path -match '^[A-Z]:\\' } ) ) ` 235 | -and $shares.Count -gt 0 ) 236 | { 237 | Write-Verbose -Message "Checking if share $share exists on $($VM.Name) - got $($shares.Count) file shares" 238 | if( -Not ( $ourShare = $shares.Where( { $_.Name -eq $share } ) ) ) 239 | { 240 | $errorMessage = "Unable to find share $share on $($VM.Name) - shares are $(($shares | Select-Object -ExpandProperty Name) -join ',')" 241 | } 242 | } 243 | elseif( $null -eq $CIMError -or $CIMError.Count -eq 0 ) ## no error so no suitable shares found 244 | { 245 | $errorMessage = "No file shares found on $($VM.Name) so cannot use $share" 246 | } 247 | 248 | if( $cimSession ) 249 | { 250 | Remove-CimSession -CimSession $cimSession 251 | $cimSession = $null 252 | } 253 | 254 | if( $errorMessage ) 255 | { 256 | if( $quitOnError ) 257 | { 258 | Throw $errorMessage 259 | } 260 | else 261 | { 262 | Write-Error -Message $errorMessage 263 | continue 264 | } 265 | } 266 | if( -Not ( Test-Path -Path $destination -ErrorAction SilentlyContinue -ErrorVariable testPathError ) ) 267 | { 268 | Write-Verbose -Message "Creating folder $destination in $($VM.Name)" 269 | if( -Not ( $newFolder = New-Item -Path $destination -Force -ItemType Directory ) ) 270 | { 271 | Write-Warning -Message "Problem creating folder $newFolder" 272 | } 273 | } 274 | Write-Verbose -Message "Copying script to $destination" 275 | if( -Not ( Copy-Item -Path $scriptFile -Destination $destination -Container -Force -PassThru ) ) 276 | { 277 | $errorMessage = "Problem copying script to $destination" 278 | if( $quitOnError ) 279 | { 280 | Throw $errorMessage 281 | } 282 | else 283 | { 284 | Write-Error -Message $errorMessage 285 | continue 286 | } 287 | } 288 | else 289 | { 290 | ## remove any leading \ characters since it must be a local share 291 | [string]$remoteScriptFile = Join-Path -Path ($copyTo -replace '^\\+' -replace '\$' , ':') -ChildPath (Split-Path -Path $scriptFile -Leaf) 292 | Write-Verbose -Message "Remote script path in $($VM.Name) is $remoteScriptFile" 293 | $decodeScript = @" 294 | & `"$remoteScriptFile`" $scriptParameters 295 | Remove-Item -Path `"$remoteScriptFile`" 296 | "@ 297 | } 298 | } 299 | else 300 | { 301 | if( -Not ( $result = Invoke-VMScript -VM $VM -ScriptType Powershell -ScriptText $scriptBlock ) -or [string]::IsNullOrEmpty( $result.ScriptOutput ) ) 302 | { 303 | $errorMessage = "Failed to create temporary file on $($VM.Name)" 304 | if( $quitOnError ) 305 | { 306 | Throw $errorMessage 307 | } 308 | else 309 | { 310 | Write-Error -Message $errorMessage 311 | continue 312 | } 313 | } 314 | 315 | [string]$remoteTempFile = $result.ScriptOutput.Trim() 316 | 317 | Write-Verbose -Message "Remote temp file is $remoteTempFile" 318 | 319 | [int]$chunk = 0 320 | [int]$chunkStart = 0 321 | [int]$remaining = -1 322 | [int]$thisChunkSize = $chunkSizeBytes 323 | 324 | ## read chunks of base64 encoded string , send to VM, decode and append to results file 325 | do 326 | { 327 | $chunk++ 328 | ## if last chunk ensure not too big 329 | $remaining = $base64encoded.Length - $chunkStart 330 | if( $remaining -lt $chunkSizeBytes ) 331 | { 332 | $thisChunkSize = $remaining 333 | } 334 | if( $thisChunkSize -gt 0 ) 335 | { 336 | Write-Verbose -Message "Chunk $chunk : offset $chunkStart size $thisChunkSize length $($base64encoded.Length)" 337 | [string]$thisChunk = $base64encoded.Substring( $chunkStart , $thisChunkSize ) 338 | Write-Verbose -Message "`tchunk string length $($thisChunk.Length)" 339 | 340 | if( ! ( $result = Invoke-VMScript -VM $VM -ScriptType Bat -ScriptText "echo | set /p=`"$thisChunk`" >> $remoteTempFile" ) ) ## don't check exit code as can be non-zero when worked 341 | { 342 | $errorMessage = "Failed to copy $($thisChunk.Length) bytes at offset $chunkStart to $remoteTempFile on $($VM.Name)" 343 | if( $quitOnError ) 344 | { 345 | Throw $errorMessage 346 | } 347 | else 348 | { 349 | Write-Error -Message $errorMessage 350 | continue 351 | } 352 | } 353 | 354 | $chunkStart += $chunkSizeBytes 355 | } 356 | } while( $remaining -gt 0 ) 357 | 358 | [string]$remoteScriptFile = $remoteTempFile -replace "\.\w*$" , '.ps1' ## change .txt extension to .ps1 359 | 360 | Write-Verbose -Message "Will decode $remoteTempFile to $remoteScriptFile" 361 | 362 | ## from https://github.com/guyrleech/Microsoft/blob/master/Bincoder%20GUI.ps1 363 | $decodeScript = @" 364 | [byte[]]`$transmogrified = [System.Convert]::FromBase64String( (Get-Content -Path `"$remoteTempFile`" )) 365 | if( `$transmogrified.Count ) 366 | { 367 | if( `$fileStream = New-Object System.IO.FileStream( `"$remoteScriptFile`" , [System.IO.FileMode]::Create , [System.IO.FileAccess]::Write ) ) 368 | { 369 | `$fileStream.Write( `$transmogrified , 0 , `$transmogrified.Count ) 370 | `$fileStream.Close() 371 | 372 | Remove-Item -Path `"$remoteTempFile`" 373 | & `"$remoteScriptFile`" $scriptParameters 374 | Remove-Item -Path `"$remoteScriptFile`" 375 | } 376 | else 377 | { 378 | Throw "Failed to create $remoteScriptFile" 379 | } 380 | } 381 | else 382 | { 383 | Throw "No data retrieved from $remoteTempFile" 384 | } 385 | "@ 386 | } 387 | 388 | ## decode and run the file 389 | if( ! ( $result = Invoke-VMScript -VM $VM -ScriptType Powershell -ScriptText $decodeScript ) -or $result.ExitCode -ne 0) 390 | { 391 | $errorMessage = "Failed to convert base64 data or execute script in $($VM.Name)" 392 | if( $result ) 393 | { 394 | $errorMessage += " exit code $($result.ExitCode) : $($result.ScriptOutput)" 395 | } 396 | if( $quitOnError ) 397 | { 398 | Throw $errorMessage 399 | } 400 | else 401 | { 402 | Write-Error -Message $errorMessage 403 | continue 404 | } 405 | } 406 | else 407 | { 408 | Write-Verbose -Message "Good result from running script in $($VM.Name)" 409 | 410 | $result.ScriptOutput 411 | } 412 | } 413 | } 414 | } 415 | 416 | # SIG # Begin signature block 417 | # MIINRQYJKoZIhvcNAQcCoIINNjCCDTICAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB 418 | # gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR 419 | # AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUw/jmdMBa9BOMXHgMhCe6W4jS 420 | # OQOgggqHMIIFMDCCBBigAwIBAgIQBAkYG1/Vu2Z1U0O1b5VQCDANBgkqhkiG9w0B 421 | # AQsFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD 422 | # VQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVk 423 | # IElEIFJvb3QgQ0EwHhcNMTMxMDIyMTIwMDAwWhcNMjgxMDIyMTIwMDAwWjByMQsw 424 | # CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu 425 | # ZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEyIEFzc3VyZWQgSUQg 426 | # Q29kZSBTaWduaW5nIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA 427 | # +NOzHH8OEa9ndwfTCzFJGc/Q+0WZsTrbRPV/5aid2zLXcep2nQUut4/6kkPApfmJ 428 | # 1DcZ17aq8JyGpdglrA55KDp+6dFn08b7KSfH03sjlOSRI5aQd4L5oYQjZhJUM1B0 429 | # sSgmuyRpwsJS8hRniolF1C2ho+mILCCVrhxKhwjfDPXiTWAYvqrEsq5wMWYzcT6s 430 | # cKKrzn/pfMuSoeU7MRzP6vIK5Fe7SrXpdOYr/mzLfnQ5Ng2Q7+S1TqSp6moKq4Tz 431 | # rGdOtcT3jNEgJSPrCGQ+UpbB8g8S9MWOD8Gi6CxR93O8vYWxYoNzQYIH5DiLanMg 432 | # 0A9kczyen6Yzqf0Z3yWT0QIDAQABo4IBzTCCAckwEgYDVR0TAQH/BAgwBgEB/wIB 433 | # ADAOBgNVHQ8BAf8EBAMCAYYwEwYDVR0lBAwwCgYIKwYBBQUHAwMweQYIKwYBBQUH 434 | # AQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQwYI 435 | # KwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFz 436 | # c3VyZWRJRFJvb3RDQS5jcnQwgYEGA1UdHwR6MHgwOqA4oDaGNGh0dHA6Ly9jcmw0 437 | # LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwOqA4oDaG 438 | # NGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RD 439 | # QS5jcmwwTwYDVR0gBEgwRjA4BgpghkgBhv1sAAIEMCowKAYIKwYBBQUHAgEWHGh0 440 | # dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCgYIYIZIAYb9bAMwHQYDVR0OBBYE 441 | # FFrEuXsqCqOl6nEDwGD5LfZldQ5YMB8GA1UdIwQYMBaAFEXroq/0ksuCMS1Ri6en 442 | # IZ3zbcgPMA0GCSqGSIb3DQEBCwUAA4IBAQA+7A1aJLPzItEVyCx8JSl2qB1dHC06 443 | # GsTvMGHXfgtg/cM9D8Svi/3vKt8gVTew4fbRknUPUbRupY5a4l4kgU4QpO4/cY5j 444 | # DhNLrddfRHnzNhQGivecRk5c/5CxGwcOkRX7uq+1UcKNJK4kxscnKqEpKBo6cSgC 445 | # PC6Ro8AlEeKcFEehemhor5unXCBc2XGxDI+7qPjFEmifz0DLQESlE/DmZAwlCEIy 446 | # sjaKJAL+L3J+HNdJRZboWR3p+nRka7LrZkPas7CM1ekN3fYBIM6ZMWM9CBoYs4Gb 447 | # T8aTEAb8B4H6i9r5gkn3Ym6hU/oSlBiFLpKR6mhsRDKyZqHnGKSaZFHvMIIFTzCC 448 | # BDegAwIBAgIQBP3jqtvdtaueQfTZ1SF1TjANBgkqhkiG9w0BAQsFADByMQswCQYD 449 | # VQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGln 450 | # aWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEyIEFzc3VyZWQgSUQgQ29k 451 | # ZSBTaWduaW5nIENBMB4XDTIwMDcyMDAwMDAwMFoXDTIzMDcyNTEyMDAwMFowgYsx 452 | # CzAJBgNVBAYTAkdCMRIwEAYDVQQHEwlXYWtlZmllbGQxJjAkBgNVBAoTHVNlY3Vy 453 | # ZSBQbGF0Zm9ybSBTb2x1dGlvbnMgTHRkMRgwFgYDVQQLEw9TY3JpcHRpbmdIZWF2 454 | # ZW4xJjAkBgNVBAMTHVNlY3VyZSBQbGF0Zm9ybSBTb2x1dGlvbnMgTHRkMIIBIjAN 455 | # BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr20nXdaAALva07XZykpRlijxfIPk 456 | # TUQFAxQgXTW2G5Jc1YQfIYjIePC6oaD+3Zc2WN2Jrsc7bj5Qe5Nj4QHHHf3jopLy 457 | # g8jXl7Emt1mlyzUrtygoQ1XpBBXnv70dvZibro6dXmK8/M37w5pEAj/69+AYM7IO 458 | # Fz2CrTIrQjvwjELSOkZ2o+z+iqfax9Z1Tv82+yg9iDHnUxZWhaiEXk9BFRv9WYsz 459 | # qTXQTEhv8fmUI2aZX48so4mJhNGu7Vp1TGeCik1G959Qk7sFh3yvRugjY0IIXBXu 460 | # A+LRT00yjkgMe8XoDdaBoIn5y3ZrQ7bCVDjoTrcn/SqfHvhEEMj1a1f0zQIDAQAB 461 | # o4IBxTCCAcEwHwYDVR0jBBgwFoAUWsS5eyoKo6XqcQPAYPkt9mV1DlgwHQYDVR0O 462 | # BBYEFE16ovlqIk5uX2JQy6og0OCPrsnJMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUE 463 | # DDAKBggrBgEFBQcDAzB3BgNVHR8EcDBuMDWgM6Axhi9odHRwOi8vY3JsMy5kaWdp 464 | # Y2VydC5jb20vc2hhMi1hc3N1cmVkLWNzLWcxLmNybDA1oDOgMYYvaHR0cDovL2Ny 465 | # bDQuZGlnaWNlcnQuY29tL3NoYTItYXNzdXJlZC1jcy1nMS5jcmwwTAYDVR0gBEUw 466 | # QzA3BglghkgBhv1sAwEwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNl 467 | # cnQuY29tL0NQUzAIBgZngQwBBAEwgYQGCCsGAQUFBwEBBHgwdjAkBggrBgEFBQcw 468 | # AYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tME4GCCsGAQUFBzAChkJodHRwOi8v 469 | # Y2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRTSEEyQXNzdXJlZElEQ29kZVNp 470 | # Z25pbmdDQS5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAU9zO 471 | # 9UpTkPL8DNrcbIaf1w736CgWB5KRQsmp1mhXbGECUCCpOCzlYFCSeiwH9MT0je3W 472 | # aYxWqIpUMvAI8ndFPVDp5RF+IJNifs+YuLBcSv1tilNY+kfa2OS20nFrbFfl9QbR 473 | # 4oacz8sBhhOXrYeUOU4sTHSPQjd3lpyhhZGNd3COvc2csk55JG/h2hR2fK+m4p7z 474 | # sszK+vfqEX9Ab/7gYMgSo65hhFMSWcvtNO325mAxHJYJ1k9XEUTmq828ZmfEeyMq 475 | # K9FlN5ykYJMWp/vK8w4c6WXbYCBXWL43jnPyKT4tpiOjWOI6g18JMdUxCG41Hawp 476 | # hH44QHzE1NPeC+1UjTGCAigwggIkAgEBMIGGMHIxCzAJBgNVBAYTAlVTMRUwEwYD 477 | # VQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAv 478 | # BgNVBAMTKERpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25pbmcgQ0EC 479 | # EAT946rb3bWrnkH02dUhdU4wCQYFKw4DAhoFAKB4MBgGCisGAQQBgjcCAQwxCjAI 480 | # oAKAAKECgAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIB 481 | # CzEOMAwGCisGAQQBgjcCARUwIwYJKoZIhvcNAQkEMRYEFDaX3bWbbyqVWaVKQDA0 482 | # kG0dosbUMA0GCSqGSIb3DQEBAQUABIIBADbjG1eJ1q38b7qYOq3lVwizWN4/9Wla 483 | # Dyhgzf5uq09rvzAqORGR9pj6jF+7Klzjy1Tbq+XyB75fUEjB8+hEYtHe9adwdEzG 484 | # KYoeyxnd0NJxmSD20qX8PSZNTAHd1XK1AfaOPLYkkhfw5iZ6W/ZZivQ4D8k0xLsf 485 | # CTvxJKFKQsHmbADgKRsHubXs+lwkbqpb6ErVb1GLHM6GThKewg6Rf7T3NldsixsE 486 | # VXDkBoaCNFU3AAlywTQdP8YPHbATcKzGDvoEBGQogsgC2CC2JSd+/U14nKarwRV/ 487 | # 6cz+vzGSyjotjSjVAxuWsMKsFUf+9rwlHYkmuuPEt6UXzgCvstBxNJ8= 488 | # SIG # End signature block 489 | -------------------------------------------------------------------------------- /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 |