MSEndpointMgr

Patching – How to automate your maintenance windows

A long time ago I saw a really cool post on Johan Arwidmarks blog from a contributer Mattias Benninge about how to automate the creation of maintenacne windows with an offset from Patch Tuesday and when I saw it I thougth wow that’s really cool!

https://deploymentresearch.com/Research/Post/662/Create-a-Maintenance-Window-in-ConfigMgr-with-an-offset-from-patch-Tuesday

However then I experienced some sadness. If you’ve ever worked at a large scale organization creating patch windows that are ‘useable’ can be a real challenge. So becuase I’m super lazy I stole this script (All credit goes to him fror the createmw.ps1 script) to do some math and then I wrote something else that calculates all of the maintenace windows I might have. So let’s take a step back and talk about methodology.

Some companies are lucky, and they can get away with patching every single week with recurring maintenacne windows. Some companies are not so lucky and instead have windows scattered all over the place. At teh end of the day what really matters is ensuring that there is a set of collections ‘Maintenance Collections’ and those ‘Maintenance Collections’ need to be clearly named and identified. We have some rules that we know we need to follow when we create these collections we know:

  1. Windows MUST be at least 4 hours
    1. There are 24 hours in a day that means that each ‘Day’ of patching (like the 12 days of christmas but every month right? right!) – has ‘6’ -possible time slices for patching.
  2. We need to be consistent in our naming structure – I’ve chosen the following naming structure you don’t have to follow it but I mean I think it’s pretty good.
    1. “MAINT – SERVER – D1W1” – That means “Maintenance window for servers Day 1 window 1” where day 1 = 1 day after patch tuesday and Window1 = 0000 – 0400 in the morning

This means the first thing we need to do is determine the following pieces of information:

  1. How many days of christmas… I mean patching does your organization have.
  2. How many days AFTER patch tuesday do you start.
  3. How many collections are you willing to have.

Once we know those the test is easy. For my “Example” here I have decided that patching will take place over 5 days. I don’t care when people patch their servers during those five days but it’s going to be over five days. Second, I have decided that we patch starting the MONDAY after patch tuesday so that’s an offset of 5 days. With these two pieces of information in mind I prepare for battle.

If you want to follow along you can download all of these scripts from GitHub – as always released versions with the blog are at the SCConfigMgr GitHub incremental changes that are not blogged about are on my personal GitHub.

JordanTheItGuy – https://github.com/JordanTheITGuy/ProblemResolution/tree/master/PowerShell/MaintenanceWindow

SCConfigMgr – https://github.com/MSEndpointMgr/ConfigMgr/tree/master/Maintenance%20Windows

This solution while it doesn’t have a name yet – comes in three parts

  1. Create Maintenance Window Collections
  2. Create Maintenance WINDOWS
  3. Remove Maintenance Windows
  4. -TODO – Create Collection Membership rules (I have a script that does this using AD groups.. it’s just not ‘done’ yet)

Lets start with creating some collections. The first script we have is “Create-MaintenanceCollections.PS1” – This script has the following parameters:

LimitingCollectionID: This one is pretty obvious all these new collections need to be tied to a limiting collection. Enter the collection ID here. Since I’m dealing with servers I’ve gone ahead and put my collection “All Servers with the client installed” in here.

NumberOfDays: This is the length of our patch process. We have already decided that our patching process takes 5 days. So we will create five days worth of collections for patching

FolderPath: This is the location you would like these collections to reside in. I don’t know about you but I don’t like manually moving tons of collections. I’ve specified a folder I created specifically for maintenance window collections.

That makes the command look like this:

.\Create-MaintenaneCollections.ps1 -LimitingCollectionID "PR100079" -NumberofDays 5 -FolderPath 'PR1:\DeviceCollection\Software Updates\Maintenance Collections'

Once this is executed we get the following output – notice there is a prompt to confirm that based on the number of days you specified it will create X number of collections in this example – 30 collections.

Select h if you want to abort, and A or Y if you want to continue. This will then continue the process of creating all of the planned collections.

