Create dynamic property names for a collection

I am extracting CPU properties from my Cisco IMC UCS rack servers, and some rack servers have one, two or four CPUs. My goal is to collect the CPU information and add them to my collection which is exported to CSV. My problem is that the collection should look like this, but it is not resulting this way:

Item 1: CPUID_1, CPUID_2
Item 2: CPUID_1, CPUID_2, CPUID_3, CPUID_4
Item 3: CPUID_1, CPUID_2
Item 4: CPUID_1

So I am trying to write my code so that the properties of the collection change to match the number of CPUs, however I cannot get this to work. My results are:

Item 1: CPUID_$CPUCounter, CPUID_$CPUCounter
Item 2: CPUID_$CPUCounter, CPUID_$CPUCounter, CPUID_$CPUCounter, CPUID_$CPUCounter
Item 3: CPUID_$CPUCounter, CPUID_$CPUCounter
Item 4: CPUID_$CPUCounter

Can you advise how I can create dynamic property headers?

My Code:

$CPUCounter = 1
        ForEach ($IMCServerProcessor in $IMCServerProcessors)
            {
            $Item | Add-Member -type NoteProperty -Name 'CPUID_$CPUCounter' -Value @($IMCServerProcessor.Id)
            $Item | Add-Member -type NoteProperty -Name 'CPUModel_$CPUCounter' -Value @($IMCServerProcessor.Model)
            $Item | Add-Member -type NoteProperty -Name 'OperState_$CPUCounter' -Value @($IMCServerProcessor.OperState)
            $Item | Add-Member -type NoteProperty -Name 'Presence_$CPUCounter' -Value @($IMCServerProcessor.Presence)
            $Item | Add-Member -type NoteProperty -Name 'SocketDesignation_$CPUCounter' -Value @($IMCServerProcessor.SocketDesignation)
            $Item | Add-Member -type NoteProperty -Name 'CPUVendor_$CPUCounter' -Value @($IMCServerProcessor.Vendor)
            $Item | Add-Member -type NoteProperty -Name 'CPUIMC_$CPUCounter' -Value @($IMCServerProcessor.Imc)
            $Item | Add-Member -type NoteProperty -Name 'Dn_$CPUCounter' -Value @($IMCServerProcessor.Dn)
            $Item | Add-Member -type NoteProperty -Name 'Rn_$CPUCounter' -Value @($IMCServerProcessor.Rn)
            $CPUCounter += 1
            }

I figgered out a way to do this. The minimum properties for a server in my case is 19 which includes one CPU’s worth of properties.
Before collecting all server properties, this is assigned to a variable which records the maximum properties collected for any one server. This is stored in variable $MaxPropertiesCount.
The code then enumerates all the server properties including the CPU properties and places them in a collection called “$IMCRackServerInventory”. It counts the number of properties for the current server and stores this count in the variable $ItemPropertyCount.
While it is enumerating all the properties, it compares the number of server properties enumerated ($ItemPropertyCount) against the most properties enumerated so far ($MaxPropertiesCount). If the current server has more properties than the last ones, the $MaxPropertiesCount is updated to match this value.
Once all servers’ properties have been enumerated, the variable $IMCRackServerInventory now contains varying number of properties per record. The records with less properties than $MaxPropertiesCount need to have the appropriate number of “dummy” variables added. This is the cool part.
The ForEach cycles thru all records of the collection $IMCRackServerInventory, assigning each record to the variable $ServerData. If the current $ServerData has less properties then the maximum counted, a loop is initiated where the dummy properties are added to $ServerData.
This is the interesting part. $ServerData seems to be dynamically linked to each record in $IMCRackServerInventory- what I mean here is that any changes to $ServerData will change the respective value in $IMCRackServerInventory via the ForEach.
Here’s the code:





#
# ================================FUNCTION DECLARATION START====================================================================
#
 
