Exporting results of foreach loop to CSV - powershell

The script reads in a .TXT file of computer names. It then checks to see if Windows Update is set to Auto or Manual; the computers name and update type are then exported to a CSV. At first, it was only exporting the final computer and it's type. I did some research and saw that creating an object to hold the name and type and then adding that object to an array would work. However, exporting the array to a CSV results in interesting data which is related to the array instead of the computer name and type. On top of that, I have the array printed out in the console pre-export and it looks to be the last computer and its type listed 9 times, the amount of computers on the one .TXT file I'm reading in. I know export-CSV now has an append parameter that could take care of this but we have much more PS 1.0 and 2.0 in the office and need it to work on those version.
clear-host
$item = new-object PSObject
$list = #()
$c=Get-Credential
$a = gc "c:\Scripts\pcList.txt"
ForEach ($b in $a)
{
$pcName = Get-WmiObject win32_service -filter 'name="wuauserv"' -computer $b -credential $c -ErrorAction SilentlyContinue |
select-object name,
#{N="System Name";E={$_.Systemname}}
$name = $pcName.'System Name'
$item | Add-Member NoteProperty Name $name -Force
$pcType = Get-WmiObject win32_service -filter 'name="wuauserv"' -computer $b - credential $c -ErrorAction SilentlyContinue |
select-object name, #{N="Startup Type";E={$_.StartMode}}
$type = $pcType.'Startup Type'
$item | Add-Member NoteProperty StartupType $type -Force
$list += $item
}
Write-Host $list
export-csv -Path "c:\Scripts\pcListCSV" -inputobject $list -notype

Ok, a couple things. The script looks fairly good really, all things considered, and a couple little tweaks and it should work fine. Let's approach this a tad different and just construct the object all at once instead of making it then adding noteproperties.
Also, we'll just make 1 call to GWMI (short for Get-WMIObject). I think this will work better for you. First off, making the GWMI call once means we have to select more than one thing when we make it. Since you are really making the exact same call twice, and selecting two different things from it each time, this just kind of makes sense. We're going to skip defining things in a select, and just let $pcName store all the data from the GWMI call. Though, first we're going to make sure that there's no old data in $pcName just to make sure we don't get duplicates.
clear-host
$item = new-object PSObject
$list = #()
$c=Get-Credential
$a = gc "c:\Scripts\pcList.txt"
ForEach ($b in $a)
{
if(Get-Variable pcName -ea SilentlyContinue){Remove-Variable pcName}
$pcName = Get-WmiObject win32_service -filter 'name="wuauserv"' -computer $b -credential $c -ErrorAction SilentlyContinue
Ok, now we create the object, and we'll just add it directly to the array. Here's where we define things like Name and StartupType as object properties, so when you display the array later it shows those two properties for each item. After that we'll close the ForEach loop and both display the list to the screen and pipe it to Export-CSV to save the results.
$list += New-Object PSObject -Property #{
Name = $pcName.SystemName
StartupType = $pcName.StartMode
}
}
$list
$list | Export-Csv "C:\Scripts\pcList.CSV" -notype
So, this doesn't check to make sure that the systems are online, or do any error checking like that, but if that's OK with you then I think this should do exactly what you need.

Related

Trying to add PSCustomObjects to an ArrayList on a remote machine

