29 December 2013

Get VM Disks and RDMs via PowerCLI

Need to get info about a VM's hard disks?  Like, "regular" virtual disks and RDMs, in a handy and fast manner?  We need to, occasionally, and so wrote a function to do so (a while ago -- just finally posting it).  It gets things like the hard disk name, the SCSI ID, the storage device display name, the disk size, the SCSI canonical name, and [optionally] the full datastore path for the disk files.  Here again, not the first bit of code around to retrieve such things, but a version that is written to do things most quickly.

The script:
<# .Description
    Function to get a VM's hard disk and RDM info
    Originally from Sep 2011, updated Dec 2013 -- vNugglets.com
    .Example
    Get-VMDiskAndRDM -vmName someVM -ShowVMDKDatastorePath | ft -a
    VMName HardDiskName ScsiId DeviceDisplayName SizeGB ScsiCanonicalName                    VMDKDStorePath
    ------ ------------ ------ ----------------- ------ -----------------                    --------------
    someVM Hard disk 1  0:0                          50                                      [dstore0] someVM/someVM.vmdk
    someVM Hard disk 2  1:0    someVM-/log_dir       20 naa.60000945618415615641111111111111 [dstore0] someVM/someVM_1.vmdk
    Get the disks (including RDMs) for "someVM", and include the datastore path for each VMDK
#>
function Get-VMDiskAndRDM {
    param(
        ## name pattern of the VM guest for which to get info
        [parameter(Mandatory=$true)][string]$vmName_str = "myVM",
        ## switch to specify that VMDK's datastore path should also be returned
        [switch]$ShowVMDKDatastorePath_sw
    )

    ## the cool, FaF way (using .NET View objects)
    ## get the VM object(s)
    $arrVMViewsForStorageInfo = Get-View -Viewtype VirtualMachine -Property Name, Config.Hardware.Device, Runtime.Host -Filter @{"Name" = "$vmName_str"}
    if (($arrVMViewsForStorageInfo | Measure-Object).Count -eq 0) {Write-Warning "No VirtualMachine objects found matching name pattern '$vmName_str'"; exit} ## end if

    $arrVMViewsForStorageInfo | %{
        $viewVMForStorageInfo = $_
        ## get the view of the host on which the VM currently resides
        $viewHostWithStorage = Get-View -Id $viewVMForStorageInfo.Runtime.Host -Property Config.StorageDevice.ScsiLun

        $viewVMForStorageInfo.Config.Hardware.Device | ?{$_ -is [VMware.Vim.VirtualDisk]} | %{
            $hdThisDisk = $_
            $oScsiLun = $viewHostWithStorage.Config.StorageDevice.ScsiLun | ?{$_.UUID -eq $hdThisDisk.Backing.LunUuid}
            ## the properties to return in new object
            $hshThisVMProperties = @{
                VMName = $viewVMForStorageInfo.Name
                ## the disk's "name", like "Hard disk 1"
                HardDiskName = $hdThisDisk.DeviceInfo.Label
                ## get device's SCSI controller and Unit numbers (1:0, 1:3, etc)
                ScsiId = &{$strControllerKey = $_.ControllerKey.ToString(); "{0}`:{1}" -f $strControllerKey[$strControllerKey.Length - 1], $_.Unitnumber}
                DeviceDisplayName = $oScsiLun.DisplayName
                SizeGB = [Math]::Round($_.CapacityInKB / 1MB, 0)
                ScsiCanonicalName = $oScsiLun.CanonicalName
            } ## end hsh
            ## the array of items to select for output
            $arrPropertiesToSelect = "VMName,HardDiskName,ScsiId,DeviceDisplayName,SizeGB,ScsiCanonicalName".Split(",")
            ## add property for VMDKDStorePath if desired
            if ($ShowVMDKDatastorePath_sw -eq $true) {$hshThisVMProperties["VMDKDStorePath"] = $hdThisDisk.Backing.Filename; $arrPropertiesToSelect += "VMDKDStorePath"}
            New-Object -Type PSObject -Property $hshThisVMProperties | Select $arrPropertiesToSelect
        } ## end foreach-object
    } ## end foreach-object
} ## end function

Some example usage:
PS vN:\> Get-VMDiskAndRDM -vmName myVM01 -ShowVMDKDatastorePath | ft -a

VMName HardDiskName ScsiId DeviceDisplayName SizeGB ScsiCanonicalName                    VMDKDStorePath
------ ------------ ------ ----------------- ------ -----------------                    --------------
myVM01 Hard disk 1  0:0                          50                                      [dstore0] myVM01/myVM01.vmdk
myVM01 Hard disk 2  1:0    myVM01-/data001      660 naa.60000946665554443331111111111111 [dstore0] myVM01/myVM01_1.vmdk


The disks with no values for the DeviceDisplayName or ScsiCanonicalName properties are the "regular" virtual disks, and the others are RDMs.  And, this VM has hard disks on two separate SCSI controllers.

