Using an Advanced Function with Invoke-Command FilePath (HELP PLEASE)

So what I am trying to do is, run my advanced function that I finally finished and use it with invoke-command filepath so it can run on 1100 computers faster than one at a time. What I did in my function is I have it set to output results, errors, and offline computers to their own csv files. The function works great but how do I get it to create these files on my computer that is executing the invoke-command. So it will bring back all of that info from the 1100 computers and save it into the 3 files on my computer. I am only asking because I have spent a lot of time reading all of google and could not find a way to make this work. I spent a lot of time learning correct formatting and the correct way to create an advanced function tool. So I do not want to have something sloppy for the invoke-command part. I have seen many functions written with invoke-command but they all seems to lose the blasting effect and are one computer at a time. That’s not my goal here. I like to blast it out to all at the same time. Please can anyone help me with this. I just want to run the filepath and function and create the output files on my computer instead of on each remote computer. thank you all for your time and help.

 

 

function Get-ComputerSystemInfo
{
<#
.SYNOPSIS
This function will gather computer system information using Get-WmiObject.

.DESCRIPTION
This function will gather computer system information, from multiple
computers and provide error logging information.

.PARAMETER ComputerName
This parameter supports multiple computer names to gather Data from. This parameter is Mandatory.

.EXAMPLE
Getting information from a local computer.
Get-ComputerSystemInfo

.EXAMPLE
Getting information from one or more remote computers.
Get-ComputerSystemInfo -ComputerName 'comp1','comp2'

.EXAMPLE
Getting information from remote computers and exporting the info to a csv excel file.
Get-ComputerSystemInfo -ComputerName (Get-Content -Path 'C:\Temp\ADComputers.txt') -ExportToCSV -ExportErrors -ExportOffline

.EXAMPLE
Export all Windows Computers from AD to a text file.  One computer name per line.
$PCList = Get-ADComputer  -Filter {(enabled -eq "true") -and (OperatingSystem -Like "*Windows 10*")} | Select-Object -ExpandProperty Name | Sort-Object Name
$PCList | Out-File 'C:\Temp\ADComputers.txt'

.NOTES
David Newsom
#>
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$False,
                    ValueFromPipeline=$true,
                    ValueFromPipelineByPropertyName=$true)]
        [String[]]$ComputerName = $env:COMPUTERNAME,

        [Switch]$ExportToCSV,
        [Switch]$ExportErrors,
        [Switch]$ExportOffline
    )

    Begin
    {
        if ($ExportToCSV)
        {
            if (-not (Test-Path 'C:\Temp'))
            {
                New-Item -ItemType Directory -Path 'C:\Temp'
            }
        }

        if ($ExportErrors)
        {
            if (-not (Test-Path 'C:\Temp'))
            {
                New-Item -ItemType Directory -Path 'C:\Temp'
            }
        }

        if ($ExportOffline)
        {
            if (-not (Test-Path 'C:\Temp'))
            {
                New-Item -ItemType Directory -Path 'C:\Temp'
            }
        }

        Write-Host "Total Computer Count: $(($ComputerName).Count)" -ForegroundColor Green
    }

    Process
    {
        foreach($Computer in $ComputerName)
        {
            if (Test-Connection -ComputerName $computer -Count 1 -ErrorAction SilentlyContinue)
            {
                Write-Verbose "Connecting to computer: $computer" -Verbose
                Try
                {
                    $OS = Get-WmiObject -ComputerName $Computer -Class Win32_OperatingSystem -ErrorAction Stop
                    $DiskC = Get-WmiObject -ComputerName $Computer -Class Win32_LogicalDisk -Filter "DeviceID='C:'" -ErrorAction Stop
                    $Proc = Get-WmiObject -Class Win32_Processor -ComputerName $Computer -ErrorAction Stop
                    $Sys = Get-WmiObject -Class Win32_ComputerSystem -ComputerName $Computer -ErrorAction Stop
                    $Bios = Get-WmiObject -Class Win32_BIOS -ComputerName $Computer -ErrorAction Stop
                    $Bios2 = Get-WmiObject -Class Win32_SystemEnclosure -ComputerName $Computer -ErrorAction Stop
                    $Net = Get-WmiObject -Class Win32_NetworkAdapterConfiguration -ComputerName $Computer -Filter "IpEnabled = TRUE" -ErrorAction Stop

                    $ObjInfo = [PSCustomObject][ordered]@{
                        'ComputerName'                    = $computer
                        'Manufacturer'                    = $sys.Manufacturer
                        'Model'                           = $sys.Model
                        'Service Tag'                     = $bios.SerialNumber
                        'Asset Tag'                       = $bios2.SMBIOSAssetTag
                        'BIOS Version'                    = $bios.SMBIOSBIOSVersion
                        'OS Name'                         = $os.caption
                        'OS Architecture'                 = $os.OSArchitecture
                        'OS Version'                      = $os.Version
                        'OS Build'                        = $os.buildnumber
                        'OS Install Date'                 = $os.ConvertToDateTime($os.InstallDate)
                        'Last Boot Time'                  = $os.ConvertToDateTime($os.LastBootupTime)
                        'UserName'                        = $sys.UserName
                        'Domain'                          = $sys.domain
                        'IP Address'                      = $net.IPAddress[0]
                        'Subnet Mask'                     = $net.IPSubnet[0]
                        'Default Gateway'                 = $net.DefaultIPGateway
                        'DNS Servers'                     = $net.DNSServerSearchOrder
                        'MAC Address'                     = $net.MACAddress
                        'NIC Description'                 = $net.Description
                        'Processor'                       = $proc.name
                        'Number of Processors'            = $sys.NumberofProcessors
                        'Number of Logical Processors'    = $sys.NumberofLogicalProcessors
                        'Memory'                          = $sys.TotalPhysicalMemory / 1MB -as [int]
                        'Drive'                           = $DiskC.DeviceID
                        'Drive Total Size'                = $DiskC.Size / 1GB -as [int]
                        'Drive FreeSpace'                 = $DiskC.freespace / 1GB -as [int]
                        'Drive Percent Free'              = $DiskC.FreeSpace / $DiskC.Size * 100 -as [int]
                    }

                    if ($ExportToCSV)
                    {
                        $ObjInfo | Export-CSV 'C:\Temp\ComputerSystemInfo-Export.csv' -Append -NoTypeInformation
                    }
                    else
                    {
                        $ObjInfo
                    }
                }
                Catch
                {
                    if ($ExportErrors)
                    {
                        $ObjErrors = [PSCustomObject]@{
                            ComputerName = $Computer
                            Error = $PSItem.Exception.Message
                        }
                        $ObjErrors | Export-CSV 'C:\Temp\ComputerSystemInfo-Errors.csv' -Append -NoTypeInformation
                    }
                    Write-Error "ERROR: $($Computer) - $($PSItem.Exception.Message)"
                }
            }
            else
            {
                if ($ExportOffline)
                {
                    Write-Output "$Computer" | Out-File 'C:\Temp\ComputerSystemInfo-Offline.txt' -Append
                }
                Write-Warning "OFFLINE: $Computer"
            }
        }
    }

    End
    {
        Write-Host; Write-Host; Write-Host
        Write-Host "Info that was exported to a file can be found at this location: ""C:\Temp""" -ForegroundColor Blue -BackgroundColor Yellow
    }

}

