I was trying to take advantage of CIM's built-in parallel processing to get information about all the installed printers against a given subnet of computers. The script works faster than my WMI variation, but doesn't return the same information and doesn't always return as much as the Get-WmiObject call.
EDIT: The information the script drops is information about entire computers.
Here's the CIM version:
$Computer = Get-Content -Path c:\Scripts\input.txt
$Objects = foreach ($ComputerName in $Computer) {
# New CIM Instance
Write-Host Collecting information on $ComputerName
$Cim = New-CimSession -ComputerName $ComputerName
# Collect Printer Info
Get-CimInstance -CimSession $Cim -Class Win32_printer -Property deviceid, drivername, portname, systemName
# Define Hashtable properties
$ObjectProperties = #{
SystemName = $Cim.systemName
DeviceID = $Cim.deviceid
DriverName = $Cim.drivername
PortName = $Cim.portname
}
# Create new object
New-Object PSObject -Property $ObjectProperties
}
# Export Results
$Objects | Select DeviceID, DriverName, PortName, SystemName |
Export-Csv - NoTypeInformation -Path c:\Scripts\output.csv
Here's the WMI version:
$results = #()
$Computer = Get-Content -Path c:\Scripts\input.txt
# Check each computer in the list
foreach ($ComputerName in $Computer) {
$results += Get-WmiObject -Class Win32_printer -cn $ComputerName |
Select deviceid, drivername, portname, systemName
Start-Sleep -Milliseconds 500
}
# Export to CSV file
$Results | Select DeviceID, DriverName, PortName, SystemName |
Export-Csv -NoTypeInformation -Path c:\Scripts\output.csv
We sometimes need to run this script against multiple subnets. I moved to the CIM sessions because it reduced the total run of the script to consistently under 5 minutes, but if it's not going to return all of the information, it might be better to wait.
Does anyone have any idea on how to prevent CIM from dropping information?
It should be noted that WinRM is not enabled by default on these machines and the script has to force enable CIMs with the following command.
& "c:\Scripts\SnIPT\psexec.exe" \\$ComputerName -s -d -n 5 winrm.cmd quickconfig -q -force
The same WMI-class should return the same data (however CIM-cmdlets convert dates ++). Since you haven't explained what's different I'd guess it's missing output for certain computers. Usually this is because the target computer is missing Windows Management Framework 3.0 or later (think PS 3.0+) which is required for CIM. If that's the case, it should generate an error that you can catch and use to use DCOM (same as WMI) as a fallback. Ex:
$Computer = Get-Content -Path c:\Scripts\input.txt
$DCOM = New-CimSessionOption -Protocol Dcom
$Objects = ForEach($ComputerName in $Computer)
{
#New Cim Instance with fallback to DCOM
Write-Host Collecting information on $ComputerName
$Cim = $null
try {
$Cim = New-CimSession -ComputerName $ComputerName -ErrorAction Stop
} catch [Microsoft.Management.Infrastructure.CimException] {
#CIM not available on target (requires WMF 3.0+). Using DCOM (used by WMI)
try { $Cim = New-CimSession -ComputerName $ComputerName -SessionOption $DCOM -ErrorAction Stop }
catch { Write-Host $_.Exception.Message }
}
#Collect Printer Info
Get-CimInstance -CimSession $Cim -Class Win32_printer -Property DeviceID, DriverName, PortName, SystemName
#Best practice to store the original object.
#No need to create a new one with a few properties when you do it during export anyways.
#If you really need it, add "| Select-Object -Property DeviceID, DriverName, PortName, SystemName" to the previous line
}
#Export Results
$Objects | Select-Object -Property DeviceID, DriverName, PortName, SystemName | Export-Csv - NoTypeInformation -Path c:\Scripts\output.csv
Related
I hope someone can help me with this. We want to see which computers have a HDD and SDD. I have an excel.csv of the computers. I import the computers. But when I export them I never see the csv or its incomplete. Can you tell what part of my script is incorrect. Thank you
$computers = Import-csv -path "C:\Temp\MediaType\Computers.csv"
foreach ($computer in $computers) {
Write-Host "`nPulling Physical Drive(s) for $computer"
if((Test-Connection -BufferSize 32 -Count 1 -ComputerName $computer -Quiet)){
Invoke-Command -ComputerName $computer -ScriptBlock {
Get-WmiObject -Class MSFT_PhysicalDisk -Namespace root\Microsoft\Windows\Storage | Select-Object sort -Property PSComputerName, Model, SerialNumber, MediaType
Export-Csv C:\Temp\devices.csv
}
}
}
Update: 11/11/2021
Thank you everyone for you help
This script worked for me:
$ExportTo = "C:\Temp\devices.csv"
$computers = Import-csv -path "C:\Temp\Computers.csv"
{} | Select "ComputerName", "Status", "Model", "SerialNumber", "MediaType" | Export-Csv $ExportTo
$data = Import-csv -path $ExportTo
foreach ($computer in $computers) {
$Online = Test-Connection -BufferSize 32 -Count 1 -ComputerName $computer.computer -Quiet
if ($Online) {
Write-Host $computer.computer " is Online"
$OutputMessage = Get-CimInstance -ClassName MSFT_PhysicalDisk -Namespace root\Microsoft\Windows\Storage -ComputerName $computer.computer | Select-Object -Property PSComputerName,#{N='Status';E={'Online'}}, Model, SerialNumber, MediaType
$data.ComputerName = $computer.computer
$data.Status = $OutputMessage.Status
$data.Model = $OutputMessage.Model
$data.SerialNumber = $OutputMessage.SerialNumber
$data.MediaType = $OutputMessage.MediaType
$data | Export-Csv -Path $ExportTo -Append -NoTypeInformation
} else {
Write-Host $computer.computer " is Offline"
$data.ComputerName = $computer.computer
$data.Status = "Offline"
$data.Model = ""
$data.SerialNumber = ""
$data.MediaType = ""
$data | Export-Csv -Path $ExportTo -Append -NoTypeInformation
}
}
Continuing from my comment. . . as is, you would be exporting the results to the remote machine. That's if it was piped properly. You're currently missing a pipe (|) before Export-Csv.
Also, there's no need to invoke the command, as Get-WMIObject has a parameter for remote computers: -ComputerName. It's also a deprecated cmdlet that has been replaced by Get-CimInstance.
$ExportTo = "C:\Temp\devices.csv"
$computers = Import-csv -path "C:\Temp\MediaType\Computers.csv"
foreach ($computer in $computers)
{
Write-Host "`nPulling Physical Drive(s) for $computer"
if (Test-Connection -BufferSize 32 -Count 1 -ComputerName $computer -Quiet) {
Get-CimInstance -ClassName MSFT_PhysicalDisk -Namespace root\Microsoft\Windows\Storage -ComputerName $computer |
Select-Object -Property PSComputerName, Model, SerialNumber, MediaType |
Export-Csv -Path $ExportTo -Append -NoTypeInformation
}
}
Side Note: Get-CimInstance accepts an array of strings, meaning you can pass the entirety of $Computers to it. This should allow it to perform the the query in parallel, vs serial (one at a time):
$ExportTo = "C:\Temp\devices.csv"
$computers = Import-csv -path "C:\Temp\MediaType\Computers.csv"
Get-CimInstance -ClassName MSFT_PhysicalDisk -Namespace root\Microsoft\Windows\Storage -ComputerName $computers -ErrorAction SilentlyContinue |
Select-Object -Property PSComputerName, Model, SerialNumber, MediaType |
Export-Csv -Path $ExportTo -Append -NoTypeInformation
Performing queries one at a time doesn't necessarily mean it's bad. You can actually have more control over the control of flow for your script.
EDIT:
Following up on your comment...you're no longer using your if statement to check if the computer is online before connecting. So given that you keep the if statement, and add an else condition, you can create a calculated property to add another property to export of Status. Then, you can pass it a value of Online, or Offline depending on if the machine is online or not:
$ExportTo = "C:\Temp\devices.csv"
$computers = Import-csv -path "C:\Temp\MediaType\Computers.csv"
foreach ($computer in $computers)
{
if (Test-Connection -BufferSize 32 -Count 1 -ComputerName $computer -Quiet) {
Write-Host -Object "`nPulling Physical Drive(s) for $computer"
Get-CimInstance -ClassName MSFT_PhysicalDisk -Namespace root\Microsoft\Windows\Storage -ComputerName $computer |
Select-Object -Property PSComputerName,#{N='Status';E={'Online'}}, Model, SerialNumber, MediaType |
Export-Csv -Path $ExportTo -Append -NoTypeInformation -Force
}
else {
Write-Host -Object "`n$Computer is Offline"
[PSCustomObject]#{PSComputerName=$Computer;Status='Offline'} | Export-Csv -Path $ExportTo -Append -Force
}
}
Also:
Always remember that even if you can ping a machine, it doesn't mean you can connect to it.
This can be mitigated by using a CIM Session, or PSSession depending on the type of commands you're running.
To specifically answer the question:
How do I correctly export a CSV file (use Export-Csv)?
You might want to read about PowerShell pipelines and PowerShell cmdlets.
Basically, a cmdlet is a single command that participates in the pipeline semantics of PowerShell. A well written cmdlet is implemented for the Middle of a Pipeline which means that it processes ("streams") each individual item received from the previous cmdlet and passes it immediately to the next cmdlet (similar to how items are processed in an assembly line where you can compare each assembly station as a cmdlet).
To better show this, I have created an easier minimal, complete and verifiable example (MVCE) and replaced your remote command (Invoke-Command ...) which just an fake [pscustomobject]#{ ... } object.
With that;
I have used Get-Content rather then Import-Csv as your example suggest that Computers.csv is actually a text file which list of computers and not a Csv file which would require a (e.g. Name) header and using this property accordingly (like $Computer.Name).
To enforce the pipeline advantage/understanding, I am also using the ForEach-Object cmdlet rather than the foreach statement which is usually considered faster but this is probably not the case here as for the foreach statement it is required to preload all $Computers into memory where a well written pipeline will immediately start processing each item (which in your case happens on a remote computer) while still retrieving the next computer name from the file.
Now, coming back on the question "How do I correctly export a CSV file" which a better understanding of the pipeline, you might place Export-Csv within the foreach loop::
Get-Content .\Computers.txt |ForEach-Object {
[pscustomobject]#{
PSComputerName = $_
Model = "Model"
SerialNumber = '{0:000000}' -f (Get-Random 999999)
MediaType = "MydiaType"
} |Export-Csv .\Devices.csv -Append
}
As commented by #lit, this would require the -Append switch which might not be desired as every time you rerun your script this would append the results to the .\Devices.csv file.
Instead you might actually want do this:
Get-Content .\Computers.txt |ForEach-Object {
[pscustomobject]#{
PSComputerName = $_
Model = "Model"
SerialNumber = '{0:000000}' -f (Get-Random 999999)
MediaType = "MydiaType"
}
} |Export-Csv .\Devices.csv
Note the differences: the Export-Csv is placed outside the loop and the -Append switch is removed.
Explanation
As with e.g. the ForEach-Object cmdlet, the Export-Csv cmdlet has internally Begin, Process and End blocks.
In the Begin block (which runs when the pipeline is started), the Export-Csv cmdlet prepares the csv file with a header row etc. and overwrites any existing file.
In the Process block (which runs for each item received from the pipeline) it appends each line (data record) to the file.
The majority of this code was pulled from a blog online, but I think it's exactly the way I need to be tackling this. I want to get the top 4 machines from an OU based on uptime, and run a script that lives on each of the top 4 machines. I know that the problem involves the Array losing access to the Get-ADComputer properties, but I'm unsure of how to pass these new properties back to their original objects. This works as expected until it gets to the foreach loop at the end.
$scriptBlock={
$wmi = Get-WmiObject -Class Win32_OperatingSystem
($wmi.ConvertToDateTime($wmi.LocalDateTime) – $wmi.ConvertToDateTime($wmi.LastBootUpTime)).TotalHours
}
$UpTime = #()
Get-ADComputer -Filter 'ObjectClass -eq "Computer"' -SearchBase "OU=***,OU=***,OU=***,DC=***,DC=***" -SearchScope Subtree `
| ForEach-Object { $Uptime += `
(New-Object psobject -Property #{
"ComputerName" = $_.DNSHostName
"UpTimeHours" = (Invoke-Command -ComputerName $_.DNSHostName -ScriptBlock $scriptBlock)
}
)
}
$UpTime | Where-Object {$_.UpTimeHours -ne ""} | sort-object -property #{Expression="UpTimeHours";Descending=$true} | `
Select-Object -Property ComputerName,#{Name="UpTimeHours"; Expression = {$_.UpTimeHours.ToString("#.##")}} | Select-Object -First 4 |`
Format-Table -AutoSize -OutVariable $Top4.ToString()
foreach ($Server in $Top4.ComputerName) {
Invoke-Command -ComputerName $Server -ScriptBlock {HOSTNAME.EXE}
}
I'm not married to Invoke-Command in the last foreach but am having the same issues when I try to use psexec. Also, I'm running hostname.exe as a check to make sure I'm looping through the correct machines before I point it at my script.
Here's a streamlined version of your code, which heeds the comments on the question:
# Get all computers of interest.
$computers = Get-ADComputer -Filter 'ObjectClass -eq "Computer"' -SearchBase "OU=***,OU=***,OU=***,DC=***,DC=***" -SearchScope Subtree
# Get the computers' up-times in hours.
# The result will be [double] instances, but they're also decorated
# with .PSComputerName properties to identify the computer of origin.
$upTimes = Invoke-Command -ComputerName $computers.ConputerName {
((Get-Date) - (Get-CimInstance -Class Win32_OperatingSystem).LastBootUpTime).TotalHours
}
# Get the top 4 computers by up-time.
$top4 = $upTimes | Sort-Object -Descending | Select-Object -First 4
# Invoke a command on all these 4 computers in parallel.
Invoke-Command -ComputerName $top4.PSComputerName -ScriptBlock { HOSTNAME.EXE }
#Listing machine from which we will Query
$Machines = Get-ADComputer -Filter * -SearchBase 'OU=Laptops,OU=Win10Modern,OU=LN,OU=Workstations,DC=cooley,DC=com' | Select-Object Name
#Getting the Network Adapter version for Wi-Fi Adapter
ForEach ($Machine in $Machines) {
Get-NetAdapter | Select-Object Name,InterfaceDescription,DriverVersion,DriverDate,DriverProvider
}
Currently, your code loops over objects in variable $Machines, where each object has a single property called Name.
In order to get just the name values, either use Select-Object -ExpandProperty Name or get the array of names like this:
# get an array of computernames
$Machines = (Get-ADComputer -Filter * -SearchBase 'OU=Laptops,OU=Win10Modern,OU=LN,OU=Workstations,DC=cooley,DC=com').Name
Next loop over these computernames and have each computer run the Get-NetAdapter cmdlet:
# capture the output(s) in variable $result
$result = foreach ($Machine in $Machines) {
if (Test-Connection -ComputerName $Machine -Count 1 -Quiet) {
Invoke-Command -ComputerName $Machine -ScriptBlock {
Get-NetAdapter | Select-Object SystemName,Name,InterfaceDescription,
DriverVersion,DriverDate,DriverProvider,Status,AdminStatus
}
}
else {
Write-Warning "Computer '$Machine' does not respond"
}
}
# output on screen
$result
# or to GridView
$result | Out-GridView -Title 'NetAdapterInfo'
# or to CSV file
$result | Export-Csv -Path 'X:\NetAdapterInfo.csv' -NoTypeInformation
AdminStatus is a setting (enabled --> 'up'; disabled --> 'down')
Status is operational status (connected --> 'up'; disconnected --> 'down')
I don't think you can use Get-NetAdapter to connect to remote computers.
You can however use Get-WmiObject Win32_NetworkAdapter -ComputerName .
Like this:
ForEach ($Machine in $Machines) {
Get-WmiObject -Class Win32_NetworkAdapter -Filter "NetConnectionStatus = 2" -ComputerName $Machine
}
You need to become familiar with the properties of the Win32_NetworkAdapter class. You can see all of the properties by running this command:
Get-WmiObject -Class Win32_NetworkAdapter -ComputerName "Localhost" | fl * -Force
or you can use this command to see all of the properties (and methods) available to you.
Get-WmiObject -Class Win32_NetworkAdapter -ComputerName "Localhost" | Get-Member
Most computers will have more than 1 network card (some are hidden) and you have to filter the irrelevant ones out.
$comps = get-content C:\xyz\test.txt
foreach($comp in $comps)
{
$result = Get-WmiObject -Class Win32_Product -Computer $comp | sort-object Name | select Name | where { $_.Name -match “abc”}
$result | out-file -Append out.txt
}
There is a application abc , im looking to know whether this is installed in multiple servers.
Check this "Hey Scripting Guy" link. This link describes how to query for installed applications via the registry.
From the link:
Win32_Product: The Good, the Bad, and the Ugly
[Good] The Win32_Product WMI class represents products as they are installed >by Windows Installer.
If you choose to query Win32_Product class by using Get-WmiObject, you’ll find >yourself [Bad] waiting for your query (or application) to return [Ugly] a >consistency check of packages that are installed as it attempts to verify and >repair installs. (For more information, see Event log message indicates that >the Windows Installer reconfigured all installed applications).
...
Based on above link you can query your installed products via:
Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* | Select-Object DisplayName, DisplayVersion, Publisher, InstallDate | Format-Table –AutoSize
Additionally you can use Test-Path to check if a requested registry entry exists. Example:
if (test-path HKLM:\Software\abc) { write-host "Found" } else { Write-Host "Not found" }
To check for installed software on multiple servers:
$servers = Get-Content C:\xyz\servers.txt
$results = #()
foreach ($server in $servers) {
$session = New-PSSession -ComputerName $server -Credential (Get-Credential)
$results += Invoke-Command -Session $session -ScriptBlock {
test-path HKLM:\Software\abc
}
}
$results.GetType() # Dump type
$results | gm # Dump properties
$results | Format-Table
Hope that helps.
First time poster here, I'm a bit of a beginner and I've been keen to get my PowerShell scripting skills up to scratch and I'm come across something rather confusing...
I've made a script to query a collection of computers and I want to query Win32_OperatingSystem but only extrapolate the Build number so I can populate my PSObject with it. I'm trying to add some If logic so that if the build number is 7601, I can write a message under my OS column.
The problem I'm having is that the BuildNumber values are coming out as #{BuildNumber=7601} instead of 7601 for instance. That, and my If statement is borked.
$Machines = Get-Content .\Computers.txt
Foreach($Machine in $Machines)
{
$sweet = (Get-WmiObject -Class Win32_OperatingSystem -computer $Machine | Select-Object BuildNumber)
$dversion = if ($sweet -eq "#{BuildNumber=7601}") {Yes!} else {"Nooooo!"}
New-Object PSObject -Property #{
ComputerName = $Machine
Sweet = $sweet
OS = $dversion
}
}
The issue is that the Get-WMIObject cmdlet is returning a Hash Table. Then the Select-Object is returning just the BuildNumber section you want, the BuildNumber property and it's value. You need to add the -ExpandProperty parameter to only get the value back, not the name/value pair.
Get-WMIObject -Class Win32_OperatingSystem | Select-Object BuildNumber
Returns
#{BuildNumber=7601}
With ExpandProperty
Get-WMIObject -Class Win32_OperatingSystem | Select-Object -ExpandProperty BuildNumber
Returns
7601
Just another option with a ping test to skip unavailable machines.
Get-Content .\Computers.txt | Where-Object {Test-Connection -ComputerName $_ -Count 1 -Quiet} | Foreach-Object {
$sweet = Get-WmiObject -Class Win32_OperatingSystem -ComputerName $_ | Select-Object -ExpandProperty BuildNumber
New-Object PSObject -Property #{
ComputerName = $_.__SERVER
Sweet = $sweet
OS = if ($sweet -eq 7601) {'Yes!'} else {'Nooooo!'}
}
}