I have some experience with PowerShell, and usually Google or searching forums like these yields the answers when I have questions - but not this time.
I'm trying to collect the number of .log files in a directory on a remote server, then I'd like to store the location (drive letter and folder path) and the count in an array list for later. So far everything is working as I'd expect, but I'm running into trouble adding my PSCustomObjects to the array list. I'm not sure if it's because I'm executing on a remote server or if something else is causing the problem. Here is my code:
$server = Read-Host -Prompt 'Please enter the server name'
[System.Collections.ArrayList]$returnObj = #()
Invoke-Command -ComputerName $server {
$drives = Get-PSDrive -PSProvider FileSystem |
Where-Object {$_.Description -like "ExVol*"} |
Select-Object Root
foreach ($d in $drives) {
Set-Location -Path $d.Root
$folders = Get-ChildItem -Path $d.Root |
Where-Object {$_.Name -like "*.log"} |
Select-Object Name
foreach ($f in $folders) {
$count = (Get-ChildItem -Path $f.Name).Count
$obj = [PSCustomObject]#{
LogFolder = $d.Root.Trim() + $f.Name
LogFileCount = $count
}
Write-Host $obj
$returnObj.Add($obj | Select-Object DatabaseFolder,LogFileCount)
}
}
}
$returnObj
In this format I get a syntax error on the line
$returnObj.Add($obj | Select-Object DatabaseFolder,LogFileCount)
If I change the above line to $returnObj.Add($obj) I avoid the syntax error, but instead I get an error saying I cannot call a method on a null valued expression.
I've tried creating the ArrayList inside the Invoke-Command and I've tried using New-Object instead of PSCustomObject to no avail.
I think your mixing stuff a bit up, this will do:
$returnObj = Invoke-Command -ComputerName $server {
$drives = Get-PSDrive -PSProvider FileSystem |
Where-Object {$_.Description -like "ExVol*"} |
Select-Object Root
foreach ($d in $drives) {
Set-Location -Path $d.Root
$folders = Get-ChildItem -Path $d.Root |
Where-Object {$_.Name -like "*.log"} |
Select-Object Name
foreach ($f in $folders) {
$count = (Get-ChildItem -Path $f.Name).Count
[PSCustomObject]#{
LogFolder = $d.Root.Trim() + $f.Name
LogFileCount = $count
}
}
}
}
$returnObj
The problem is this line:
[System.Collections.ArrayList]$returnObj = #()
is declared outside of the Invoke-Command -ScriptBlock. This means it's not available within the session on the remote machine, and as such can not be used there.
On a side note, you cannot fill an array like you fill a Hashtable with data.
Arrays are filled like $MyArray = #(); $MyArray += 'MyValue'
Hashtables like $MyHash=#{}; $MyHash.SomeKey = 'SomeValue' or as you indicated $MyHash.Add('SomeKey', 'SomeValue')
ArrayLists are filled like [System.Collections.ArrayList]$MyArrayList = #(); $MyArrayList.Add('SomeValue')
I hope this makes it a bit more clear. The return values can always be catched before the Invoke-Command or even before a simple foreach (). For example $result = 0..3 | ForEach-Object {$_} is perfectly valid too.
You need to actually return your object from the remote system to your local system since you cannot use your $returnObj within the remote session.
As an example:
$returnValue = Invoke-Command -ComputerName $server {
$obj = [PSCustomObject]#{
LogFolder = $d.Root.Trim() + $f.Name
LogFileCount = $count
}
#return the object via the pipline
$obj
}
$returnObj.Add($returnValue | Select-Object DatabaseFolder,LogFileCount)
The above example is lacking of proper error handling, therefore you would get an error if the remote system is not reachable but it's a start.

Powershell Multipart WMI query output

