#requires -version 2 <# .SYNOPSIS Svendsen Tech's generic Get-WmiObject wrapper. Primarily designed for data collection from a list of computers, with output to XML. There's a parser designed for the XML as well. Copyright (c) 2012, Svendsen Tech. All rights reserved. Author: Joakim Svendsen .DESCRIPTION The script also offers a WMI timeout parameter in the form of a time span object, but be aware that this only affects successful queries. You might still experience lengthy timeouts. See the comprehensive online documentation and examples at: http://www.powershelladmin.com/Get-wmiobject_wrapper .PARAMETER ComputerName Computer or computers to process. Use a file with "(gc computerfile.txt)". .PARAMETER OutputFile Output file name. NB! A full path is needed. You will be asked to overwrite it if it exists unless -Clobber is specified. If the script detects you didn't provide a full path, you will be asked to use the current working directory prepended to the filename you provided. .PARAMETER MultiClassProperty Multiple WMI classes with the property or properties to extract. Designed for XML output. A string in the format: "wmi_class1:prop1,prop2|wmi_class2:prop1|wmi_class3:prop1,prop2,prop3", and so on. .PARAMETER Timeout The WMI timeout, represented as a time span object. Default "0:0:10" (10 seconds). .PARAMETER NoPingTest Specify this if you do NOT want to skip computers that do not respond to ICMP ping/echo. .PARAMETER Clobber Overwrite output file or files if they exist without prompting. .PARAMETER Scope Usually not necessary. By default "\\${Computer}\root\cimv2" will be used, while this lets you replace the part "root\cimv2" with what you specify instead. .PARAMETER CustomSql The default SQL query is "SELECT prop1, prop2 FROM Win32_ClassHere", while this parameter lets you append something like: WHERE DriveType="3". This "custom SQL" will be used in all the queries, so if the property/condition doesn't apply to other classes, you will see errors. #> param( [Parameter(Mandatory=$true)][string[]] $ComputerName, [Parameter(Mandatory=$true)][string] $OutputFile, [Parameter(Mandatory=$true)][string] $MultiClassProperty, [string] $CustomSql, [timespan] $Timeout = '0:0:10', [switch] $NoPingTest, [switch] $Clobber, [string] $Scope = 'root\cimv2' ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Continue' ############################## ##### START OF FUNCTIONS ##### ############################## function Get-Class-Property-Hash { # Remember that there's also a $MultiClassProperty in the outer scope param([Parameter(Mandatory=$true)][string] $MultiClassProperty) $ClassPropertyHash = @{} # The strings should be in the form: # "class1:prop1,prop2|class2:prop3,prop4,prop5|class3:prop6" - and so on... $ClassesAndProperties = $MultiClassProperty -split '\s*\|\s*' foreach ($ClassAndProperties in $ClassesAndProperties) { if ($ClassAndProperties -match '\A([^:]+?)\s*:\s*([^:]+)\z') { $ClassPropertyHash.($Matches[1]) = ,($Matches[2] -split '\s*,\s*') } else { Write-Host -ForegroundColor Red "WARNING: The following invalid class and property element was ignored:`n$ClassAndProperties" } } $ClassPropertyHash } ############################ ##### END OF FUNCTIONS ##### ############################ ##### Start of script <# This was used when I used the [xml] Save() method which requires a full path. Now I'm using Out-File, so I removed it. # A crude check to see if a full path was specified; if not, # prepend the current path, since [xml].Save() needs a full path. # In case this breaks on something I didn't consider, I decided # to prompt the user. if ($OutputFile -match '^\.[\\/]' -or $OutputFile -notmatch '[\\/]' -or $OutputFile -notmatch '^(?:[\\/]{2}|[a-z]:[\\/])') { $XmlFilename = Join-Path (Get-Location).Path ($OutputFile -replace '^\.[\\/]') Write-Host "It looks like the output file was specified without a full path. I need a full path." Write-Host "You provided: '$OutputFile'" Write-Host "Suggested path: '$XmlFilename'`n" $Answer = Read-Host 'Use the suggested path? (y/n) [yes]' if ($Answer -imatch 'n') { $XmlFilename = $OutputFile } } else { $XmlFilename = $OutputFile } #> # Register and display script start time $StartTime = Get-Date "`nScript start time: $StartTime" # Handle parameters if (-not $Clobber -and (Test-Path $OutputFile)) { $Answer = Read-Host "XML output file, '$OutputFile', already exists and -Clobber not specified. Overwrite? (y/n) [yes]" if ($Answer -imatch 'n') { Write-Output 'Aborted.'; exit; } } $ClassPropertyHash = Get-Class-Property-Hash $MultiClassProperty #"ClassPropertyHash:" #$ClassPropertyHash.GetEnumerator() # Main data hash $Data = @{} $Start = 0 foreach ($Computer in $ComputerName) { # Display progress on one line, except in ISE v3, where "`r" is broken... if ($Host.Name -like '*PowerShell ISE*') { if (++$Start -eq 1) { "You're running PowerShell ISE so you will get one line per computer to show progress due to ``r being broken. Single-line with other hosts." } Write-Host -ForegroundColor Green "Processing ${Computer}..." } else { Write-Host -NoNewline -ForegroundColor Green "`rProcessing ${Computer}... " } $Classes = $ClassPropertyHash.Keys $Data.$Computer = New-Object PSObject $PingDone = $false foreach ($Class in $Classes) { # There's something odd going on. This shouldn't be necessary, # but apparently is... Better not have spaces in properties. $PropertiesString = (([string[]] $ClassPropertyHash.$Class) -split '\s+') -join ',' $Properties = [string[]] ($PropertiesString -split ',') #'PropertiesString: "' + $PropertiesString + '"' # Create a new object for each class, as a note property, to hold the properties Add-Member -InputObject $Data.$Computer -MemberType NoteProperty -Name $Class -Value $(New-Object PSObject) ## DEBUG #[string[]]$ClassPropertyHash.$Class -join ',' $Query = "SELECT $PropertiesString FROM $Class" if ($CustomSql) { $Query += " $CustomSql" } # Ping stuff. I had to put it in here or rewrite quite a bit, in order for # Gwmi-Wrapper-Report to work properly, and to get it easily parseable... Hmm. # At least I managed to avoid pinging more than once with two bools. if (-not $PingDone) { if (-not $NoPingTest -and -not (Test-Connection -ComputerName $Computer -Quiet -Count 1)) { Add-Member -Name 'NoPing' -Value $(New-Object PSObject -Property @{ 0 = 'No ping reply' }) -MemberType NoteProperty -InputObject $Data.$Computer.$Class $PingReply = $false continue } else { $PingReply = $true } $PingDone = $true } else { if ($PingReply -eq $false) { Add-Member -Name 'NoPing' -Value @(New-Object PSObject -Property @{ 0 = 'No ping reply' }) -MemberType NoteProperty -InputObject $Data.$Computer.$Class continue } } $Searcher = $null try { $Searcher = [WmiSearcher] $Query # Not as useful as I had hoped... $Searcher.Options.Timeout = $Timeout $ConnectionOptions = New-Object Management.ConnectionOptions $ManagementScope = New-Object Management.ManagementScope("\\${Computer}\$Scope", $ConnectionOptions) $Searcher.Scope = $ManagementScope # Having to loop and extract the properties individually with -ExpandProperty # is ugly, but I simply can't find any other way while maintaining dynamic # parameters, properties and classes... I've been banging my head against # the wall for a while with this. # I think all errors are terminating with $Searcher.Get() $SearcherResults = $Searcher.Get() $PropertyValues = @() foreach ($Prop in $Properties) { # Preserve array structure for those with multiple values and enforce it # for one-element values. # This is what's supposed to work, but it's a bug: http://connect.microsoft.com/PowerShell/feedback/details/657211/select-object-expand-property-throws-and-exception-and-dies-when-property-is-null #$PropertyValues += ,($SearcherResults | Select-Object -ExpandProperty $Prop) $PropertyValues += ,($SearcherResults | ForEach-Object { if (-not $_.$Prop) { '' } else { $_.$Prop } }) } if ($Properties.Count -ne $PropertyValues.Count) { #Write-Host -ForegroundColor Red "Error: ${Computer}: ${Class}: Count of properties does not match count of retrieved values! This shouldn't happen. Skipping..." Add-Member -Name 'Error' -Value $(New-Object PSObject -Property @{ 0 = "Error: ${Computer}: ${Class}: Count of properties does not match count of retrieved values!" }) -MemberType NoteProperty -InputObject $Data.$Computer.$Class continue } $PropertyCount = $Properties.Count for ( $i = 0; $i -lt $PropertyCount; $i++ ) { Add-Member -Name $Properties[$i] -Value $(New-Object PSObject) -MemberType NoteProperty -InputObject $Data.$Computer.$Class $ValueCount = @($PropertyValues[$i]).Count # Don't break the XML parsing if there are no values by inserting at least one empty value. if ($ValueCount -eq 0) { Add-Member -Name 0 -Value 'EMPTY' -MemberType NoteProperty -InputObject $Data.$Computer.$Class.($Properties[$i]) continue } for ($k=0; $k -lt $ValueCount; ++$k) { #'Adding ' + $i + ' (i) ' + $Value + ' (value) ' + $Class + ' (class)' $Value = @($PropertyValues[$i])[$k] Add-Member -Name $k -Value $Value -MemberType NoteProperty -InputObject $Data.$Computer.$Class.($Properties[$i]) } } } # end of try statement catch { #Write-Host -ForegroundColor Red "WMI error: $($Error[0].ToString())" Add-Member -Name 'Error' -Value $(New-Object PSObject -Property @{ 0 = $Error[0].ToString() }) -MemberType NoteProperty -InputObject $Data.$Computer.$Class } } # end of class foreach } # end of computers foreach #Write-Host -Fore Green "`nCreating XML..." $XmlString = "`n" foreach ($Key in $Data.Keys | Sort) { $Computer = $Key $XmlString += " `n $Computer`n" foreach ($ClassName in $Data.$Computer | Get-Member -MemberType NoteProperty | Select -Exp Name) { $XmlString += " `n $ClassName`n `n" #'Class count: ' + @($Data.$Computer.$ClassName | Get-Member -MemberType NoteProperty).Count foreach ($Property in $Data.$Computer.$ClassName | Get-Member -MemberType NoteProperty | Select -Exp Name) { #"Property: $Property" #$Data.$Computer.$ClassName.$Property | Out-Host $Count = @($Data.$Computer.$ClassName.$Property | Get-Member -MemberType NoteProperty).Count #'Property count: ' + $Count $XmlString += " `n $Property`n `n" for ($i=0; $i -lt $Count; ++$i) { $Value = $Data.$Computer.$ClassName.$Property.$i $XmlString += " $Value`n" } $XmlString += " `n `n" } $XmlString += " `n `n" } $XmlString += " `n" } $XmlString += "`n" '' #$Xml = [xml] $XmlString #$Xml.Save($XmlFilename) # Had some encoding issues, so I'm now using this to save it as UTF-8 # Had to play with this a bit to keep the line breaks preserved. $XmlString = '' + "`n" + $XmlString $XmlString -split "`n" | Out-File -Width 10000 -Encoding utf8 -FilePath $OutputFile if ($?) { "Successfully saved '$OutputFile'" } else { "Failed to save '$OutputFile': $($Error[0].ToString())" } $Global:WmiData = $Data @" Script start time: $StartTime Script end time: $(Get-Date) Exposed data hash as `$Global:WmiData. Access it with "`$WmiData.GetEnumerator()" from the shell. "@