Note:  the vmName parameter is used as a regular expression when getting the .NET View object of the given VM.  As such, one can use a pattern, and can get info on multiple VMs that share the same name pattern.

And, that is that:  some juicy disk/RDM info for VMs, and on the quick!  Thanks, Get-View, for keeping things FaF!  Enjoy

BTW:  related post, Get VM by RDM with PowerCLI:  how to get the VM that is using a particular SAN storage device as an RDM.

24 December 2013

Get VM By RDM with PowerCLI

Sometimes one needs to find a VM by RDM.  This is not a new thought, and there are plenty of spots on the internet that give ways to achieve this.  When this need arose for us a couple of years ago, we found no spot that gave a way to achieve this quickly.  So, with vNugglets' focus on speed, I put together a function to do just that:  quickly find the VM that is using a given SAN device as an RDM.

Then, just recently, a VMware Communities forum post made me realize that we had not yet shared this function with the world.  So, here we go:
<#    .Description
    Function to find what VM (if any) is using a LUN as an RDM, based on the LUN's SCSI canonical name. Assumes that the best practice of all hosts in a cluster seeing the same LUNs is followed.
    vNugglets, originally from Nov 2011
    .Example
    Get-VMWithGivenRDM -CanonicalName naa.60000970000192602222333031314444 -Cluster myCluster0 | ft -a
    Find a VM using the given LUN as an RDM, formatting output in auto-sized table.  Output would be something like:
    VMName            VMDiskName     DeviceDisplayName   CanonicalName
    ------            ----------     -----------------   -------------
    myvm033.dom.com   Hard disk 10   myvm033-data3FS     naa.60000970000192602222333031314444
    .Outputs
    Zero or more PSObjects with info about the VM and its RDM
#>
function Get-VMWithGivenRDM {
    param(
        ## Canonical name of the LUN in question
        [parameter(Mandatory=$true)][string[]]$CanonicalNameOfRDMToFind_arr,
        ## Cluster whose hosts see this LUN
        [parameter(Mandatory=$true)][string]$ClusterName_str
    ) ## end param

    ## get the View object of the cluster in question
    $viewCluster = Get-View -ViewType ClusterComputeResource -Property Name -Filter @{"Name" = "^$([RegEx]::escape($ClusterName_str))$"}
    ## get the View of a host in the given cluster (presumably all hosts in the cluster see the same storage)
    $viewHostInGivenCluster = Get-View -ViewType HostSystem -Property Name -SearchRoot $viewCluster.MoRef | Get-Random
    ## get the Config.StorageDevice.ScsiLun property of the host (retrieved _after_ getting the View object for speed, as this property is only retrieved for this object, not all hosts' View objects)
    $viewHostInGivenCluster.UpdateViewData("Config.StorageDevice.ScsiLun")

    ## if matching device(s) found, store some info for later use
    $arrMatchingDisk = &{
    ## get the View objects for all VMs in the given cluster
    Get-View -ViewType VirtualMachine -Property Name, Config.Hardware.Device -SearchRoot $viewCluster.MoRef | %{$viewThisVM = $_
        ## for all of the RDM devices on this VM, see if the canonical name matches the canonical name in question
        $viewThisVM.Config.Hardware.Device | ?{($_ -is [VMware.Vim.VirtualDisk]) -and ("physicalMode","virtualMode" -contains $_.Backing.CompatibilityMode)} | %{
            $hdThisDisk = $_
            $lunScsiLunOfThisDisk = $viewHostInGivenCluster.Config.StorageDevice.ScsiLun | ?{$_.UUID -eq $hdThisDisk.Backing.LunUuid}
            ## if the canonical names match, create a new PSObject with some info about the VirtualDisk and the VM using it
            if ($CanonicalNameOfRDMToFind_arr -contains $lunScsiLunOfThisDisk.CanonicalName) {
                New-Object -TypeName PSObject -Property @{
                    VMName = $viewThisVM.Name
                    VMDiskName = $hdThisDisk.DeviceInfo.Label
                    CanonicalName = $lunScsiLunOfThisDisk.CanonicalName
                    DeviceDisplayName = $lunScsiLunOfThisDisk.DisplayName
                } ## end new-object
            } ## end if
        } ## end foreach-object
    } ## end foreach-object
    } ## end scriptblock

    ## if a matching device was found, output its info
    if ($arrMatchingDisk) {$arrMatchingDisk | Select VMName, VMDiskName, DeviceDisplayName, CanonicalName}
    ## else, say so
    else {Write-Verbose -Verbose "Booo. No matching disk device with canonical name in '$CanonicalNameOfRDMToFind_arr' found attached to a VM as an RDM in cluster '$ClusterName_str'"}
} ## end fn
Some example usage:
PS vN:\> Get-VMWithGivenRDM -CanonicalName naa.60000123412342602222333031314444 -Cluster myMegaCluster

VMName            VMDiskName     DeviceDisplayName   CanonicalName
------            ----------     -----------------   -------------
myvm002.dom.com   Hard disk 13   myvm002-dataFS_7    naa.60000123412342602222333031314444
...

