Creating a PowerShell Script from Written Processes and Procedures

Creating a PowerShell Script from Written Processes and Procedures

This article was written by Bill Kindle. He is a frequent contributor to the Adam the Automator (ATA) blog. If you'd like to read more from this author, check out his ATA author page. Be sure to also check out more how-to posts on cloud computing, system administration, IT, and DevOps on adamtheautomator.com!

As a mentor, I'm often asked, "How do you get inspiration for a PowerShell script?", followed by something sounding similar to, "I just don't know what I can script or where to start." When I'm told that, the person saying it sounds defeated and about to give up. This was a question & feeling I had myself early in my PowerShell journey too.

"So, what's the answer, Bill?" you might ask. Well...the answer you seek, young grasshopper is...

Documentation.

How I Started

Many years ago, I struggled to make scripts. No matter the language, it was an awful feeling of imposter syndrome. I could read some code and stumble around clumsily figuring out some bits here and there, but it was a constant struggle. It wasn't until I started documenting my IT processes that I began to correlate written word to small bits of code in my head that I could then translate in to PowerShell one-liners. Once I started doing that, things got a lot easier.

Sample Scenario

I have some maintenance tasks that I have to perform at least twice a month for user acceptance testing (UAT), quality assurance (QA) and production environments. Lucky for me, these tasks are already written down and stored in a team KB article. With half the battle already won, I carefully read through the documented steps for taking systems and applications down gracefully for maintenance.

The tasks progresses something like this:

  1. Place monitoring agents in maintenance mode
  2. Stop IIS application pools 1,2 & 3 on server #1
  3. Stop IIS application pool 4 on Y server #2
  4. Stop services on A,B,C & D servers
  5. Log into WSUS, approve & deadline OS updates to specific groups
  6. Allow reboots to occur.

My predecessors were manually performing these steps for years. Well, I'm not my predecessors. There's enough information here to begin making a script. Let's begin.

Task 1

This task could be automated, but for the purposes of this post I'm skipping it because not all monitoring platforms are the same. Moving on.

Task 2

Now we have something to work with. Using keywords, I begin by discovering what commands I have available that might stop an IIS application pool:

Get-Command -Module 'WebAdministration' -verb 'Stop'

Awesome. Stop-WebAppPool appears to be exactly what I need to complete this task.

Spend a minute or two reading the help if it's the first time you've seen this cmdlet: Get-Help Stop-WebAppPool -Online

Now I know how to tackle Task 2 and Task 3. My code now looks like this:

Invoke-Command -ComputerName 'X' -ScriptBlock {
    Stop-WebAppPool -Name '$MyAppPool1'
    Stop-WebAppPool -Name '$MyAppPool2'
    Stop-WebAppPool -Name '$MyAppPool3'
}

Invoke-Command -ComputerName 'Y' -ScriptBlock { Stop-WebAppPool -Name '$MyAppPool4' }

Task 4

Now this one should be simple for anyone who is new to PowerShell, as it's a common task that is demonstrated in a lot of training material. This task will make use of the Stop-Service cmdlet. There are a few ways this can be done, but I'll keep it simple for now so we don't get into the weeds and detract from the overall goal.

On each host, there are two services that work concert with each other as part of an application that was hosted on the IIS servers in Tasks 2 & 3. Stop-Service will allow us to enter multiple values in the ComputerName parameter, and since the naming scheme I'm using is short, it's not a big deal to enter them all here.

I'll also be using the ServiceName parameter, which also accepts multiple string values. When finished assembling the code, it looks like this:

Stop-Service -ComputerName 'A','B','C','D' -ServiceName 'MyService1','MyService2'

Great! I've just saved a few minutes of not having to RDP to each of these systems, or use Server Manager, or type this all out in a PowerShell terminal.

Using the PoshWSUS Module

The whole reason for shutting all these services down gracefully is to be able to apply Windows security patches to the server OS without screwing up the applications if they were still being used during a scheduled maintenance window (humor me for a moment and save the snark about Windows Updates).

How can I work with WSUS? There has to be a module I can use...

This handy PowerShell module contains exactly what I need for the final component in my scripted task. There are a lot of cmdlets available in this module and I'm not going to explain all of them right now.

In order to complete the last step, I need to:

  1. Connect to my WSUS server. Connect-PSWSUSServer -WsusServer localhost -Port 8530 -Verbose
  2. Store the KBs to be deployed as a variable. $Updates = Get-Content 'C:\PScripts\Maintenance\updates.txt'
  3. Store a deadline of 1 hour ahead of the time the script executes as a variable. $Deadline = (get-date).addHours(1)
  4. Get updates, approve then set install flag along with the deadline flag to assigned groups. Get-PSWSUSUpdate -Update $Updates | Approve-PSWSUSUpdate -Group 'UAT','QA' -Action Install -Deadline $Deadline
  5. Close the connection. Disconnect-PSWSUSServer