OK so now we’ve got 30 collections these 30 collections represent the 6 possible windows someone can select for our 5 days of christmas I mean patching.

But what about maintenance windows for these collections? Like we talked about earlier Mattias Benninge wrote an awesome script that you could use if you only needed to do this once. But in our example we would need to do it 30 times and thats 30 times less time I have for Ramen or Coffee.

So enter our second script – “New-YearlyMWindow.PS1” this script uses the information we have defined and our collection naming structure to determine how the windows should look and then passes that information to the script written by Mattias.

This script also has some paremters:

PatchTuesdayOffsetDays: The number of days after Patch Tuesday you would like your window series to start. That means that if I wanted to start my patches on the monday following patch tuesday I would use “5”

Year: The year I would like to create maintenance windwos for in this case “2019”

CollectionNamingStructure: The naming structure to use to find all of the window collections as LONG as your collections end in ” – D*W*” the script will work this allows you to create other naming conventions based on your organizations needs – (Assuming you made the change in the create collections script).

SiteCode: You can specifiy your servers site code but I’ve written a method in there that it will use to attempt to find the site code if you’re running this from the SCCM server and if you’re not there is another function in there that can be used as long as the cm module is loaded.

OK So let’s make some maintenance windows. Like a lot of them. First our execution of the script should look like this:

.\New-YearlyMWindow.ps1 -PatchTuesdayOffsetDays 5 -Year 2019

Once we hit enter we can go get some coffee

If you so desire you can watch it scroll by but you’ll see that it creates maintenance window 1 for Day 1 at 0-4 AM 6 days after patch tuesday which becomes Monday! YAY!

The final script is useful if you want to remove all of the maintenacne windows in preperation for the next year just run it with no changes and you’ll be fine.

In closing here are the scripts and I hope you find this helpful!

<#
.SYNOPSIS
    This scripts creates maintenance window collections for servers based on provided criteria. 

.DESCRIPTION
    Use this script to create and move maintenace window collections to a desired location in your configuration manager environment. 
        

.EXAMPLE
    This script uses some parameters here is an example of usage:
    PR1:\> C:\scripts\Create-MaintenanceCollections.Ps1 -LimitingCollectionID "SMS00001" -NumberofDays 5 -FolderPath "PR1:\DeviceCollections\SUM - PatchingCollections\Maintenance Collections"

.NOTES
    FileName:    Create-MaintenanceCollections.PS1
    Author:      Jordan Benzing
    Contact:     @JordanTheItGuy
    Created:     2019-04-09
    Updated:     2019-04-09

    Version 1.0.1 - It works and creates stuff with no parameters and is cardcoded
    Version 1.0.2 - Added the ability to utilize Parameters
    Version 1.0.3 - Added verbosity to show each step as it goes along and some error checking.
    Version 1.0.4 - Updated to use standard helper functions to validate and remediate a connection to CM Provider
                    Updated to remove extraneous verbose information
                    Updated to return you to the original directory you were in when you ran the script
#>