The point of having a Cluster param is to focus the scope of the search (one generally knows to what set of hosts a particular LUN has been presented).

And, some thoughts on how to make this function even better:
  • take cluster input from pipeline
  • add ability to check entire vCenter inventory, but keep things fast by first narrowing scope to only the VMHosts that see given device, then check those hosts' clusters for a VM with RDM using said device (removes need for "Cluster" param)
  • maybe:  add other info to output object, if there are other things that would be useful/valuable
So, how does this do -- FaF for you, too?  Yeah, just as we thought!

21 November 2013

Get VMs by Virtual Port Group with PowerCLI

How to find out what VMs use a particular virtual port group, and quickly?  Well, we have just the thing.  And, it also helps to illustrate the practical use of LinkedViews and the UpdateViewData() method of .NET View objects (about which we posted a while back in Even Faster PowerCLI Code with Get-View, UpdateViewData() and LinkedViews), along with the kind of speed increase that said method brings.

So, the function:
<#  .Description
    Function to get info about what VMs are on a virtual portgroup.  vNugglets, Oct 2013
    Highlights the use of the UpdateViewData() method of a .NET View object
    .Outputs
    PSObject
#>
function Get-VMOnNetworkPortGroup {
    param(
        ## name of network to get; regex pattern
        [parameter(Mandatory=$true)][string]$NetworkName_str
    ) ## end param

    ## get the .NET View objects for the network port groups whose label match the given name
    $arrNetworkViews = Get-View -ViewType Network -Property Name -Filter @{"Name" = $NetworkName_str}
    if (($arrNetworkViews | Measure-Object).Count -eq 0) {Write-Warning "No networks found matching name '$NetworkName_str'"; exit}

    ## get the networks' VMs' names, along with the name of the corresponding VMHost and cluster
    $arrNetworkViews | %{$_.UpdateViewData("Vm.Name","Vm.Runtime.Host.Name","Vm.Runtime.Host.Parent.Name")}
    ## create a new object for each VM on this network
    $arrNetworkViews | %{
        $viewNetwk = $_
        $viewNetwk.LinkedView.Vm | %{
            New-Object -TypeName PSObject -Property @{
                VMName = $_.Name
                NetworkName = $viewNetwk.Name
                VMHost = $_.Runtime.LinkedView.Host.Name
                Cluster = $_.Runtime.LinkedView.Host.LinkedView.Parent.Name
            } | Select-Object VMName,NetworkName,VMHost,Cluster
        } ## end foreach-object
    } ## end foreach-object
} ## end fn
And, some sample usage:
PS vN:\> Get-VMOnNetworkPortGroup -NetworkName "223|237"

VMName        NetworkName   VMHost           Cluster
------        -----------   ------           -------
dubuntu0      223.Prod      esxi01.dom.com   Cluster0
vma5          223.Prod      esxi02.dom.com   Cluster0
vcsa02        223.Prod      esxi02.dom.com   Cluster0
tmpl_test1    237.Dev       esxi12.dom.com   Cluster1
...
This illustrates the use of a regular expression pattern to match virtual networks whose names contain "223" or "237".

And, on the topic of speed increases:
## using standard cmdlets:
PS vN:\> Measure-Command {Get-VirtualPortGroup -Name SomeNetworkName | Get-VM | select name,@{n="VMHostName"; e={$_.VMHost.name}},@{n="ClusterName"; e={$_.VMHost.Parent.Name}}}
...
TotalSeconds      : 25.4870345


## using vNugglets function:
PS vN:\> Measure-Command {Get-VMOnNetworkPortGroup -NetworkName SomeNetworkName}
...
TotalSeconds      : 0.9676149      ## big winner!

So, while similar results can be had with native PowerCLI cmdlets, leveraging this function definitely comes in handy, especially when you are as impatient as we can be here at vNugglets.  Enjoy!

13 October 2013

Get VM by IP or MAC address via PowerCLI

