VMware Cloud Community
JohnnyScript
Contributor
Contributor
Jump to solution

Lessons on looping and global variables in functions

I am attempting to gather a comprehensive inventory of virtual machines and hosts in my environment.  For the virtual machines, I am trying to gather the information seen below but having issue looping through instances of hard disks and network adapters.  For example, on the hard disks, I receive the following output:

VMNIC1             : Network adapter 1
NICMAC1            : 00:50:56:87:01:e6
NICNetwork1        : dvPortGroup6666

NICState1          : Connected, GuestControl, StartConnected
HDName1            : Hard disk 2
StorageFormat1     : Thin
HDCapacityGB1      : 100
Datastore1         : [RXCXN00020] host108a/host108a_1.vmdk

As you can see, only the last instance of the disk is saved to my output.  In other words, HDNamex is never incremented but the value (here "Hard disk 2") always represents the last disk i.e., this virtual machine has two disks.  I have some virtuals with five disk and, in that case, it has HDName 1 with a value of Hard disk 5.  How do I properly loop through each instance of my hard disks?

A key part of my information on Virtual Machines is the Cluster it resides upon.  This value, however, is only available through the host object's parent attribute.  As such, I created a hash table to try and perform this lookup but it appears that the hash table is lost outside of Get-HostInfo function.  Note, the function Get-VMInfo attempts to access the values in this hash table with the following output:

Cannot index into a null array.
At D:\scripts\VMInventory.ps1:36 char:82
+ $VMStuff | Add-Member -type noteproperty -name HostCluster -value $HostHashTa
ble[ <<<< $VMStuff.Host]
    + CategoryInfo          : InvalidOperation: (host012.domain.com:VMHostIm
   pl) [], RuntimeException
    + FullyQualifiedErrorId : NullArray

It looks like I need this hash table to be 'global' and not restricted to its function.  Any help with either of these two issues would be greatly appreciated.  Below is my code:

Function Get-HostInfo {
Begin {}
Process {
$HostStuff=new-object psobject
$HostHashTable=@{}

$HostStuff | Add-Member -type noteproperty -name HostName -value $_
$HostStuff | Add-Member -type noteproperty -name PowerState -value $_.Powerstate
$HostStuff | Add-Member -type noteproperty -name CPUQty -value $_.NumCpu
$HostStuff | Add-Member -type noteproperty -name Build -value $_.Build
$HostStuff | Add-Member -type noteproperty -name Version -value $_.Version
$HostStuff | Add-Member -type noteproperty -name Manufacturer -value $_.Manufacturer
$HostStuff | Add-Member -type noteproperty -name MemoryTotalGB -value $_.MemoryTotalGB
$HostStuff | Add-Member -type noteproperty -name Model -value $_.Model
$HostStuff | Add-Member -type noteproperty -name NetworkInfo -value $_.NetworkInfo
$HostStuff | Add-Member -type noteproperty -name Cluster -value $_.Parent
$HostStuff | Add-Member -type noteproperty -name Storage -value $_.StorageInfo

$HostHashTable[$HostStuff.HostName]=$HostStuff.Parent

write-output $HostStuff
}
End {}
}