When using this code:
$Prodservers = Get-ADComputer -Filter {OperatingSystem -like '*Server*'} -SearchScope Subtree -SearchBase $ProdSB -Server $DCprod -Credential $ProdCred -ErrorAction SilentlyContinue |
select -Expand DnsHostname
foreach ($P in $Prodservers) {
[PSCustomObject]#{
Hostname = $P
'Support team' = (Invoke-Command -ComputerName $P -ScriptBlock {$env:supportteam} -Credential $ProdCred)
'Local Admins' = (Invoke-Command -ComputerName $P -ScriptBlock {$ADSIComputer = [ADSI]('WinNT://localhost,computer');$lgroup = $ADSIComputer.psbase.children.find('Administrators', 'Group');$lgroup.psbase.invoke('members') | % {$_.GetType().InvokeMember('Name', 'GetProperty', $null, $_, $null)}} -Credential $ProdCred)
'Host Reachable' = [bool](Invoke-Command -ComputerName $P -ScriptBlock {1} -Credential $ProdCred)
}
}
This works, however an group membership of more than two members in the local administrators group returns similar to this:
{Administrator, Domain Admins, Prod Server Admin...
How would I expend the output to show the full membership?
Also after pointers for selecting only certain groups that match group name x or y or return True is group x is present etc.
You might be running into output display formatting issues, where the column data exceeds the displayable width in table format in PowerShell.
You can try use the Format-List cmdlet to display things in a list instead to see if your local administrators group with multiple members displays correctly. Check out the link above to see how it helps, but a basic example of using it would be:
Get-Service | Format-List
As for your filtering question, it looks like you're using reflection to invoke methods that collect that data, so it would be harder to use PS cmdlets to help there, so I would suggest getting that data as you do now, but do it separately, into a temporary variable, then filter the data there selecting your specific groups you want using something like this to match your group names, and in the if statement, put the relevant data into another variable, which you then use for your final output.
if ($item -match "groupNameX") { #Then... }
Finally worked it out.
Came across this answer.
First, found a script block that outputted the memberships as a PSObject property:
$SB = {
$members = net localgroup administrators |
where {$_ -AND $_ -notmatch "command completed successfully"} |
select -skip 4
New-Object PSObject -Property #{
Members=$members
}
}
Then modified the local admins column:
'Local Admins' = $admins.Members -join ','
The output is still truncated, however now instead of export-CSV showing the column contents as System.Object[] it now shows the full output with the separator specified in -join.

Can't get members of other domains and PS version

I am learning to create new objects and combine properties from other objects. In this script I want to find out what the PS version is and also add some other properties like OS, IP etc... but I am running into 2 problems. We have 6 domains and I can't seem to iterate over each domain. I tried (Get-ADForest).Domains and can see the list of domains. It still only returns objects in the domain my workstation belongs to. The second issue is the Invoke-Command. The version always returns 5. I know many of the servers being returned do not have PSVersion 5.
function Get-PSVersion {
(Invoke-Command -Scriptblock {$PSVersionTable.PSVersion}) | Select Major
}
$servers = Get-ADComputer -Filter {(enabled -eq $true) -and (OperatingSystem -like "Windows Server* *")} -Properties * |
ForEach-Object {
$ps = Get-PSVersion
$server = $_
New-Object -TypeName PSObject -Property #{
Name = $server.Name
OS = $server.OperatingSystem
IPAddress = $server.IPv4Address
Location = $server.CanonicalName
PSVersion = $ps.Major
}
}
$servers | Select Name,Location,OS,IPAddress,PSVersion | FT -AutoSize
Ok so starting with the Invoke-Command, You need to tell that cmdlet which server to target, just calling it as you loop over server names will keep calling it on your local computer, so you'll need to use the -computername parameter, and provide your function an argument to pass to invoke-command. Which would look something like this:
function Get-PSVersion($name) {
(Invoke-Command -ComputerName $name -Scriptblock {$PSVersionTable.psversion | Select Major})
}
You'll notice I also moved your select, this isn't strictly necessary but imo it looks cleaner and means slightly less data gets sent over the network. note that this will create an object with a single property called Major, if you want just the version number returned as an integer you'd want to do it like this
function Get-PSVersion($name) {
(Invoke-Command -ComputerName $name -Scriptblock {$PSVersionTable.psversion.Major})
}
You'll need to add an extra loop into the script if you want to target more than one domain, basically you want an array of the domains you wish to target and then loop over that array calling get-adcomputer for each and specifying the domain name for the -server parameter. I've put a simplified example below you can incorporate into your own code.
$arr = #("test.domain","othertest.domain")
foreach($domain in $arr){
Get-ADComputer -Filter * -Server $domain
}
Hope that helps!
Got it to work. Thanks for the assistance.
clear-host
$arr = #("x.local","x.local")
foreach($domain in $arr){
$servers = (Get-ADComputer -Filter {(enabled -eq $true) -and (OperatingSystem -like "Windows Server* *")}-Server $domain -Properties *|Select -First 10)
}
$MasterList = #()
foreach ($server in $servers) {
$MyObj = New-Object PSObject -Property #{
Name = $server.Name
Os = $server.OperatingSystem
Ip = $server.IPv4Address
PSV = Invoke-Command -ComputerName $server.Name -ScriptBlock {$PSVersionTable.psversion}
}
$MasterList += $MyObj
}
$MasterList|Select Name,PSV,OS,IP

Get-Service results to array

