Powershell script: create loop for ResponseTime - powershell

I am having an issue with the way that my ping results "roll" out on the screen. I am using this code:
$servers = "192.168.2.10","192.168.2.80","192.168.2.254"
$collection = $()
foreach ($server in $servers)
{
$status = #{ "ServerName" = $server; "TimeStamp" = (Get-Date -f s) }
$testconnection = (Test-Connection $server -Count 1 -ea 0)
$response = ($testconnection | select ResponseTime)
if ($response)
{
$status["Results"] = "Up"
$status["Responsetime"] = $response
}
else
{
$status["Results"] = "Down"
}
New-Object -TypeName PSObject -Property $status -OutVariable serverStatus
$collection += $serverStatus
}
$collection | Export-Csv -Path ".\ServerStatus.csv" -NoTypeInformation
I would to like create a loop for the ResponseTime
The code that I am using now gives one response.
When I give a count of 2, it prints the ResponseTime next to eachother per IP-adres.
Output:
TimeStamp Responsetime Results ServerName
--------- ------------ ------- ----------
2014-10-22T23:30:17 {#{ResponseTime=6}, #{ResponseTime=4}} Up 192.168.2.10
2014-10-22T23:30:18 Down 192.168.2.80
2014-10-22T23:30:25 {#{ResponseTime=1}, #{ResponseTime=3}} Up 192.168.2.254
What I want is, that the script prints each ResponseTime under eachother like this:
TimeStamp Responsetime Results ServerName
--------- ------------ ------- ----------
2014-10-22T23:11:50 #{ResponseTime=419} Up 192.168.2.10
2014-10-22T23:11:51 #{ResponseTime=415} Up 192.168.2.10
2014-10-22T23:11:51 Down 192.168.2.80
2014-10-22T23:11:52 #{ResponseTime=470} Up 192.168.2.254
2014-10-22T23:11:52 #{ResponseTime=7} Up 192.168.2.254
Or like this:
TimeStamp Responsetime Results ServerName
--------- ------------ ------- ----------
2014-10-22T23:11:50 #{ResponseTime=419} Up 192.168.2.10
2014-10-22T23:11:51 Down 192.168.2.80
2014-10-22T23:11:51 #{ResponseTime=415} Up 192.168.2.254
2014-10-22T23:11:52 #{ResponseTime=470} Up 192.168.2.10
2014-10-22T23:11:51 Down 192.168.2.80
2014-10-22T23:11:52 #{ResponseTime=7} Up 192.168.2.254
It doesn't matter which one, my preference is the second one
Could you please help me with this matter. Even if it is not possible tell me aswell.
Thank you,
Chris

I'll chime in late, not because the other answer are wrong by any means, they are both functional, but more so because nobody has pointed out that you are recreating the wheel.
You test the connection, and specify an erroraction for it that silently continues leaving your variable null. Then you have to test to see if the variable has results, and treat it one way, or if it doesn't treat it another way. What you have just done is made your own Try/Catch scenario. If you actually use the error to stop you can use the built in Try/Catch. Consider this approach:
$servers = "www.google.com","localhost","www.amazon.com"
$collection = #()
foreach ($server in $servers)
{
Try{
$testconnection = Test-Connection $server -Count 2 -ErrorAction Stop
$testconnection | ForEach{$collection += New-Object PSObject -Property ([ordered]#{
'TimeStamp' = Get-Date -Format s
'Server' = $server
'ResponseTime' = $_.responsetime
'Results' = 'Up'})
}
}
Catch{
$collection += New-Object PSObject -Property ([ordered]#{
'TimeStamp' = Get-Date -Format s
'Server' = $server
'ResponseTime' = $null
'Results' = 'Unreachable'
})
}
}
$collection #| Export-Csv -Path ".\ServerStatus.csv" -NoTypeInformation
That tries to ping the server, and if it can it adds a custom object to the $collection array with the desired information. If the ping fails it also adds an object to the $collection showing that the server was unreachable.
Also, you had $collection = $(). I assume you were trying to create an empty array, which is correctly done $collection = #() (corrected in my suggested code). Now, I commented out the Export-CSV so I could see the results. This is what I saw:
TimeStamp Server ResponseTime Results
--------- ------ ------------ -------
2014-10-22T17:54:22 www.google.com 9 Up
2014-10-22T17:54:22 www.google.com 12 Up
2014-10-22T17:54:23 localhost 0 Up
2014-10-22T17:54:23 localhost 0 Up
2014-10-22T17:54:27 www.amazon.com Unreachable
Amazon didn't let me ping it, so it shows as unreachable.
Moving on to why your desired results are not practical... What you describe shows you pinging your servers and getting results from them at non-consecutive times. To do that you would have to do -count 1, and loop through the ForEach loop twice, so it would ping server 1 for 1 result, then server 2 for 1 result, then server 3 for 1 result. Then it would go back and ping server 1 for a second result, then server 2 for a second result, and then server 3 for a second result. If you wanted to do that you could I suppose, and it should give you your desired results, you would have to do something like this:
$servers = "www.google.com","localhost","www.amazon.com"
$collection = #()
$count = 2
for($i=1;$i -le $count;$i++){
ForEach($server in $servers){
do stuff to ping servers as described above, except change -count to 1
}
}
$collection | export-CSV '.\ServerStatus.csv' -notype
That will give you your desired results, but it is slower. If you have to run this against more than a few servers it will be noticeably slower. For just those three servers listed it made the entire process go from taking 3.7240945 seconds to taking 7.6104075 seconds (roughly double).