# Single Computer
#Get-ComputerSystemInfo -ComputerName 'tech-hlpdsk'

# List of Computers
#Get-ComputerSystemInfo -ComputerName (Get-Content -Path 'C:\Temp\ADComputers.txt') -ExportToCSV -ExportErrors -ExportOffline

Remember you don’t want your function doing too much. It collects computer info, it should not export to CSV. I recommend the following changes.

  • Remove any exporting or out-file'ing.
  • Make your function export an object, including any success or errors you plan to log. One object that you can manipulate down the pipeline.
  • Make write-verbose and other messages only write where appropriate. (It tells you something is exported regardless of if you chose to or not)
Now if you intend to run this remotely on other computers, forget about message output in your function, no one is there watching it. You can have your script that calls the function show you feedback, as well as retrieve all the output objects. Invoke-command is going to return all the output to the calling session, then you can export and write as you wish. Being ran as a local script with no parameters is perfect to take advantage of invoke-commands ability to run all the jobs at the same time. Either of these will help you achieve your goal.
$allcomputernames
$results = Invoke-Command -computername $allcomputernames -scriptblock $yourscript
#All the output will be in $results
$results.success
$results.errors
$allcomputernames
Invoke-Command -computername $allcomputernames -scriptblock $yourscript -outvariable Results
#All the output will be in $results - note you don't include the $ for out variable, just the name of the variable.
$results.errors | out to error log
$results.success | export to csv

 

 

 