Function Get-VMInfo {
Begin {}
Process {
$VMStuff=new-object psobject
$VMHashTable=@{}

$VMStuff | Add-Member -type noteproperty -name VMName -value $_
$VMStuff | Add-Member -type noteproperty -name Mnmemonic -value $_.Folder
$VMStuff | Add-Member -type noteproperty -name Host -value $_.Host
#Link the VM to its cluster location
$VMStuff | Add-Member -type noteproperty -name HostCluster -value $HostHashTable[$VMStuff.Host]
$VMStuff | Add-Member -type noteproperty -name PowerState -value $_.PowerState
$VMStuff | Add-Member -type noteproperty -name ProvisionedSpaceGB -value $_.ProvisionedSpaceGB
$VMStuff | Add-Member -type noteproperty -name UsedSpace -value $_.UsedSpaceGB
$VMStuff | Add-Member -type noteproperty -name Version -value $_.Version
#Get Network Adapter Information on each instance
$AdapterInfo=Get-NetworkAdapter $_
foreach ($Adapter in $AdapterInfo) {
$j=1
$VMStuff | Add-Member -type noteproperty -name VMNIC$j -value $Adapter.Name
$VMStuff | Add-Member -type noteproperty -name NICMAC$j -value $Adapter.MACAddress
$VMStuff | Add-Member -type noteproperty -name NICNetwork$j -value $Adapter.NetworkName
$VMStuff | Add-Member -type noteproperty -name NICState$j -value $Adapter.ConnectionState
$j++
} #end foreach adapter loop
#Get hard disk information for each instance
$HardDisks=Get-Harddisk $_
foreach ($HD in $Harddisks) {
$i=1
$VMStuff | Add-Member -type noteproperty -name HDName$i -value $HD.Name -force
$VMStuff | Add-Member -type noteproperty -name StorageFormat$i -value $HD.StorageFormat -force
$VMStuff | Add-Member -type noteproperty -name HDCapacityGB$i -value $HD.CapacityGB -force
$VMStuff | Add-Member -type noteproperty -name Datastore$i -value $HD.Filename -force
$i++
} #end foreach HD loop
#Select only the windows machines for the obtaining information from WMI
if ($_ -like "w*"){
$OSinfo=Get-WmiObject -Class Win32_OperatingSystem -computer $_
$VMStuff | Add-Member -type noteproperty -name OSSerialNumber -value $OSinfo.SerialNumber
$VMStuff | Add-Member -type noteproperty -name OSVersion -value $OSinfo.Version
$VMStuff | Add-Member -type noteproperty -name OSVServicePack -value $OSinfo.ServicePackMajorVersion
$VMStuff | Add-Member -type noteproperty -name OSCaption -value $OSinfo.Caption
$VMStuff | Add-Member -type noteproperty -name OSInstallDate -value $OSinfo.InstallDate
$VMStuff.OSInstallDate=([WMI]'').ConvertToDateTime(($OSInfo).InstallDate).ToString("yyyy-MM-dd HH:mm:ss")
$VMStuff | Add-Member -type noteproperty -name OSLastBoot -value $OSinfo.LastBootupTime
$VMStuff.OSLastBoot=([WMI]'').ConvertToDateTime(($OSInfo).LastBootUpTime).ToString("yyyy-MM-dd HH:mm:ss")
$ComputerInfo=Get-WmiObject -Class Win32_ComputerSystem -computer $_
$VMStuff | Add-Member -type noteproperty -name OSSystemType -value $ComputerInfo.SystemType
#Get IP information from each adapter
$OSNICArray=@()
$OSNICrecord=get-wmiobject Win32_NetworkAdapterConfiguration -filter IPEnabled=TRUE -computer $_|select Caption,IPAddress,IPSubnet,DefaultIPGateway,DNSServerSearchOrder,DNSDomainSUffixSearchOrder
$OSNICArray+=$OSNICrecord
foreach ($LAN in $OSNICArray) {
$VMStuff | Add-Member -type noteproperty -name OSAdapterName -value $LAN.Caption
$VMStuff | Add-Member -type noteproperty -name OSIPAddress -value $LAN.IPAddress
$VMStuff | Add-Member -type noteproperty -name OSIPSubnet -value $LAN.IPSubnet
$VMStuff | Add-Member -type noteproperty -name OSGateway -value $LAN.DefaultIPGateway
$VMStuff | Add-Member -type noteproperty -name OSDNSSearchOrder -value $LAN.DNSServerSearchOrder
$VMStuff | Add-Member -type noteproperty -name OSDNSSuffix -value $LAN.dnsdomainsuffixsearchorder
} #end foreach LAN loop
} #end if

$VMHashTable[$VMStuff.VMName]=$VMStuff.Host

write-output $VMStuff
}
End {}

}
#Specify location to search
$Location="My-Place"
Connect-VIServer "VIServer1"