I had need to get VMs based on IP, but, not particular IPs, just IPs in a certain subnet. Previously here on vNugglets we had a post about how to Find VM by NIC MAC Address. I have since added this logic to a function, and added the ability to get a VM by its IP address. With my recent need, I added another piece that allows for the IP address "wildcarding". The function, Get-VMByAddress, with its three ways to find a VM by IP- or MAC address:
function Get-VMByAddress {
    <#  .Description
        Find all VMs w/ a NIC w/ the given MAC address or IP address (by IP address relies on info returned from VMware Tools in the guest, so those must be installed).  Includes FindByIPWildcard, so that one can find VMs that approximate IP, like "10.0.0.*"
        .Example
        Get-VMByAddress -MAC 00:50:56:00:00:02
        VMName        MacAddress
        ------        ----------
        dev0-server   00:50:56:00:00:02,00:50:56:00:00:04

        Get VMs with given MAC address, return VM name and its MAC addresses
        .Example
        Get-VMByAddress -IP 10.37.31.120
        VMName         IPAddr
        ------         ------
        dev0-server2   192.168.133.1,192.168.253.1,10.37.31.120,fe80::...

        Get VMs with given IP as reported by Tools, return VM name and its IP addresses
        .Example
        Get-VMByAddress -AddressWildcard 10.0.0.*
        VMName   IPAddr
        ------   ------
        someVM0  10.0.0.119,fe80::...
        someVM2  10.0.0.138,fe80::...
        ...

        Get VMs matching the given wildcarded IP address
    #>

    [CmdletBinding(DefaultParametersetName="FindByMac")]
    param (
        ## MAC address in question, if finding VM by MAC; expects address in format "00:50:56:83:00:69"
        [parameter(Mandatory=$true,ParameterSetName="FindByMac")][string]$MacToFind_str,
        ## IP address in question, if finding VM by IP
        [parameter(Mandatory=$true,ParameterSetName="FindByIP")][ValidateScript({[bool][System.Net.IPAddress]::Parse($_)})][string]$IpToFind_str,
        ## wildcard string IP address (standard wildcards like "10.0.0.*"), if finding VM by approximate IP
        [parameter(Mandatory=$true,ParameterSetName="FindByIPWildcard")][string]$AddressWildcard_str
    ) ## end param


    Process {
        Switch ($PsCmdlet.ParameterSetName) {
            "FindByMac" {
                ## return the some info for the VM(s) with the NIC w/ the given MAC
                Get-View -Viewtype VirtualMachine -Property Name, Config.Hardware.Device | Where-Object {$_.Config.Hardware.Device | Where-Object {($_ -is [VMware.Vim.VirtualEthernetCard]) -and ($_.MacAddress -eq $MacToFind_str)}} | select @{n="VMName"; e={$_.Name}},@{n="MacAddress"; e={($_.Config.Hardware.Device | Where-Object {$_ -is [VMware.Vim.VirtualEthernetCard]} | %{$_.MacAddress} | sort) -join ","}}
                break;
                } ## end case
            {"FindByIp","FindByIPWildcard" -contains $_} {
                ## scriptblock to use for the Where clause in finding VMs
                $sblkFindByIP_WhereStatement = if ($PsCmdlet.ParameterSetName -eq "FindByIPWildcard") {{$_.IpAddress | Where-Object {$_ -like $AddressWildcard_str}}} else {{$_.IpAddress -contains $IpToFind_str}}
                ## return the .Net View object(s) for the VM(s) with the NIC(s) w/ the given IP
                Get-View -Viewtype VirtualMachine -Property Name, Guest.Net | Where-Object {$_.Guest.Net | Where-Object $sblkFindByIP_WhereStatement} | Select @{n="VMName"; e={$_.Name}}, @{n="IPAddr"; e={($_.Guest.Net | %{$_.IpAddress} | sort) -join ","}}
            } ## end case
        } ## end switch
    } ## end process
} ## end function

A few examples of using the function to find VMs by various addresses (as shown in the comment-based help, too):
PS vN:\> Get-VMByAddress -MAC 00:50:56:00:00:02
VMName        MacAddress
------        ----------
dev0-server   00:50:56:00:00:02,00:50:56:00:00:04


PS vN:\> Get-VMByAddress -IP 10.37.31.120
VMName         IPAddr
------         ------
dev0-server2   192.168.133.1,192.168.253.1,10.37.31.120,fe80::...


PS vN:\> Get-VMByAddress -AddressWildcard 10.0.0.*
VMName    IPAddr
------    ------
someVM0   10.0.0.119,fe80::...
someVM2   10.0.0.138,fe80::...
...

There are several places on the web that talk about getting VMs' IPs, getting their MAC addresses, exporting them, and so on, but this is for getting VMs based on the given address. Note: Since IP address info in vCenter is gotten from guest OSes via VMware Tools, this function is going to rely on that info. And, since the function uses everyone's favorite cmdlet, Get-View, and the calls with this cmdlet are properly tuned, you know that it is going to be FaF!

20 August 2013

Copy VIRole via PowerCLI

I had the occasion of needing to copy some VIRoles from one vCenter to another.  There are export/import scripts out there for exporting VIRoles to txt files, and then importing them elsewhere.  But, so as to help simplify the copying of VIRoles, I decided to make a Copy-VIRole function that does the copy straight-away.