param(
    

[parameter(Mandatory = $true)]

[string]$LimitingCollectionID,

[parameter(Mandatory = $true)]

[int]$NumberofDays, [Parameter(Mandatory = $true)] [string]$FolderPath ) #region HelperFunctions function Get-CMModule #This application gets the configMgr module { [CmdletBinding()] param() Try { Write-Verbose “Attempting to import SCCM Module” #Retrieves the fcnction from ConfigMgr installation path. Import-Module (Join-Path $(Split-Path $ENV:SMS_ADMIN_UI_PATH) ConfigurationManager.psd1) -Verbose:$false Write-Verbose “Succesfully imported the SCCM Module” } Catch { Throw “Failure to import SCCM Cmdlets.” } } function Test-ConfigMgrAvailable #Tests if ConfigMgr is availble so that the SMSProvider and configmgr cmdlets can help. { [CMdletbinding()] Param ( [Parameter(Mandatory = $false)] [bool]$Remediate ) try { if((Test-Module -ModuleName ConfigurationManager -Remediate:$true) -eq $false) #Checks to see if the Configuration Manager module is loaded or not and then since the remediate flag is set automatically imports it. { throw “You have not loaded the configuration manager module please load the appropriate module and try again.” #Throws this error if even after the remediation or if the remediation fails. } write-Verbose “ConfigurationManager Module is loaded” Write-Verbose “Checking if current drive is a CMDrive” if((Get-location -Verbose:$false).Path -ne (Get-location -PSProvider ‘CmSite’ -Verbose:$false).Path) #Checks if the current location is the – PS provider for the CMSite server. { Write-Verbose -Message “The location is NOT currently the CMDrive” if($Remediate) #If the remediation field is set then it attempts to set the current location of the path to the CMSite server path. { Write-Verbose -Message “Remediation was requested now attempting to set location to the the CM PSDrive” Set-Location -Path (((Get-PSDrive -PSProvider CMSite -Verbose:$false).Name) + “:”) -Verbose:$false Write-Verbose -Message “Succesfully connected to the CMDrive” #Sets the location properly to the PSDrive. } else { throw “You are not currently connected to a CMSite Provider Please Connect and try again” } } write-Verbose “Succesfully validated connection to a CMProvider” return $true } catch { $errorMessage = $_.Exception.Message write-error -Exception CMPatching -Message $errorMessage return $false } } function Test-Module #Function that is designed to test a module if it is loaded or not. { [CMdletbinding()] Param ( [Parameter(Mandatory = $true)] [String]$ModuleName, [Parameter(Mandatory = $false)] [bool]$Remediate ) If(Get-Module -Name $ModuleName) #Checks if the module is currently loaded and if it is then return true. { Write-Verbose -Message “The module was already loaded return TRUE” return $true } If((Get-Module -Name $ModuleName) -ne $true) #Checks if the module is NOT loaded and if it’s not loaded then check to see if remediation is requested. { Write-Verbose -Message “The Module was not already loaded evaluate if remediation flag was set” if($Remediate -eq $true) #If the remediation flag is selected then attempt to import the module. { try { if($ModuleName -eq “ConfigurationManager”) #If the module requested is the Configuration Manager module use the below method to try to import the ConfigMGr Module. { Write-Verbose -Message “Non-Standard module requested run pre-written function” Get-CMModule #Runs the command to get the COnfigMgr module if its needed. Write-Verbose -Message “Succesfully loaded the module” return $true } else { Write-Verbose -Message “Remediation flag WAS set now attempting to import module $($ModuleName)” Import-Module -Name $ModuleName #Import the other module as needed – if they have no custom requirements. Write-Verbose -Message “Succesfully improted the module $ModuleName” Return $true } } catch { Write-Error -Message “Failed to import the module $($ModuleName)” Set-Location $StartingLocation break } } else { #Else return the fact that it’s not applicable and return false from the execution. { Return $false } } } } #endregion HelperFunctions #Ensure the Configuration Manager Module is loaded if it’s not loaded see blog post on how to load it at www.scconfigmgr.com $StartingLocation = Get-Location if(!(Test-ConfigMgrAvailable -Remediate:$true -Verbose)){ Write-Error -Message “Nope that’s horribly broken” break } #Ensure the folder path you would like to move the collections to exists Write-Verbose -Message “Now testing if the location to move the collections to exists and is written out properly.” -Verbose if(!(Test-Path -Path $FolderPath)){ Write-Error -Message “The Path does not exist please re-run the script with a valad path” break } Write-Verbose “The location to move the collections to EXISTS and IS written out properly.” -Verbose #Set the naming standard for the collection name you MAY change this it’s highly reccomended that you do NOT. $MWName = “MAINT – SERVER – D” Write-Verbose “The naming standard for your maintenance collections will be $($MWNAME) with the day after patch tuesday and window indication afterwords” #Set the date counter to 0 $DayCounter = 0 #Create a list to store the collection names in. $list = New-Object System.Collections.ArrayList($null) #Create a CMSchedule object – This sets the refresh on the collections you may change the below line otherwise collections will refresh weekly on saturday. $Schedule = New-CMSchedule -Start (Get-Date) -DayOfWeek Saturday -RecurCount 1 Do { #Add one to the day counter $DayCounter++ #Create the new string – Collection name plus the count of days after patch tuesday. $NewString = $MWName + $DayCounter #Store the string into the list $List.add($NewString) | Out-Null } #Do this until the number of days you would like to have MW’s for is reached. while($DayCounter -ne $NumberofDays) Write-Verbose “Created Day Names” -Verbose #Create the Full list object – this will now add in the MW information (6 created per day each one is 4 hours long allowing you to patch anytime of the day) $FullList = New-Object System.Collections.ArrayList($null) #For each DAY/COLLECTION in the previous list CREATE 6 maintenance window collection names. foreach($Object in $list) { #Set the window counter back back to 0 [int32]$WindowCounter = 0 do { #Add one to the window counter $WindowCounter++ #Create the new collection name and add the nomenclature of W3 to it. $NewCollection = $Object + “W” + $($WindowCounter.ToString()) #Compile and store the finalized list name. $FullList.Add($NewCollection) | Out-Null } #Do this until you reach 6 of them – you can of course change that if you really wanted to… but why? while ($($WindowCounter.ToString()) -ne “6”) } #For each collection name in the FULL list of (MAINT – SERVER – D1W1 (example)) – create a collection limited to the specified limit and refresh weekly on Saturday. Write-Warning -Message “The Action you are about to perfom will create $($FullList.Count) collections do you want to continue?” -WarningAction Inquire Write-Verbose -Message “Created all MW Collection Names now creating the MW Collections” -Verbose ForEach($CollectionName in $FullList) { try{ #Create the collection Write-Verbose -Message “Now creating $($collectionName)” -Verbose #Change the below information to change information about the collection. $Object = New-CMCollection -collectionType Device -Name $CollectionName -LimitingCollectionId $LimitingCollectionID -RefreshSchedule $Schedule -RefreshType Periodic #Move the collection to its final destination. Move-CMObject -FolderPath $FolderPath -InputObject $Object Write-Verbose -Message “Successfully created and moved $($collectionName) to its destination” -Verbose } catch { Write-Error -Message $_.Exception.Message } } set-location -Path $StartingLocation.Path Write-Output -InputObject $(“Completed the script succesfully”)