Instead of
$response = ($testconnection | select ResponseTime)
if ($response)
{
$status["Results"] = "Up"
$status["Responsetime"] = $response
}
do
if($testconnection)
{
$testconnection | % {
$status = #{"ServerName" = $server; "TimeStamp" = (Get-Date -f s); "Results" = "Up"; "Responsetime"= $_.responsetime};
New-Object -TypeName PSObject -Property $status -OutVariable serverStatus;
$collection += $serverStatus }
}
else
{
$status = #{"ServerName" = $server; "TimeStamp" = (Get-Date -f s); "Results" = "Down"};
New-Object -TypeName PSObject -Property $status -OutVariable serverStatus;
$collection += $serverStatus
}
The problem is that $testconnection or in your case $response is an array if the count of Test-Connection is greater then 1, so you have to loop through it and add the single entries to your collection.
Also to get the Value instead of the gibberish you get you have to call the .responsetime property.

In hopes I didn't make it too complicated I present this solution
$servers = "10.50.10.100","8.8.8.8","169.254.54.1"
$servers | ForEach-Object{
$server = $_
$timeStamp = (Get-Date -f s)
$testconnection = Test-Connection $server -Count 2 -ErrorAction 0
If(!$testconnection){
$props = #{
Server = $server
TimeStamp = $timeStamp
ResponseTime = ""
Results = "Down"
}
New-Object -TypeName PSObject -Property $props
} Else {
$testconnection | ForEach-Object{
$_ | Select-Object #{l='Server';e={$server}},#{l='TimeStamp';e={$timeStamp}},#{l='ResponseTime';e={$_.ResponseTime}},#{l='Results';e={"Up"}}
}
}
} | Export-Csv -Path ".\ServerStatus.csv" -NoTypeInformation
So your logic is still here but as you can see some things have been changed. Paul was right, in that you needed to loop for each ResponseTime element you had. I also have done that but with a different approach that, if nothing else, will show you some of the Power in PowerShell. A break down of the code
Pipe $servers into a ForEach-Object. ForEach in works fine however I wanted to skip the saving the variables and just output straight to Export-CSV which is why I changed it.
So if you use Test-Connection on a server that does not exist or errors for some reason then you need to create an object to represent that. Using the desired properties, build a object with required values. This is output to the pipe instead of using a temporary variable.
When a connection test is successful then we need to output a number or variables to match the number of returns.
Continuing from #3 we use Select-Object to output the desired values. l stand for label and e for expression. Yes you could easily just use another $props variable. Just illustrating another option.
Since we changed the ForEach in the first step we can just output straight to Export-CSV
Sample output
Server TimeStamp ResponseTime Results
------ --------- ------------ -------
10.50.10.100 2014-10-22T20:22:01 0 Up
10.50.10.100 2014-10-22T20:22:01 0 Up
8.8.8.8 2014-10-22T20:22:02 43 Up
8.8.8.8 2014-10-22T20:22:02 39 Up
169.254.54.1 2014-10-22T20:22:03 Down

