PowerShell – Use SCCM Automated Reports to Create Software Update Rollups

A while back, I showed off how you could use PowerShell to create Software Update packages and deployments for SCCM.  I was asked about my process for grabbing outstanding updates and adding them into the package.  I’ve since automated this method, and thought I’d share an update with the class.

SCCM gives you the ability to create subscriptions to automatically generate reports and email them or deposit them on a file share.  For the purpose of rollup creation, I use ‘Compliance 3 – Update group (per update) under Software Updates – A Compliance’.  The process is simple – Create a software update group of all of the updates you track and right-click on the report, click create subscription, and fill out the necessary fields.  Make sure you select the report to be delivered by Windows File Share, and Overwrite an existing file with a newer version.

Once you’ve completed the process and SCCM has generated its first report, you can use it to mine for the data you want.  First, we’ll need to import the CSV file.

Import-Csv "\\Server01\Patching\Critical and Security Updates All Workstations.csv"

CMReports1

As you will very quickly find, you’re not going to immediately get the data you want.  This is because the canned report adds 6 rows of header data that’s completely unusable for our purposes.  Not to worry though!  It can easily be remedied using Get-Content and re-exporting the data you want to a new CSV.

 Get-Content "\\server01\Patching\Critical and Security Updates All Workstations.csv" | Select-Object -Skip 6 | Out-File "\\server01\Patching\OutstandingWksUpds.csv"
 (Import-csv -Path "\\server01\Patching\OutstandingWksUpds.csv")

CMReports2This gives us a little something more to work with.  But we’re dealing with a few hundred lines of information, so let’s thin this out a bit.  For our purposes, I’m going to just look for updates for Windows components and Office for my workstations.  So we’ll filter out the following as a base:

  • Updates that are missing on 0 machines
  • Updates that have Server in the description
  • Updates that are applicable to applications we don’t wish to include. (Lync, SQL, Onedrive, etc.)

So now we’ll have a filter statement that looks like this:

(Import-csv -Path "\\Server01\Patching\OutstandingWksUpds.csv").where({$PSItem.Details_Table0_Missing -ne 0 -and $PSItem.Details_Table0_Vendor0 -notlike "*Server*" -and $PSItem.Details_Table0_Vendor0 -notlike "*Lync*" -and $PSItem.Details_Table0_Vendor0 -notlike "*Skydrive*"  -and $PSItem.Details_Table0_Vendor0 -notlike "*Onedrive*"-and $PSItem.Details_Table0_Vendor0 -notlike "*Sharepoint*"}) | Select-Object Details_Table0_Vendor0,Details_Table0_Missing

There’s a lot in there,  but once you’ve filtered out the things you don’t want, you’ll wind up with a nicely ordered list:CMReports3

Now that we have the updates, we can incorporate them into a Software Update Group using some of my previous code.

Important note: If you attempt to run the first line of the script while mapped to your SCCM PSDrive, you will get an error on the Out-File “Cannot open file because the current provider (AdminUI.PS.Provider\CMSite) cannot open a file.”  So make sure you’re not in your CM PSDrive until you’re ready to execute your CM cmdlets.

Get-Content "\\Server01\Patching\Critical and Security Updates All Workstations.csv" | Select-Object -Skip 6 | Out-File "\\Server01\Patching\OutstandingWksUpds.csv"
$update = (Import-csv -Path "\\Server01\Patching\OutstandingWksUpds.csv").where({$PSItem.Details_Table0_Missing -ne 0 -and $PSItem.Details_Table0_Vendor0 -notlike "*Server*" -and $PSItem.Details_Table0_Vendor0 -notlike "*Lync*" -and $PSItem.Details_Table0_Vendor0 -notlike "*Skydrive*"  -and $PSItem.Details_Table0_Vendor0 -notlike "*Onedrive*"-and $PSItem.Details_Table0_Vendor0 -notlike "*Sharepoint*"}) | Select-Object Details_Table0_Vendor0,Details_Table0_Missing


$PowerShellPath = "\\Server01\C$\Program Files\Microsoft Configuration Manager\AdminConsole\bin\ConfigurationManager.psd1"
$CMSiteCode = "A00"
Import-Module $PowerShellPath
CD ("$CMSiteCode" + ":")

         $UpdateGroupArgs = @{
                            'Name' = 'Workstations Update Rollup';
                            'Description' = 'Created with Powershell!'
                            'UpdateID' = '16802522','16801092','16803970'
                            }
New-CMSoftwareUpdateGroup @UpdateGroupArgs
    ForEach ($upd in $update){Add-CMSoftwareUpdateToGroup -SoftwareUpdateName $upd.Details_Table0_Vendor0 -SoftwareUpdateGroupName $UpdateGroupArgs.Name }

Execute the script, and then check your Software Update Groups and you’ll find you have a basic rollup package to test and deploy to your systems.

Using the same methods, you can streamline your process for creating new Software Update groups for your monthly deployments as well.  Nothing beats a set of eyeballs to review the list and make sure you’ve got what you want to push, but importing a list of outstanding updates and adding them straight to an update group sure as heck beats selecting them individually through the console.

PowerShell – Being Informative the Write Way