Create Maintenance Windows

<#
.SYNOPSIS
    This script creates Maintenance windows for an entire year

.DESCRIPTION
    Use this to remove all maintenance windows from collections that match certain criteria
        

.EXAMPLE
  The script is static and does not have any functions to use for an example.

.NOTES
    FileName:    New-YearlyMWindow.PS1
    Author:      Jordan Benzing
    Contact:     @JordanTheItGuy
    Created:     2018-12-13
    Updated:     2019-04-08

    1.0.0 - (2018-12-13) Original Written version Mike Hiser/Jordan Benzing
    1.0.1 - (2019-04-08) Updated with custom variables at the start of the script and dynamic discover of the site server
    1.0.2 - (2019-04-09) Updated to dynamically figure out the number of days/Patch ranges to do the offset math
    1.0.3 - (2019-04-09) Updated to include version history
    1.0.4 - (2019-04-09) Updated the ConfigMgr Helper Function remove extra 'verbose' stuff
                         Updated the logic of the running function to remove extra/duplicate checks for the configmgr module
    1.0.5 - (2019-04-09) Updated the script to include a parameter option to specify the year and an offset from patch tuesday.
    1.0.6 - (2019-04-09) Removed the parameter from the MW creation function to utilize the already sent variable with a default of 0 if none is selected.
                         NOTE - Offset is always calculated from the day AFTER patch tuesday 0 = Patch Wednesday
    1.0.7 - (2019-04-09) Added parameter for CollectionName Structure
                        NOTE - DONT CHANGE THIS IF YOU USED THE DEFAULT CREATE-MAINTENANCECOLLECTIONS.PS1 SCRIPT. 

#>