Related

PowerShell Test-Connection -efficient Method

One Bronze medal = Newbi 😊, have scoured this site for years, so a million belated thanks, once again!
Just finished up a few different PowerShell ver 5.1 scripts that perform a test-connection on the same 2000 devices regularly.
Out to a csv file
And they are all slow, some minor speed improvements when tweaking it down to the bare bones.
I’ve scaled the script to the minimal, IPADDRESS and NAME only
I’ve run a few comparison tests against a batch file (ping 123.456.789.001 >> PingTest.txt ) and these tests APPEAR to run quicker than my PS script (will run a full apple to apples test tomorrow), but confident something is amiss in the PS code.
Am looking at arrays, and how piping works, but am afraid I’m putting the cart in front of the horse, or there is something going on with how PS handles the csv format and/or ping list in its memory
My most efficient current script below,
Is the following link pointing me in the right direction,? Specifically the asJob switch
PowerShell Mass Test-Connection
$info = "" | Select IPaddress,Name
$OutputFile = new-item -itemType File -path C:\Temp\Results.csv -force -value "IPaddress,Name`r`n" | out-null
Import-csv C:\Temp\GetList.csv | ForEach-Object {
if ($_.IPaddress) {
if (-Not (Test-Connection -ComputerName $_.IPaddress -Quiet -Count 2 -ErrorAction SilentlyContinue)) {
$info.IPaddress = $_.IPaddress
$info.Name = $_.Name
add-content -value "$($info.IPaddress),$($info.Name)" -path C:\Temp\Results.CSV
}
}
}
The export-csv is faster, thank you. I'm not sure if I have it in the right place, but it works, will run some tests on it tomorrow. I may have to include the passes tests though , still getting my head around the get-job cmdlet.
$info = "" | Select IPaddress,Name,Status
Import-csv c:\Temp\GetList.csv | ForEach-Object {
if($_.Ipaddress) {
if (-Not(Test-Connection -ComputerName $_.IPaddress -Quiet -Count 2 -ErrorAction SilentlyContinue)) {
$info.IPaddress = $_.IPaddress
$info.Name = $_.Name
$info.Status = "Failed"
$info | export-csv C:\Temp\Results1.csv -Force -Notypeinformation -Append
}
}
}
$list = Import-Csv C:\Temp\GetList.csv
ForEach ($item In $list) {
Start-Job -ScriptBlock {
param($item)
if (Test-Connection -Computername $item.IPaddress -Quiet -Count 1) {
Add-Content -value "$($item.IPaddress),$($item.Name),$($item.Stauts)" -Path C:\Temp\xlistlist.csv
$list | Export-csv c:\Temp\xCSvreults.csv -Force -NoTypeInformation -Append
}
} -argumentlist $list
}
Thanks Santiago, I did some exploring here, just ran out of time for the day. THANK YOU you for the code examples they cleared a few things up for me :-) –
Have Computer
Dec 2 at 3:59
Would you mind adding some inline comments so I can follow along? Just trynna understand multi-threading (using runspaces) a bit better. –
Abraham Zinala
Dec 2 at 4:58
1
#AbrahamZinala is there a specific part you're struggling to understand? i'm not an expert on this and wouldn't like to give false information. Might be better to pick up all the parts you don't understand and ask a new question –
Santiago Squarzon
Dec 2 at 12:19
Nah man, sorry, I figured it out lol –
Abraham Zinala
Dec 2 at 13:43
1
#AbrahamZinala the only hard part to understand is the [powershell] instance part imo, and you can think of it as a Start-Job but much faster and less memory consuming, you add your scriptblock with .AddScript({...}) and then the parameters with .AddParameters([hashtablehere]). It's important to save the PSInstance in a variable so that we can get the output out of it and it's handler (Status = $psinstance.BeginInvoke()) to know when the instance has completed. The rest is just pure blueprint, once you have one you can copy paste it :P –
Santiago Squarzon
Dec 2 at 13:50
#HaveComputer I have no idea what your comment means. –
Santiago Squarzon
Dec 5 at 1:10
the 2 solutions presented work much faster, my code however is lacking. –
Have Computer
Dec 5 at 1:16
#HaveComputer I know that, this can perform 50x faster or even more depending on hardware and bandwidth. –
Santiago Squarzon
This is a good place where you can use runspaces, I would love to test if Test-Connection -AsJob performs better than this but for some reason it is not available on PS Core on Linux.
Code below took around 10 seconds to scan 254 IPs using my private network IP range. There is a lot of tweaking that can be done, i.e. you can poke around the $Threshold variable, it's currently running 100 runspaces at a time, Count and TimeoutSeconds for Test-Connection have been set to 2 you can tweak that too.
$results variable can be exported using Export-Csv.
# Change this value for tweaking
$Threshold = 100
$RunspacePool = [runspacefactory]::CreateRunspacePool(1, $Threshold)
$RunspacePool.Open()
# This is for testing, use your CSV here instead
# => $list = Import-Csv C:\Temp\GetList.csv
$list = 1..254 | ForEach-Object {
[pscustomobject]#{
IPAddress = "192.168.1.$_"
Hostname = "ExampleHost$_"
}
}
$scriptBlock = {
param($ip, $hostname)
$params = #{
Quiet = $true
Count = 2
TimeoutSeconds = 2
ComputerName = $ip
}
$status = Test-Connection #params
[pscustomobject]#{
Hostname = $hostname
IPAddress = $ip
Status = ('Failed','Success')[[int]$status]
}
}
$runspaces = foreach($line in $list)
{
$params = #{
ip = $line.IPAddress
hostname = $line.Hostname
}
$psinstance = [powershell]::Create().AddScript($scriptBlock).AddParameters($params)
$psinstance.RunspacePool = $RunspacePool
[pscustomobject]#{
Instance = $psinstance
Status = $psinstance.BeginInvoke()
}
}
while($runspaces.Status.IsCompleted -contains $false)
{
Start-Sleep -Milliseconds 500
}
$results = $runspaces.ForEach({ $_.Instance.EndInvoke($_.Status) })
$RunspacePool.Dispose()
$results Sample:
Hostname IPAddress Status
-------- --------- ------
ExampleHost1 192.168.1.1 Success
ExampleHost2 192.168.1.2 Failed
ExampleHost3 192.168.1.3 Success
ExampleHost4 192.168.1.4 Failed
ExampleHost5 192.168.1.5 Success
ExampleHost6 192.168.1.6 Failed
ExampleHost7 192.168.1.7 Failed
ExampleHost8 192.168.1.8 Failed
ExampleHost9 192.168.1.9 Failed
ExampleHost10 192.168.1.10 Failed
...
...
Start-Job -ScriptBlock { Test-Connection -computername (Get-Content -Path “C:\Temp\GetList.csv”) }
Its lacking the fundamentals, works, DE fast.

