Archive for the ‘Scripting’ Category

h1

Scripted Microsoft Patch Removal

September 19, 2011

Many patch management systems have the ability to uninstall previously deployed patches. This functionality is typically used when a conflict with the patch is discovered after deployment.

Unfortunately, many of the automated removal mechanisms depend on the patch developer supporting and including a patch removal mechanism for each patch. When a patch doesn’t support this functionality, the management platform falls short in supporting this function.

As a workaround, here are a couple of scripts that will provide a semi-automated approach that can be used to remove patches remotely.

First, this script will list the patches installed on a system in the past X days where X is a command line parameter:


#Get list of patched installed in the past X days
param($Argument1,$Argument2)

$ComputerName = $Argument1
$intDays = $Argument2
$bolDaysValid = $true

#Validate second paramter – must be a number between 1 and 1000
If ([Microsoft.VisualBasic.Information]::isnumeric($intDays)) {
    If ($intDays -lt 1 -or $intDays -gt 1000) {$bolDaysvalid = $false }
    else {$bolDaysValid = $true}
}
else {
    $bolDaysValid = $false
}

If ($bolDaysValid -eq $false) {
    write-host "Invalid days parameter. Please use a number between 1 and 1000."
    write-host "Example: .\PatchList.ps1 mycomputer 30"
    write-host "Exiting"
    Exit
}

#Validate first paramter – WMI call to get OS
$OS = Get-WmiObject -Class win32_OperatingSystem -namespace "root\CIMV2" -ComputerName $computerName -ErrorAction silentlycontinue
if ($OS -eq $NULL) {
    write-host "Can’t access computer $ComputerName. Exiting."
Exit
}
#Get list of updates
Get-WmiObject -Computername $ComputerName Win32_QuickFixEngineering | ? {$_.InstalledOn} | where { (Get-date($_.Installedon)) -gt (get-date).adddays(-$intDays) }


Then, once the research is complete and the offending patch is found, the following script can be used to remotely remove the patch.


param($Argument1,$Argument2)
Add-Type -AssemblyName Microsoft.VisualBasic

$computername=$Argument1
$hotfixid=[string]$Argument2

#Second paramter validation – make sure it starts with ‘KB’ followed by a number
If (-not (($hotfixid.substring(0,2) -eq "KB") -and ([Microsoft.VisualBasic.Information]::isnumeric($hotfixid.substring(2))))) {
write-host "Invalid hotfix parameter. Please use ‘KB’ and the article number."
write-host "Example: .\PatchRemove.ps1 mycomputer KB976432"
write-host "Exiting"
Exit
}

#First paramter validation – get OS to be used later. If call fails, bad parameter
$OS = Get-WmiObject -Class win32_OperatingSystem -namespace "root\CIMV2" -ComputerName $computerName -ErrorAction silentlycontinue
if ($OS -eq $NULL) {
write-host "Can’t access computer $ComputerName. Exiting."
Exit
}
#Get hotfix list from target computer
$hotfixes = Get-WmiObject -ComputerName $computername -Class Win32_QuickFixEngineering |select hotfixid           

#Search for requested hotfix
if($hotfixes -match $hotfixID) {
    $hotfixNum = $HotfixID.Replace("KB","")
    Write-host "Found the hotfix KB " $HotfixNum
    Write-Host "Uninstalling the hotfix"
    #Windows 2008/R2 use WUSA to uninstall patch
    if ($OS.Version -like "6*") {
        $UninstallString = "cmd.exe /c wusa.exe /uninstall /KB:$hotfixNum /quiet /norestart"
          $strProcess = "wusa"
    }
    #Windows 2003 use spuninst in $NTuninstall folder to uninstall patch
    elseif ($OS.Version -like "5*") {
        $colFiles = Get-WMIObject -ComputerName $computername -Class CIM_DataFile -Filter "Name=`"C:\\Windows\\`$NtUninstall$HotFixID`$\\spuninst\\spuninst.exe`""
        if ($colfiles.FileName -eq $NULL) {
        Write-Host "Could not find removal script, please remove the hotfix manually."
        }
        else {
            $UninstallString = "C:\Windows\`$NtUninstallKB$hotfixNum`$\spuninst\spuninst.exe /quiet /z"
              $strProcess = "spuninst"
        }
    }
    #Send removal command
    ([WMICLASS]"\\$computername\ROOT\CIMV2:win32_process").Create($UninstallString) | out-null           
    #Wait for removal to finish
    while (@(Get-Process $strProcess -computername $computername -ErrorAction SilentlyContinue).Count -ne 0) {
        Start-Sleep 3
        Write-Host "Waiting for update removal to finish …"
    }
    #Test removal by getting hotfix list again
    $afterhotfixes = Get-WmiObject -ComputerName $computername -Class Win32_QuickFixEngineering |select hotfixid           
    if($afterhotfixes -match $hotfixID) {
        write-host "Uninstallation of $hotfixID succeeded"
    }
    else {
        write-host "Uninstallation of $hotfixID failed"
    }
}
else {           
    write-host "Hotfix $hotfixID not found"
return
}           


Note that these scripts are tested on servers running Windows 2003 or later.

h1

Remote PowerShell with Office 365

September 2, 2011

The Office 365 deployment assistant is a great tool to assist with deploying and configuring an Office 365 migration. Several steps require PowerShell work and you can use PowerShell deployed locally on an on-premises server to configure Office 365 remotely. This is very convenient but holds a little gotcha.