I recently gave a lunch and learn for using methods and techniques beyond the basics for a group of my colleagues.  During this seminar, I covered Write-Host, and why it shouldn’t be used in a script.  Beyond Don Jones saying that if you use Write-Host, God kills a puppy, there are very good reasons for using alternate methods.  I read through Mr. Snover’s blog post on the matter to gain more clarification before covering the topic myself.

Mr. Snover covers two scenarios in which a person may wish to use Write-Host.  They are:

  • For the purpose of conveying comforting information to the user.
  • For the purpose of conveying results.

I’m not one for teaching by PowerPoint, so I needed to find a good way to relay this information by example.  So I decided to show the differences between Write-Verbose, Write-Output, and Write-Host.  So let’s start by taking a look at the offending Write-Host, and why it shouldn’t be used in a script.

Write-Host "This is a text message"

Write-Host in itself seems harmless enough.  But whenever it is added to a script, it will always be executed regardless if the user cares about seeing your messages or not.  There is no switch to make these messages go away, so you’re always stuck with them.  Furthermore, if you take a closer look…

Write-Host "This is a text message" | Get-Member

You’ll get this return:

WriteWay2

Write-Host doesn’t even output a usable object.  This means that not only can you not get rid of the annoying messages, but you can’t even use it further in the pipe to add to a PSCustomObject output for reporting or any other purpose.

When we write a script, especially complex ones, we often feel we have an obligation to provide comforting information to the user (ie – “This script is going to do X”).  This is very helpful the first time you’re executing a script, but not something that they’ll care about often after that – unless something breaks of course.

Write-Verbose "This is a text message"

When you execute this as is, you get a blank return.  However, if you do this:

Write-Verbose "This is a text message" -Verbose

You get this:

WriteWay1

So now you have a message that you can relay to the user, and give them the choice of looking at it, or not, so long as you have the cmdletbinding function enabled.  There are a lot of reasons that you should probably do this regardless.

The caveat with Write-Verbose, of course, is that it’s not really an object.  So if you wanted to send the message to a file or Out-Gridview (one of my personal favorites), you’ll need to find a different approach.  That’s where Write-Output comes into play.

WriteWay3

Write-Output is for those times when you need to display information to the user, but you might want this information to be used further in the pipe as well.  So let’s see how this works:

Write-Output "This is text"

WriteWay4

Well that looks easy enough.  Just like a Write-Host.  But let’s do this:

Write-Output "This is text" | Get-Member

WriteWay5

Well now that’s different!

Where I find that Write-Output can be really useful is if I’m running a script where the potential output may be null.  In those instances, you don’t necessarily want to see nothing return.  A real world example of this is a script that I use to see what updates are pending on an SCCM client.  Using the CCM_SoftwareUpdate class in the ccm\ClientSDK namespace, I can query a machine and find out.

$ComputerName = "server01"
 FOREACH ($Computer in $ComputerName){
 get-wmiobject -ComputerName $Computer -Namespace root\ccm\ClientSDK -Class CCM_SoftwareUpdate | Select-Object * |
 ForEach-Object {
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"0","None");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"1","Available");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"2","Submitted");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"3","Detecting");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"4","PreDownload");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"5","Downloading");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"6","WaitInstall");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"7","Installing");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"8","PendingReboot");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"9","PendingReboot");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"10","PendingReboot");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"11","Verifying");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"12","InstallComplete");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"13","Error");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"14","WaitServiceWindow");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"15","WaitUserLogon");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"16","WaitUserLogoff");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"17","WaitJobUserLogon");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"18","WaitUserReconnect");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"19","PendingUserLogoff");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"20","PendingUpdate");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"21","WaitingRetry");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"22","WaitPresModeOff");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"23","WaitForOrchestration");
 return $PSItem;}

}#End FOREACH

When I run  this against a machine that has updates outstanding, I get a return like so:
WriteWay6

But when I run this against a fully compliant machine, I just get an empty return.  So instead, we modify our code with some logic to Write-Output in event of no updates found, and wrap that in a label that will be recognized :

$ComputerName = "SERVER01"
 FOREACH ($Computer in $ComputerName){
 $Update = 
 get-wmiobject -ComputerName $Computer -Namespace root\ccm\ClientSDK -Class CCM_SoftwareUpdate | Select-Object * |
 ForEach-Object {
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"0","None");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"1","Available");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"2","Submitted");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"3","Detecting");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"4","PreDownload");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"5","Downloading");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"6","WaitInstall");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"7","Installing");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"8","PendingReboot");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"9","PendingReboot");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"10","PendingReboot");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"11","Verifying");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"12","InstallComplete");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"13","Error");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"14","WaitServiceWindow");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"15","WaitUserLogon");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"16","WaitUserLogoff");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"17","WaitJobUserLogon");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"18","WaitUserReconnect");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"19","PendingUserLogoff");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"20","PendingUpdate");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"21","WaitingRetry");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"22","WaitPresModeOff");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"23","WaitForOrchestration");
 return $PSItem;} | Select-Object -First 1
If ($Update.count -eq 0){$Update = Write-Output "$Computer does not have any updates available."
 [PSCustomObject]@{
 "Name" = $Update}
 }#EndPSCustomObject
Else {$Update}
}

Now when we run our script:

WriteWay7b

And that looks a lot better.  Better yet, we can array our machines, and send it to Out-Gridview and we have a nice little report with a full accounting of the list.