#Gather the host information
$HostatSite=get-vmhost -Location $Location
$HostatSite|Get-Hostinfo
#$HostatSite|Get-Hostinfo|export-csv 'd:\scripts\server-report\'$Location'-HostReport.csv'

#Gather the VM information
$VMatSite=get-vm -Location $Location
#$VMatSite|Get-VMInfo|export-csv d:\scripts\server-report\$Location-VMReport.csv -NoTypeInformation
#$VMatSite|Get-VMInfo|ConvertTo-Html|Set-Content d:\scripts\server-report\$Location-VMReport.htm
$VMatSite|Get-VMInfo

Tags (2)
Reply
0 Kudos
1 Solution

Accepted Solutions
RvdNieuwendijk
Leadership
Leadership
Jump to solution

The problem is that the Export-CSV cmdlet looks at the properties of the first record it reads and outputs only those properties for all the records. You can let the script write a dummy record as the first record with all the columns that are possible. That will solve this problem. 

Blog: https://rvdnieuwendijk.com/ | Twitter: @rvdnieuwendijk | Author of: https://www.packtpub.com/virtualization-and-cloud/learning-powercli-second-edition

View solution in original post

Reply
0 Kudos
11 Replies
RvdNieuwendijk
Leadership
Leadership
Jump to solution

You can get the cluster name from a VirtualMachineImpl object by $_.VMHost.Parent.Name. So you don't need the hashtable.

In the Get-VMInfo function the "$i=1" and "$j=1" lines should be before the foreach startement and not inside the loop.

Blog: https://rvdnieuwendijk.com/ | Twitter: @rvdnieuwendijk | Author of: https://www.packtpub.com/virtualization-and-cloud/learning-powercli-second-edition
markdjones82
Expert
Expert
Jump to solution

I have a general question on the functions he wrote.  In order to accept the object from the pipeline do you have to have the begin, process, end?

http://www.twitter.com/markdjones82 | http://nutzandbolts.wordpress.com
Reply
0 Kudos
JohnnyScript
Contributor
Contributor
Jump to solution

Out of habit, I put in the begin{} and end{} sections of the function although it is not needed here.

Reply
0 Kudos
JohnnyScript
Contributor
Contributor
Jump to solution

Ahh, I think you're correct. I will try it first thing in the morning and then mark it as completed.  Thanks.

Reply
0 Kudos
RvdNieuwendijk
Leadership
Leadership
Jump to solution

To accept objects from the pipeline you only need the process section. The begin section is used to do things you want to do before processing the objects in the pipeline.The end section is used to do things after processing the objects in the pipeline.

The functions shown only accept pipeline input and don't accept parameter values. The next function shows the framework of a function that accepts pipeline and parameter input. It also shows the use of the begin section.

function Get-VMInfo {
<#
  .SYNOPSIS
    This function retrieves info about virtual machines.

  .DESCRIPTION
    This function retrieves info about virtual machines.

  .PARAMETER  VM
    Specifies virtual machines whose information you want to retrieve.

  .EXAMPLE
    PS C:\> Get-VMInfo -VM VM1,VM2

  .EXAMPLE
    PS C:\> Get-VMHost -Name ESX1.yourdomain.com | Get-VM | Get-VMInfo

  .INPUTS
    System.String,VMware.VimAutomation.ViCore.Impl.V1.Inventory.VirtualMachineImpl

  .OUTPUTS
    PSObject

#>

  [CmdletBinding()]
  [OutputType([PSObject])]
  param(
    [parameter(Mandatory = $false,
               ValueFromPipeline = $true,
               ValueFromPipelineByPropertyName = $true)]
    $VM = "*"
  )
  
  begin
  {
    if ($VM.GetType().Name -eq "string")
    {
      $VM = Get-VM -Name $VM
    }
  }
  
  process
  {
    foreach ($CurrentVM in $VM)
    {
      if ($CurrentVM.GetType().Name -eq "VirtualMachineImpl")
      {
        New-Object -TypeName PSObject -Property @{
          Name = $CurrentVM.Name
          Cluster = $CurrentVM.VMHost.Parent.Name
        }
      }
      else
      {
        Write-Warning "Parameter VM it's value should be of type String or VirtualMachineImpl."
      }
    }
  }
}