param(
    [Parameter()]
    [string]$PatchTuesdayOffsetDays = 0,
    [Parameter(Mandatory=$true)]
    $Year,
    [Parameter()]
    $CollectionNameStructure = "MAINT - Server - D*",
    [Parameter()]
    $SiteCode = "$(((Get-WmiObject -namespace "root\sms" -class "__Namespace").Name).substring(8-3))"
)

################################# Variables ################################################
$MWName = "Patching"
$MWDescription = "Patching Window"
$MWDuration = 4
$StartMinute = 0
$MinuteDuration = 0
############################################################################################
#region HelperFunctions
function Get-CMModule
#This application gets the configMgr module
{
    [CmdletBinding()]
    param()
    Try
    {
        Write-Verbose "Attempting to import SCCM Module"
        #Retrieves the fcnction from ConfigMgr installation path. 
        Import-Module (Join-Path $(Split-Path $ENV:SMS_ADMIN_UI_PATH) ConfigurationManager.psd1) -Verbose:$false
        Write-Verbose "Succesfully imported the SCCM Module"
    }
    Catch
    {
        Throw "Failure to import SCCM Cmdlets."
    } 
}

function Test-ConfigMgrAvailable
#Tests if ConfigMgr is availble so that the SMSProvider and configmgr cmdlets can help. 
{
    [CMdletbinding()]
    Param
    (
        [Parameter(Mandatory = $false)]
        [bool]$Remediate
    )
        try
        {
            if((Test-Module -ModuleName ConfigurationManager -Remediate:$true) -eq $false)
            #Checks to see if the Configuration Manager module is loaded or not and then since the remediate flag is set automatically imports it.
            { 
                throw "You have not loaded the configuration manager module please load the appropriate module and try again."
                #Throws this error if even after the remediation or if the remediation fails. 
            }
            write-Verbose "ConfigurationManager Module is loaded"
            Write-Verbose "Checking if current drive is a CMDrive"
            if((Get-location -Verbose:$false).Path -ne (Get-location -PSProvider 'CmSite' -Verbose:$false).Path)
            #Checks if the current location is the - PS provider for the CMSite server. 
            {
                Write-Verbose -Message "The location is NOT currently the CMDrive"
                if($Remediate)
                #If the remediation field is set then it attempts to set the current location of the path to the CMSite server path. 
                    {
                        Write-Verbose -Message "Remediation was requested now attempting to set location to the the CM PSDrive"
                        Set-Location -Path (((Get-PSDrive -PSProvider CMSite -Verbose:$false).Name) + ":") -Verbose:$false
                        Write-Verbose -Message "Succesfully connected to the CMDrive"
                        #Sets the location properly to the PSDrive.
                    }

                else
                {
                    throw "You are not currently connected to a CMSite Provider Please Connect and try again"
                }
            }
            write-Verbose "Succesfully validated connection to a CMProvider"
            return $true
        }
        catch
        {
            $errorMessage = $_.Exception.Message
            write-error -Exception CMPatching -Message $errorMessage
            return $false
        }
}

function Test-Module
#Function that is designed to test a module if it is loaded or not. 
{
    [CMdletbinding()]
    Param
    (
        [Parameter(Mandatory = $true)]
        [String]$ModuleName,
        [Parameter(Mandatory = $false)]
        [bool]$Remediate
    )
    If(Get-Module -Name $ModuleName)
    #Checks if the module is currently loaded and if it is then return true.
    {
        Write-Verbose -Message "The module was already loaded return TRUE"
        return $true
    }
    If((Get-Module -Name $ModuleName) -ne $true)
    #Checks if the module is NOT loaded and if it's not loaded then check to see if remediation is requested. 
    {
        Write-Verbose -Message "The Module was not already loaded evaluate if remediation flag was set"
        if($Remediate -eq $true)
        #If the remediation flag is selected then attempt to import the module. 
        {
            try 
            {
                    if($ModuleName -eq "ConfigurationManager")
                    #If the module requested is the Configuration Manager module use the below method to try to import the ConfigMGr Module.
                    {
                        Write-Verbose -Message "Non-Standard module requested run pre-written function"
                        Get-CMModule
                        #Runs the command to get the COnfigMgr module if its needed. 
                        Write-Verbose -Message "Succesfully loaded the module"
                        return $true
                    }
                    else
                    {
                    Write-Verbose -Message "Remediation flag WAS set now attempting to import module $($ModuleName)"
                    Import-Module -Name $ModuleName
                    #Import  the other module as needed - if they have no custom requirements.
                    Write-Verbose -Message "Succesfully improted the module $ModuleName"
                    Return $true
                    }
            }
            catch 
            {
                Write-Error -Message "Failed to import the module $($ModuleName)"
                Set-Location $StartingLocation
                break
            }
        }
        else {
            #Else return the fact that it's not applicable and return false from the execution.
            {
                Return $false
            }
        }
    }
}
#endregion HelperFunctions