Write-Host allows you to do fun and cool things like change the color of the text and stuff.  But when we’re building tools, we’re looking to build something useful that requires as little intervention by the user as possible.  Leave the coloring and formatting to Word and Excel when you need it.

PowerShell – Fun With PSCustomObjects

PowerShell is a superb tool for digging directly into the systems and getting the information you need.  Sometimes though, getting exactly what you’re looking for isn’t readily available.  For example, say I want to get the ACLs for my scripting directory, I just:

Get-Acl -Path c:\scripts | Format-Table -AutoSize -Wrap

And I get this:

PSC11

Cool to be sure, but if you look closely, you’ll see that the Access field is concatenated data.  The user, the access control type, and file system rights are all one string.  Not bad for reading, but if I want some kind of report to send to the boss (a la CSV), or use the output at a later time to reapply/compare permissions, it might not be acceptable.  Furthermore, we’ve formatted the data, making it essentially useless at this point if we want to move it through the pipeline.  But if you expand the Access property like so:

$Path = "c:\scripts"
$Directory = Get-Acl -Path $Path
$Directory.Access

We get the data broken down by it’s subproperties:

PSCO1

This is something we can work with!  But maybe I want the path too.  So let’s see if it’s available.

$Directory.Access | Get-Member

And we get:

PSCO2

Not exactly screaming out any winners here.  But we did have the file path in the Get-ACL.  So how do we put this all together in a single return?  How about with a PSCustomObject?

PSCustomObjects are a lot like hash tables.  They use name-value pairs like hash tables, but offer you a little more flexibility like allowing you to control the order in which they’re presented.  This makes them very nice for gathering the data you need, ordering it however you like (as far left as possible), and letting you move it through the pipe to be used or exported.  There’s some good reading to be had regarding PSCustomObjects in about_Object_Creation in PowerShell’s Get-Help or on TechNet.

So for our first example, let’s get the file path, the directory owner, and then the group, the access type, and the folder rights.  To do this, we’ll need to grab data from our Path variable, our Get-ACL cmdlet, and the Access property.  So we modify our original lines of code with a ForEach statement to call out the Access property to get our group list:

$Path = "c:\scripts"
$Directory = Get-Acl -Path $Path
ForEach ($Dir in $Directory.Access){}

And then we’ll create our PSCustomObject table inside the ForEach statement:

$Path = "c:\scripts"
$Directory = Get-Acl -Path $Path
ForEach ($Dir in $Directory.Access){
[PSCustomObject]@{
Path = $Path
Owner = $Directory.Owner
Group = $Dir.IdentityReference
AccessType = $Dir.AccessControlType
Rights = $Dir.FileSystemRights
}#EndPSCustomObject
}#EndForEach

And then we execute…

PSCO3

Success!  So now we can make our script a function and feed it some parameters and we can get our ACLs by group for a directory!

But we’re not done yet!

Oftentimes, I get asked from my colleagues if there’s a quick and easy way to retrieve network adapter information from machines on the network.  Those requests often include IP Address, DNS Settings, driver information and link speed.  Using Get-CIMInstance, we can retrieve the network adapter configuration for a machine by leveraging the Win32_NetworkAdapterConfiguration class.

PSCO4

This class gives us a wealth of information on the adapter config, but lacks the driver info and link speed that I mentioned earlier.  Using the Get-NetAdapter cmdlet, however, will net us that information that we’re missing.

For your information, you could also use the Get-CIMInstance cmdlet and target the MSFT_NetAdapter class in the root\standardcimv2 Namespace, but since we have a cmdlet that does all of the heavy lifting for us, we’ll go ahead and use that.

PSCO5

So, now that we know how to get the data we want, let’s go ahead and put together our list in a PSCustomObject like before:

$Computer = "server01"
$AdapterCfg = (Get-CIMInstance Win32_NetworkAdapterConfiguration -ComputerName $Computer).where({$PSItem.IPEnabled})
$NetAdapter = Get-NetAdapter -CimSession $Computer
[PSCustomObject]@{
System = $AdapterCfg.PSComputerName
Description = $AdapterCfg.Description
IPAddress = $AdapterCfg.IPAddress
SubnetMask = $AdapterCfg.IPSubnet
DefaultGateway = $AdapterCfg.DefaultIPGateway
DNSServers = $AdapterCfg.DNSServerSearchOrder
DNSDomain = $AdapterCfg.DNSDomain
DNSSuffix = $AdapterCfg.DNSDomainSuffixSearchOrder
FullDNSREG = $AdapterCfg.FullDNSRegistrationEnabled
WINSLMHOST = $AdapterCfg.WINSEnableLMHostsLookup
WINSPRI = $AdapterCfg.WINSPrimaryServer
WINSSEC = $AdapterCfg.WINSSecondaryServer
DOMAINDNSREG = $AdapterCfg.DomainDNSRegistrationEnabled
DNSEnabledWINS = $AdapterCfg.DNSEnabledForWINSResolution
TCPNETBIOSOPTION = $AdapterCfg.TcpipNetbiosOptions
IsDHCPEnabled = $AdapterCfg.DHCPEnabled
AdapterName = $NetAdapter.name
Status = $NetAdapter.status
LinkSpeed = $NetAdapter.linkspeed
Driverinformation = $NetAdapter.driverinformation
DriverFilename = $NetAdapter.DriverFileName
MACAddress = $AdapterCfg.MACAddress
}#EndPSCustomObject