#
# Check the required columns for this script to run, are present in the user-selected CSV import file.
Function CheckCSVColumnsExist
    {
    Param(
        [Object]$CSVImportFile,
        [Array]$ColumnsToCheck = ''    
        )
 
    $ColumnHeaders = (Import-Csv $CSVImportFile | Get-Member -MemberType NoteProperty).Name
    $MissingColumnHeaders = @()
    ForEach( $ColumnToCheck in $ColumnsToCheck)
        {
        $MissingColumnName = New-Object PSObject
        If ($ColumnHeaders -match $ColumnToCheck )
            {
            # Nothing to do.
            }
        Else
            {
            $MissingColumnName | Add-Member -type NoteProperty -Name 'Column_Name' -Value $ColumnToCheck
            $MissingColumnHeaders += $MissingColumnName
            }
         
        }
    Return $MissingColumnHeaders
    }
   
# This function shows the Open File dialog to the user so the user can
# select the import CSV file.
#
Function Get-FileName($InitialDirectory)
    {
    [System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms") | Out-Null
  
    $OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog
    $OpenFileDialog.initialDirectory = $initialDirectory
    $OpenFileDialog.filter = "CSV (*.csv) | *.csv"
    $OpenFileDialog.ShowDialog() | Out-Null
    $OpenFileDialog.FileName
    }
 
#
# ================================FUNCTION DECLARATION FINISH===================================================================
#
 
#
# ================================SCRIPT START==================================================================================
#
#
Import-Module Cisco.IMC

# The input CSV file contains the details required for the script
# to log into each Cisco UCS server.
#
# Get the CSV import file name and path:
$ImportCSVFile = Get-FileName

Write-Host "Script starting." -ForegroundColor Green
 
# Check if the user cancelled the request.
if ($ImportCSVFile -eq "")
    { # They did!
    Throw "No file selected. Ending script"
    }

# Load the CSV file contents.
$UCSRackServerReport = Import-CSV $ImportCSVFile -ErrorAction SilentlyContinue
# Ask the user for the CIMC credential details.
$IMCServerCredential = Get-Credential admin

$IMCRackServerInventory = @()
# Read each entry in the import CSV file, then try to logon to the 
# CIMC of each one and collect hardware details.
$MaxPropertiesCount = 19
ForEach ($vRow in $UCSRackServerReport)
    {
    $Item = New-Object PSObject
    # Get the server CIMC IP.
    $RackServerIMCIP = $vRow."IP Address"
    # Get the server User Label and Host Name.
    $RackServerIMCUserLabel = $vRow."User Label"
    $RackServerIMCHostName = $vRow."Host Name"

    # If the User Label is blank, use the Host Name for console output updates.
    If ([string]::IsNullOrWhiteSpace($RackServerIMCUserLabel) )
        {
        $StringLabel = "[$RackServerIMCHostName/$RackServerIMCIP]"
        }
    Else
        {
        $StringLabel = "[$RackServerIMCUserLabel/$RackServerIMCIP]"
        }
    Try
        {
        # Let's connect to the CIMC.
        $Connected = $True
        Write-Host "$StringLabel Attempting a connection to server IMC." -ForegroundColor Yellow
        $IMCHandle = Connect-IMC -Name $RackServerIMCIP -Credential $IMCServerCredential -NotDefault -ErrorAction Stop
        }

    Catch [System.NullReferenceException]
        {
        Write-Host "$StringLabel Error: $($PSItem.ToString())" -ForegroundColor Red
        $Connected = $False
        $ErrorPropertyValue = "Null Reference Error"
        }

    Catch [System.Exception]
        {
        # Did not connect due to connection issue.
        Write-Host "$StringLabel Error: $($PSItem.ToString())" -ForegroundColor Red
        If ($PSItem.Exception.ToString().Contains("Unable to connect"))
            {
            # Set connection flag false.
            $Connected = $False
            $ErrorPropertyValue = "Not Connected"
            }
        Else
            {
            $Connected = $False
            $ErrorPropertyValue = "System Exception"
            Write-Host "Not a connection issue." -ForegroundColor Red
            }
        }
    Catch
        {
        Write-Host "$StringLabel Error: $($PSItem.ToString())" -ForegroundColor Red
        $Connected = $False
        $ErrorPropertyValue = "Unknown Error"
        }

    
    # Did we connect to the CIMC?
    If (!$Connected)
        {

        # No we did not. Record this event.
        If ([string]::IsNullOrWhiteSpace($RackServerIMCUserLabel) )
            {
            $NewUserLabel = $RackServerIMCHostName
            }
        Else
            {
            $NewUserLabel = $RackServerIMCUserLabel 
            }
        # Set collection details and the properties not read in will be set to $ErrorPropertyValue.
        $Item | Add-Member -type NoteProperty -Name 'UserLabel' -Value $NewUserLabel
        $Item | Add-Member -type NoteProperty -Name 'Model' -Value $ErrorPropertyValue
        $Item | Add-Member -type NoteProperty -Name 'Serial' -Value $ErrorPropertyValue
        $Item | Add-Member -type NoteProperty -Name 'ServerID' -Value $ErrorPropertyValue
        $Item | Add-Member -type NoteProperty -Name 'CIMC_IP' -Value $RackServerIMCIP
        $Item | Add-Member -type NoteProperty -Name 'Vendor' -Value $ErrorPropertyValue
        $Item | Add-Member -type NoteProperty -Name 'IMCName' -Value $ErrorPropertyValue
        $Item | Add-Member -type NoteProperty -Name 'DN' -Value $ErrorPropertyValue
        $Item | Add-Member -type NoteProperty -Name 'Rn' -Value $ErrorPropertyValue
        $Item | Add-Member -type NoteProperty -Name 'BMCFirmwarePackage' -Value $ErrorPropertyValue
        $Item | Add-Member -type NoteProperty -Name 'CPUID_1' -Value $ErrorPropertyValue
        $Item | Add-Member -type NoteProperty -Name 'CPUModel_1' -Value $ErrorPropertyValue
        $Item | Add-Member -type NoteProperty -Name 'OperState_1' -Value $ErrorPropertyValue
        $Item | Add-Member -type NoteProperty -Name 'Presence_1' -Value $ErrorPropertyValue
        $Item | Add-Member -type NoteProperty -Name 'SocketDesignation_1' -Value $ErrorPropertyValue
        $Item | Add-Member -type NoteProperty -Name 'CPUVendor_1' -Value $ErrorPropertyValue
        $Item | Add-Member -type NoteProperty -Name 'CPUIMC_1' -Value $ErrorPropertyValue
        $Item | Add-Member -type NoteProperty -Name 'Dn_1' -Value $ErrorPropertyValue
        $Item | Add-Member -type NoteProperty -Name 'Rn_1' -Value $ErrorPropertyValue
        }
    Else
        {
        # We connected OK.
        Write-Host "$StringLabel Connection to server IMC established." -ForegroundColor Cyan
        Write-Host "$StringLabel Gathering Rack Unit data." -ForegroundColor Cyan
        # Get rack server hardware details.
        $IMCServerUnit = Get-IMCRackUnit -Imc $IMCHandle
        # Check if User Label is blank and if so, use the IMC field as the server label.
        # Get the firmware package details.
        $IMCFirmwarePackage = Get-ImcFirmwareRunning -Imc $IMCHandle | Where {$_.Deployment -eq "system" -and $_.Type -eq "blade-controller"}
        # Add the firmware package details to the collection.
        
        If ([string]::IsNullOrWhiteSpace($IMCServerUnit.UsrLbl) )
            {
            $NewUserLabel = $IMCServerUnit.Imc
            }
        Else
            {
            $NewUserLabel = $IMCServerUnit.UsrLbl
            }
        # Record rack server hardware details.
        $Item | Add-Member -type NoteProperty -Name 'UserLabel' -Value $NewUserLabel
        $Item | Add-Member -type NoteProperty -Name 'Model' -Value $($IMCServerUnit.Model)
        $Item | Add-Member -type NoteProperty -Name 'Serial' -Value $($IMCServerUnit.Serial)
        $Item | Add-Member -type NoteProperty -Name 'ServerID' -Value $($IMCServerUnit.ServerId)
        $Item | Add-Member -type NoteProperty -Name 'CIMC_IP' -Value $RackServerIMCIP
        $Item | Add-Member -type NoteProperty -Name 'Vendor' -Value $($IMCServerUnit.Vendor)
        $Item | Add-Member -type NoteProperty -Name 'IMCName' -Value $($IMCServerUnit.Imc)
        $Item | Add-Member -type NoteProperty -Name 'DN' -Value $($IMCServerUnit.Dn)
        $Item | Add-Member -type NoteProperty -Name 'Rn' -Value $($IMCServerUnit.Rn)
        $Item | Add-Member -type NoteProperty -Name 'BMCFirmwarePackage' -Value $($IMCFirmwarePackage.Version)
        Write-Host "$StringLabel Gathering Processor data." -ForegroundColor Cyan
        $CPUCounter = 1
        # Get the rack server processor details.
        $IMCServerProcessors = Get-IMCProcessorUnit -Imc $IMCHandle
        # For multiple processors, cycle thru each object found.
        ForEach ($IMCServerProcessor in $IMCServerProcessors)
            {
            # Dynamically create the property name with "_#" at the end for each processor. Record processor details.
            $FieldName = "CPUID_$CPUCounter"; $Item | Add-Member -type NoteProperty -Name $FieldName -Value $($IMCServerProcessor.Id)
            $FieldName = "CPUModel_$CPUCounter"; $Item | Add-Member -type NoteProperty -Name $FieldName -Value $($IMCServerProcessor.Model)
            $FieldName = "OperState_$CPUCounter"; $Item | Add-Member -type NoteProperty -Name $FieldName -Value $($IMCServerProcessor.OperState)
            $FieldName = "Presence_$CPUCounter"; $Item | Add-Member -type NoteProperty -Name $FieldName -Value $($IMCServerProcessor.Presence)
            $FieldName = "SocketDesignation_$CPUCounter"; $Item | Add-Member -type NoteProperty -Name $FieldName -Value $($IMCServerProcessor.SocketDesignation)
            $FieldName = "CPUVendor_$CPUCounter"; $Item | Add-Member -type NoteProperty -Name $FieldName -Value $($IMCServerProcessor.Vendor)
            $FieldName = "CPUIMC_$CPUCounter"; $Item | Add-Member -type NoteProperty -Name $FieldName -Value $($IMCServerProcessor.Imc)
            $FieldName = "Dn_$CPUCounter"; $Item | Add-Member -type NoteProperty -Name $FieldName -Value $($IMCServerProcessor.Dn)
            $FieldName = "Rn_$CPUCounter"; $Item | Add-Member -type NoteProperty -Name $FieldName -Value $($IMCServerProcessor.Rn)
            $CPUCounter += 1
            }
        Write-Host "$StringLabel Gathering Firmware data." -ForegroundColor Cyan

        }

    $ItemPropertyCount = ($Item | Get-Member -MemberType NoteProperty ).Count
    Write-Host "$StringLabel ItemPropertyCount  = [$ItemPropertyCount]."
    Write-Host "$StringLabel MaxPropertiesCount = [$MaxPropertiesCount]."
    If ($ItemPropertyCount -gt $MaxPropertiesCount)
        {
        $MaxPropertiesCount = $ItemPropertyCount
        }
    Else
        {
        #
        }
    $IMCRackServerInventory += $Item
    Write-Host "$StringLabel Data collection completed." -ForegroundColor Blue
    # Close the connection to the server's CIMC.
    If ($IMCHandle -ne $Null )
        {
        $CloseIMCHandle = Disconnect-IMC -Imc $IMCHandle
        }
    Write-Host "$StringLabel Closing session to server IMC." -ForegroundColor Blue
    Write-Host ""
    }

ForEach ($ServerData in $IMCRackServerInventory)
    {
    # Compare the number of properties in ServerData to the minimum number.
    $ServerDataPropertyCount = ($ServerData | Get-Member -MemberType NoteProperty ).count
    If ($ServerDataPropertyCount -lt $MaxPropertiesCount)
        {
        $MaxCPUs = ($MaxPropertiesCount - 10) /9
        $TotalCPUs =  ($ServerDataPropertyCount - 10) /9
        # ServerData contains less properties - thus less CPU properties.
        $NextCPUField = $TotalCPUs + 1
        # Create dummy CPU properties to fill the collection properties for this record.
        For ($Index = $NextCPUField; $Index -le $MaxCPUs; $Index++)
            {
            
            $FieldName = "CPUID_$Index"; $ServerData | Add-Member -type NoteProperty -Name $FieldName -Value "NA"
            $FieldName = "CPUModel_$Index"; $ServerData | Add-Member -type NoteProperty -Name $FieldName -Value "NA"
            $FieldName = "OperState_$Index"; $ServerData | Add-Member -type NoteProperty -Name $FieldName -Value "NA"
            $FieldName = "Presence_$Index"; $ServerData | Add-Member -type NoteProperty -Name $FieldName -Value "NA"
            $FieldName = "SocketDesignation_$Index"; $ServerData | Add-Member -type NoteProperty -Name $FieldName -Value "NA"
            $FieldName = "CPUVendor_$Index"; $ServerData | Add-Member -type NoteProperty -Name $FieldName -Value "NA"
            $FieldName = "CPUIMC_$Index"; $ServerData | Add-Member -type NoteProperty -Name $FieldName -Value "NA"
            $FieldName = "Dn_$Index"; $ServerData | Add-Member -type NoteProperty -Name $FieldName -Value "NA"
            $FieldName = "Rn_$Index"; $ServerData | Add-Member -type NoteProperty -Name $FieldName -Value "NA"
            }
        }
        

    }




# Write the collected data to CSV file.
$IMCRackServerInventory | Export-CSV -Path "UCS_IMC_RackServer_Inventory.csv" -NoTypeInformation -UseCulture
Write-Host "Script Finished." -ForegroundColor Green

Sorry for the late answer.

It seems like you’re trying to press hierarchical data into a structured data format. Depending on the purpose of these data there might be a better way. What’s the actual purpose of the data collection?

Regardless of that … using Add-Member is a kind of outdated, quite cumbersome to write and hard to read. Nowadays - since PowerShell version 3.0 - we use [PSCustomObject] for those purposes.

Regardless of that … using Write-Host for logging purposes is a bad idea.

Regardless of that …

Do these CPUs from one Cisco IMC UCS rack server differ from each other? I’d expect these CPUs to be the same. In this case it should be enough to get the information about the CPUs once and add a counter how many of these CPUs are in this particular server, or am I wrong?

Thanks for your reply. I have read thru the documents you linked and they are helpful. The CPUs of each rack server differs depending on when the server was built as we keep adding more servers over time and the processors generally evolve as well. The code has to enumerate the CPUs of each server it logs into in order to get the CPU info, so the data has been obtained once per server. Remember that the results are exported to CSV so if I used a counter for each CPU type, I’d have to re-convert the counters back to actual CPU details in order for those details to appear in the CSV.

It looks to me like using the code:

ForEach ($IMCServerProcessor in $IMCServerProcessors)

Seems to create a linked record in $IMCServerProcessor to the parent $IMCServerProcessors so I was able to “pad-out” the “missing” properties and it seems to work really well.

Thanks again.

Hmmm … I think I didn’t make myself clear. What I meant and understood …

You wrote initially …

And you listed them like this:

I assumed Item 1 is one server. And if one single server uses only one particular type of CPU it should be enough when you put it like this somehow:

ServerName CPU-ID    CPU-Count
---------- ------    ---------
Serer01    CPUID_1           4
Serer02    CPUID_2           2
Serer03    CPUID_12          6
Serer04    CPUID_6          24

Anyway …

I’m glad to hear that you’ve found a solution yourself and that it works for you. :+1:t3:

To clarify, if a server has one physical CPU, the record in the collection for that server will go to CPU_1 which will have say 9 properties such as:
Server1: CPUID_1: 1
Server1: CPUModel_1: Xeon Gold 6145
Server1: OperState_1: Operational
Server1: Presence_1: Available
Server1: SocketDesignation_1: 1
Server1: CPUVendor_1: Cisco
Server1: CPUIMC: True
Server1: Dn_1: cpu-1
Server1: Rn_1: sys\rack_server_1\cpu-1

So a server with 2 CPUs will have 18 CPU properties. And these properties will become just columns in a CSV file. The aim is to extract the server physical details, firmware version and CPU types from each rack server so that I can determine the firmware upgrade path- the firmware to use is determined by the CPU Model.

And yeah, quite by surprise I was able to “pad” the records so that all records in the collection had the same number of properties/columns so that the Export-CSV would export ALL the properties of ALL the records. Like I said before, if ONE record has only 9 properties and all the others have say 28, then only 9 properties are exported to CSV by the Export-CSV command.

OK, but aren’t the properties of these two CPUs exactly the same? That’s what I meant.

1 Like

I agree with Olaf, your example here there would still be only 9 properties. You’d only get different properties if you have DIFFERENT cpus which is what I believe you are trying to say. If I understood correctly, then you probably want to say something more like this.

Server1 has 1 or more CPU that has a specific set of properties.
Server2 has 1 or more CPUs with specific set of properties that may include property names that are the same as server1 but may also have different properties.
You want to normalize the properties across all the CPUs

Here is how I handle situations like this.

# silly sample list to illustrate the point

$cpulist = [PSCustomObject]@{
    Brand = 'Intel'
    Speed = 'Fast'
    Cache = 'Yes please'
},
[PSCustomObject]@{
    Brand = 'Intel'
    Speed = 'Fast'
    Cache = 'Only on Tuesdays'
    Firmware = '0.1.0'
},
[PSCustomObject]@{
    Brand = 'Intel'
    Speed = 'Fast'
    Firmware = '0.1.0'
    Pins = 420
},
[PSCustomObject]@{
    Brand = 'AMD'
    Cache = '3MB'
    L2Cache = '6MB'
    Mood = 'cranky'
}

Now when you output these, you’ll see the “columns” are dictated by the first item.

$cpulist

Brand Speed Cache           
----- ----- -----           
Intel Fast  Yes please      
Intel Fast  Only on Tuesdays
Intel Fast                  
AMD         3MB

If a different CPU was first you’d have different columns

$cpulist | Select-Object -Skip 1

rand Speed Cache            Firmware
----- ----- -----            --------
Intel Fast  Only on Tuesdays 0.1.0   
Intel Fast                   0.1.0   
AMD         3MB                      

To see each of these list their properties, you could force it with something like this

$cpulist | Foreach-Object {$_ | Out-Host}
Brand Speed Cache     
----- ----- -----     
Intel Fast  Yes please

Brand Speed Cache            Firmware
----- ----- -----            --------
Intel Fast  Only on Tuesdays 0.1.0   

Brand Speed Firmware Pins
----- ----- -------- ----
Intel Fast  0.1.0     420

Brand Cache L2Cache Mood  
----- ----- ------- ----  
AMD   3MB   6MB     cranky

But of course, that doesn’t look good and it definitely won’t export properly. We need to build a dynamic list of properties

$propertylist = New-Object System.Collections.Generic.list[string]

foreach($cpu in $cpulist){
    foreach($property in $cpu.psobject.properties.name){
        if($property -notin $propertylist){
            $propertylist.Add($property)
        }
    }
}

Now we can see all the unique properties and export to csv/json/xml

$cpulist | Select-Object -Property $propertylist | Format-Table

Brand Speed Cache            Firmware Pins L2Cache Mood  
----- ----- -----            -------- ---- ------- ----  
Intel Fast  Yes please                                   
Intel Fast  Only on Tuesdays 0.1.0                       
Intel Fast                   0.1.0    420                
AMD         3MB                            6MB     cranky

Please note the use of Format-Table was only to make the output pretty for the console. You do not want to use that for normal processing.

# Make a new list of CPUs with all the dynamic properties
$newcpulist = $cpulist | Select-Object -Property $propertylist

# Export
$newcpulist | ConvertTo-Csv -NoTypeInformation

The psobject hidden member can be quite useful in situations like this.

1 Like