As you'll see above, I've thought out the logical steps and created some pseudo code to get started. It's the same process you'll follow when trying to create your own scripts. It's almost as if there's a theme developing here!

Now on to what you've been waiting for. Let's assemble all the bits into the final script:

<#
    My-MaintenanceTask.ps1

Description:

    This script will perform documented tasks that were once done manually.

#>

I like to place variables at the top of the script to make it easy to change them without digging through the entire script.

$Updates = Get-Content 'C:\PScripts\Maintenance\updates.txt' $Deadline = (get-date).addHours(1)

Need this module loaded

Import-Module PoshWSUS

Task 2-3

Invoke-Command -ComputerName 'X' -ScriptBlock { Stop-WebAppPool -Name 'MyAppPool1' Stop-WebAppPool -Name 'MyAppPool2' Stop-WebAppPool -Name 'MyAppPool3' }

Invoke-Command -ComputerName 'Y' -ScriptBlock { Stop-WebAppPool -Name 'MyAppPool4' }

#Task 4 Stop-Service -ComputerName 'A','B','C','D' -ServiceName 'MyService1','MyService2'

Task 5

Connect-PSWSUSServer -WsusServer localhost -Port 8530 -Verbose

Deploy OS updates

Get-PSWSUSUpdate -Update $Updates | Approve-PSWSUSUpdate -Group 'UAT','QA' -Action Install -Deadline $Deadline

Clean up, pick up, put away. Clean up, everyday!

Disconnect-PSWSUSServer

The key thing to remember here is, if you can write it down, you probably can script it. So go back and look at some of your documented processes and procedures, and you'll soon discover that you'll have enough inspiration to keep you busy for a while making PowerShell scripts.

Implementing Pester Tests

If you've read my past blog post on "How I Learned Pester by Building a Domain Controller Infrastructure Test", it should be pretty obvious that I'm a fan and love using Pester now. I even build small tests for small scripts like the one above.

If you'd like to learn more about Pester and testing, be sure to check out the Getting Started with Pester blog post or when you're ready to learn all the things about Pester, check out the Pester Book!

I need to a quick test with some visual output since I'm typically running this script from a PowerShell terminal manually. So, with the same pseudo code used earlier, let's build a simple test that will verify all the actions in our script did what we expected:

# My-MaintenanceTask.tests.ps1

Describe -Name 'Test Task 2 & 3 Results' { It 'MyAppPool1 is Stopped' { $AppPool = Invoke-Command -ComputerName 'X' -ScriptBlock { Get-WebAppPoolState -Name 'MyAppPool1' } $AppPool.Value | Should -BeExactly 'Stopped' } It 'MyAppPool2 is Stopped' { $AppPool = Invoke-Command -ComputerName 'X' -ScriptBlock { Get-WebAppPoolState -Name 'MyAppPool2' } $AppPool.Value | Should -BeExactly 'Stopped' } It 'MyAppPool3 is Stopped' { $AppPool = Invoke-Command -ComputerName 'X' -ScriptBlock { Get-WebAppPoolState -Name 'MyAppPool3' } $AppPool.Value | Should -BeExactly 'Stopped' } It 'MyAppPool4 is Stopped' { $AppPool = Invoke-Command -ComputerName 'Y' -ScriptBlock { Get-WebAppPoolState -Name 'MyAppPool4' } $AppPool.Value | Should -BeExactly 'Stopped' } }

Describe -Name 'Test Task 4 Server A Results' { It -Name 'MyService1 is Stopped' { $Svc = Invoke-Command -ComputerName 'A' -ScriptBlock { Get-Service -ServiceName 'MyService1' } $Svc.Status | Should -BeExactly 'Stopped' } It -Name 'MyService2 is Stopped' { $Svc = Invoke-Command -ComputerName 'A' -ScriptBlock { Get-Service -ServiceName 'MyService2' } $Svc.Status | Should -BeExactly 'Stopped' } }

I left out tests for servers B,C & D because they would be identical to the test shown for server A in the above example. Now all that is required to run the this test as part of the My-MaintenanceTask.ps1 script would be to add this line at the end of that script:

Invoke-Pester -Script '.\My-MaintenanceTask.tests.ps1'

Once the script has completed, you will then see output in the terminal showing the results of the tests.

Summary

If you really want to gussy up a script up to include some progress bars and have your pester results placed in a nice report you can give to a manager, then I would strongly recommend reading Adam Bertram's "A Better Way to Use Write-Progress" & watching Nick Rimmer's "How to Create a Simple Pester Test Report in HTML" to supercharge your maintenance scripts.