PSCO6

Success!  Well…almost…

Once thing you might have noticed is that the data you’ve collected concatenates the data for all network adapters, so if you have multiple ethernet or wireless adapters, the data is all together and you might not want that.  Also, for my purposes, I only want the adapters that are connected to the network.  So:

$Computer = "server01"
$NetAdapter = (Get-NetAdapter -CimSession $Computer).where({$PSItem.LinkSpeed -gt 1})

This will give me only my network adapters that are showing a link speed greater than 1.

PSCO7

Now that I’ve got my adapters, I’ll feed them through a loop to gather the additional data from the Win32_NetworkAdapterConfiguration class.  I’ll use the MAC Address to match up the network adapters.  To do that, I need to modify the MAC Address string because Get-NetAdapter displays the MAC Address with dashes, and Win32_NetworkAdapterConfiguration stores it with colons.:

ForEach ($Net in $NetAdapter){
$NetMAC = $Net.MACAddress -replace "-",":"
$AdapterCfg = (Get-CIMInstance Win32_NetworkAdapterConfiguration -ComputerName $Net.PSComputerName).where({$PSItem.MACAddress -eq $NetMAC})
}

PSCO8So for the one MAC Address, we have two instances in our WMI class.  But looking further, we see that only one is actually IPEnabled.  So let’s go with that one:

(Get-CIMInstance Win32_NetworkAdapterConfiguration -ComputerName $Net.PSComputerName).where({$PSItem.MACAddress -eq $NetMAC}) | Where-Object IPEnabled -EQ $True

PSCO9

Now that looks a bit better!  So now let’s create our PSCustomObject table.

[PSCustomObject]@{
System = $AdapterCfg.PSComputerName
Description = $AdapterCfg.Description
IPAddress = $AdapterCfg.IPAddress
SubnetMask = $AdapterCfg.IPSubnet
DefaultGateway = $AdapterCfg.DefaultIPGateway
DNSServers = $AdapterCfg.DNSServerSearchOrder
DNSDomain = $AdapterCfg.DNSDomain
DNSSuffix = $AdapterCfg.DNSDomainSuffixSearchOrder
FullDNSREG = $AdapterCfg.FullDNSRegistrationEnabled
WINSLMHOST = $AdapterCfg.WINSEnableLMHostsLookup
WINSPRI = $AdapterCfg.WINSPrimaryServer
WINSSEC = $AdapterCfg.WINSSecondaryServer
DOMAINDNSREG = $AdapterCfg.DomainDNSRegistrationEnabled
DNSEnabledWINS = $AdapterCfg.DNSEnabledForWINSResolution
TCPNETBIOSOPTION = $AdapterCfg.TcpipNetbiosOptions
IsDHCPEnabled = $AdapterCfg.DHCPEnabled
AdapterName = $Net.name
Status = $Netr.status
LinkSpeed = $Net.linkspeed
Driverinformation = $Net.driverinformation
DriverFilename = $Net.DriverFileName
MACAddress = $AdapterCfg.MACAddress
InterfaceName = $Net.InterfaceDescription
}#EndPSCustomObject

And our return looks good!

PSC10

Now we can put the finishing touches on our script to make it a full function!

PSCustomObjects can help you create some exciting tools to gather information in your environment, or feed information from different sources into other applications (such as Active Directory or SQL).  So take a little time to get to know the PSCustomObject class and it might save you some time on the back-end!

Go here if you’d like to download a copy of my ACL function.

Go here if you’d like to download a copy of my NetAdapter

Sidenote: You might notice that I’m using Where-Object as a method in my examples (.where), but my downloads leverage Where-Object in the code.  This is something specific to PowerShell 4 and later that I learned from Jeff Hicks’ presentation on PowerShell V4 New Features on Pluralsight.  If you have access to the course, I highly recommend taking a look!  I’ll be talking about the different methods available in PowerShell v4 at a later date, and why I’ve gotten so hooked on them!

PowerShell – Strings Are Objects Too!

I find that as I become more comfortable with my new skills as a PowerShell junkie, I enjoy answering questions more and more.  Oftentimes, I find questions on various forums that challenge my knowledge and skill level to become even more adept at PowerShell.  Recently, I came across a task that not only challenged my skills, but also reinforced the need to break away from certain habits and concepts that I’ve been using over the years with my scripts.  This week’s lesson: Strings are not fixed data!

The challenge was an interesting one:  You have a CSV file with a bunch of names.  Some include First, Middle, and Last – some don’t.  Retrieve the first letter of the first name and the entire last name to generate an email address.

Easy enough if your CSV consists of three columns.  First we’ll use Import-CSV and see what we’ve got here.

Import-CSV 'C:\scripts\email.csv'

Strings0

Ah.  Looks like the file has no headers.  Easy enough of a fix:

Import-Csv 'C:\scripts\email.csv' -Header First,Middle,Last

Strings1Now that looks a lot better!  So we’ve got our data.  Let’s go ahead and break this thing down, starting with the first name.

$FName = $Name.First

This will return the names in the First column only.  Now what we’ll do is use the substring method to extract the first character from the string.  The two parameters after substring mark your starting point (which is 0, or before the first letter), and how many characters you want (which is the first one).

