Extending Objects in Windows PowerShell, Part 4

Over the last several articles, I’ve demonstrated how to extend an object in PowerShell and why you might want to do so. In this article, I’ll show you how to take these concepts and use them to manage servers from a PowerShell prompt.
Extending Objects in PowerShell Article Series:


One of the reasons I find this approach appealing is that it forces PowerShell users to start thinking about managing at scale. In the pre-PowerShell days, you probably managed one thing at a time. With PowerShell, I’m trying to get people to think about managing 10 or 100 things at a time. Don’t be in the mindset of managing one server at a time when you can manage 2, 20, or 200.
The premise for my management tool is to create an object for each server I want to manage. The object will have a few properties and methods for reporting and to at least serve as a proof of concept. The function I wrote is by no means complete. You could easily extend it to include additional properties and methods to meet your business needs. In some ways this is like a simpler, console-based version of System Center, except that you can completely customize it.
Here’s my New-ManagedComputer function.

Function New-ManagedComputer {
<#
.Synopsis
Create a custom object for managing a server
.Description
This command will create a custom object for a given server that can be used to easily manage the server. The custom object has its own type name so you should run this command as well:
Update-TypeData -TypeName "my.ManagedComputer" -DefaultDisplayPropertySet Computername,OS,PSVersion,LastBoot,Uptime
.Example
PS C:\> $core01 = New-ManagedComputer -computername chi-core01
This first command creates a management object for CHI-CORE01.
PS C:\> $core01
Computername : CHI-CORE01
OS           : Microsoft Windows Server 2012 R2 Datacenter
PSVersion    : 4.0
LastBoot     : 7/12/2015 12:13:04 PM
Uptime       : 03:25:06.1554444
PS C:\> $core01.GetLocalAdmins() | format-table Name,Caption,PSComputername -AutoSize
Please wait...
Name          Caption                  PSComputerName
----          -------                  --------------
Administrator CHI-CORE01\Administrator CHI-CORE01
LocalAdmin    CHI-CORE01\LocalAdmin    CHI-CORE01
#>
[cmdletbinding()]
Param(
[Parameter(Position=0,ValueFromPipeline)]
[ValidateNotNullOrEmpty()]
[Alias("CN")]
[string]$Computername = $env:computername
)
Begin {
    Write-Verbose "Starting $($MyInvocation.Mycommand)"
} #begin
Process {
    Write-Verbose "Creating a custom management object for $Computername"
    $os = Get-Ciminstance -ClassName Win32_operatingsystem -ComputerName $computername
    $PSVersion = Invoke-Command {$PSVersionTable.PSVersion} -HideComputerName -computername $Computername
    $managed = [pscustomobject]@{
      Computername = $computername.ToUpper()
      PSVersion = $PSVersion
      OperatingSystem = $os.Caption
      ServicePack = $os.CSDVersion
      LastBoot = $os.LastBootUpTime
    }
    #adding properties
    $managed | Add-Member -MemberType ScriptProperty -Name Uptime -Value { (Get-Date) - (Get-Ciminstance win32_operatingsystem -computername $this.computername).lastBootUptime  }
    $managed | Add-Member -MemberType PropertySet -Name Boot -Value Computername,LastBoot,Uptime -force
    $managed | Add-Member -MemberType AliasProperty -name OS -Value OperatingSystem
    $managed | Add-Member -MemberType ScriptProperty -Name Shares -value { Get-SMBShare -CimSession $this.computername}
    #adding methods
    $managed | Add-Member -MemberType ScriptMethod -Name GetProcesses -Value {Get-CimInstance -class Win32_process -ComputerName $this.computername}
    $managed | Add-Member -MemberType ScriptMethod -Name Ping -Value {Param([int]$Count=4) Test-Connection -ComputerName $this.computername -Count $count}
    $managed | Add-Member -MemberType ScriptMethod -Name GetFreeSpace -Value {
    Param([string]$Drive="c:")
    $disk = Get-CimInstance -Classname win32_logicaldisk -filter "deviceid = '$drive'" -ComputerName $this.computername
    $disk.Freespace
    }
    $managed | Add-Member -MemberType ScriptMethod -Name Reboot -value { Restart-Computer -ComputerName $this.computername -force}
    $managed | Add-Member -MemberType ScriptMethod -Name Shutdown -value { Stop-Computer -ComputerName $this.computername -force}
    $managed | Add-Member -MemberType ScriptMethod -Name GetLocalAccounts -value { get-ciminstance -class win32_useraccount -computername $this.computername -filter "LocalAccount='True'"}
    $managed | Add-Member -MemberType ScriptMethod -Name GetLocalAdmins -value {
    Write-host "Please wait..." -ForegroundColor Cyan
    Get-CimInstance -class win32_group -filter "name='Administrators'" -computername $this.computername | Get-CimAssociatedInstance -ResultClassName Win32_useraccount
    }
    $refresh = {
        $this.PSVersion = Invoke-Command {$PSVersionTable.PSVersion} -HideComputerName -computername $this.Computername
        $os = Get-Ciminstance -ClassName Win32_operatingsystem -ComputerName $this.computername
        $this.OperatingSystem = $os.Caption
        $this.LastBoot = $os.LastBootUpTime
        $this.ServicePack = $os.CSDVersion
    }
    $managed | Add-Member -MemberType ScriptMethod -Name Refresh -value $refresh
    Write-Verbose "Inserting a type name"
    $myType = "my.ManagedComputer"
    $managed.psobject.TypeNames.Insert(0, $myType)
    #write object to the pipeline
    $managed
} #process
End {
    Write-Verbose "Ending $($MyInvocation.Mycommand)"
} #end
} #close function