#region Get-PatchWindow -Window $Arg 
Function Get-PatchWindowTime
{
    [Cmdletbinding()]
    Param
    (
        [Parameter(Mandatory = $True)]
        $Window
    )

    Switch ($Window)  # Determine Window
    {
        # Window 1 00:00 to 04:00
        'W1' {
            $Description = 'Window 1 00:00 to 04:00'
            $StartHour = '0'
        }

        # Window 2 04:00 to 08:00
        'W2' {
            $StartHour = '4'
            $Description = 'Window 2 04:00 to 08:00'
        }

        # Window 3 08:00 to 12:00
        'W3' {
            $StartHour = '8'
            $Description = 'Window 3 08:00 to 12:00'
        }
               
        # Window 4 12:00 to 16:00
        'W4' {
            $StartHour = '12'
            $Description = 'Window 4 12:00 to 16:00'
        }

        # Window 5 16:00 to 20:00
        'W5' {
            $StartHour = '16'
            $Description = 'Window 5 16:00 to 20:00'
        }

        # Window 6 20:00 to 00:00
        'W6' {
            $StartHour = '20'
            $Description = 'Window 6 20:00 to 00:00'
        }

        # If group name match fails, log name, do not create schedule
        Default {
            write-verbose -message "Start Time failed." -Verbose
        }

    } # End switch

    Return $StartHour,$Description
}
#endregion
#region Get-PatchStartDay -DayType $Arg 
Function Get-PatchWindowDate
{
    

[cmdletbinding()]

Param ( [Parameter(Mandatory = $True)] $DayType ) [int]$WinType = 0 $DaysAdded = $WinType + $DaysAfter Return $DaysAdded } #endregion #Gather specific collections for processing with the MW script Function start-WindowCreation{

[cmdletbinding()]