$First = $FName.substring(0,1)

So we’ll just run our script real quick here and…Strings2

 

 

 

 

 

There we go!  Part one accomplished!  The last name and email parts are pretty easy.  Just add in these lines:

$Last = $Name.Last
$Email = $First + $Last + '@company.com'

And you get…

Strings3 And there you have it!

But what if the full names are in a single column only?  Well, that’ll take a little bit different of an approach.  Fortunately, it doesn’t involve completely scrapping the script.  For the first initial, you can still use the substring method to grab it, but you need an easy way to identify the last name.  So let’s split the incoming data on the spaces:

$Last = $FName.Split(" ")

And we’ll get this return:

Strings4Now I started thinking, “Well that’s great!  But how do I tell PowerShell which one is which!?”  It took a little time, but I remembered that with PowerShell, we’re moving objects through the pipe, not fixed data.   And like any other object, I can filter them!

$Last = $FName.Split(" ") | Select-Object -Last 1

And voila!

Strings5Now you might decide, “you know, I think I’d like to have the middle name in there.  How are you going to do that, huh!?”  Well, I’ll show you. Just:

$Middle = $FName.Split(" ") | Select-Object -First 2 | Select-Object -Last 1

Strings6

Oh, but look.  Some of the strings have a middle name instead of just an initial.  We can easily remedy this by feeding our filtered object into a substring like we did with the first name:

$Middle = ($FName.Split(" ") | Select-Object -First 2 | Select-Object -Last 1).substring(0,1)

And now we get:

Strings7So then we put this all together and update our Email string…

Strings8Success!  Now we can start issuing emails from that list that HR sent us without having to do any manual formatting.  And we also reinforce the lesson that everything, including strings, are not fixed data; but objects that can be moved, and manipulated, through the pipe.

Have a happy holiday!

A PowerShell Life: Moving To Server Core

powershell3We’ve all had a good, long time to settle into the daily routine the goes with being a Windows Server administrator or engineer.  We’ve had time to learn all of the nuances behind the graphical user interface.  We know where to go to get our management consoles, what commands to run to make the tweaks we need and get the things we need to get done completed.  And I, like many of my counterparts, looked at Server Core when it was first introduced with Server 2008 with apprehension.  Why would I leave the confines of my comfortable UI for something so stripped down?  What was this BS that Microsoft was feeding us?  I’m not a UNIX guy!

Sure, you have a smaller foot print for your OS when it comes to storage; and yeah, it uses less compute and memory too.  Sure, it requires less patching, and fewer reboots.  But what about my tools!?  What about my Start button!?

Truth be told, ever since I started working in PowerShell, I’ve found that I’ve used the administration consoles less and less.  Oftentimes, working in the console requires a one-at-a-time mentality, whereas with PowerShell I can manage multiple machines at once.  Even remoting to a server’s desktop was far too time consuming, especially if I could just get the information or execute the command I wanted through PowerShell Remoting.  As a ConfigMan engineer, I’ve even challenged myself to move outside of the box and create my own modules to service SCCM Clients through PowerShell so I don’t even have to open my console to do any troubleshooting, and can share these tools with my colleagues so they too can troubleshoot without directly interacting with a console or the client.

So when it came to be time to start taking a good hard look at my environment, and the footprint it was leaving in virtualization resources, it was time to take a look at myself as an admin and engineer.  To get a little philosophical – we, as architects and stewards of technology, can view the design of our environment as a mirror reflection of our own selves.  Our knowledge and experience makes up just as much of the environment as the best practices and company directives that guide our hand in creating the infrastructure that will support our users for the next life cycle.  Once I realized that the graphical interface served no purpose other than being that comfortable space that I’ve known for the last 18 years, it no longer made sense to incur additional costs in resources and a larger attack surface in my environment.

Of course, we have our ways of easing into the water.  I’ve been testing the applications I’m responsible for in a core instance for a few months now.  Some applications made the cut and were able to be used effectively in Core, and some just weren’t.  When I was finally ready to take the leap with those that would, I was able to use a simple PowerShell script that removed the GUI and rebooted the server.

Piece of cake.

It’s since been a couple of weeks since that implementation, and so far so good.  If anything, the most notable change that I’ve encountered is with myself.  I wrote a lot of scripts when I began learning PowerShell, and now that I’m working with core, I’m writing new scripts more frequently and refining old ones as my knowledge grows.  My environment is using fewer resources, and I’m becoming a smarter PowerShell Administrator.  I’d say that’s a win-win scenario.

Getting Your Uptime – The PowerShell Way!

powershell3I remember, back in my admin days, we had an executable called uptime.exe.  It didn’t do much, but it gave us the amount of time that a system had been up and running for – which was something useful for checking if you suspected a server had gone offline and didn’t want to log in to verify if it crashed or not.

Some months back, I wrote a PowerShell script to give me the uptime on a remote system.  It worked well, but the formatting left something to be desired.  Using Get-CimInstance, and a little PowerShell math, I came up with this:

Get-CimInstance Win32_OperatingSystem -ComputerName Server01 | Select-Object CSName,LastBootUpTime,@{Name = 'Uptime';Expression = {$PSItem.LocalDateTime - $PSItem.LastBootUpTime }}