Thank you Doug, for the info. This is my first advanced function I have made. I learned and wanted to make it look professional. So thanks for the invoke-command examples. For now my question is if I take out the offline computers, the errors and the results that are going to a file, how do i run this and get those results into files? How do I make one object that gets the good info i want and the errors and offline. I am not asking you to re-write my function. Unless you know how to do it better. What was trying to do is make a template for local and remote functions and a template for invoke-command. I just learned how to do pscustomobjects. How would I get this same info out to files if it’s not built in to the function. I understand what you are saying. Make a function that does one thing. Gets the info. Then make something else that does something with the info. I just don’t know what to do. I am learning but I also want to do it the right way. Thank you again., Dave

If you want to leave it as is then it seems you’ll have no choice but to gather the contents of all the logs you wrote locally and concatenate them into their respective, collective log.

Thank you again how can I do it differently I guess I just don’t know how I’ve messed with it for three hours today I don’t know how how would you take out the writing to log files and how would you get that information if you could please share some examples thank you again for your time that’s why I love this community site because it’s created by some awesome power so guys and it has a lot of good people as part of the community and I hope to be answering these questions one day to. Thank you again how can I do it differently I guess I just don’t know how I’ve messed with it for three hours today I don’t know how how would you take out the writing to log files and how would you get that information if you could please share some examples thank you again for your time that’s why I love this community site because it’s created by some awesome power so guys and it has a Lotta good people as part of the community and I hope to be answering these questions one day to

So I made a pc.txt file with 5 computers that are offline, 5 computers that I know have a remote wmi access error, and 5 computers that get good results. So I took your examples and made this and it seems to work, it gets the 5 good computers and it gets 3 of the computers that have wmi errors so I guess they don’t throw errors when run locally. It gets the 5 offline computers and 2 of the wmi error computers and puts those in the errorvariable results. The $success variable I gave the invoke-command has the good results. I didn’t even need a try and catch in the scriptblock. I am still learning this but I didn’t know the errorvariable would get the scriptblock errors too, I just thought it got the computers the invoke-command could not run on so like the offline ones and ones with winrm errors. But it seems to work. Is it really this simple? Am i getting it wrong? Thanks for the help.

 

$PCList = Get-Content -Path 'C:\Temp\pc.txt'

$Success = Invoke-Command -ComputerName $PCList -SessionOption (New-PSSessionOption -NoMachineProfile) -ErrorAction SilentlyContinue -ErrorVariable FailedList -ScriptBlock {

$computer = $env:computername

$OS = Get-WmiObject -ComputerName $computer -Class Win32_OperatingSystem -ErrorAction Stop
$Sys = Get-WmiObject -Class Win32_ComputerSystem -ComputerName $computer -ErrorAction Stop

[PSCustomObject][ordered]@{
ComputerName = $computer
Last_Boot_Time = $os.ConvertToDateTime($os.LastBootupTime)
UserName = $sys.UserName
Status = 'ok'
}

} # END SCRIPTBLOCK

