2013 PowerShell Scripting Games Advanced Event 6 – The Grand Finale

For me, the Scripting Games have been a great learning experience this year. I’ve used many PowerShell features that I hadn’t used before such as splatting, ADSI, Workflows, and producing html webpages with PowerShell. I plan to write detailed followup blog articles on each of these topics over the next few months.

Event 6 was definitely challenging since I hadn’t used workflows before but I also knew that’s what was really needed to accomplish the given task properly (In my opinion). While you could accomplish the task without a workflow, the scripting games are about learning. I decided that I could maximize the return of investment on my time that I would invest in event 6 by using a workflow since I knew very little about them.

I start out by specifying the version of PowerShell and the module that is required. The “#Requires -Version 3.0” statement will prevent this function from being dot-sourced on a machine with anything less than PowerShell version 3. Using a requires version statement is going to be even more important in the future as additional versions of PowerShell are released in the future. PowerShell version 4 was announced at Microsoft TechEd North America 2013 this week.

Specifying that the DHCP module is required prevents the function from being dot-sourced on a machine that does not have the DHCPServer module installed which is part of the Remote Server Administration Tools. The additional benefit to this is that the module will be automatically imported when the function is dot-sourced which keeps you from having to manually import it if the $PSModuleAutoLoadingPreference happens to be set to none. Some people dislike the module auto-load feature and set this preference variable to none in their profile so you can’t assume that modules auto-load when sharing tools such as this one with others.

The help is collapsed in the image of the script, but providing comment based help is extremely important. For more information on this topic, run “help about_Comment_Based_Help” from within PowerShell:

2013sg-e6h-png

I validate the format of the values provided for the MAC address parameter and return a meaningful error message if they are not provided in the proper format.

I don’t validate the computers with the Test-Connection cmdlet because a Windows Server 2012 machine blocks ICMP (ping) requests by default which means it would fail to validate all of the computers we are trying to rename and add to the domain.

I provide parameters and defaults for all of the different items specified in the scenario requirements which can be found here.

I obfuscate the passwords so they’re not just in plain text in the script for anyone to see, although this is not a secure method of storing a password, and if someone runs that portion of the code, they will see the password in clear text.

2013sg-e6a-png

The BEGIN Block:

The first thing I do in the begin block is to check to see if PowerShell is running as an administrator. If not, the function ends and the user receives a message with instructions on how to run PowerShell as an administrator since PowerShell is unable to participate in User Access Control (UAC) and ask for elevated privileges.

You may ask why do I need to run PowerShell as an administrator? Well, to start with you shouldn’t be managing servers by logging into the console or remote desktoping into them. That means that this tool will be run from a client computer and by default on Windows 8, the WinRM service is not running. In order to access the trusted host list, WinRM has to be running. I retrieve what’s in the trusted host list and store it in a variable for use later. Specifying the -Force parameter of the Get-Item cmdlet starts the WinRM service without prompting the user if it’s not already running.

Another best practice is that you should never be logged into your computer or running PowerShell as a domain administrator. In order to run PowerShell as an admin, you’ll have to specify admin credentials when running it, but those should have local admin rights and be a domain user, not a domain admin. Specify elevated credentials on an as needed, per command basis. Remember, use the principal of least privilege.

This means the user you’re running PowerShell as won’t have access to the DHCP server and in order to specify a credential parameter with the Get-DHCPServerv4Lease cmdlet, you’ll need to create a CimSession. The other benefit to creating a CimSession is when the MAC addresses are piped in, we can retrieve all of the IP Addresses over the single connnection instead of having the overhead of setting up and tearing down ten different connections in this scenario.

I use a nested Try/Catch which attempts to create the CimSession using WSMAN and then attempts to use DCOM if it is unable to connect with WSMAN.

2013sg-e6b-png

The Workflow:

I nested the actual workflow into my begin block since it only needs to run once to load it into memory and then it can be called as many times as necessary, almost like dot-sourcing a function.

Comment based help is not supported in a workflow so I’ve added some inline help where applicable. I didn’t use a workflow instead of a function for this entire process because the way I understood the scenario, you need to accept the MAC addresses via pipeline input and pipeline input is not supported in workflows. Advanced parameter validation is also not supported per the information I found on the Hey, Scripting Guy Blog! article that I read, and per what I read in the PowerShell in Depth book.

A brand new machine can be added to the domain and renamed in one line using the Add-Computer cmdlet, but there’s an issue where if you revert the VM to a snapshot and delete the computer account from Active Directory, the rename portion fails with a “Directory service is busy” error, although it is added to the domain with the existing computer name:

2013sg-e6g-png

For this reason, I decided to rename the computer in one step, reboot, and then add it to the domain and reboot again because that process works reliably every time and I’ll take reliability over a little performance any day. I see having to reboot twice as a non-issue since the entire process is automated.

I was at Microsoft TechEd this week and I actually had a chance to speak to members of the PowerShell team about the “Directory service is busy” issue. They confirmed that it is an issue and said they had previously run into the same problem. They stated that they had spoken to the Active Directory team about the issue and were told to do the rename and the add to the domain in two steps as I had done in my workflow.

I started out with a sequence block in my workflow and had a difficult time finding the definitive answer on whether it was needed or not, but I finally found the answer on page 818 of Lee Holmes’s new Windows PowerShell Cookbook, 3rd edition book:

“Unlike the parallel statement, the lines within the braces of the parallel -foreach statement are treated as a unit and are invoked sequentially.”

This means the sequence statement was not needed. While it wouldn’t have necessarily hurt anything to have it in there, I don’t like having unnecessary items in my code.

2013sg-e6c-png

Here’s the part of the script I discussed earlier where it gives you a warning if you’re not running PowerShell as an administrator:

2013sg-e6d-png

The PROCESS Block:

The objective of the process block in my function is to translate the MAC addresses to IP addresses by querying the DHCP server. If the MAC addresses are provided via parameter input, it only runs once and the command can translate all of them in one shot so a foreach loop is not needed, but if the MAC addresses are specified via pipeline input, the process block will run once per MAC address and retrieve them one at a time which is not a problem either based on the way the command in the process block is written:

2013sg-e6e-png

The END Block:

One unique thing about my function is it performs a good portion of the work in the End block. This is because I need these items to run one time and only one time regardless of whether the MAC addresses are provided via parameter or pipeline input and that wouldn’t be the case if this portion of the function was added to the PROCESS block.

It starts out with a foreach loop to build a hash table of server names and IP addresses. While it’s looping through each individual IP address, it also adds just the specific IP addresses to the trusted host list along with not overwriting what’s already in it to prevent issues with other applications that may have already modified this setting. The individual IP’s are added to maximize security and I think of it like adding holes in a firewall, you want the holes to be as small as possible to reduce the risk on a security incident. You should NEVER add “*” to the trusted host list. For more information about the trusted host list see the free “Secrets of PowerShell Remoting” ebook on PowerShellBooks.com.

It’s also not possible to modify the trusted hosts list unless PowerShell is running as an administrator. This is just one more reason your tool needs to validate that PowerShell is running as an administrator, otherwise you’re only wasting time by processing the entire function to only fail in the workflow because the computers aren’t trusted:

2013sg-e6i-png

At the very end, I put the trusted host back the way it was to start with as I don’t want to leave unnecessary items in the trusted host list because it reduces security. We do however, want to put the trusted host list back the way it was to start with to prevent any issues with applications that may have previously modified this setting.

2013sg-e6f-png

Update 02/09/14
Posting my solution here as well since I’ve received some requests for it:

This PowerShell script can also be downloaded from the TechNet script repository.

µ

Leave a Reply

%d bloggers like this: