MSEndpointMgr

Patching – How to automate your maintenance windows part 2

With the alarming number of views on part one of automating your maintenance windows, I decided I had better get to work right away on part 2! Part two has a specific focus on creating the membership rules and AD groups that are needed for these collections. Let’s do some simple math real quick.

We already know that in our example we created 30 collections – 5 days of patching – 6 windows a day – that is 30 AD groups we have to make to match those collections and on top of that 30 collection rules we have to make to tie those servers together and that just sounds painful and not like something I want to do, like at all. So once again we turn to our friend PowerShell when I wrote this script I had several goals in mind

1) – Validate the AD Module is available and if it’s not then don’t run. Because without the AD module it’s pretty challenging to create AD groups

2) – Validate the OU path is valid – this is pretty simple most people don’t want all of these AD groups to suddenly appear in a weird built in location.

3) – Validate that from the server the script is running that we can load the SCCM module and connect to the PS drive again if we can’t create rules that’s a problem

These are all simple starting points that also reflect something about how I write code. When I first sit down with a project I write out a list of goals and “psuedo code” the path I will take to get there. This frequently allows me to re-use a LOT of code I’ve already written as functions. At times it means it’s not the most ‘streamlined’ of code but it drastically increases the re-usability and piece by piece nature of my code. On to what you really came here for.

This script DOES have parameters they use the following:

OUPath: – This parameter accepts a string and uses LDAP style nomenclature for the destination you would like your groups to land in. Currently the Groups do NOT support – creation of an OU path if it’s missing it will simply stop processing if the OU doesn’t exist.

GroupType: – This parameter accepts an option vhoice of “DomainLocal” , “Global” or “Universal”  – This attribute allows you to specifiy the group type. The default option is DomainLocal – I really recommend that you use this option instead of any of the others options.

CMColNameStructure: – This parameter is also a string that accepts the naming structure ONLY use this if you have changed the collection naming standard from the previous script usage.

This script now uses the begin and processs blocks in PowerShell to first load all of the functions that will be used and then process to use those functions to then execute the functions as needed. The first part of the process block checks to validate if the OU path exists or not and if it doesn’t then breaks.
Once we’ve validated if the OU exists or not we then connect to the Configuration Manager environment by importing the ConfigMgr Cmdlet set. – (This DOES have the requirement of having the SCCM console installed).

We then pause and wait for user input with a warning message to show the number of AD groups we will create based on the collections we pulled in. If you agree push A if you don’t push H.
From here the groups will then get created , and the groups will be linked to the collections!

 

 

<#
.SYNOPSIS
    This script creates the AD Groups and the collection membership query rules for the blog post released at SCCofnigMgr.com

.DESCRIPTION
    Use this script hand in hand with the other Maintenace Scripts provided at SCConfigMgr.com to help better plan and manage your patching in the long term
        

.EXAMPLE
    .\New-MWADGroupsCollectionRules.ps1 -OUPath "OU=GROUPS,OU=MANAGED,DC=PROBRES,DC=ORG" -GroupType DomainLocal
    Example of specifying the group type

.Example
    .\New-MWADGroupsCollectionRules.ps1 -OUPath "OU=GROUPS,OU=MANAGED,DC=PROBRES,DC=ORG"
    Example of not setting the group type. 

.NOTES
    FileName:    NewMWADGroupsCollectionRules.PS1
    Author:      Jordan Benzing
    Contact:     @JordanTheItGuy
    Created:     2019-4-12
    Updated:     2019-04-12

    Version 1.0.0 - Wrote source script imported functions from other scripts made functional
    Version 1.0.1 - Added comments into the original source code and added in notes above it. 

#>

[cmdletbinding()]
param(
    [Parameter(Mandatory=$true)]
    [string]$OUPath,
    [Parameter(HelpMessage = "Specify the group type it is highly reccomended that you use domain local groups and that is the default.")]
    [ValidateSet("DomainLocal","Global","Universal")]
    [string]$GroupType = "DomainLocal",
    [Parameter(HelpMessage = "Specify the collection naming structure if you used something that is NOT the default")]
    [string]$CMColNameStructure = "MAINT - SERVER - D*"
)