It is pretty straight forward:  it creates a VIRole of the given name in a destination vCenter, based on the name of a VIRole and source vCenter.  This does involve the additional step of getting the VIPrivileges (handled in the function via Get-VIPrivilege) from the destination vCenter -- the PrivilegeList itself did not suffice.  The function definition:
function Copy-VIRole {
<#    .Description
    Copy a role to another role, either in same vCenter or to a different vCenter
    This assumes that connections to source/destination vCenter(s) are already established.  If role of given name already exists in destination vCenter, will stop.
    Author:  vNugglets.com -- Jul 2013
    .Example
    Copy-VIRole -SrcRoleName SysAdm -DestRoleName SysAdm_copyTest -SrcVCName vcenter.com -DestVCName labvcenter.com
    .Outputs
    VMware.VimAutomation.ViCore.Impl.V1.PermissionManagement.RoleImpl if role is created/updated, String in Warning stream and nothing in standard out otherwise
#>
    param(
        ## source role name
        [parameter(Mandatory=$true)][string]$SrcRoleName_str,
        ## destination role name
        [parameter(Mandatory=$true)]$DestRoleName_str,
        ## source vCenter connection name
        [parameter(Mandatory=$true)]$SrcVCName_str,
        ## destination vCenter connection name
        [parameter(Mandatory=$true)]$DestVCName_str,
        ## WhatIf switch
        [switch]$WhatIf_sw
    ) ## end param

    ## get the VIRole from the source vCenter
    $oSrcVIRole = Get-VIRole -Server $SrcVCName_str -Name $SrcRoleName_str -ErrorAction:SilentlyContinue
    ## if the role does not exist in the source vCenter
    if ($null -eq $oSrcVIRole) {Write-Warning "VIRole '$DestRoleName_str' does not exist in source vCenter '$SrcVCName_str'. No source VIRole from which to copy. Exiting"; exit}
    ## see if there is VIRole by the given name in the destination vCenter
    $oDestVIRole = Get-VIRole -Server $DestVCName_str -Name $DestRoleName_str -ErrorAction:SilentlyContinue

    ## if the role already exists in the destination vCenter
    if ($null -ne $oDestVIRole) {Write-Warning "VIRole '$DestRoleName_str' already exists in destination vCenter '$DestVCName_str'. Exiting"; exit}
    ## else, create the role
    else {
        $strNewVIRoleExpr = 'New-VIRole -Server $DestVCName_str -Name $DestRoleName_str -Privilege (Get-VIPrivilege -Server $DestVCName_str -Id $oSrcVIRole.PrivilegeList){0}' -f $(if ($WhatIf_sw) {" -WhatIf"})
        Invoke-Expression $strNewVIRoleExpr
    } ## end else
} ## end function

An example of using the function to copy a role from one vCenter to another, with a new role name:
PS vN:\> Copy-VIRole -SrcRoleName MyRole0 -SrcVCName myvcenter.dom.com -DestRoleName MyNewRole -DestVCName vcenter2.dom.com
Name            IsSystem
----            --------
MyNewRole       False

PS vN:\> Get-VIRole MyNewRole -server vcenter2*
Name            IsSystem
----            --------
MyNewRole       False

One can also use this function to clone a VIRole in the same vCenter -- just use the same vCenter for the Source and Destination vCenter parameters.

Note:  this function expects/requires that the PowerCLI session already has a connection to each of the vCenter(s) (one or two) involved in the operation.

For another way of copying a VIRole (between separate vCenters, particularly), see Grzegorz's post at http://psvmware.wordpress.com/2012/07/19/clone-roles-between-two-virtual-center-servers/.

Enjoy.

06 January 2013

Determine VMs' Uplink vmnic on vSwitch with PowerCLI

Ever have the need to determine which uplink vmnic on which vSwitch a VM is using?  We have, too.  And, we have relied on traditional hardware level info in the past to get such info:  get with the network team and have them tell us on what physical switch port they see the MAC address of the given VM, and then correlate that with the VMHost's vmnic.  Straight forward, fast, easy, and self-service.  Not.

The standard ways described elsewhere on the web are to use esxtop on a VMHost or resxtop from somewhere with the vCLI installed (press "n" once in [r]esxtop).  But, wouldn't it be nice if there was a way without having to enable SSH on a host, or SSH'ing to a vMA appliance (if you do not already have the vCLI installed).  Maybe, say, with PowerCLI, which everybody has installed (right?).  Oh, wait -- there is:  Get-EsxTop.

Here is a function that uses Get-EsxTop to get, for VMs on a VMHost, their vSwitch and vmnic uplink information:
<#  .Description
Function to retrieve Netports' (portgroup ports') client, uplink info, vSwitch, etc. info.  Includes things like VMKernel ports and Management uplinks.
    Author:  vNugglets.com -- Nov 2012
    .Outputs
    PSObject