Blog: https://rvdnieuwendijk.com/ | Twitter: @rvdnieuwendijk | Author of: https://www.packtpub.com/virtualization-and-cloud/learning-powercli-second-edition
Reply
0 Kudos
JohnnyScript
Contributor
Contributor
Jump to solution

I made the adjustments and it works when I simply output the data to the screen.  When I export to CSV, however, the extra columns that the windows-based virtual machines have (designated with the  "if ($_ -like "w*)" statement) are not exported to the CSV.  Any ideas on how to gather these extra columns for select machines into a CSV?

Reply
0 Kudos
RvdNieuwendijk
Leadership
Leadership
Jump to solution

The problem is that the Export-CSV cmdlet looks at the properties of the first record it reads and outputs only those properties for all the records. You can let the script write a dummy record as the first record with all the columns that are possible. That will solve this problem. 

Blog: https://rvdnieuwendijk.com/ | Twitter: @rvdnieuwendijk | Author of: https://www.packtpub.com/virtualization-and-cloud/learning-powercli-second-edition
Reply
0 Kudos
LucD
Leadership
Leadership
Jump to solution

There is no need to insert a dummy record, just order descending on the number of properties of each object.

Something like this

$VMatSite| Get-VMInfo| 
Sort-Object
-Descending -Property {$_ | Get-Member | Measure-Object | Select -ExpandProperty Count} |
Export-Csv d:\scripts\server-report\$Location-VMReport.csv -NoTypeInformation

The disadvantage is that your CSV file will not be ordered in a nice way


Blog: lucd.info  Twitter: @LucD22  Co-author PowerCLI Reference

RvdNieuwendijk
Leadership
Leadership
Jump to solution

This is a nice trick that will work if only the number of objects of one type is variable. It might not work in this case because the number of network adapters, hard disks and NIC's are variable. For example if the vm's with the mosts disks don't have the most network adapters then it is possible that not all the network adapters will be in the csv file.

Blog: https://rvdnieuwendijk.com/ | Twitter: @rvdnieuwendijk | Author of: https://www.packtpub.com/virtualization-and-cloud/learning-powercli-second-edition
Reply
0 Kudos
JohnnyScript
Contributor
Contributor
Jump to solution

LucD,

I put in the code but it seems to simply hang.  It initially creates the file but it remains at 0kb for over an hour.  Granted, I have many VMs but all my others processed well under that time frame.  I would be Ok with creating dummy columns in advance to the dump.  For example, I would be OK to limit the number of network adapters to two and disks to eight (they appear to be my maximum) as well as saving room for all the windows servers to report their extra columns (e.g., SystemType).  How would I do that?

Reply
0 Kudos
LucD
Leadership
Leadership
Jump to solution

There is of course an overhead due to the Sort step, but over 1 hour seems indeed very long.

And with multiple variable properties that trick will indeed not work, as was remarked correctly.

You could start with a dummy record, that contains the maximum number of all properties, and then add that dummy record first.

To create such a dummy record you could use the same Add-Member cmdlets as you did now, just provide a $null value.

And since you make sure that is the first entry in the array, you don't need the Sort step anymore of course.


Blog: lucd.info  Twitter: @LucD22  Co-author PowerCLI Reference

Reply
0 Kudos