$Success
$Failed = $FailedList
$Failed.TargetObject

You are on the right track! I’d probably approach it like this. Return one object for every computer that is attempted. You collect all the info you want in this object then you just process the object in the end to log files or whatever.

Create the object in the beginning with whatever info you have so far

[pscustomobject]$results = @{
    Computername = "$computername"
}

 

Then as you progress with your script you can add more properties, including your systeminfo object if successful.

$results.online = $resultofonlinecheck
$results.success = $resultofthescript
$results.errors = $anyerrorsyouwanttolog
$results.sysinfo = $yoursysinfoobject

 

Once all are collected, process them.

foreach($item in $results){
    if($item.success){
        $item.sysinfo | export to csv
    }
    if(!$item.online){
        $item.computername | out to offline file
    }
    if($item.errors){
        $item.errors | out to error file
    }
}

Get-WMIObject is deprecated, you should really be using Get-CimInstance. There are 7 WMI queries, which you are making 7 separate queries in 7 separate sessions. With CimSession, you are establishing a single session and then doing 7 queries. I’m with @KrzyDoug and don’t think having all of the exports should be in the function, especially as a switch without a path. Putting everything in an object, including status, provides a common schema that can be filtered to find specific results:

function Get-ComputerInventory {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$False,
            ValueFromPipeline=$true,
            ValueFromPipelineByPropertyName=$true
        )]
        [String[]]$ComputerName = $env:COMPUTERNAME
    )

    begin{
        $inventoryObj = [PSCustomObject][ordered]@{
            'ComputerName'    = $null
            'Manufacturer'    = $null
            'Model'           = $null
            'OS Name'         = $null
            'OS Architecture' = $null
            'OS Version'      = $null
            'OS Build'        = $null
            'OS Install Date' = $null
            'Last Boot Time'  = $null
            'Status'          = $null
        }
    }
    process {
        foreach ( $Computer in $ComputerName ) {
            
            $inventory = $inventoryObj.PSObject.Copy()
            $inventory.ComputerName = $Computer

            Write-Verbose -Message ("Testing connection to computer: {0}" -f $inventory.ComputerName)

            if ( Test-Connection -ComputerName $computer -Count 1 -Quiet) {

                Write-Verbose -Message ("Connection successful. Attemting WMI connection to computer: {0}" -f $inventory.ComputerName)
 
                try {
                    try {
                        $sessionParams = @{
                            SessionOption = New-CimSessionOption -Protocol 'WSMAN'
                            ComputerName  = $ComputerName
                            ErrorAction   = 'Stop'
                        }

                        $session = New-CimSession @sessionParams
                    }
                    catch {

                        Write-Verbose -Message 'WSMAN connection failed, attempting DCOM'

                        try {
                            $sessionParams.SessionOption = New-CimSessionOption -Protocol 'DCOM'
                            $session = New-CimSession @sessionParams
                        }
                        catch {
                            Throw 'WMI Connection Failed with WSMAN and DCOM protocols'
                        }
                    }   

                    $params = @{
                        CimSession  = $session 
                        ErrorAction = 'Stop'
                    }

                    $Sys = Get-CimInstance @params -ClassName Win32_ComputerSystem
                    
                    $inventory.'Manufacturer' = $sys.Manufacturer
                    $inventory.'Model'        = $sys.Model
                    
                    $OS = Get-CimInstance @params -ClassName Win32_OperatingSystem

                    $inventory.'OS Name'         = $os.caption
                    $inventory.'OS Architecture' = $os.OSArchitecture
                    $inventory.'OS Version'      = $os.Version
                    $inventory.'OS Build'        = $os.buildnumber
                    $inventory.'OS Install Date' = $os.InstallDate
                    $inventory.'Last Boot Time'  = $os.LastBootupTime

                    $inventory.'Status' = 'Success'

                    Remove-CimSession -CimSession $session

                }
                catch {
                   $inventory.'Status' = 'Fail: {0}' -f $_
                }
            }
            else {
                $inventory.'Status' = 'Offline'
            }

            Write-Verbose -Message ("Inventory Complete for computer {0}" -f $inventory.ComputerName)
            
            $inventory
            
        }
    }
    end {

    }
}