#>
function Get-VNUplinkNic {
    param(
        ## the VMHost DNS name whose VMs' uplink info to get (not VMHost object name -- so, do not use wildcards)
        [parameter(Mandatory=$true)][string]$VMHostToCheck_str,
        ## PSCredential to use for connecting to VMHost; will prompt for credentials if not passed in here
        [System.Management.Automation.PSCredential]$CredentialForVMHost_cred
    ) ## end param

    $strThisVMHostName = $VMHostToCheck_str

    ## check if VMHost name given is responsive on the network; if not, exit
    if (-not (Test-Connection -Quiet -Count 3 -ComputerName $strThisVMHostName)) {
        Write-Warning "VMHost '$strThisVMHostName' not responding on network -- not proceeding"; exit}

    ## set/get the credential to use for connecting to the VMHost (get if not already passed as param)
    $credToUseForVMHost = if ($CredentialForVMHost_cred) {$CredentialForVMHost_cred} else 
        {$host.ui.PromptForCredential("Need credentials to connect to VMHost", "Please enter credentials for '$strThisVMHostName'", $null, $null)}

    ## connect to the given VIServer (VMHost, here)
    $oVIServer = Connect-VIServer $strThisVMHostName -Credential $credToUseForVMHost

    ## if connecting to VMHost failed, write warning and exit
    if (-not $oVIServer) {Write-Warning "Did not connect to VMHost '$strThisVMHostName' -- not proceeding"; exit}

    ## array with PortID to vSwitch info, for determining vSwitch name from PortID
    ## get vSwitch ("PortsetName") and PortID info, not grouped
    $arrNetPortEntries = (Get-EsxTop -Server $strThisVMHostName -TopologyInfo NetPort).Entries

    ## calculated property for vSwitch name
    $hshVSwitchInfo = @{n="vSwitch"; e={$oNetportCounterValue = $_; ($arrNetPortEntries | ?{$_.PortId -eq $oNetportCounterValue.PortId}).PortsetName}}

    ## get the VM, uplink NIC, vSwitch, and VMHost info
    Get-EsxTop -Server $strThisVMHostName -CounterName NetPort | select @{n="VMName"; e={$_.ClientName}},TeamUplink,$hshVSwitchInfo,@{n="VMHostName"; e={$_.Server.Name}}

    Disconnect-VIServer $strThisVMHostName -Confirm:$false
} ## end function

The comments should pretty much explain what is happening, if it is not already clear.  Once you have defined the function, you can use it like:

PS vNuggs:\> Get-VNUplinkNic -VMHostToCheck somehost

Need credentials to connect to VMHost
Please enter credentials for 'somehost'
User: root
Password for user root: *************

VMName         TeamUplink   vSwitch     VMHostName
----------     ----------   -------     ----------
Management     n/a          vSwitch0    somehost
vmk1           vmnic0       vSwitch0    somehost
vmk0           vmnic0       vSwitch0    somehost
dev0-vm0       vmnic0       vSwitch0    somehost
dev0-vm12323   vmnic5       vSwitch0    somehost
dev0-vm2       vmnic5       vSwitch0    somehost
dev0-vm32237   vmnic4       vSwitch1    somehost
dev0-vm9       vmnic8       vSwitch1    somehost
...

Pretty handy.  And, it does not meet up with the same limitation that you might face with [r]esxtop -- that you can only get uplink info for the top X number of VMs on a host, where X is the number of rows available for display in your console session.  So, you can get the uplink info for all VMs on the host.  Then, you can sort/filter as needed -- VMs on a given uplink is a nice to know, for example.

There you go:  another tasty vNugglet.

Postscript:  After writing this function, and in the process of composing this post, I found a VMware Communities thread (http://communities.vmware.com/thread/341604) that does also provide a portion of this goodness, but that thread did not include the cool sauce for getting the vSwitch info -- so, at least that part is new -- yeah-hooey.

16 December 2012

Evacuating VMs and Templates From Datastores

In many environments, ours included, it is not always possible or desired to store all of a VM's virtual disks on the same datastore.  For example, the storage performance may not be ideal if the VM has high I/O requirements, you may have a policy to keep database logs on separate datastores from the actual data files, or you might just have different tiers of storage for your OS disks vs. your data disks.  Whatever the rationale, it is somewhat troublesome if you need to evacuate the datastore for some reason, such as moving to a new storage array.  Why is this?  Consider the following scenario:



Let's assume DATASTORE1 is the datastore that needs to be evacuated in this example.  Initially, you may consider a simple Move-VM command such as this:

Get-Datastore DATASTORE1 | Get-VM | Move-VM -Datastore DATASTORE4

The problem with this command is that it will attempt to move all four VMs to DATASTORE4 but will fail because there isn't enough free space.  At first glance, you may be thinking "What are you talking about?  The VMs on DATASTORE1 only add up to 400GB and there is 500GB free on DATASTORE4!"  Well, the problem with the Move-VM cmdlet is that it will consolidate all the config files and virtual disks that make up a VM at the destination datastore.  If you take a closer look at the diagram, the total capacity needed to host all four VMs is actually 1.4TB.  So Move-VM is not an option here unless we get a larger datastore, but remember, there was a valid reason for splitting the virtual disks across multiple datastores, so we really don't want to lose the VMs' layout on this migration.

To the VMware (VMTN) Communities I went to begin researching this and big surprise, LucD had the answer for someone.  However, this solution had a few issues for us:

1) It does not move any templates that may be located on the datastore you are evacuating.  Surely you use templates, right?