param() if(!(Test-ConfigMgrAvailable -Remediate:$true -Verbose)){ Write-Error -Message “Soemthing went wrong with the helper functions review verbose messages” break } $MWCollections = Get-CMDeviceCollection -Name $CollectionNameStructure | Select-Object name,collectionid Set-Location -Path $(Split-Path $script:MyInvocation.MyCommand.Path) Foreach ($Collection in $MWCollections) { $MWString = $Collection.Name.Split(” – “)[($($Collection.Name.Split(” – “)).length)-1] $CharPosition = New-Object System.Collections.ArrayList($null) foreach($char in [char[]]$MWString){ if($Char -match “[a-z]”){ $CharPosition.Add($MWString.IndexOf($Char)) | Out-Null } } $TotalDaysAdded = $MWString.Substring($($CharPosition[0] +1), $($CharPosition[1] – 1)) $TotalDaysAdded = [int]$TotalDaysAdded + [int]$PatchTuesdayOffsetDays $Window = $MWString.Substring($($CharPosition[1])) # Function call to determine patch window only $WindowInfo = Get-PatchWindowTime -Window $Window $StartHour = $WindowInfo[0] $MWDescription = $Day + ” ” + $WindowInfo[1] Write-Verbose -Message “$($Collection.Name) ` Start Hour : $StartHour ` End Hour : $([int]$StartHour + [int]$MWDuration) Days to add after Patch Tuesday: $TotalDaysAdded” -Verbose Write-Verbose -Message “Creating maintenance windows for collection $($collection.name.ToUpper())” -Verbose #write-host “.\createmw.ps1 -sitecode $Sitecode -MaintenanceWindowName `”$MWNameDetail`” -CollectionID $($Collection.collectionid) -HourDuration $MWDuration -MinuteDuration $MinuteDuration -swtype Updates -PatchTuesday -AddDays $TotalDaysAdded -StartYear $Year -StartMinute $StartMinute -AddMaintenanceWindowNameMonth -MaintenanceWindowDescription `”$MWDescription`”” Invoke-Expression “.\createmw.ps1 -sitecode $Sitecode -MaintenanceWindowName `”$MWName`” -CollectionID $($Collection.collectionid) -HourDuration $MWDuration -MinuteDuration $MinuteDuration -swtype Updates -PatchTuesday -AddDays $TotalDaysAdded -StartYear $Year -StartHour 0 -StartMinute $StartMinute -AddMaintenanceWindowNameMonth -MaintenanceWindowDescription `”$MWDescription`”” } } start-WindowCreation -Verbose

Remove Maintenance Windows

<#
.SYNOPSIS
    This script is part of the package to create standard MW Collections and Windows and remove them

.DESCRIPTION
    Use this script to REMOVE Maintenacne windows from the standardized MW collections.
        

.EXAMPLE
    This script uses some parameters here is an example of usage:
    .\Remove-YearlyMYindow.PS1

.NOTES
    FileName:    Remove-YearlyMYindow.PS1
    Author:      Jordan Benzing
    Contact:     @JordanTheItGuy
    Created:     2019-04-09
    Updated:     2019-04-09

    Version 1.0.1 - It works and removes maintenance windows.
    Version 1.0.2 - Added Parameter for Collection Naming Standard so it can be used with OTHER collection structures
                - adjusted some of the verbose statements from the hardcoded original version to be more precise
    Version 1.0.3 (2019-04-09) - Adjusted verbose statement to be more accurate

#>

param(
    [Parameter(Mandatory=$true)]
    [string]$CollectionNamingStandard
)
#region HelperFunctions
function Get-CMModule
#This application gets the configMgr module
{
    [CmdletBinding()]
    param()
    Try
    {
        Write-Verbose "Attempting to import SCCM Module"
        #Retrieves the fcnction from ConfigMgr installation path. 
        Import-Module (Join-Path $(Split-Path $ENV:SMS_ADMIN_UI_PATH) ConfigurationManager.psd1) -Verbose:$false
        Write-Verbose "Succesfully imported the SCCM Module"
    }
    Catch
    {
        Throw "Failure to import SCCM Cmdlets."
    } 
}

function Test-ConfigMgrAvailable
#Tests if ConfigMgr is availble so that the SMSProvider and configmgr cmdlets can help. 
{
    [CMdletbinding()]
    Param
    (
        [Parameter(Mandatory = $false)]
        [bool]$Remediate
    )
        try
        {
            if((Test-Module -ModuleName ConfigurationManager -Remediate:$true) -eq $false)
            #Checks to see if the Configuration Manager module is loaded or not and then since the remediate flag is set automatically imports it.
            { 
                throw "You have not loaded the configuration manager module please load the appropriate module and try again."
                #Throws this error if even after the remediation or if the remediation fails. 
            }
            write-Verbose "ConfigurationManager Module is loaded"
            Write-Verbose "Checking if current drive is a CMDrive"
            if((Get-location -Verbose:$false).Path -ne (Get-location -PSProvider 'CmSite' -Verbose:$false).Path)
            #Checks if the current location is the - PS provider for the CMSite server. 
            {
                Write-Verbose -Message "The location is NOT currently the CMDrive"
                if($Remediate)
                #If the remediation field is set then it attempts to set the current location of the path to the CMSite server path. 
                    {
                        Write-Verbose -Message "Remediation was requested now attempting to set location to the the CM PSDrive"
                        Set-Location -Path (((Get-PSDrive -PSProvider CMSite -Verbose:$false).Name) + ":") -Verbose:$false
                        Write-Verbose -Message "Succesfully connected to the CMDrive"
                        #Sets the location properly to the PSDrive.
                    }

                else
                {
                    throw "You are not currently connected to a CMSite Provider Please Connect and try again"
                }
            }
            write-Verbose "Succesfully validated connection to a CMProvider"
            return $true
        }
        catch
        {
            $errorMessage = $_.Exception.Message
            write-error -Exception CMPatching -Message $errorMessage
            return $false
        }
}

function Test-Module
#Function that is designed to test a module if it is loaded or not. 
{
    [CMdletbinding()]
    Param
    (
        [Parameter(Mandatory = $true)]
        [String]$ModuleName,
        [Parameter(Mandatory = $false)]
        [bool]$Remediate
    )
    If(Get-Module -Name $ModuleName)
    #Checks if the module is currently loaded and if it is then return true.
    {
        Write-Verbose -Message "The module was already loaded return TRUE"
        return $true
    }
    If((Get-Module -Name $ModuleName) -ne $true)
    #Checks if the module is NOT loaded and if it's not loaded then check to see if remediation is requested. 
    {
        Write-Verbose -Message "The Module was not already loaded evaluate if remediation flag was set"
        if($Remediate -eq $true)
        #If the remediation flag is selected then attempt to import the module. 
        {
            try 
            {
                    if($ModuleName -eq "ConfigurationManager")
                    #If the module requested is the Configuration Manager module use the below method to try to import the ConfigMGr Module.
                    {
                        Write-Verbose -Message "Non-Standard module requested run pre-written function"
                        Get-CMModule
                        #Runs the command to get the COnfigMgr module if its needed. 
                        Write-Verbose -Message "Succesfully loaded the module"
                        return $true
                    }
                    else
                    {
                    Write-Verbose -Message "Remediation flag WAS set now attempting to import module $($ModuleName)"
                    Import-Module -Name $ModuleName
                    #Import  the other module as needed - if they have no custom requirements.
                    Write-Verbose -Message "Succesfully improted the module $ModuleName"
                    Return $true
                    }
            }
            catch 
            {
                Write-Error -Message "Failed to import the module $($ModuleName)"
                Set-Location $StartingLocation
                break
            }
        }
        else {
            #Else return the fact that it's not applicable and return false from the execution.
            {
                Return $false
            }
        }
    }
}
#endregion HelperFunctions

$StartingLocation = Get-Location
if(!(Test-ConfigMgrAvailable -Remediate:$true -Verbose)){
    Write-Error -Message "Nope that's horribly broken"
    break  
}

Write-Verbose -Message "Now getting Collection list" -Verbose
$Collections = Get-CMCollection -Name $CollectionNamingStandard
Write-Verbose -Message "Succesfully retrieved Collections" -Verbose
Write-Verbose -Message "Now cleaning Maintenance Windows pardon our dust..." -Verbose
foreach($Collection in $Collections)
    {
        Write-Verbose -Message "Retrieving maintenance windows from $($Collection.Name)" -Verbose
        $MaintenanceWindows = Get-CMMaintenanceWindow -CollectionName $Collection.Name
        foreach($MaintenanceWindow in $MaintenanceWindows)
            {
                Write-Verbose "Removing Maintenance window $($MaintenanceWindow.Name) from collection $($Collection.Name)" -Verbose
                Remove-CMMaintenanceWindow -CollectionName $Collection.Name -MaintenanceWindowName $MaintenanceWindow.Name -Force
                Write-Verbose "SUCCESFULLY Removed Maintenance window $($MaintenanceWindow.Name) from collection $($Collection.Name)" -Verbose
            }
    }
Set-Location -Path $StartingLocation.Path

(10380)

Jordan Benzing

Jordan has been working in the Industry since 2009. Since starting he’s worked with Active Directory, Group Policy, SCCM, SCOM and PowerShell. Jordan most recently worked in the healthcare industry as an SCCM Infrastructure Team lead supporting over 150,000 endpoints. Jordan currently works as a Senior consultant for TrueSec Inc in the U.S. Most recently his focus has been in SQL Reporting for SCCM, creation of PowerShell scripts to automate tasks and PowerBI.

Add comment

Sponsors