I'm sure this should be straight forward but I've been stuck on it for a while now...
I am trying to get the service names (for sql server) into an array but can't figure out how to do it. I basically want the array contents to look something like the output of this:
Get-Service -computername $server_name -name sql* | format-table -property name
I have tried things like this but the contents of $service_name are very odd:
$service_name = (Get-Service -computername $server_name -name sql* -exclude *sqlwriter | format-table -property name)
Whatever I try either errors or gives some strange message in my array. Should this be easy/possible? I'm guessing I could dump the results in a text file then use the contents of that but it's a bit messy and more overhead than seems necessary.
Not sure, but where you looking for this (with whatever variation on the pattern for -Name and/or -Exclude)?
PS> $names = (Get-Service -Name Sql* | select name)
PS> $names.GetType().IsArray
True
PS> $names.Length
3
PS> $names
Name
----
SQLBrowser
SQLSERVERAGENT
SQLWriter
Or even along the lines of the following if you really want the "Name" as a System.String array.
PS> $names = (Get-Service -Name Sql* | foreach { $_.Name -as [string]})
PS> $names[0].GetType().FullName
System.String
PS> $names
SQLBrowser
SQLSERVERAGENT
SQLWriter
But also keep the good advice in #alroc's answer in mind - maybe you want to keep the actual type of Get-Service's result (System.ServiceProcess.ServiceController) as long as possible and access/use the Name property of it as late as possible. YMMV.
You're using format-table in the pipeline. Anytime you use a format-* cmdlet, that's the end of the line for your data - it's now just a formatted bunch of text, it's no longer data that you can actually use.
Try this to get the names formatted as a table for viewing:
$service_name = (Get-Service -computername $server_name -name sql* -exclude *sqlwriter);
$service_name | format-table -property name;
That said, I ran both your version and mine above and got the same visual output - the critical difference is that mine stores data in $service_name, not just a bunch of text (as yours does).
edit:
In response to:
All I really needed was the service names in an array to then use elsewhere
$service_name = (Get-Service -computername $server_name -name sql* -exclude *sqlwriter) | select-object -expandproperty name;
or:
$service_name = (Get-Service -computername $server_name -name sql* -exclude *sqlwriter).name;
No need to invoke WMI directly.

Get WMI Data From Multiple Computers and Export to CSV

So having some good old fashion Powershell frustrations today. What I need to do is this:
Get a list of computers from a file
Query those computers for "CSName" and "InstallDate" from Win32_OperatingSystem
Convert InstallDate into a useable date format.
Export all that to a .Csv
I've tried so many different iterations of my script. I run into 2 major issues. One is that I can't export and append to .Csv even with Export-Csv -Append. It just takes the first value and does nothing with the rest. The 2nd is that I can't get the datetime converter to work when piping |.
Here's a few samples of what I've tried - none of which work.
This sample simply errors a lot. Doesn't seem to carry $_ over from the WMI query in the pipe. It looks like it is trying to use data from the first pipe, but I'm not sure.
Get-Content -Path .\Computernames.txt | Foreach-Object {
gwmi Win32_OperatingSystem -ComputerName $_) |
Select-Object $_.CSName, $_.ConvertToDateTime($OS.InstallDate).ToShortDateString()
} | Export-Csv -Path Filename -Force -Append -NoTypeInformation
}
This one simply exports the first value and gives up on the rest when exporting .Csv
$Computers = Get-Content -Path .\Computernames.txt
foreach ($Computer in $Computers) {
echo $Computer
$OS = gwmi Win32_OperatingSystem -ComputerName $Computer
$OS | Select-Object
$OS.CSName,$OS.ConvertToDateTime($OS.InstallDate).ToShortDateString() |
Export-Csv -Path $Log.FullName -Append
}
This one does get the data, but when I try to select anything, I get null values, but I can echo just fine.
$OS = gwmi Win32_OperatingSystem -ComputerName $Computers
$OS | Foreach-Object {
Select-Object $_.CSName,$_.ConvertToDateTime($OS.InstallDate).ToShortDateString() |
Export-Csv -Path $Log.FullName -Force -Append -NoTypeInformation
}
This feels like it should be ridiculously simple. I can do this in C# with almost no effort, but I just can't get PS to do what I want. Any help would be much appreciated!
Here you go,
$Array = #() ## Create Array to hold the Data
$Computers = Get-Content -Path .\Computernames.txt
foreach ($Computer in $Computers)
{
$Result = "" | Select CSName,InstallDate ## Create Object to hold the data
$OS = Get-WmiObject Win32_OperatingSystem -ComputerName $Computer
$Result.CSName = $OS.CSName ## Add CSName to line1
$Result.InstallDate = $OS.ConvertToDateTime($OS.InstallDate).ToShortDateString() ## Add InstallDate to line2
$Array += $Result ## Add the data to the array
}
$Array = Export-Csv c:\file.csv -NoTypeInformation