2) It moves the VM's config files even if they are not on the datastore you are evacuating.  Not a huge deal, but some people like to keep the config files on the same datastore as the "OS disk" and this could break that.  For example, if we wanted to evacuate DATASTORE2 instead, VM2 would have not only its 500GB disk moved to the new datastore, but it would also grab the config files from DATASTORE1 as well, which might lead to some confusion since they now reside with the second virtual disk instead of the first (OS disk).

3) If the VM only has its config files stored on the datastore you are evacuating, then we've got a larger problem.  Let's go back to our original example.  We want to migrate the VMs on DATASTORE1 to DATASTORE4, but keep their existing layout so it all fits.  With the script from the Community, it'll work great except on VM3 (and sort of VM4, see above).  With VM3, only the config files are stored there, but because the script sets the "datastore" property of the VirtualMachineRelocateSpec object regardless of where the config files reside, it'll not only move those files, but also all virtual disks associated with that VM because the "disk" property is optional.  Think of it kind of like a "default datastore" to use for migrating the VM and its virtual disks so you don't need to bother specifying the destination datastore on each disk if you desire.  If you do the math on that, it'll move 400GB of virtual disks just for VM3.  That brings the total for all VMs on DATASTORE1 to 800GB, which again, will not fit on DATASTORE4.

So the fix for #3, of course, is to specify the destination datastore for each and every disk--either the new destination datastore if they need moved or the current datastore if they need to stay put.  This is done through the VirtualMachineRelocateSpec object's "disk" property, which is itself an array of VirtualMachineRelocateSpecDiskLocator objects.  Without further adieu, the final code looks like this:

<#
    .Description
    Script to evacuate virtual disks and/or VM config files from a given datastore; does not move the entire VM and all its disks if they reside elsewhere. Created 12-Dec-2012 by vNugglets.com.
    .Example
    EvacuateDatastore.ps1 -SourceDatastore datastoreToEvac -DestDatastore destinationDatastore

    Move virtual disks and/or VM config files (if any) from source datastore to the destination datastore
#>

## Params for source and destination datastore
param(
    ## The name of the source datastore (the one to evacuate).  Required.
    [parameter(Mandatory=$true)][string]$SourceDatastore_str,
    ## The name of the destination datastore.  Required.
    [parameter(Mandatory=$true)][string]$DestDatastore_str
) ## end parameter


## Set proper variable names from the supplied parameters
$strSrcDatastore = $SourceDatastore_str
$strDestDatastore = $DestDatastore_str

## Get the .NET view of the source datastore
$viewSrcDatastore = Get-View -ViewType Datastore -Property Name -Filter @{"Name" = "^${strSrcDatastore}$"}
## Get the linked view that contains the list of VMs on the source datastore
$viewSrcDatastore.UpdateViewData("Vm.Config.Files.VmPathName", "Vm.Config.Hardware.Device", "Vm.Config.Template", "Vm.Runtime.Host", "Vm.Name")
## Get the .NET view of the destination datastore
$viewDestDatastore = Get-View -ViewType Datastore -Property Name -Filter @{"Name" = "^${strDestDatastore}$"}
## Create a VirtualMachineMovePriority object for the RelocateVM task; 0 = defaultPriority, 1 = highPriority, 2 = lowPriority (per http://pubs.vmware.com/vsphere-51/index.jsp?topic=%2Fcom.vmware.wssdk.apiref.doc%2Fvim.VirtualMachine.MovePriority.html)
$specVMMovePriority = New-Object VMware.Vim.VirtualMachineMovePriority -Property @{"value__" = 1}
## Create empty arrays to track templates and VMs
$arrVMList = $arrTemplateList = @()

## For each VM managed object, sort into separate arrays based on whether it is a VM or a template
$viewSrcDatastore.LinkedView.Vm | % {
    ## If object is a template, add to template array
    if ($_.Config.Template -eq "True") {$arrTemplateList += $_}
    ## Else, add it to the VM array
    else {$arrVMList += $_}
}