Input and Output of foreach into columns in table

I currently want to check if a list of processes are running, then display the result within a table such as:
Process Status
======= ======
Process 1 Running
Process 2 Not Running
Process 3 Running
I have the below code which produces an output showing each input and output as a string, but it looks messy depending on the length of the Process name.
$Node = Read-Host -Prompt 'Input Node name'
$Process = #("Process1", "Process2", "Process3")
$Process | foreach-object {if(!(Get-Process -Name $_ -ComputerName $Node - ErrorAction SilentlyContinue)) {"$_ - Not Running"} else {"$_ - Running"}}
I am at a loss. All help appreciated.
Better (faster) to make a single remoting call to get all the processes, than one per process, so do that and store all the results - at least the names of the processes.
The next part is non-trivial. The way PowerShell and the neatly formatted tables work, is by making one object (bundle of things all together) for each table row, with each object having properties for each column name.
# Processes to look for
$Process = #("Process1", "Process2", "Process3")
$Node = Read-Host -Prompt 'Input Node name'
# Get running processes, and only keep their names
$runningProcesses = Get-Process -ComputerName $Node -ErrorAction SilentlyContinue |
Select-Object -ExpandProperty Name
$Process | ForEach-Object {
# For each process name to look for, generate a hashtable of
# columns and their values,
# then cast it into a PS Object
[PSCustomObject]#{
'ProcessName' = $_
'Status' = if ($runningProcesses -contains $_) { "Running" } else { "Not Running" }
}
}
This gives a neat formatted table output, and is also structured data so you can feed the output of this into | ForEach-Object { $_.Status } and pick out the individual parts by name, something you can't do as neatly with your string formatted approach.
Try this:
$node = Read-Host -Prompt 'Input Node name'
$processList = "Process1", "Process2", "Process3"
$processList |
ForEach-Object {
[PsCustomObject]#{
NodeName = $node
ProcessName = $_
IsRunning = (Get-Process -Name $_ -ComputerName $node -ErrorAction SilentlyContinue | Select-Object -First 1) -ne $null
}
}
Output will be like this:
NodeName ProcessName IsRunning
-------- ----------- ---------
Node1 Process1 True
Node1 Process2 True
Node1 Process3 False