begin{
#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
            }
        }
    }
}

function Write-TsxOutPut{
    <#
        .SYNOPSIS 
            This function is designed to allow someone who is first learning PowerShell to make better calls back to the host screen without
            needing to understand all of the ins and outs of Write-Error / Verbose / Warnings. This also allows you to bypass things like write-host completely.
        
        .DESCRIPTION
            This function can be called through out a script to show visual progress to a user of where the script is at when it's running. This is useful as it does not 
            trigger terminating error code messages or true 'warnings' that otherwise might be caused by a .NET write-error or other method. Also allows the user to color co-ordinate messages
            without using the write-host prompt.
        
        .NOTES
                FileName:    Write-TsxOutput.PS1
                Author:      Jordan Benzing
                Contact:     @JordanTheItGuy
                Created:     2019-04-11
                Updated:     2019-04-11
    
                Version 0.0.0 (2019-04-10) - Wrote original function with no comments and no explanation
                Version 1.0.0 (2019-04-11) - Wrote function notes into the script added the help section to explain usage
    
        .LINK
            https://github.com/JordanTheITGuy/ProblemResolution/blob/master/PowerShell/Functions/write-TsxOutput.ps1
        
        .PARAMETER MsgLevel
            This parameter only accepts a set of choices you can use the "Tab Key" to rotate through the options. The options are:
            Warning - Sets the font color to Yellow to indicate it didn't do what you wanted but didnt fail either
            Default - Sets the font color to Cyan - a neutral color that is readable and conveys progress
            Success - Sets the font color to green - You did something you wanted or a function completed succesfully
    
        .PARAMETER Message
            This parameter accepts strings and allows you to pass through strings that are supposed to be displayed in a specific color to convey status. 
    
        .EXAMPLE 
            Write-TsxOutPut -MsgLevel Warning -Message "Something is amiss"
            Example of running the code to generate a warning level message - or maybe it just indicates a tricky part is happening
    
        .EXAMPLE
            Write-TsxOutPut -MsgLevel Default -Message "This is normal execution"    
            Example of normal execution message back to the user. 
            
        .EXAMPLE
            Write-TsxOutPut -MsgLevel Success -Message "You made it"
            Example of success or finalized out put message it's green and happy.
    #>
        [cmdletbinding()]
        Param(
        [Parameter(Mandatory = $true,
        HelpMessage = "You must select one of these options, this is what sets the color of the output message from Yellow, Cyan, or Green.")]
        [validateSet("Warning","Default","Success")]
        [string]$MsgLevel,
        [Parameter(Mandatory = $true,
        HelpMessage = "This parameter accespts a string that should be printed in the color font you would like to use")]
        [string]$Message = $false
        )
        #Start the try block
        try{
            #Capture the original state of the foreground for text in line
            $originState = $Host.UI.RawUI.ForegroundColor
            #Start a switch to evaluate the msg level
            switch ($MsgLevel) {
                "Warning" { 
                    #If the type is warning then set the font to Yellow
                    $Host.UI.RawUI.ForegroundColor = "Yellow"
                    #Write the message using write-output and string concat
                    Write-Output "$($Message)"
                }
                "Default"{
                    #If the type is default or 'running as expected" then set the font color to cyan
                    $Host.UI.RawUI.ForegroundColor = "Cyan"
                    #Write the message using write-output and string concat
                    Write-Output "$($Message)"
                }
                "Success"{
                    #If the type is Success or 'running as expected" then set the font color to cyan
                    $Host.UI.RawUI.ForegroundColor = "Green"
                    #Write the message using write-output and string concat
                    Write-Output "$($Message)"
                }
                #Default should never be triggered as this uses a Validate Set statement
                Default {}
            }
        }
        #In the even that something goes wrong with the write-output such as an object or something other than a string is properly passed through write an error. 
        catch{
            Write-Error -Message "Something went wrong"
        }
        finally{
            #Always set the color back. 
            $Host.UI.RawUI.ForegroundColor = $originState
        }
    }
    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
    }