To use the function, you’ll also need to update type data if you want a more user-friendly default display.

Update-TypeData -TypeName "my.ManagedComputer" -DefaultDisplayPropertySet Computername,OS,PSVersion,LastBoot,Uptime


Although I’m talking about scale, you still might want to create a variable for each computer. This could be helpful in my test domain when it comes to troubleshooting a problem, where I can easily do something like this:

"chi-dc01","chi-test01","chi-srv03" | foreach {
 $var = $_.split("-")[1]
 New-Variable -Name $var -Value (New-ManagedComputer -Computername $_)
}

Because my server names have a hyphen, I can’t use the name as my variable name, so I have an extra step to split the name on the dash and take the last part, which I know will be unique. The end result is variable that looks like this:
071215 1931 AllAboutTha1
And has these methods:
071215 1931 AllAboutTha2
The script methods are the ones that I added. We’ll look at some of these in a moment.
The other approach is to create a single variable for all your managed computers.

$all = "chi-dc01","chi-test01","chi-srv03" | New-ManagedComputer

And there’s no law that says you can’t do both. With this approach, you can use typical PowerShell commands for reporting purposes.
071215 1931 AllAboutTha3
This example is using the custom property set. Or you can use any of the properties.
071215 1931 AllAboutTha4
If you are running PowerShell 4.0 or later, you can also use the new Where method, which is often faster.

$all.where({ $_.psversion.major -gt 3}) | Select Computername,PSVersion,OS

The results will be the same.
Another interesting technique is that you can run a method for all computers at once.
071215 1931 AllAboutTha5
For a small number of computers, this might be too bad. Although you’ll probably want to do this instead:
071215 1931 AllAboutTha6
But depending on your method, you might not need to do anything special.
071215 1931 AllAboutTha7
I hope you are noticing that for the most part I’m getting complete objects. I probably should have done the same for disk information but I wanted to show you how you could get a single value.
I can easily manage information in aggregate:

$all.processes().where({$_.workingsetsize -ge 25MB}) | sort WorkingsetSize -Descending | Select PSComputername,Name,WorkingSetSize

071215 1931 AllAboutTha8
Or individually:

$all.foreach({$_.processes() | sort WorkingsetSize -Descending | Select -first 5})

071215 1931 AllAboutTha9
And of course I have the computername property, which I can leverage with any cmdlet that takes pipeline binding by property name.

$all | get-service wuauserv | Select Name,Status,Machinename

071215 1931 AllAboutTha10

When you think about objects in the pipeline, you can accomplish a great deal with minimal effort. I recognize that many IT pros think about using PowerShell to script everything, but there is an equally amount of amazing work you can do from an interactive prompt. I hope these last few articles have helped prove that. Questions or comments? Throw ’em in the comments.