$results = 'BadComputer',"$env:ComputerName",'Google.com' | Get-ComputerInventory
$results

Output:

ComputerName    : BadComputer
Manufacturer    : 
Model           : 
OS Name         : 
OS Architecture : 
OS Version      : 
OS Build        : 
OS Install Date : 
Last Boot Time  : 
Status          : Offline

ComputerName    : DESKTOP-MGT9HIB
Manufacturer    : LENOVO
Model           : 81EQ
OS Name         : Microsoft Windows 10 Home
OS Architecture : 64-bit
OS Version      : 10.0.18363
OS Build        : 18363
OS Install Date : 11/12/2019 10:07:40 AM
Last Boot Time  : 5/18/2020 8:59:12 AM
Status          : Success

ComputerName    : Google.com
Manufacturer    : 
Model           : 
OS Name         : 
OS Architecture : 
OS Version      : 
OS Build        : 
OS Install Date : 
Last Boot Time  : 
Status          : Fail: WMI Connection Failed with WSMAN and DCOM protocols

@Rob Simmers That is a beautiful script. I must copy and paste.

Thank you for that advanced function re-write. It looks great and was what I wanted to create. Instead of outputting to 3 files it can all go to one file and then I can sort that CSV file. I just did not know how to make the PSCustomObject collect all 3. This was my first attempt at creating a correctly formatted advanced function like Don Jones says are tools in his training videos. I just didn’t have the knowledge to make it the way you made this one. I can now use this as an example for all advanced functions for collecting info good or bad in one csv file.

Just wondering if you know how to use this with invoke-command filepath and have it write the results to a csv file that is locally on my computer. Everything I do seems to create the csv file on the remote computer.

thanks again both of you for the great info. I love learning powershell. It makes the work hours go by faster.

 

Glad you’re having fun with Powershell, it can do a lot of cool things. Invoke-Command is invoking the command on the remote computer. As Doug and I were trying to explain, GET functions return a PsObject. They do not typically export in the function. Recommend you look at other Powershell examples in Invoke-Command help and how other commands work like Get-Service or Get-Process as a basic examples:

$services = Invoke-Command -ComputerName Server1, Server2 -ScriptBlock {Get-Service}
$services #| Export-CSV -Path C:\Scripts\Services.csv -NoTypeInformation

I guess I don’t know how error handling works with invoke-command then. I want to catch the same info, good results, errors, and offline computers. Will the variable for the invoke-command catch all of that in the variable? If so then that seems like an easy solution. I can then separate it and export it.

When you are using Get-CimInstance, you are connecting to the remote system with WSMan or DCOM to gather information. There is no need for Invoke-Command. Provide the function with a list of computers and it will create a session with the remote computers and collect the information and return it.

I know but I have 1100 computer desktops to get info from. One at a time takes hours. But blasting it out to all of them takes 5 minutes. Do you know of a good way to still collect all of that same info? Thanks again.

Yes, 5 minutes isn’t a long time, but there are ways to expedite things. Look at Jobs, which would allow you to split the computers into jobs, like 225 computers for each job and run in parallel, but the you would take the job results and combine them to have a single result:

https://devblogs.microsoft.com/scripting/parallel-processing-with-jobs-in-powershell/

The second option is using workflows to execute in parallel, this would be processing in a foreach loop:

https://docs.microsoft.com/en-us/powershell/module/psworkflow/about/about_foreach-parallel?view=powershell-5.1