function New-ADGroupQuery{
    [cmdletbinding()]
    param(
        [parameter(Mandatory = $true)]
        [string]$GroupName,
        [parameter(Mandatory = $true)]
        [string]$CollectionName
        )
$GroupName = "$((Get-ADForest).Name)\\$GroupName"
$Query = @"
select SMS_R_SYSTEM.ResourceID,SMS_R_SYSTEM.ResourceType,SMS_R_SYSTEM.Name,SMS_R_SYSTEM.SMSUniqueIdentifier,SMS_R_SYSTEM.ResourceDomainORWorkgroup,SMS_R_SYSTEM.Client from SMS_R_System where SMS_R_System.SystemGroupName = "$groupName"
"@
        Add-CMDeviceCollectionQueryMembershiprule -CollectionName $CollectionName -RuleName "All devices that are a member of AD Group $($GroupName)" -QueryExpression $Query
        }

#endregion helperfunctions
}

Process{
    if(Test-Module -ModuleName ActiveDirectory -Remediate:$True)
    #This command checks to see if the ActiveDirectory module is loaded - if it's not then load the module
    {
        try {
            #Validate the OU Path existt
            if(!(Test-Path -Path "AD:\$($OUPath)"))
            {
                #Error in the event on this
                throw "Tested the OU Path and the OU Path doesn't exist"
            }
            #Write the out put with the OU Connection Test
            Write-TsxOutPut -MsgLevel Default -Message "Passed the OU connection test"
            $OriginLocation = Get-Location
            #Now we load the ConfigMgr module
            if(Test-ConfigMgrAvailable -Remediate:$true){
                #Retrieve the CMCollections
                Write-TsxOutPut -MsgLevel Default -Message "Now retrieving the collections"
                $CollectionList = Get-CMCollection -Name $CMColNameStructure | Select-Object Name,CollectionID
                #Now do some evaluation and write back the information about the collections we retrieve to the screen.
                Write-TsxOutPut -MsgLevel Success -Message "Retrieved all of the collections in a list"
                Write-TsxOutPut -MsgLevel Warning -Message "We now need to CREATE a bunch of AD groups please validate the information before we create $($CollectionList.Count) groups"
                Write-TsxOutPut -MsgLevel Warning -Message "The validated OU Path is $($OUPath)"
                Write-TsxOutPut -MsgLevel WARNING -Message "The GROUP TYPE is $($GroupType)"
                #Write a warning about what we are going to do and confirm the action
                Write-Warning -Message "If you continue this WILL create Groups if you do NOT want to create groups please enter H otherwise hit A" -WarningAction Inquire
                #Start the foreach loop to evaluate each collection and then create the information needed for the creation of the AD groups
                foreach($Collection in $CollectionList){
                    Write-TsxOutPut -MsgLevel Default -Message "Now Creating AD Group - $($Collection.Name)"
                    #use this to build the description information indicating the day number and time that the windows starts and ends.
                    $DayMWString = $Collection.Name.Split(" - ")[($($Collection.Name.Split(" - ")).length)-1]
                    $CharPosition = New-Object System.Collections.ArrayList($null)
                    foreach($char in [char[]]$DayMWString){
                    if($Char -match "[a-z]"){
                        $CharPosition.Add($DayMWString.IndexOf($Char)) | Out-Null
                        }
                    }
                    $Window = $DayMWString.Substring($($CharPosition[1]))
                    $WindowInfo = Get-PatchWindowTime -Window $Window
                    #Create the Active Directory Group 
                    New-ADGroup -Name $Collection.Name -GroupScope $GroupType -Description "This Group provides a maintenance window for servers from $($WindowInfo[1])" -Path $OUPath
                    Write-TsxOutPut -MsgLevel Success -Message "Created AD group - $($Collection.Name)"
                    Write-TsxOutPut -MsgLevel Default -Message "Now starting the process of Generating the rule and attaching to the ConfigMgr Collection"
                    #Create the AD group Query name
                    New-ADGroupQuery -GroupName $Collection.Name -CollectionName $Collection.Name
                }
                }
                #Return to the original location away from the CMProvider
                Set-Location -Path $OriginLocation.Path
            }
        catch {
            Write-Error $_.Exception.Message
            break
        }
        
    }
}

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

Categories

MSEndpointMgr.com use cookies to ensure that we give you the best experience on our website.