Making a Change Log Easier With PowerShell

Having a Change Log is a good thing. A quick and simple place to find out what has changed on a server and when. This can be invaluable when troubleshooting, matching a change to a symptom especially when assessed alongside your performance counter collection. Here is a simple way to make use of a change log and automate it

Create a simple table


USE [MDW]
GO

CREATE TABLE [dbo].[ChangeLog](
 [ChangeID] [int] IDENTITY(1,1) PRIMARY KEY ,
 [Date] [datetime] NOT NULL,
 [Server] [varchar](50) NOT NULL,
 [UserName] [nvarchar](50) NOT NULL,
 [Change] [nvarchar](max) NOT NULL,
)

GO

You can keep this on a central server or create a database on each server, whichever fits your needs best. You can add other columns if you want your information in a different format

Once you have your table you can create a couple of Powershell functions to easily and quickly add to and retrieve data from the table. I make use of Invoke-SQLCMD2 in these functions

This can then be included in any automation tasks that you use to update your environments whether you are using automated deployment methods for releases or using SCCM to patch your environments making it easy to update and also easy to automate by making it part of your usual deployment process.

To add a new change

<#
.Synopsis
 A function to add a ChangeLog information
.DESCRIPTION
 Load function for adding a change to the changelog table in the MDW database on MDWSERVER.
 Use Get-ChangeLog $Server to see details
 Inputs the username of the account running powershell into the database as the user
REQUIRES Invoke-SQLCMD2
http://sqldbawithabeard.com
.EXAMPLE
 Add-ChangeLog SERVERNAME "Altered AutoGrowth Settings for TempDB to None"

 Adds ServerName UserName and Altered AutoGrowth Settings for TempDB to None to the change log table
#>
Function Add-ChangeLog
{
[CmdletBinding()]
Param(
 [Parameter(Mandatory=$True)]
 [string]$Server,

 [Parameter(Mandatory=$True)]
 [string]$Change
)

$UserName = $env:USERDOMAIN + '\' + $env:USERNAME

$Query = "INSERT INTO [dbo].[ChangeLog]
 ([Date]
 ,[Server]
 ,[UserName]
 ,[Change])
 VALUES
 (GetDate()
 ,'$Server'
 ,'$UserName'
 ,'$Change')
"
Invoke-Sqlcmd2 -ServerInstance MDWSERVER -Database "MDW" -Query $Query -Verbose
}

You can then run

Add-ChangeLog SERVERNAME "Added New Database SuperAppData"

to add the change to the change log

To retrieve the data you can use

<#
.Synopsis
 A function to get ChangeLog information
.DESCRIPTION
 Load function for finding ChangeLog information. Information is selected from the MDW Database on SERVERNAME
REQUIRES Invooke-SQLCMD2
http://sqldbawithabeard.com
.EXAMPLE
 Get-ChangeLog SERVERNAME