It does well enough, but the output format isn’t the prettiest.  In particular, I’m not really happy with the uptime output, as someone may not immediately realize that the first set of digits is days.

CSName      LastBootUpTime           Uptime
------      --------------           ------
Server01    11/19/2014 1:20:02 AM    14.08:16:27.5529900

So what to do?  I did a little reading and came across one of the Scripting Guys’ posts about the New-Timespan cmdlet and how it works.  Well, we know that the LastBootUpTime is a datetime data type; and we know that the New-Timespan cmdlet takes datetime datatypes; so we should be able to hook those two up.   So let’s do this!
#Define our current date and pull the last reboot date from a machine.
$Date = Get-Date
$Reboot = Get-CimInstance Win32_OperatingSystem -ComputerName Server01 |
Select-Object CSName,LastBootUpTime

This will give us our current date and time to feed in to the New-TimeSpan cmdlet, and the LastBootUpTime from the Win32OperatingSystem class.  I’m also grabbing the system name from the CSName property to feed into my output.  Now let’s build our command:
#Put it all together to get system uptime information
New-TimeSpan -Start $Reboot.LastBootUpTime -End $Date |
Select-Object @{Label = "System Name"; Expression = {$Reboot.CSName}},@{Label = "Last Reboot Time"; Expression = {$Reboot.LastBootUpTime}},Days,Hours,Minutes,Seconds |
Format-Table -AutoSize

I formatted the table to make it a little bit cleaner and relabeled the CSName and LastBootUpTime properties to make things a bit easier to read.  Now we get the following output:

System Name      Last Reboot Time         Days Hours Minutes Seconds
-----------      ----------------         ---- ----- ------- -------
Server01         11/19/2014 1:20:02 AM    14   9     19      29

Now just add some parameterization for the computer name, maybe give it some logic to scan multiple machines, and you’ve got yourself a very fine replacement for uptime.exe.

Feel free to download the script from the TechNet Script Center.

Invoking Your SCCM 2012 Client Remotely – With PowerShell!

powershell3I actually wrote about this some time ago when I first started PowerShell’ing.  But I’ve since improved my script, and I’m sharing it with you now.

Sometimes we need to make a change to our CM clients’, or the environment that they reside in that requires us to trigger an action on the CM client itself.  Be it pull in an updated hardware inventory, force the machine to run a Software Updates Scan, or a Machine Policy to get a machine to detect a new software deployment; it can be a painful experience for an administrator to have to go from machine to machine to run an action.