## For each VM object, initiate the RelocateVM_Task() method; for each template object, initiate the RelocateVM() method
$arrVMList, $arrTemplateList | %{$_} | %{
    $viewVMToMove = $_
    ## Create a VirtualMachineRelocateSpec object for the RelocateVM task
    $specVMRelocate = New-Object Vmware.Vim.VirtualMachineRelocateSpec
    ## Create an array containing all the virtual disks for the current VM/template
    $arrVirtualDisks = $viewVMToMove.Config.Hardware.Device | ?{$_ -is [VMware.Vim.VirtualDisk]}
    ## If the VM/template's config files reside on the source datastore, set this to the destination datastore (if not specified, the config files are not moved)
    if ($viewVMToMove.Config.Files.VmPathName.Split("]")[0].Trim("[") -eq $strSrcDatastore) {
        $specVMRelocate.Datastore = $viewDestDatastore.MoRef
    } ## end if

    ## For each VirtualDisk for this VM/template, make a VirtualMachineRelocateSpecDiskLocator object (to move disks that are on the source datastore, and leave other disks on their current datastore)
    ## But first, make sure the VM/template actually has any disks
    if ($arrVirtualDisks) {
        foreach($oVirtualDisk in $arrVirtualDisks) {
            $oVMReloSpecDiskLocator = New-Object VMware.Vim.VirtualMachineRelocateSpecDiskLocator -Property @{
                ## If this virtual disk's filename matches the source datastore name, set the VMReloSpecDiskLocator Datastore property to the destination datastore's MoRef, else, set this property to the virtual disk's current datastore MoRef
                DataStore = if ($oVirtualDisk.Backing.Filename -match $strSrcDatastore) {$viewDestDatastore.MoRef} else {$oVirtualDisk.Backing.Datastore}
                DiskID = $oVirtualDisk.Key
            } ## end new-object
            $specVMRelocate.disk += $oVMReloSpecDiskLocator
        } ## end foreach
    } ## end if

    ## Determine if template or VM, then perform necessary relocation steps
    if ($viewVMToMove.Config.Template -eq "True") {
        ## Gather necessary objects to mark template as a VM (VMHost where template currently resides and default, root resource pool of the cluster)
        $viewTemplateVMHost = Get-View -Id $_.Runtime.Host -Property Parent
        $viewTemplateResPool = Get-View -ViewType ResourcePool -Property Name -SearchRoot $viewTemplateVMHost.Parent -Filter @{"Name" = "^Resources$"}
        ## Mark the template as a VM
        $_.MarkAsVirtualMachine($viewTemplateResPool.MoRef, $viewTemplateVMHost.MoRef)
        ## Relocate the template synchronously (i.e. one at a time)
        $viewVMToMove.RelocateVM($specVMRelocate, $specVMMovePriority)
        ## Convert VM back to template
        $viewVMToMove.MarkAsTemplate()
    }
    else {
        ## Initiate the RelocateVM task (asynchronously)
        $viewVMToMove.RelocateVM_Task($specVMRelocate, $specVMMovePriority)
    }
} ## end foreach-object

To use the script, you'll need to provide the source and destination datastore as parameters as such:

PS vNuggs:\> .\EvacuateDatastore.ps1 -SourceDatastore DATASTORE1 -DestDatastore DATASTORE4

Many of the comments in the code are self-explanatory, but here are some additional details around the more important/complex parts:

Lines 35-40: In this section, we determine whether the objects from the datastore's linked view are standard VM managed objects or whether they are actually a template and put them into separate arrays.

Line 43: This is where we pipe the two arrays into a ForEach-Object loop to start the process of gathering the necessary data to relocate the VM/template to the new datastore.  We chose to begin with the VMs first because we eventually want to kick them off as vCenter tasks so that we can then focus on the templates.

Line 50: This is the part that combats problem #2 above.  Here, we parse the datastore name out of the fully-qualified path to the VM/template's config file and if it matches the source datastore, then that's the only time we set the "datastore" property of the VirtualMachineRelocateSpec object to the destination datastore as that's the only time we want to move the config files.

Lines 56-65: Now the VirtualMachineRelocateSpecDiskLocator object is populated accordingly--if the disk is on the source datastore, then its "datastore" property is set to the destination datastore so it gets moved, otherwise it is set to its current datastore so it does not get moved.  Line 63 adds it to the "disk" property of the VirtualMachineRelocateSpec object and everything repeats for the next disk if more exist.

Lines 68-78: On line 68 we have to check if the current VM object is really a VM or a template, even though we already did this earlier.  This is better than having to duplicate all the code from lines 43-65, however.  Then lines 70-73 are how we deal with problem #1 from above.  Since we determined this object was a template, we must convert it to a VM in order to relocate it to a different datastore.  It is a shame VMware still doesn't allow us to move templates for some reason.  Finally, line 75 calls the RelocateVM() method of the VM managed object and begins the relocation.  It is important to note that this occurs in a synchronous manner (i.e. one template at a time).  As you'll see in the next section, we don't do this if it is a VM object, but we really have no choice when dealing with templates because we need to wait for the relocation to complete before we can convert the VM back to a template.  This occurs on line 77.

Lines 79-82: In this section, namely line 81, we call the RelocateVM_Task() method this time since this object is a VM, not a template.  This way we kick off "RelocateVM" tasks asynchronously and let vCenter decide how many it can handle at once, which as of vSphere 5.0 and 5.1 is eight per datastore and two per host.  In addition, we piped the VM objects into the loop first so that we could kick them all off and then focus on the templates one by one.  But, if you are concerned that your storage array won't be able to handle the load of multiple storage migrations, simply change line 81 to look like line 75 and it'll behave synchronously like the templates do.

If you've read this far, you may be wondering what use this sort of script has in the world of SDRS (Storage DRS).  Yes, SDRS has features like datastore maintenance mode that would prevent the need for some of this type of work, but not everyone has SDRS available to them; either because they haven't found the time to implement it yet, haven't upgraded to vSphere 5.x yet, or just don't own the Enterprise Plus edition, so this script is likely to be of use to many people still. We hope so.