#>
Function Get-ChangeLog
{
 [CmdletBinding()]
 [OutputType([int])]
 Param
 (
 # Server Name Required
 [Parameter(Mandatory=$true,]
 $Server
 )

$a = @{Expression={$_.Date};Label="Date";width=15}, `
@{Expression={$_.Server};Label="Server";width=10},
@{Expression={$_.UserName};Label="UserName";width=20}, `
@{Expression={$_.Change};Label="Change";width=18}

Invoke-Sqlcmd2 -ServerInstance MDWSERVER -Database "MDW" -Query "SELECT * FROM dbo.ChangeLog WHERE Server = '$Server';" -Verbose|Format-table $a -Auto -Wrap

}

and use

Get-ChangeLog SERVERNAME

To find out what changed when. Happy Automating

Searching for Installed Windows Update With PowerShell

Yesterdays Post Show-WindowsUpdatesLocal does enable you to search for an installed update as follows

Show-WindowsUpdatesLocal|Where-Object {$_.HotFixID -eq ‘KB2855336’} |Select Date, HotfixID, Result,Title|Format-Table –AutoSize

image

I thought I would be able to do it quicker especially if I was searching a server with a lot of updates so I thought I would create a function to answer the  question Is this update installed on that server

It is very similar to Show-WindowsUpdatesLocal but does not include the Title or Description on the grounds that if you are searching for it you should know those!!

It also only adds the output to the collection if the KB is in the HotFixID property as shown below

image

If we use Measure-Command to compare the two we can see

image

image

From 3.89 seconds on my poor overworked machine to 1.79 seconds 🙂

You can find the code here

#####################################################################
#
# NAME: Search-WindowsUpdatesLocal.ps1
# AUTHOR: Rob Sewell http://sqldbawithabeard.com
# DATE:22/09/2013
#
# COMMENTS: Load function to show search for windows updates by KB locally
#
# USAGE: Search-WindowsUpdatesLocal KB2792100|Format-Table -AutoSize -Wrap
#    

Function Search-WindowsUpdatesLocal ([String] $Search) {
    $Search = $Search + "\d*" 
    $Searcher = New-Object -comobject Microsoft.Update.Searcher
    $History = $Searcher.GetTotalHistoryCount()
    $Updates = $Searcher.QueryHistory(1, $History)
    # Define a new array to gather output
    $OutputCollection = @()
    Foreach ($update in $Updates) {
        $Result = $null
        Switch ($update.ResultCode) {
            0 { $Result = 'NotStarted'}
            1 { $Result = 'InProgress' }
            2 { $Result = 'Succeeded' }
            3 { $Result = 'SucceededWithErrors' }
            4 { $Result = 'Failed' }
            5 { $Result = 'Aborted' }
            default { $Result = $_ }
        }
        $string = $update.title
        $SearchAnswer = $string | Select-String -Pattern $Search | Select-Object { $_.Matches } 
        $output = New-Object -TypeName PSobject
        $output | add-member NoteProperty “Date” -value $Update.Date
        $output | add-member NoteProperty “HotFixID” -value $SearchAnswer.‘ $_.Matches ‘.Value
        $output | Add-Member NoteProperty "Result" -Value $Result
        if ($output.HotFixID) {
            $OutputCollection += $output
        }
    }
    $OutputCollection
}

 

Show Windows Updates Locally With PowerShell

I wanted to be able to quickly show the Windows Updates on a server. This came about during a discussion about auditing.

Of course, there is no point in re-inventing the wheel so I had a quick Google and  found a couple of posts on from  Hey Scripting Guy blog and one from Tim Minter. Neither quite did what I wanted so I modified them as follows.

We start by creating a Update object and find the total number of updates and setting them to a variable $History which we pass to the QueryHistory Method. This enables us to show all the updates

image

Passing this to Get-Member shows

image

which doesn’t show the KB so I read a bit more and found Tom Arbuthnot’s Blog Post

image

this transforms the ResultCode Property to something meaningful and places the KB in its own column.

I have created a function called Show-WindowsUpdatesLocal It’s Local because doing it for a remote server takes a different approach but I will show that another day.

This means you can call the function and use the results however you like

Show-WindowsUpdatesLocal

image

Show-WindowsUpdatesLocal| Select Date, HotfixID, Result|Format-Table -AutoSize

image

Show-WindowsUpdatesLocal|Where-Object {$_.Result -eq ‘Failed’} |Select Date, HotfixID, Result,Title|Format-Table -AutoSize

image

Output to file Show-WindowsUpdatesLocal|Format-Table -AutoSize|Out-File c:\temp\updates.txt

image

Output to CSV Show-WindowsUpdatesLocal|Export-Csv c:\temp\updates.csv

image

You can get the code here

######################################################################
#
# NAME: Show-WindowsUpdatesLocal.ps1
# AUTHOR: Rob Sewell http://sqldbawithabeard.com
# DATE:22/09/2013
#
# COMMENTS: Load function to show all windows updates locally
#
# USAGE:  Show-WindowsUpdatesLocal
#         Show-WindowsUpdatesLocal| Select Date, HotfixID, Result|Format-Table -AutoSize
#         Show-WindowsUpdatesLocal|Where-Object {$_.Result -eq 'Failed'} |Select Date, HotfixID, Result,Title|Format-Table -AutoSize
#         Show-WindowsUpdatesLocal|Format-Table -AutoSize|Out-File c:\temp\updates.txt
#         Show-WindowsUpdatesLocal|Export-Csv c:\temp\updates.csv
#        

Function Show-WindowsUpdatesLocal {
    $Searcher = New-Object -ComObject Microsoft.Update.Searcher
    $History = $Searcher.GetTotalHistoryCount()
    $Updates = $Searcher.QueryHistory(1, $History)
    # Define a new array to gather output
    $OutputCollection = @() 
    Foreach ($update in $Updates) {
        $Result = $null
        Switch ($update.ResultCode) {
            0 { $Result = 'NotStarted'}
            1 { $Result = 'InProgress' }
            2 { $Result = 'Succeeded' }
            3 { $Result = 'SucceededWithErrors' }
            4 { $Result = 'Failed' }
            5 { $Result = 'Aborted' }
            default { $Result = $_ }
        }
        $string = $update.title
        $Regex = “KB\d*”
        $KB = $string | Select-String -Pattern $regex | Select-Object { $_.Matches }
        $output = New-Object -TypeName PSobject
        $output | add-member NoteProperty “Date” -value $Update.Date
        $output | add-member NoteProperty “HotFixID” -value $KB.‘ $_.Matches ‘.Value
        $output | Add-Member NoteProperty "Result" -Value $Result
        $output | add-member NoteProperty “Title” -value $string
        $output | add-member NoteProperty “Description” -value $update.Description
        $OutputCollection += $output
    }
    $OutputCollection
}