The Microsoft instructions for connecting remotely to an Office 365 installation include the following commands:

$LiveCred = Get-Credential

$Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://ps.outlook.com/powershell/ -Credential $LiveCred -Authentication Basic -AllowRedirection

Import-PSSession $Session

While it is true that these commands will create the remote session, after running the last command you will see a long list of cmdlets that were not redirected to the remote session. That is because those commands are already defined for the local session. This list is especially long if using PowerShell on an Exchange server which is typical since the configuration involves a mix of local and remote steps.

In order to be to use the commands that are duplicated in the cloud, there are two options.

First, you could use the following addition to the last line: Import-PSSession $Session –AllowClobber. The ‘–AllowClobber’ parameter will let PowerShell overwrite the locally registered commands with the Office 365 ones.

This approach works well but does prevent you from managing the local environment using the same commands. This can be resolved by opening another shell window or by following the second option.

The second option is to use this addition to the last line instead: Import-PSSession $Session –Prefix o365. The ‘-Prefix’ option allows both sets of commands to be available with commands that are sent remotely designated with the prefix string. So instead of running: Enable-OrganizationCustomization, the command would be: Enable-o365OrganizationCustomization.

Using the prefix option may require changes to script examples and a little getting used to but over the long term if you need to manage both an on premises and Office 365 environments, it saves a lot of time.

h1

Automatic Personal Archive Provisioning

August 23, 2011

Exchange 2010 supports automatic provisioning for new mailboxes. Unfortunately this mechanism does not extend to personal archives. As mailboxes are moved to Exchange 2010, they must be enabled for archives manually with the operator managing the size of each database and dividing the load accordingly.

The script below was created to automate this function and is intended to run automatically using a scheduled task on each CAS server.

Typically archive databases are flagged so that they do not participate in automatic provisioning for new mailboxes. The script selects the smallest archive database from the databases that are excluded from provisioning using the –IsExcludedeFromProvisioning parameter. Users are then enabled for archives using the target database.

The script also assigns one of two custom archive policies – a 180 day policy and a 360 day policy based on a group that defines users that get 360 days of retention in their mailbox. The script uses a custom attribute to overcome the issue of identifying mailboxes that are not members of the 360 day retention group.

Let me explain the issue:

PowerShell scripts often handle the ‘reverse group membership check’ issue by using a script that first assigns the common value (in this case 180 day retention) to everyone and then assigns the special value (in this case 360 day retention) to the members of a group.

The main weakness to this approach, especially for something like a retention policy is that any error with the second part of the script (say if someone renamed the group) would result in everyone getting a more restrictive retention policy and more archived items which is potentially disruptive and difficult to reverse.

My solution is to assign everyone in the org the less restrictive setting using a custom attribute field in AD once. Then adjust that custom attribute value based on the group membership and use the custom attribute value to configure the retention policy. This means that if the group is renamed or another error occurs, new members of the group might get the wrong policy but existing members would not be impacted.

Note that this can be accomplished with less effort if you deploy the Quest PowerGUI tools since the get-QADUser command does support a parameter –NotMemberOf. I didn’t use this since I was trying to create a solution that didn’t require additional software (in other words, come on Microsoft and implement this function!)

 

In addition, the script uses custom attribute 13 to identify a mailbox that shouldn’t use a personal archive. This is intended for service accounts and special purpose mailboxes.

#
#
# NAME: Maintenance.ps1
#
# AUTHOR: Guy Yardeni
#
# COMMENT: Script to run various maintenance tasks for Exchange 2010
#
#        Enable archives for mailboxes
#        Configure archive policy based on AD group
#

# Script to enable archive for any users who don’t already have one
# using the smallest archive database
#         
# Any text in Custom Attribute 13 will cause the script to skip the mailbox
#
#

# Return archive database with smallest size
$TargetDB = Get-MailboxDatabase -status | where {($_.ExchangeVersion.ExchangeBuild.Major -eq 14) -and ($_.IsExcludedFromProvisioning -eq $true)} | sort-object "DatabaseSize" | select-object -first 1

# Enable archive to relevant mailboxes to the target database
$results = Get-Mailbox | where {($_.ExchangeVersion.ExchangeBuild.Major -eq 14) -and ($_.ArchiveDatabase -eq $null) -and ($_.CustomAttribute13 -eq "")} | enable-mailbox

-archive -archivedatabase $TargetDB.Name -retentionpolicy "360 Day Default" |measure-object
 
#Write output for testing
Write-Host $results.count "mailbox(es) were enabled for archiving on database" $TargetDB.Name

# Script to set correct archiving policy
Get-Mailbox | where {($_.CustomAttribute12 -eq "")} | set-mailbox -CustomAttribute12 "180"
Get-DistributionGroupMember "Exchange Archive Users – 360 day" | Get-Mailbox | set-mailbox -CustomAttribute12 "360"
Get-Mailbox | where {($_.ExchangeVersion.ExchangeBuild.Major -eq 14) -and ($_.CustomAttribute12 -eq "180")} | set-mailbox -retentionpolicy "180 Day Default"
Get-Mailbox | where {($_.ExchangeVersion.ExchangeBuild.Major -eq 14) -and ($_.CustomAttribute12 -eq "360")} | set-mailbox -retentionpolicy "360 Day Default"

As always, comments about the code and approach are welcome!

Follow

Get every new post delivered to your Inbox.