How to Get The Real RecordData from DnsServerResourceRecord Using Powershell?

I'm using PowerShell to extract information from an Active Directory DNS server and I'm having trouble getting to the data I want.
Specifically, I'm trying to get the names of hosts that belong to a particular subnet, 10.104.128.x.
When I use the following commands:
Get-DnsServerResourceRecord -ComputerName AD_DNS_SERVER -ZoneName 104.10.in-addr.arpa -RRType Ptr | Where-Object {$_.HostName -like '*.128'}`
I get output that looks like this:
HostName RecordType Timestamp TimeToLive RecordData
-------- ---------- --------- ---------- ----------
104.128 PTR 10/19/2015 3:00:0... 00:15:00 adl5c260a86ba79.XYZ.net.
11.128 PTR 12/29/2015 6:00:0... 00:15:00 adl3c970e8d7166.XYZ.net.
110.128 PTR 1/29/2012 11:00:0... 00:15:00 nroxitow7tst.ABC.com.
114.128 PTR 1/20/2012 7:00:00 AM 00:15:00 adl5c260a86c29e.ABC.com
What I really want are the first column, (HostName), which has the last two octets of the IP; and the fifth column, (RecordData), which has the name of the host the IP is assigned to.
The hostname is the data I really want/need. And I see it right there!
So I used the select command to pare down the output in the pipe train. New command looks like this:
Get-DnsServerResourceRecord -ComputerName AD_DNS_SERVER -ZoneName 104.10.in-addr.arpa -RRType Ptr | Where-Object {$_.HostName -like '*.128'} | select HostName, RecordData
But the output looks like this:
HostName RecordData
-------- ----------
104.128 DnsServerResourceRecordPtr
11.128 DnsServerResourceRecordPtr
110.128 DnsServerResourceRecordPtr
114.128 DnsServerResourceRecordPtr
Dosen't get me the hostname though. Just the type of object the RecordData is but not the data that the object contains, perhaps?
I also tried piping the output to CSV and got the same result.
Then I tried looking at the DnsServerResourceRecord object properties with Get-Member. That showed me the object had a property called PSComputerName. I thought maybe that would have the name of the host but that came up blank when I tried to select it.
I then Googled around a bit and found a few pages that recommended a few ways to use RecordData.ipv4address to coax the data out of the DnsServerResourceRecordPtr object but I haven't gotten any of them to work yet. Output still prints blanks.
So my question is: does a reliable method exist for getting the actual hostname from a PTR record?
To select the PtrDomainName property from the DnsServerResourceRecordPtr object, use a calculated property:
... |Select-Object HostName, #{Name='RecordData';Expression={$_.RecordData.PtrDomainName}}
Yes it's really weird that you can't just call ToString on the DNS record data, it's all formatted using the PowerShell formatters which you can only access with Format-List or Format-Table, rather than just calling $resourceRecord.RecordData.ToString().
I've added more data types than Krzysztof Madej by just hacking out the PowerShell formatters from the XML file, the details are here.
http://david-homer.blogspot.com/2020/10/getting-text-representation-of.html
$dnsserver = "yourowndnsserver"
$dnszones = Get-DnsServerZone -ComputerName $dnsserver | Select-Object ZoneName
ForEach ($zone in $dnszones) {
$data = New-Object System.Object
$ZoneName = $zone.ZoneName
$data = Get-DnsServerResourceRecord $ZoneName -ComputerName $dnsserver
foreach ($registros in $data) {
$data = $ZoneName
$data += ","
$data += $registros.hostname;
$data += ","
$data += $RecordType = $registros.recordType;
$data += ","
if ($RecordType -like "PTR") {
$data += $registros.RecordData.PtrDomainName
}
elseif ($RecordType -like "A") {
$data += $([system.version]($registros.RecordData.ipv4address.IPAddressToString));
}
elseif ($RecordType -like "CNAME") {
$data += $registros.RecordData.HostNameAlias;
}
elseif ($RecordType -like "NS") {
$data += $registros.RecordData.nameserver;
}
elseif ($RecordType -like "MX") {
$data += $registros.RecordData.MailExchange;
}
elseif ($RecordType -like "SOA") {
$data += $registros.RecordData.PrimaryServer;
}
elseif ($RecordType -like "SRV") {
$data += $registros.RecordData.DomainName;
}
$data | out-file -FilePath $env:TEMP\$(Get-Date -Format dd_MM_yyyy)_DNSRecords.csv -Append
}
}

Display test-connection successes and failures in Out-Gridview

I am trying to get a list of servers and the last time they rebooted to show in a table. However, if it doesn't respond to a ping, I just need it to show in the list. I can't seem to figure out how to get it to add to the table after else.
Import-CSV $Downtime | % {
if(Test-Connection $_.server -Quiet -count 1){
Get-WmiObject Win32_OperatingSystem -ComputerName $_.server |
select #{LABEL="Name"; EXPRESSION = {$_.PSComputerName}}, #{LABEL="Last Bootup"; EXPRESSION = {$_.convertToDateTime($_.LastBootupTime)}}
}
else{#{LABEL="Name"; EXPRESSION = {$_.server}}
}
} | Out-GridView
I can always save the else results in a text file but this would be more convenient.
You need to make the same object, with the same properties!, in both cases so that PowerShell will understand the association between the two. The follwing example builds a custom hashtable using the if/else and outputs the object for each loop pass.
Import-CSV $Downtime | ForEach-Object {
$props = #{}
$server = $_.server
if(Test-Connection $server -Quiet -count 1){
$wmi= Get-WmiObject Win32_OperatingSystem -ComputerName $server
$props.Name = $wmi.PSComputerName
$props."Last Bootup" = $wmi.convertToDateTime($wmi.LastBootupTime)
}else{
$props.Name = $server
$props."Last Bootup" = "Could not contact"
}
New-Object -TypeName psobject -Property $props
} | Out-GridView
I used $server as the $_ changes context a couple of time so we wanted to be able to refer to the current row in the CSV we are processing.
I don't know what your PowerShell version is so I will assume 2.0 and create objects that support that.
In both cases an object is created with a Name and Last Bootup property which is populated based on the success of the ping.
As an aside I had a similar question a while ago about created similar object based output.

Adding objects to array to create multiple columns

I'm trying to get the DisplayName of all Software listed in AddRemovePrograms for each computer, add it to an array under the name of the computer, then export. Here's what I have:
$Computers = gc "C:\Get Software.txt"
$CSV = "C:\Get Software.csv"
$Results = #()
If (Test-Path $CSV) {
Remove-Item $CSV
}
Foreach($Computer in $Computers){
#Get DisplayName of Software Installed on Asset
$Software = Get-WmiObject Win32Reg_AddRemovePrograms -ComputerName $Computer | Select-Object -ExpandProperty DisplayName
$counter = 0
While ($counter -lt $Software.count){
#Create a PSObject. Loops through all software and adds to $Results.
$Obj = New-Object PSOBJECT
Add-member –inputobject $Obj –membertype Noteproperty -Name $Computer -Value $Software[$counter]
$counter++
$Results+=$Obj
}
}
$Results | Export-Csv $CSV -NoTypeInformation
Unfortunately, the output only lists the first Computer in the CSV. I've tried stepping through this to understand it, I just don't understand why I can't add the $Obj variable to $Results with a different Name Property (In this case I'm looping and making a new name based on the computer name in my .txt file). It seems to only take the first input and won't put a new column header then spit out the software. Below is an example of what I'm getting and what I'd like to get instead.
OUTPUT
A01234
Program1
Program2
Program3
EXPECTED OUTPUT
A01234 B05678 C09123
Program1 Program97 Program30
Program2 Program98 Program31
Program3 Program99 Program32
Hopefully this makes some sense. Any assistance would be appreciated, I'm clearly doing something wrong with the objects and I'm not seeing it. Thanks!
This is do-able, it just isn't quite as simple as you would think. What I would suggest is setting things up as a hashtable where the keys are your computer names and your values are an array of software titles for each computer. Then you find out how many titles the computer with the most software has (that's how many rows you'll have, so it's how many times you'll have to loop), and create a loop to make that many objects. Each object will have no properties to start, and then we add a property for each computer in the hashtable, and the value for that property will be the Nth item listed in the hashtable for that computer. Here's the code, it might make this all make more sense:
$Computers = 'A01234','B05678','C09123'
$SftwrPerCmp = [ordered]#{}
ForEach($Computer in $Computers){
$SftwrPerCmp.add($Computer,#())
#Randomly generate 7-15 strings as 'Programs' for current computer
$Software = 1..$(get-random -Maximum 15 -Minimum 7)|%{(Get-Random -inputobject $("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" -split ''|?{$_}) -count 6) -join ''}
ForEach($Title in $Software){
$SftwrPerCmp["$Computer"] += $Title
}
}
$MaxTitleCount = $SftwrPerCmp.Values|%{$_.count}|sort|select -last 1
$Results = #()
For($i=0;$i -lt $MaxTitleCount;$i++){
$Record = New-Object PSObject
$SftwrPerCmp.Keys | ForEach{Add-Member -InputObject $Record -NotePropertyName $_ -NotePropertyValue $SftwrPerCmp["$_"][$i]}
$Results += $Record
Clear-Variable Record
}
$Results|Format-Table -AutoSize
Now obviously you will not randomly generate software titles, you'll use your existing $Software = Get-WmiObject line that's in your existing code instead since that part was at least working for you. This should result in the listings that you were looking for. My code resulted in this:
A01234 B05678 C09123
------ ------ ------
CU7K5E 6GJWOB 97H1TY
7VCZ5T CIPWVK 760NKU
CHKPY0 J4B7D0 1QOSD3
2YEFR4 2VY6DM O68SKU
VI7ZQG WLJQN9 Q5VJAZ
ZQOKNV R9KZG1 H2XZK4
S8IZC4 GRSMPU BIZXKA
LAVNI0 TKBOUC K9DEFU
3U7KVO JZ3X4H
A6GVUK 18AC5H
NMI32Q H14GPJ
50KSZ6 XU0FWC
PAN5TC 9WXR5U
531M04