There are the right-click tools for those that have access to the console, but not everyone does, and not every company will allow you to install them.  Wouldn’t it be nice to have a function that you could include in a module you’ve built, that has the ability to invoke a CM client?  Well, now I have something to offer you:
Function Invoke-CMClient{
<# .SYNOPSIS Invoke commands remotely on an SCCM Client for a system or systems. .DESCRIPTION This function allows you to remotely trigger some of the more common actions that you would find on the local Configuration Manager console. .PARAMETER -ComputerName <string[]> Specifies the target computer for the management operation. Enter a fully qualified domain name, a NetBIOS name, or an IP address. When the remote computer is in a different domain than the local computer, the fully qualified domain name is required. This command defaults to localhost. .PARAMETER -Action Specifies the action to be taken on the SCCM Client. The available actions are as follows: HardwareInv - Runs a Hardware Inventory Cycle on the target machine. SoftwareInv - Runs a Software Inventory Cycle on the target machine. UpdateScan - Runs a Software Updates Scan Cycle on the target machine. MachinePol - Runs a Machine Policy Retrieval and Evaluation Cycle on the target machine. UserPolicy - Runs a User Policy Retrieval and Evaluation Cycle on the target machine. FileCollect - Runs a File Collection Cycle on the target machine. .INPUTS You can pipe a computer name to Invoke-CMClient .EXAMPLE Invoke-CMClientAction -ComputerName server01 -Action HardwareInv The above command will invoke the Configuration Manager Client's Hardware Inventory Cycle on the targeted computer. The return will look like the following: __GENUS : 1 __CLASS : __PARAMETERS __SUPERCLASS : __DYNASTY : __PARAMETERS __RELPATH : __PARAMETERS __PROPERTY_COUNT : 1 __DERIVATION : {} __SERVER : server01 __NAMESPACE : ROOT\ccm __PATH : \\server01\ROOT\ccm:__PARAMETERS ReturnValue : PSComputerName : server01 .NOTES Created by Will Anderson. https://lastwordinnerd.com/category/posts/powershell-scripting/ This script is provided AS IS without warranty of any kind. #>

PARAM(
[Parameter(Mandatory=$True,ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
[string[]]$ComputerName = $env:COMPUTERNAME,

[Parameter(Mandatory=$True)]
[ValidateSet('HardwareInv','SoftwareInv','UpdateScan','MachinePol','UserPolicy','DiscoveryInv','FileCollect')]
[string]$Action

)#Close Param

#$Action...actions...actions...
SWITCH ($action) {
'HardwareInv' {$_action = "{00000000-0000-0000-0000-000000000001}"}
'SoftwareInv' {$_action = "{00000000-0000-0000-0000-000000000002}"}
'UpdateScan' {$_action = "{00000000-0000-0000-0000-000000000113}"}
'MachinePol' {$_action = "{00000000-0000-0000-0000-000000000021}"}
'UserPolicy' {$_action = "{00000000-0000-0000-0000-000000000027}"}
'FileCollect' {$_action = "{00000000-0000-0000-0000-000000000010}"}
} #switch

FOREACH ($Computer in $ComputerName){
if ($PSCmdlet.ShouldProcess("$action $computer")) {

Invoke-WmiMethod -ComputerName $Computer -Namespace root\CCM -Class SMS_Client -Name TriggerSchedule -ArgumentList "$_action"

}#if
}#End FOREACH Statement

}#Close Function Invoke-CMClient
This function allows you to invoke an SCCM Client action remotely on a number of computers simultaneously.   I’ve included the actions most commonly used from my perspective, but you can add more if you like.  You can get a good list of them here, or just explore the SMS_Client class for a full list.

What You See Is Not Always What You Get-Member

powershell3Recently, I was approached by a co-worker to solve an issue they were having in PowerShell.  They wanted to get the full-path of all of the directories in a folder using Get-ChildItem.  By default, Get-ChildItem will display the permissions (Mode), LastWriteTime, Length, and Name columns.  However, the name column only displays the name.  So they asked me if I knew a good way to get the full path.

As administrators and engineers that have been in the industry for 15-20 years, it’s often hard for us to remember that PowerShell isn’t the WYSIWYG (‘What You See Is What You Get’ for you younger engineers. 🙂 ); that we’re moving .NET objects through the pipeline, and while PowerShell displays the most commonly pertinent data in a given command, it’s not everything.  Get-Member is a good way to give us a list of all of the data that’s passed down the pipe from one object to the next, and get the bits that we really need.

So let’s explore!

Get-ChildItem -Path "C:\Windows" -Directory | Get-Member

When you run a command against an object in the pipe, and pipe that to Get-Member, PowerShell looks at the object and displays the properties and methods associated with that object.  For example:

PS C:\> Get-ChildItem -Path "C:\Windows" -Directory | Get-Member

   TypeName: System.IO.DirectoryInfo

Name                      MemberType     Definition
----                      ----------     ---------- 
Mode                      CodeProperty   System.String Mode{get=Mode;}
Create                    Method         void Create(), void Create(System.Security.AccessControl.DirectorySecurity directorySecurity)
CreateObjRef              Method         System.Runtime.Remoting.ObjRef CreateObjRef(type requestedType)
CreateSubdirectory        Method         System.IO.DirectoryInfo CreateSubdirectory(string path), System.IO.DirectoryInfo CreateSubdirector...
Delete                    Method         void Delete(), void Delete(bool recursive)
EnumerateDirectories      Method         System.Collections.Generic.IEnumerable[System.IO.DirectoryInfo] EnumerateDirectories(), System.Col...
EnumerateFiles            Method         System.Collections.Generic.IEnumerable[System.IO.FileInfo] EnumerateFiles(), System.Collections.Ge...
EnumerateFileSystemInfos  Method         System.Collections.Generic.IEnumerable[System.IO.FileSystemInfo] EnumerateFileSystemInfos(), Syste...
Equals                    Method         bool Equals(System.Object obj)
GetAccessControl          Method         System.Security.AccessControl.DirectorySecurity GetAccessControl(), System.Security.AccessControl....
GetDirectories            Method         System.IO.DirectoryInfo[] GetDirectories(), System.IO.DirectoryInfo[] GetDirectories(string search...
GetFiles                  Method         System.IO.FileInfo[] GetFiles(string searchPattern), System.IO.FileInfo[] GetFiles(string searchPa...
GetFileSystemInfos        Method         System.IO.FileSystemInfo[] GetFileSystemInfos(string searchPattern), System.IO.FileSystemInfo[] Ge...
GetHashCode               Method         int GetHashCode()
GetLifetimeService        Method         System.Object GetLifetimeService()
GetObjectData             Method         void GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serializati...
GetType                   Method         type GetType()
InitializeLifetimeService Method         System.Object InitializeLifetimeService()
MoveTo                    Method         void MoveTo(string destDirName)
Refresh                   Method         void Refresh()
SetAccessControl          Method         void SetAccessControl(System.Security.AccessControl.DirectorySecurity directorySecurity)
ToString                  Method         string ToString()
PSChildName               NoteProperty   System.String PSChildName=addins
PSDrive                   NoteProperty   System.Management.Automation.PSDriveInfo PSDrive=C
PSIsContainer             NoteProperty   System.Boolean PSIsContainer=True
PSParentPath              NoteProperty   System.String PSParentPath=Microsoft.PowerShell.Core\FileSystem::C:\Windows
PSPath                    NoteProperty   System.String PSPath=Microsoft.PowerShell.Core\FileSystem::C:\Windows\addins
PSProvider                NoteProperty   System.Management.Automation.ProviderInfo PSProvider=Microsoft.PowerShell.Core\FileSystem
Attributes                Property       System.IO.FileAttributes Attributes {get;set;}
CreationTime              Property       datetime CreationTime {get;set;}
CreationTimeUtc           Property       datetime CreationTimeUtc {get;set;}
Exists                    Property       bool Exists {get;}
Extension                 Property       string Extension {get;}
FullName                  Property       string FullName {get;}
LastAccessTime            Property       datetime LastAccessTime {get;set;}
LastAccessTimeUtc         Property       datetime LastAccessTimeUtc {get;set;}
LastWriteTime             Property       datetime LastWriteTime {get;set;}
LastWriteTimeUtc          Property       datetime LastWriteTimeUtc {get;set;}
Name                      Property       string Name {get;}
Parent                    Property       System.IO.DirectoryInfo Parent {get;}
Root                      Property       System.IO.DirectoryInfo Root {get;}
BaseName                  ScriptProperty System.Object BaseName {get=$this.Name;}

That’s a lot of information, and something very promising:

PSPath     NoteProperty     System.String PSPath=Microsoft.PowerShell.Core\FileSystem::C:\Windows\addins

So let’s take a look at this:

PS C:\>Get-ChildItem-Path"C:\Windows"-Directory|Select-ObjectPSPath

PSPath
------
Microsoft.PowerShell.Core\FileSystem::C:\Windows\addins
Microsoft.PowerShell.Core\FileSystem::C:\Windows\AppCompat
Microsoft.PowerShell.Core\FileSystem::C:\Windows\apppatch
Microsoft.PowerShell.Core\FileSystem::C:\Windows\assembly
Microsoft.PowerShell.Core\FileSystem::C:\Windows\AUInstallAgent
Microsoft.PowerShell.Core\FileSystem::C:\Windows\Boot
Microsoft.PowerShell.Core\FileSystem::C:\Windows\CbsTemp
Microsoft.PowerShell.Core\FileSystem::C:\Windows\CCM

Well, that’s not quite what I was looking for.  Let’s look at some of the other properties. FullName looks promising.

PS C:\>Get-ChildItem-Path"C:\Windows"-Directory|Select-ObjectFullName

FullName
--------
C:\Windows\addins
C:\Windows\AppCompat
C:\Windows\apppatch
C:\Windows\assembly
C:\Windows\AUInstallAgent
C:\Windows\Boot
C:\Windows\CbsTemp
C:\Windows\CCM
C:\Windows\ccmcache

Well now.  That looks a lot better!

Let’s try another.

Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration

Returns

ServiceName      DHCPEnabled    Index          Description
-----------      -----------    -----          -----------
Rasl2tp          False          0              WAN Miniport (L2TP)
RasSstp          False          1              WAN Miniport (SSTP)
RasAgileVpn      False          2              WAN Miniport (IKEv2)
PptpMiniport     False          3              WAN Miniport (PPTP)
RasPppoe         False          4              WAN Miniport (PPPOE)
NdisWan          False          5              WAN Miniport (IP)
NdisWan          False          6              WAN Miniport (IPv6)
NdisWan          False          7              WAN Miniport (Network Monitor)
kdnic            True           8              Microsoft Kernel Debug Network Adapter
AsyncMac         False          9              RAS Async Adapter
NETwNe64         True           10             Intel(R) Centrino(R) Ultimate-N 6300 AGN
e1iexpress       True           11             Intel(R) 82579LM Gigabit Network Conne...
BthPan           True           14             Bluetooth Device (Personal Area Network)
vwifimp          True           15             Microsoft Wi-Fi Direct Virtual Adapter
tunnel           False          16             Microsoft ISATAP Adapter
tunnel           False          17             Microsoft ISATAP Adapter
tunnel           False          18             Microsoft Teredo Tunneling Adapter
tunnel           False          19             Microsoft ISATAP Adapter
tunnel           False          20             Microsoft ISATAP Adapter
tunnel           False          21             Microsoft ISATAP Adapter
tunnel           False          22             Microsoft ISATAP Adapter
tunnel           False          23             Microsoft ISATAP Adapter
tunnel           False          24             Microsoft ISATAP Adapter

Good information, sure enough.  But it’s lacking in some key information that you might want to see, such as the IP Address.  So let’s take a look with the following command.

Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration | Get-Member

The results of this are pretty long, so I’m not going to post it here.  But you’ll find a ton of information regarding the adapter configuration including the IP Address, IP Subnet, MAC Address, DNS information, and more.  So with a quick review, you can quickly craft a command to pull some real, pertinent data.  While we’re at it, I only want to see the adapters that have an actual IP Address as well.

PS C:\> Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration | Where-Object IPAddress -NE $null | Select-Object Description,ServiceName,IPAddress,DNSServerSearchOrder

Description                         ServiceName     IPAddress            DNSServerSearchOrder              
-----------                         -----------     ---------            --------------------              
Intel(R) Centrino(R) Ultimate-N ... NETwNe64        {192.168.0.51}   {192.168.0.2, 192.168.0.3}

And there you have it!  Just remember, if you ever feel like you’re looking in the right place, but just can’t seem to find what you’re looking for, Get-Member should be the next place you visit.

Just Because It Pings, Doesn’t Mean It SMBs (Testing Connectivity With PowerShell)

 

powershell3I’m a long time SCCM guy. Over the last couple of years, I’ve been learning more and more to love the automation possibilities with Powershell, Orchestrator, and even Service Manager. With these tools all used together, it makes a patch guru’s time so much easier in managing all things SCCM, including managing the devices themselves. Continue reading “Just Because It Pings, Doesn’t Mean It SMBs (Testing Connectivity With PowerShell)”