Skip to content

Instantly share code, notes, and snippets.

@Ioan-Popovici
Last active May 9, 2019 12:12
Show Gist options
  • Save Ioan-Popovici/f767158f238eabc35cd6 to your computer and use it in GitHub Desktop.
Save Ioan-Popovici/f767158f238eabc35cd6 to your computer and use it in GitHub Desktop.
Creates maintenance windows on SCCM Collections based on patch tuesday.
<#
.SYNOPSIS
Creates maintenance windows on SCCM collections.
.DESCRIPTION
Creates maintenance windows on SCCM collections based on patch tuesday.
.EXAMPLE
New-MaintenanceWindows.ps1
.NOTES
Written in collaboration with my good friend Octavian Cordos.
.INPUTS
System.String.
.OUTPUTS
System.String.
.NOTES
Created by Ioan Popovici and Octavian Cordos.
.LINK
https://SCCM.Zone/New-CMMaintenanceWindows
.LINK
https://SCCM.Zone/New-CMMaintenanceWindows-CHANGELOG
.LINK
https://SCCM.Zone/New-CMMaintenanceWindows-GIT
.LINK
https://SCCM.Zone/Issues
.COMPONENT
CM
.FUNCTIONALITY
Create new CM maintenance window
#>
##*=============================================
##* INITIALIZATION
##*=============================================
#region Initialization
## Get script path and name
[string]$ScriptPath = [System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Definition)
[string]$ScriptName = [System.IO.Path]::GetFileNameWithoutExtension($MyInvocation.MyCommand.Definition)
## CSV and log file initialization
# Set the CSV file name
[string]$csvFileName = $ScriptName
# Get CSV file name with extension
[string]$csvFileNameWithExtension = $ScriptName+'.csv'
# Assemble CSV and log file path
[string]$csvFilePath = (Join-Path -Path $ScriptPath -ChildPath $csvFileName) + '.csv'
[string]$LogFilePath = (Join-Path -Path $ScriptPath -ChildPath $ScriptName) + '.log'
#endregion
##*=============================================
##* END INITIALIZATION
##*=============================================
##*=============================================
##* FUNCTION LISTINGS
##*=============================================
#region FunctionListings
#region Function Write-Log
Function Write-Log {
<#
.SYNOPSIS
Writes data to file log, event log and console.
.DESCRIPTION
Writes data to file log, event log and console.
.PARAMETER EventLogEntryMessage
The event log entry message.
.PARAMETER EventLogName
The event log to write to.
.PARAMETER FileLogName
The file log name to write to.
.PARAMETER EventLogEntrySource
The event log entry source
.PARAMETER EventLogEntryID
The event log entry ID.
.PARAMETER EventLogEntryType
The event log entry type (Error | Warning | Information | SuccessAudit | FailureAudit).
.PARAMETER SkipEventLog
Skip writing to event log.
.EXAMPLE
Write-Log -EventLogEntryMessage 'Set-ClientMW was successful' -EventLogName 'Configuration Manager' -EventLogEntrySource 'Script' -EventLogEntryID '1' -EventLogEntryType 'Information'
.NOTES
This is an internal script function and should typically not be called directly.
.NOTES
This is an internal script function and should typically not be called directly.
.LINK
https://SCCM.Zone
.LINK
https://SCCM.Zone/Git
#>
[CmdletBinding()]
Param (
[Parameter(Mandatory=$false,Position=0)]
[Alias('Message')]
[string]$EventLogEntryMessage,
[Parameter(Mandatory=$false,Position=1)]
[Alias('EName')]
[string]$EventLogName = 'Configuration Manager',
[Parameter(Mandatory=$false,Position=2)]
[Alias('Source')]
[string]$EventLogEntrySource = $ScriptName,
[Parameter(Mandatory=$false,Position=3)]
[Alias('ID')]
[int32]$EventLogEntryID = 1,
[Parameter(Mandatory=$false,Position=4)]
[Alias('Type')]
[string]$EventLogEntryType = 'Information',
[Parameter(Mandatory=$false,Position=5)]
[Alias('SkipEL')]
[switch]$SkipEventLog
)
## Initialization
# Getting the date and time
[string]$LogTime = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss').ToString()
# Archive log file if it exists and it's larger than 50 KB
If ((Test-Path $LogFilePath) -and (Get-Item $LogFilePath).Length -gt 50KB) {
Get-ChildItem -Path $LogFilePath | Rename-Item -NewName { $_.Name -Replace '.log','.lo_' } -Force
}
# Create event log and event source if they do not exist
If (-not ([System.Diagnostics.EventLog]::Exists($EventLogName)) -or (-not ([System.Diagnostics.EventLog]::SourceExists($EventLogEntrySource)))) {
# Create new event log and/or source
New-EventLog -LogName $EventLogName -Source $EventLogEntrySource
}
## Error logging
If ($_.Exception) {
# Write to log
Write-EventLog -LogName $EventLogName -Source $EventLogEntrySource -EventId $EventLogEntryID -EntryType 'Error' -Message "$EventLogEntryMessage `n$_"
# Write to console
Write-Host `n$EventLogEntryMessage -BackgroundColor Red -ForegroundColor White
Write-Host $_.Exception -BackgroundColor Red -ForegroundColor White
}
Else {
# Skip event log if requested
If ($SkipEventLog) {
# Write to console
Write-Host $EventLogEntryMessage -BackgroundColor White -ForegroundColor Blue
}
Else {
# Write to event log
Write-EventLog -LogName $EventLogName -Source $EventLogEntrySource -EventId $EventLogEntryID -EntryType $EventLogEntryType -Message $EventLogEntryMessage
# Write to console
Write-Host $EventLogEntryMessage -BackgroundColor White -ForegroundColor Blue
}
}
## Assemble log line
[string]$LogLine = "$LogTime : $EventLogEntryMessage"
## Write to log file
$LogLine | Out-File -FilePath $LogFilePath -Append -NoClobber -Force -Encoding 'UTF8' -ErrorAction 'Continue'
}
#endregion
#region Function Get-PatchTuesday
Function Get-PatchTuesday {
<#
.SYNOPSIS
Get Microsoft patch Tuesday.
.DESCRIPTION
Get Microsoft patch Tuesday for a specific month and return it to the pipeline.
.PARAMETER Year
Set the year for which to calculate Patch Tuesday.
.PARAMETER Month
Set the month for which to calculate Patch Tuesday.
.EXAMPLE
Get-PatchTuesday -Year 2015 -Month 3
.INPUTS
System.String
.OUTPUTS
System.DateTime.
.NOTES
This is an internal script function and should typically not be called directly.
.LINK
https://SCCM.Zone
.LINK
https://SCCM.Zone/Git
.COMPONENT
CM
.FUNCTIONALITY
Get patch tuesday
#>
[CmdletBinding()]
Param (
[Parameter(Mandatory=$true,Position=0)]
[Alias('Yr')]
[string]$Year,
[Parameter(Mandatory=$true,Position=1)]
[Alias('Mo')]
[string]$Month
)
Begin {
## Build Target Month
[DateTime]$StartingMonth = $Month + '/1/' + $Year
}
Process {
## Search for First Tuesday
While ($StartingMonth.DayofWeek -ine 'Tuesday') {
$StartingMonth = $StartingMonth.AddDays(1)
}
## Set Second Tuesday of the month by adding 7 days
$PatchTuesday = $StartingMonth.AddDays(7)
## Return Patch Tuesday
Return $PatchTuesday
}
End {
}
}
#endregion
#region Function Get-MaintenanceWindows
Function Get-MaintenanceWindows {
<#
.SYNOPSIS
Get existing maintenance windows.
.DESCRIPTION
Get the existing maintenance windows for a collection.
.PARAMETER CollectionName
Set the collection name for which to list the maintenance Windows.
.EXAMPLE
Get-MaintenanceWindows -Collection 'Computer Collection'
.INPUTS
System.String.
.OUTPUTS
System.Object.
.NOTES
This is an internal script function and should typically not be called directly.
.LINK
https://SCCM.Zone
.LINK
https://SCCM.Zone/Git
.COMPONENT
CM
.FUNCTIONALITY
Get collection maintenance windows
#>
[CmdletBinding()]
Param (
[Parameter(Mandatory=$true,Position=0)]
[Alias('Collection')]
[string]$CollectionName
)
## Get collection ID
Try {
$CollectionID = (Get-CMDeviceCollection -Name $CollectionName -ErrorAction 'Stop').CollectionID
}
# Write to log in case of failure
Catch {
Write-Log -Message "Getting $CollectionName ID - Failed!"
}
## Get collection maintenance windows
Try {
Get-CMMaintenanceWindow -CollectionId $CollectionID -ErrorAction 'Stop'
}
# Write to log in case of failure
Catch {
Write-Log -Message "Get maintenance windows for $CollectionName - Failed!"
}
}
#endregion
#region Function Remove-MaintenanceWindows
Function Remove-MaintenanceWindows {
<#
.SYNOPSIS
Remove existing maintenance windows.
.DESCRIPTION
Remove all existing maintenance windows from a collection.
.PARAMETER CollectionName
The collection name for which to remove the maintenance windows.
.EXAMPLE
Remove-MaintenanceWindows -Collection 'Computer Collection'
.INPUTS
System.String.
.OUTPUTS
System.String.
.NOTES
This is an internal script function and should typically not be called directly.
.LINK
https://SCCM.Zone
.LINK
https://SCCM.Zone/Git
.COMPONENT
CM
.FUNCTIONALITY
Remove maintenance windows
#>
[CmdletBinding()]
Param (
[Parameter(Mandatory=$true,Position=0)]
[Alias('Collection')]
[string]$CollectionName
)
## Get collection ID
Try {
$CollectionID = (Get-CMDeviceCollection -Name $CollectionName -ErrorAction 'Stop').CollectionID
}
Catch {
# Write to log in case of failure
Write-Log -Message "Getting $CollectionName ID - Failed!"
}
## Get collection maintenance windows and delete them
Try {
Get-CMMaintenanceWindow -CollectionId $CollectionID | ForEach-Object {
Remove-CMMaintenanceWindow -CollectionID $CollectionID -Name $_.Name -Force -ErrorAction 'Stop'
Write-Log -Message ($_.Name+' - Removed!') -SkipEventLog
}
}
Catch {
# Write to log in case of failure
Write-Log -Message "$_.Name - Removal Failed!"
}
}
#endregion
#region Function New-MaintenanceWindows
Function New-MaintenanceWindows {
<#
.SYNOPSIS
Set maintenance windows.
.DESCRIPTION
Set Maintenance Windows to a Collection.
.PARAMETER CollectionName
Specifies the collection name for which to set maintenance windows.
.PARAMETER Year
Specifies the maintenance window year.
.PARAMETER Month
Specifies the maintenance window month.
.PARAMETER OffsetWeeks
Specifies the maintenance window offset number of weeks after patch Tuesday.
.PARAMETER OffsetDays
Specifies the maintenance window offset number of days after path Tuesday.
.PARAMETER StartTime
Specifies the maintenance window start time.
.PARAMETER StopTime
Specifies the maintenance window stop time.
.PARAMETER ApplyTo
Specifies the maintenance window type to ( Any | SoftwareUpdates | TaskSequences).
.EXAMPLE
New-MaintenanceWindows -CollectionName 'Computer Collection' -Year 2015 -Month 3 -OffsetWeeks 3 -OffsetDays 2 -StartTime '01:00' -StopTime '02:00' -ApplyTo SoftwareUpdates
.INPUTS
System.String.
.OUTPUTS
System.String.
.NOTES
This is an internal script function and should typically not be called directly.
.LINK
https://SCCM.Zone
.LINK
https://SCCM.Zone/Git
.COMPONENT
CM
.FUNCTIONALITY
Create new maintenance window
#>
Param (
[Parameter(Mandatory=$true,Position=0)]
[Alias('Collection')]
[string]$CollectionName,
[Parameter(Mandatory=$true,Position=1)]
[Alias('Yr')]
[int16]$Year,
[Parameter(Mandatory=$true,Position=2)]
[Alias('Mo')]
[int16]$Month,
[Parameter(Mandatory=$true,Position=3)]
[Alias('Weeks')]
[int16]$OffsetWeeks,
[Parameter(Mandatory=$true,Position=4)]
[Alias('Days')]
[int16]$OffsetDays,
[Parameter(Mandatory=$true,Position=5)]
[Alias('Start')]
[string]$StartTime,
[Parameter(Mandatory=$true,Position=6)]
[Alias('Stop')]
[string]$StopTime,
[Parameter(Mandatory=$true,Position=7)]
[Alias('Apply')]
[string]$ApplyTo
)
## Get CollectionID
Try {
$CollectionID = (Get-CMDeviceCollection -Name $CollectionName -ErrorAction 'Stop').CollectionID
}
Catch {
# Write to log in case of failure
Write-Log -Message "Getting $CollectionName ID - Failed!"
}
## Get PatchTuesday
[DateTime]$PatchTuesday = Get-PatchTuesday -Year $Year -Month $Month
## Setting Patch Day, adding offset days and weeks, reformatting date
$PatchDay = $PatchTuesday.AddDays($OffsetDays+($OffsetWeeks*7)) | Get-Date -Format 'yyyy-MM-dd'
## Check if we got ourselves in the next year and return to the main script if true
If ($PatchDay.Year -gt $Year) {
Write-Log -Message 'Year threshold detected! Ending cycle...' -SkipEventLog
Return
}
## Setting maintenance window start and stop times
$MWStartTime = Get-Date -Format 'yyyy-MM-dd HH:mm' -Date ($PatchDay+' '+$StartTime)
$MWStopTime = Get-Date -Format 'yyyy-MM-dd HH:mm' -Date ($PatchDay+' '+$StopTime)
## Create the schedule token
$MWSchedule = New-CMSchedule -Start $MWStartTime -End $MWStopTime -NonRecurring
## Set Maintenance Window Naming Convention for MW
If ($ApplyTo -eq 'Any') { $MWType = 'MWA' }
ElseIf ($ApplyTo -match 'Software') { $MWType = 'MWU' }
ElseIf ($ApplyTo -match 'Task') { $MWType = 'MWT' }
# Set maintenance window name
$MWName = $MWType+'.NR.'+(Get-Date -Uformat %Y-%B-%d $MWStartTime)+'_'+$StartTime+'-'+$StopTime
## Set maintenance window on collection
Try {
$SetNewMW = New-CMMaintenanceWindow -CollectionID $CollectionID -Schedule $MWSchedule -Name $MWName -ApplyTo $ApplyTo -ErrorAction 'Stop'
# Write to log
Write-Log -Message "$MWName - Set!" -SkipEventLog
}
Catch {
# Write to log in case of failure
Write-Log -Message "Setting $MWName on $CollectionName - Failed!"
}
}
#endregion
#region Function Send-Mail
Function Send-Mail {
<#
.SYNOPSIS
Send E-Mail to specified address.
.DESCRIPTION
Send E-Mail body to specified address.
.PARAMETER From
Source.
.PARAMETER To
Destination.
.PARAMETER CC
Carbon copy.
.PARAMETER Body
E-Mail body.
.PARAMETER SMTPServer
E-Mail SMTPServer.
.PARAMETER SMTPPort
E-Mail SMTPPort.
.EXAMPLE
Set-Mail -Body 'Test' -CC 'email@domain.com'
.INPUTS
System.String.
.OUTPUTS
System.String.
.NOTES
This is an internal script function and should typically not be called directly.
.LINK
https://SCCM.Zone
.LINK
https://SCCM.Zone/Git
.COMPONENT
Mail
.FUNCTIONALITY
Send mail
#>
[CmdletBinding()]
Param (
[Parameter(Mandatory=$false,Position=0)]
[string]$From = 'SCCM Site Server <noreply@domain.com>',
[Parameter(Mandatory=$false,Position=1)]
[string]$To = 'SCCM Team <email@domain.com>',
[Parameter(Mandatory=$false,Position=2)]
[string]$CC,
[Parameter(Mandatory=$false,Position=3)]
[string]$Subject = 'Info: Maintenance Window Set!',
[Parameter(Mandatory=$true,Position=4)]
[string]$Body,
[Parameter(Mandatory=$false,Position=5)]
[string]$SMTPServer = 'smtpserver.domain.no',
[Parameter(Mandatory=$false,Position=6)]
[string]$SMTPPort = "25"
)
Try {
If ($CC) {
Send-MailMessage -From $From -To $To -Subject $Subject -CC $CC -Body $Body -SmtpServer $SMTPServer -Port $SMTPPort -ErrorAction 'Stop'
}
Else {
Send-MailMessage -From $From -To $To -Subject $Subject -Body $Body -SmtpServer $SMTPServer -Port $SMTPPort -ErrorAction 'Stop'
}
}
Catch {
Write-Log -Message 'Send Mail - Failed!'
}
}
#endregion
#endregion
##*=============================================
##* END FUNCTION LISTINGS
##*=============================================
##*=============================================
##* SCRIPT BODY
##*=============================================
#region ScriptBody
## Import SCCM PSH module and changing context
Try {
Import-Module $env:SMS_ADMIN_UI_PATH.Replace('\bin\i386','\bin\configurationmanager.psd1') -ErrorAction 'Stop'
}
Catch {
Write-Log -Message 'Importing SCCM PSH module - Failed!'
}
# Get the CMSITE SiteCode and change connection context
$SiteCode = Get-PSDrive -PSProvider 'CMSITE'
# Change the connection context
Set-Location "$($SiteCode.Name):\"
## Import the CSV file
Try {
$csvFileData = Import-Csv -Path $csvFilePath -Encoding 'UTF8' -ErrorAction 'Stop'
}
Catch {
Write-Log -Message 'Importing CSV Data - Failed!'
}
## Process imported CSV file data
$csvFileData | ForEach-Object {
# Check if we need the remove existing maintenance windows
If ($_.RemoveExisting -eq 'YES' ) {
# Write to log
Write-Log -Message ('Removing maintenance windows from: ' + $_.CollectionName) -SkipEventLog
# Remove maintenance window
Remove-MaintenanceWindows $_.CollectionName
}
# Check if we need to set maintenance windows for the whole year
If ($_.SetForWholeYear -eq 'YES') {
# Write to log
Write-Log -Message ('Setting maintenance windows on: ' + $_.CollectionName) -SkipEventLog
# Set maintenance windows for 12 months
For ($Month = [int]$_.Month; $Month -le 12; $Month++) {
New-MaintenanceWindows -CollectionName $_.CollectionName -Year $_.Year -Month $Month -OffsetWeeks $_.OffsetWeeks -OffsetDays $_.OffsetDays -StartTime $_.StartTime -StopTime $_.StopTime -ApplyTo $_.ApplyTo
}
}
Else {
# Write to log
Write-Log -Message $('Setting maintenance window on: ' + $_.CollectionName) -SkipEventLog
# Run without removing maintenance windows and set just one maintenance window
New-MaintenanceWindows -CollectionName $_.CollectionName -Year $_.Year -Month $_.Month -OffsetWeeks $_.OffsetWeeks -OffsetDays $_.OffsetDays -StartTime $_.StartTime -StopTime $_.StopTime -ApplyTo $_.ApplyTo
}
}
## Get maintenance windows for unique collections
# Initialize result array
[array]$Result = @()
# Parsing CSV collection names
$csvFileData.CollectionName | Select-Object -Unique | ForEach-Object {
# Getting maintenance windows for collection (split to new line)
$MaintenanceWindows = Get-MaintenanceWindows -CollectionName $_ | ForEach-Object { $_.Name + "`n" }
# Assemble result with descriptors
$Result += "`n Listing all maintenance windows for: " + $_ + " " + "`n " + $MaintenanceWindows
}
# Convert the result to string and write it to log
[string]$ResultString = Out-String -InputObject $Result
Write-Log -Message $ResultString
## E-Mail result
#Send-Mail -Body $ResultString
## Return to Script Path
Set-Location $ScriptPath
## Remove SCCM PSH Module
Remove-Module 'ConfigurationManager' -Force -ErrorAction 'Continue'
#endregion
##*=============================================
##* END SCRIPT BODY
##*=============================================
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment