Tuesday, October 31, 2017

Powershell Script to Email an Alert if a Scheduled Task fails

-------------------------------------------------------------
Monitor-ScheduleTasks.ps1
-------------------------------------------------------------

<#
.Synopsis
   Determines which scheduled tasks (if any) have failed in the last 30 minutes 
   and sends an email listing those failures.
.DESCRIPTION
   Determines which scheduled tasks (if any) have failed in the last 30 minutes 
   and sends an email listing those failures.
.USAGE
    Create a scheduled task that has three triggers.  
    The triggers are on Task Scheduler log events 101, 103, and 202.
    Be sure that the scheduled task is running under an account with 
    enough credentials to read the log and to run this script, and 
    is running whether or not that account is logged on.
    Obviously, this script should be the action taken on trigger (program is
    powershell.exe, this script path\filename as parameter)
    Adjust the variables that begin with "$Email" below to be correct for your
    environment.
#>

$EmailUsername = "anonymous"
$EmailPassword = ConvertTo-SecureString -String "anonymous" -AsPlainText -Force
$EmailFrom = "$env:COMPUTERNAME@mydomain.com"
$EmailSMTPServer = "MySMTPServer.Domain.com"

$events = get-scheduledtask | where {$_.State -ne "Disabled"| where {((Get-ScheduledTaskInfo $_).LastTaskResult -ne "0"-and ((Get-ScheduledTaskInfo $_).LastTaskResult -ne "267009"-and((Get-ScheduledTaskInfo $_).LastRunTime -ge ((Get-Date).AddMinutes(-30)) -and ((Get-ScheduledTaskInfo $_).TaskPath -notlike "\Microsoft\*" )) -and $_.TaskName -notlike"User_Feed_Synchronization*"| Select TaskName
$count = ($events.TaskName).Count
$EmailBody = ""
if ($count -ge 1){
    foreach ($event in $events){
        $FailedTaskMessage = "A scheduled task on $env:COMPUTERNAME has failed.`n`r`n`rFailed Task:" + $event.TaskName + "`n`r"
        $EmailBody = $EmailBody + $FailedTaskMessage
    }
}
else{
    $EmailBody = "Task Scheduler Engine reported a failed task.`n`r`n`rAutomation is unable to determine which task failed."
}
$EmailCredentials = New-Object System.Management.Automation.PSCredential($EmailUsername,$EmailPassword)
$EmailSubject = "A scheduled task on $env:COMPUTERNAME has failed"
Write-Output $EmailBody
Send-MailMessage -To $EmailTo -From $EmailFrom -Subject $EmailSubject -SmtpServer $EmailSMTPServer -Credential $EmailCredentials -Body $EmailBody

Friday, September 22, 2017

Powershell script to create SCCM maintenance window collections based on an external source (CMDB)

As written this will download the external data from a URL but that is easily converted to a simple file copy (or completely removed) if a URL is not how you get your CMDB data.  As always, don't just download and run stuff that you find on the internet, check it over for possible malicious code before you do anything with it.  Enjoy!

-------------------------------------------------------------
Update-MWCollections.ps1
-------------------------------------------------------------
 <#
.Synopsis
   Maintains Software Updates Maintenance Window Collections in SCCM
   based on information from an external source (CMDB)
.DESCRIPTION
   ***********************************************************
   Look for the "#User Variables" to make any necessry changes
   The script will not download a copy the CMDB information
   while in test mode (will use the test file).  It will,
   however, create maintenance windows on any collections that
   it creates so ensure that you only have test machines in
   your test file's data.
   ***********************************************************
   Downloads data from CMDB website and stores a local copy as a .csv
   Reads the CMDB data from the .csv into an array of objects
   Reads SCCM data directly from SCCM into an array of objects
   Loops through the array of CMDB objects, checking each against the SCCM objects
   Sets a count of SCCM maintenance windows collections to the number that the CMDB object belong to
   If the count of SCCM maintenance windows collections is more than 1, 
     it removes the computer from all SCCM maintenance windows and sets the count to 0
   If the count of SCCM maintenance windows collections is exactly 1,
     it checks to ensure that the one maintenance window collection to which the computer belongs is correct
     if it is not correct it removes the computer from its current collection and sets the count to 0
   If the count of SCCM maintenance windows collections is 0,
     it checks to see if the correct maintenance window collection exists, if not it creates it
     it adds the computer to the correct maintenance window collection
.NOTES
   Created by Mark Randol - randoltech.blogspot.com
#>

function Get-OccuranceOfDayOfWeek{
    [CmdletBinding(DefaultParameterSetName='OccuranceOfDayOfWeek', 
                  SupportsShouldProcess=$true, 
                  PositionalBinding=$false)]
    [OutputType([DateTime])]
    Param(
        # Day of the Week
        [Parameter(Mandatory=$true, 
                   ValueFromPipeline=$true,
                   ValueFromPipelineByPropertyName=$true, 
                   ValueFromRemainingArguments=$false, 
                   Position=0,
                   ParameterSetName='OccuranceOfDayOfWeek')]
        [ValidateNotNull()]
        [ValidateNotNullOrEmpty()]
        [ValidateLength(0,15)]
        [ValidateSet("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday")]
        [Alias("day","dayname","dow")]
        [String]
        $DayOfWeek,

        # Occurance
        [Parameter(Mandatory=$true, 
                   ValueFromPipeline=$true,
                   ValueFromPipelineByPropertyName=$true, 
                   ValueFromRemainingArguments=$false, 
                   Position=1,
                   ParameterSetName='OccuranceOfDayOfWeek')]
        [ValidateNotNull()]
        [ValidateNotNullOrEmpty()]
        [ValidateSet(1,2,3,4)]
        [Alias("occ")]
        [Int]
        $Occurance
    )
    Begin{
    }
    Process{
        [Int]$TestDayNum = 0
        [Arr]$OccuranceDates = @()
        do
        {
            $TestDayNum = $TestDayNum + 1
            $TestDayName = (Get-Date -Day $TestDayNum).DayOfWeek
        }
        until ($TestDayName -eq $DayOfWeek)
        $OccuranceDate = $TestDayNum
        do{
            $OccuranceDates += @($OccuranceDate)
            $OccuranceDate = $OccuranceDate + 7
        }
        while ($OccuranceDate -lt 32)
    }
    End{
        [Int]$OutputInt = $Occurance - 1
        [DateTime]$MyOutput = (Get-Date -Year (Get-Date).Year -Month (Get-Date).Month -Day $OccuranceDates[$OutputInt]).Date
        Return $MyOutput
    }
}

function Add-SCCMArrayMember{
    [CmdletBinding(DefaultParameterSetName='CollectionMembership', 
                  SupportsShouldProcess=$true, 
                  PositionalBinding=$false)]
    [OutputType([object])]
    Param(
        [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$true,
                   Position=0,
                   ParameterSetName='CollectionMembership')]
        [string]$MaintenanceWindow,

        [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$true,
                   Position=1,
                   ParameterSetName='CollectionMembership')]
        [string]$CollectionID,

        [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$true,
                   Position=2,
                   ParameterSetName='CollectionMembership')]
        [string]$ComputerName,

        [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$true,
                   Position=3,
                   ParameterSetName='CollectionMembership')]
        [string]$ResourceID
    )
    Begin{
        $CollectionMembership = new-object PSObject
    }
    Process{
        $properties = @{'MaintenanceWindow'=$MaintenanceWindow;
                        'CollectionID'=$CollectionID;
                        'ComputerName'=$ComputerName;
                        'ResourceID'=$ResourceID}
        $CollectionMembership = New-Object -Property $properties -TypeName PSObject
    }
    End{
        return $CollectionMembership
    }
}

function Add-CMDBArrayMember{
    [CmdletBinding(DefaultParameterSetName='CMDBArrayMember', 
                  SupportsShouldProcess=$true, 
                  PositionalBinding=$false)]
    [OutputType([object])]
    Param(
        [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$true,
                   Position=0,
                   ParameterSetName='CMDBArrayMember')]
        [string]$MaintenanceWindow,

        [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$true,
                   Position=1,
                   ParameterSetName='CMDBArrayMember')]
        [string]$ComputerName
    )
    Begin{
        $Membership = new-object PSObject
    }
    Process{
        $properties = @{'MaintenanceWindow'=$MaintenanceWindow;
                        'ComputerName'=$ComputerName}
        $Membership = New-Object -Property $properties -TypeName PSObject
    }
    End{
        return $Membership
    }
}

function Create-NewSCCMMWCollection{
    [CmdletBinding(DefaultParameterSetName='NewSCCMMWCollection', 
                  SupportsShouldProcess=$true, 
                  PositionalBinding=$false)]
    [OutputType([object])]
    Param(
        [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$true,
                   Position=0,
                   ParameterSetName='NewSCCMMWCollection')]
        [string]$CollectionName,

        [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$true,
                   Position=1,
                   ParameterSetName='NewSCCMMWCollection')]
        [string]$TargetFolderID,

        [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$true,
                   Position=2,
                   ParameterSetName='NewSCCMMWCollection')]
        [string]$Schedule,

        [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$true,
                   Position=3,
                   ParameterSetName='NewSCCMMWCollection')]
        [string]$LimitingCollectionID,


        [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$true,
                   Position=4,
                   ParameterSetName='NewSCCMMWCollection')]
        [int]$MaintDuration,

        [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$true,
                   Position=5,
                   ParameterSetName='NewSCCMMWCollection')]
        [string]$DeploymentCollectionID,

        [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$true,
                   Position=5,
                   ParameterSetName='NewSCCMMWCollection')]
        [string]$SiteCode
    )
    Begin{
    }
    Process{
        #Create the collection
        $ActivityMessage = "Adding new SCCM Maintenance Window collection " + $CollectionName
        $StatusMessage = "Creating collection named " + $CollectionName
        Write-Output $StatusMessage
        $newColl = New-CMDeviceCollection -Name $CollectionName -limitingcollectionid $LimitingCollectionID -RefreshType Continuous #Create the collection
        $NewCollectionID = $($newColl).collectionid
        $NewCollectionName = $($newColl).Name
        
        #Move the to the correct folder
        $CurrentFolderID = 0
        $ObjectTypeID = 5000
        $StatusMessage = "Moving " + $NewCollectionName + " to Maintenance Windows Folder"
        Write-Progress -Activity $ActivityMessage -Status $StatusMessage
        Write-Output $StatusMessage
        $Namespace = "Root\SMS\Site_" + $SiteCode
        Invoke-WmiMethod -Namespace $Namespace -Class SMS_objectContainerItem -Name MoveMembers -ArgumentList $CurrentFolderID,$NewCollectionID,$ObjectTypeID,$TargetFolderID

        #Put a maintenance window on the collection
        #Turn the schedule variable into a comma delimited array
        $CommaDelimitedSchedule = $Schedule.Replace("st-",",").Replace("nd-",",").Replace("rd-",",").Replace("th-",",").Replace("st&","").Replace("nd&","").Replace("rd&","").Replace("th&","").Replace("@",",").Replace("-",",")
        #Initialize variables from the array contents
        $WeekOrderStr = $CommaDelimitedSchedule.Split(',')[0]
        $DayOfWeek = $CommaDelimitedSchedule.Split(',')[1]
        $StartTimeStr = $CommaDelimitedSchedule.Split(',')[2]
        $StartHour = Get-Date $schedule.split('@')[1]
        [Int]$WeekOrder = 0
        $EndTime = $StartTime.AddHours($maintDuration)
        if ($WeekOrderStr -eq "every"){
            $WeekOrder = 0 #Set up for every week
            $MWWindowName = "$prefix $WeekOrderStr-$DayOfWeek@$StartTimeStr"
            $StatusMessage = "Adding $MWWindowName to collection " + $NewCollectionName
            Write-Progress -Activity $ActivityMessage -Status $StatusMessage
            Write-Output $StatusMessage
            IF ($DayOfWeek -eq "Day" -or $DayOfWeek -eq "day"){ #schedule is every day
                $StartTime = ((get-date -Day 1).Date).AddHours($StartHour.Hour)
                $EndTime = $StartTime.AddHours($maintDuration)
                $StatusMessage = "Adding $MWWindowName to collection " + $NewCollectionName
                Write-Progress -Activity $ActivityMessage -Status $StatusMessage
                Write-Output $StatusMessage
                $schedtoken = New-CMSchedule -Start $StartTime -End $EndTime -RecurInterval Days -RecurCount 1
                New-CMMaintenanceWindow -CollectionID $NewCollectionID -Name $MWWindowName -Schedule $schedtoken
            } #schedule is every day
            else{ #schedule is one day of week
                $StartTime = (Get-OccuranceOfDayOfWeek -DayOfWeek $DayOfWeek -Occurance 1).AddHours($StartHour.Hour)
                $EndTime = $StartTime.AddHours($maintDuration)
                $StatusMessage = "Adding $MWWindowName to collection " + $NewCollectionName
                Write-Progress -Activity $ActivityMessage -Status $StatusMessage
                Write-Output $StatusMessage
                $schedtoken = New-CMSchedule -DayOfWeek $DayOfWeek -Start $StartTime -End $EndTime -RecurCount 1
                New-CMMaintenanceWindow -CollectionID $NewCollectionID -Name $MWWindowName -Schedule $schedtoken
            } #schedule is one day of week
        }
        else {
            $WeekOrder = $WeekOrderStr.Substring(0,1) #Set up for the week in the 1st character (monthly and fortnightly)
            $StartTime = (Get-OccuranceOfDayOfWeek -DayOfWeek $DayOfWeek -Occurance $WeekOrder).AddHours($StartHour.Hour)
            $EndTime = $StartTime.AddHours($maintDuration)
            $MWWindowName = $prefix + (($WeekOrderStr.Substring(0,1)).replace("1","1st").replace("2","2nd")).replace("3","3rd").replace("4","4th") + "-$DayOfWeek@$StartTimeStr"
            $StatusMessage = "Adding $MWWindowName to collection " + $NewCollectionName
            Write-Progress -Activity $ActivityMessage -Status $StatusMessage
            Write-Output $StatusMessage
            $schedtoken = New-CMSchedule -WeekOrder $WeekOrder -DayOfWeek $DayOfWeek  -Start $StartTime -End $EndTime
            New-CMMaintenanceWindow -CollectionID $NewCollectionID -Name $MWWindowName -Schedule $schedtoken
            if ($WeekOrderStr.Length -eq 2){
                $WeekOrder = $WeekOrderStr.Substring(1,1) #Set up for the week in the 2nd character (fortnightly)
                $StartTime = (Get-OccuranceOfDayOfWeek -DayOfWeek $DayOfWeek -Occurance $WeekOrder).AddHours($StartHour.Hour)
                $EndTime = $StartTime.AddHours($maintDuration)
                $MWWindowName = $prefix + (($WeekOrderStr.Substring(1,1)).replace("1","1st").replace("2","2nd")).replace("3","3rd").replace("4","4th") + "-$DayOfWeek@$StartTimeStr"
                $StatusMessage = "Adding $MWWindowName to collection " + $NewCollectionName
                Write-Progress -Activity $ActivityMessage -Status $StatusMessage
                Write-Output $StatusMessage
                $schedtoken = New-CMSchedule -WeekOrder $WeekOrder -DayOfWeek $DayOfWeek  -Start $StartTime -End $EndTime
                New-CMMaintenanceWindow -CollectionID $NewCollectionID -Name $MWWindowName -Schedule $schedtoken
            }
        }

        #Include the collection in the deployment collection membership
        $StatusMessage = "Adding " + $NewCollectionName + " as an include to the deployment collection"
        Write-Progress -Activity $ActivityMessage -Status $StatusMessage
        Write-Output $StatusMessage
        Add-CMDeviceCollectionIncludeMembershipRule -CollectionId $DeploymentCollectionID -IncludeCollectionId $NewCollectionID
    }
    End{
    }
}

function Remove-AllCurrentMWMembership{
    [CmdletBinding(DefaultParameterSetName='AllCurrentMWMembership', 
                  SupportsShouldProcess=$true, 
                  PositionalBinding=$false)]
    [OutputType([int])]
    Param(
        [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$true,
                   Position=0,
                   ParameterSetName='AllCurrentMWMembership')]
        [string]$ComputerName,

        [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$true,
                   Position=1,
                   ParameterSetName='AllCurrentMWMembership')]
        [string]$SiteServerName,

        [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$true,
                   Position=2,
                   ParameterSetName='AllCurrentMWMembership')]
        [string]$SiteCode,

        [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$true,
                   Position=3,
                   ParameterSetName='AllCurrentMWMembership')]
        [string]$Prefix

    )

    Begin{
        $NameSpace = "root\SMS\site_" + $SiteCode
        $QueryText = "SELECT SMS_Collection.* FROM SMS_FullCollectionMembership, SMS_Collection where name = '" + $ComputerName + "' and SMS_FullCollectionMembership.CollectionID = SMS_Collection.CollectionID and SMS_FullCollectionMembership.CollectionName like '" + $Prefix + "%'"
    $Collections = Get-WmiObject -Query $QueryText -Namespace $NameSpace
    }
    Process{
        foreach ($Collection in $Collections){
            $CollectionName = $Collection.name
            $ActivityMessage = “Removing computer from a collection.”
            $StatusMessage = “Removing $ComputerName from $CollectionName”
            Write-Output $StatusMessage
            Write-Progress -Activity $ActivityMessage -Status $StatusMessage
            Remove-CMCollectionDirectMembershipRule -CollectionName $CollectionName -ResourceName $ComputerName -Force
        }
    }
    End{
    }
}

Import-Module -Name "$(split-path $Env:SMS_ADMIN_UI_PATH)\ConfigurationManager.psd1" #Load the Configuration Manager Module

#Initialize program variables
    $Date = Get-Date -Format yyyy-MM-dd_HHmm
    $StartTime = Get-Date
    $SCCMMWTable = @([pscustomobject])
    $CMDBMWTable = @([pscustomobject])
    [Int]$OuterLoopCounter = -1
    [Int]$MainCounter = -1
    [Int]$InnerLoopCounter = -1

#User Variables
    $TestMode = $true #Set to $false to run production - provides quick and easy switch to/from test mode.
    $SiteCode = "SIT"
    $SiteServer = "siteserver.yourdomain.org"
    $WorkingFolder = "D:\Folder\Sub-Folder" #What is our main folder to work with.  This just keeps things clean and stops stuff from ending up in places like C:\
    $LogFolder = $WorkingFolder + "\Logs" #Where to put our logs
    $CMDBDownloadURL = "https://sum.dum.url"
    $ProxyServerURL = "http://proxyserver.yourdomain.org:8015"
    $LogFile = $LogFolder + "\Update-SUPMWCollections." + $Date + ".log" #Where to put the log file and what to name it.
    $LocalCMDBpathProd = $WorkingFolder + "\CMDBData.$Date.csv" #production - we download to this file and then work with it locally
    $LocalCMDBpathTest = $WorkingFolder + "\CMDBData.Test.csv" #test - for testing use this instead of the downloaded file to speed things up
    $CMDBExportPath = $LogFolder + "\CMDBMWs." + $Date + ".csv" #Where to put the CMDB reference list. This is so we can check the scripts work later.
    $PreChangeExportPath = $LogFolder + "\Pre-UpdateMWs." + $Date + ".csv" #Where to put the MW reference list before changes have been made.  This is so we can check the scripts work later.
    $PostChangeExportPath = $LogFolder + "\Post-UpdateMWs." + $Date + ".csv" #Where to put the MW reference list after changes have been made  This is so we can check the scripts work later.
    $DeploymentCollectionIDProd = "SIT12345" #Production - ID of the collection to which the software updates are actually deployed.  Maintenance window collections are "include" collections on this collection.
    $DeploymentCollectionIDTest = "SIT23456" #Testing - ID of the collection to which the software updates are actually deployed.  Maintenance window collections are "include" collections on this collection.
    $PrefixTest = "Software Updates Maintenance Window (TEST) - " #Test - Prefix for new software update collections when created
    $PrefixProd = "Software Updates Maintenance Window (PROD) - " #Prefix for new software update collections when created
    $MWFolderID = 12345678 #The ID of the folder in SCCM where you would like these collections to reside, otherwise they will all end up in the root folder of your console and make it all ugly
    $LimitingCollectionID = "SIT34567" #Limiting collection for new software update collections when created - usually all systems but could be something more limiting like all SCCM clients
    $MaintDuration = 2 #How long do you want your software updates maintenance windows to be (in hours)

$location = $SiteCode + ":\"
set-location $SiteCode #Change to local instance of ConfigMgr
Start-Transcript -path ${LogFile}

if ($TestMode = $false){
    $LocalCMDBpath = $LocalCMDBpathProd
    $DeploymentCollectionID = $DeploymentCollectionIDProd
    $Prefix = $PrefixProd

    #Download the CMDB data file to local location
    $web = New-Object System.Net.WebClient
    $proxy = new-object System.Net.WebProxy
    $proxy.Address = $ProxyServerURL
    $proxy.useDefaultCredentials = $true #once we get access for the service account, this line can go away
    $web.Credentials = Get-Credential
    $web.proxy = $proxy
    $web.DownloadFile($CMDBDownloadURL,$LocalCMDBpath)
}
else {
    $LocalCMDBpath = $LocalCMDBpathTest
    $DeploymentCollectionID = $DeploymentCollectionIDTest
    $Prefix = $PrefixTest
}

#Import CMDB MW Data into an array of objects
$ActivityMessage = “Importing CMDB Data”
$StatusMessage = ""
Write-Progress -Activity $ActivityMessage
Write-Output "Importing CMDB Data" 
$CMDBRawFile = Import-Csv -LiteralPath $LocalCMDBpath
$ActivityMessage = "Getting CMDB maintenance window information"
$StatusMessage = "Getting CMDB maintenance window information"
Write-Output $StatusMessage
Write-Progress -Activity $ActivityMessage -Status $StatusMessage
$LoopCounter = -1
foreach ($CMDBComputer in $CMDBRawFile){
    $LoopCounter++
    if ($CMDBComputer.sch_name -ne $null -and $CMDBComputer.sch_name -ne "" -and $CMDBComputer.win_host_name -ne $null -and $CMDBComputer.win_host_name -ne ""){
        $CMDBComputerName = ($CMDBComputer.win_host_name).ToUpper()
        $StatusMessage = "Importing information for computer $CMDBComputerName"
        $progress = (($LoopCounter / ($CMDBRawFile | Measure-Object).Count) * 100)
        Write-Progress -Activity $ActivityMessage -Status $StatusMessage -PercentComplete $progress
        $CMDBMWTable += Add-CMDBArrayMember -MaintenanceWindow $CMDBComputer.sch_name -ComputerName $CMDBComputerName
    }
}
$ActivityMessage = “Completed reading in the CMDB information”
$StatusMessage = "Completed reading in the CMDB information - writing the CMDB file."
Write-Output $StatusMessage
Write-Progress -Activity $ActivityMessage -Status $StatusMessage
$CMDBMWTable | Sort-Object -Property computername | Export-Csv -Path $CMDBExportPath -NoClobber -Encoding Default -NoTypeInformation

#Import SCCM data into an array of objects
$ActivityMessage = “Getting SCCM collection information”
$StatusMessage = "Getting SCCM collection membership data"
Write-Output $StatusMessage
Write-Progress -Activity $ActivityMessage -Status $StatusMessage
$IncludeRulesOfDeploymentCollection = Get-CMCollectionIncludeMembershipRule -CollectionId $DeploymentCollectionID 
$OuterLoopCounter = -1
foreach ($IncludeRule in $IncludeRulesOfDeploymentCollection){
    $OuterLoopCounter++
    $SCCMCollectionID = $IncludeRule.IncludeCollectionID
    $SCCMCollectionName = (Get-CMDeviceCollection -Id $SCCMCollectionID).Name
    $SCCMMaintenanceWindow = $SCCMCollectionName.Replace($Prefix,"")
    $StatusMessage = "Importing membership information for collection $SCCMCollectionName which is an 'include' rule member of " + (Get-CMDeviceCollection -Id $DeploymentCollectionID).Name
    $progress = (($OuterLoopCounter / $IncludeRulesOfDeploymentCollection.Count) * 100)
    Write-Progress -Activity $ActivityMessage -Status $StatusMessage -PercentComplete $progress
    $SCCMIncludedCollectionMembers = Get-CMCollectionMember -CollectionId $SCCMCollectionID
    $InnerLoopCounter = -1
    foreach  ($member in $SCCMIncludedCollectionMembers){
        $InnerLoopCounter++
        $MainCounter++
        $SCCMMWTable += Add-SCCMArrayMember -MaintenanceWindow $SCCMMaintenanceWindow -CollectionID $SCCMCollectionID -ComputerName $member.Name -ResourceID $member.ResourceID
    }
}
$ActivityMessage = “Completed reading in the list of 'include' Collections from the Deployment Collection”
$StatusMessage = "Done getting SCCM collection membership information - writing the Pre-Change file."
Write-Output $StatusMessage
Write-Progress -Activity $ActivityMessage -Status $StatusMessage
$SCCMMWTable | Sort-Object -Property ComputerName | Export-Csv -Path $PreChangeExportPath -NoClobber -Encoding Default -NoTypeInformation

#Compare the two arrays of objects
$ActivityMessage = “Comparing the SCCM data to the CMDB data”
$StatusMessage = "Comparing the SCCM data to the CMDB data and making any necessary adjustments to SCCM"
Write-Output $StatusMessage
Write-Progress -Activity $ActivityMessage -Status $StatusMessage
$LoopCounter = -1
#Main comparison loop.  This is what will take the longest amount of time in the run cycle.
foreach  ($CMDBMWComputer in $CMDBMWTable){ #Check each computer in the CMDB
    $LoopCounter++
    $progress = (($LoopCounter / $CMDBMWTable.Count) * 100)
    $CountOfSCCMMWs = 0
    #Only continue if there is actually a computername in CMDB (yes, it happens)
    if ($CMDBMWComputer.ComputerName -ne $null -and $CMDBMWComputer.ComputerName -ne ""){
        $CountOfSCCMMWs = (($SCCMMWTable | ? { $_.computername -eq $CMDBMWComputer.ComputerName }) | Measure-Object).Count
        $CheckSCCMMW = ($SCCMMWTable | ? { $_.computername -eq $CMDBMWComputer.ComputerName })
        #Only continue if there is a maintenance window in CMDB (yeah, that happens too)
        if($CMDBMWComputer.MaintenanceWindow -ne $null -and $CMDBMWComputer.MaintenanceWindow -ne ""){
            #Only continue if the computer is also in SCCM
            $SCCMComputerObject = (Get-CMDevice -Name $CMDBMWComputer.ComputerName)
            if ($SCCMComputerObject){
                $StatusMessage = "Checking maintenance windows for " + $CMDBMWComputer.ComputerName
                Write-Progress -Activity $ActivityMessage -Status $StatusMessage -PercentComplete $progress
                #If the computer is in more than one maintenance window collection then remove it from all of them
                if ($CountOfSCCMMWs -gt 1){ 
                        $StatusMessage = $CMDBMWComputer.ComputerName + "`tis in multiple maintenance window collections."
                        Write-Progress -Activity $ActivityMessage -Status $StatusMessage -PercentComplete $progress
                        Write-Output $StatusMessage
                        Remove-AllCurrentMWMembership -ComputerName $CMDBMWComputer.ComputerName -SiteServerName $SiteServer -SiteCode $SiteCode -Prefix $Prefix
                        $CountOfSCCMMWs = 0 #Set the count to zero since the computer is now a member of no maintenance window collections
                } #End - If the computer is in more than one maintenance window collection then remove it from all of them and set the count to 0
                #If the computer is in only one maintenance window collection we need to check that it is the correct one
                if ($CountOfSCCMMWs -eq 1){ 
                    #If the computer is not in the correct maintenance window collection then remove it from the one that it is in and set the count to 0
                    if ($CMDBMWComputer.MaintenanceWindow -ne ($CheckSCCMMW.MaintenanceWindow).Replace($Prefix,"")){ 
                        $StatusMessage = $CMDBMWComputer.ComputerName + "`tis in the wrong maintenance window collection.`tSCCM collection = " + $CheckSCCMMW.MaintenanceWindow + "`tCMDB Maintenance Window = " + $CMDBMWComputer.MaintenanceWindow
                        Write-Progress -Activity $ActivityMessage -Status $StatusMessage -PercentComplete $progress
                        Write-Output $StatusMessage
                        Remove-AllCurrentMWMembership -ComputerName $CMDBMWComputer.ComputerName -SiteServerName $SiteServer -SiteCode $SiteCode -Prefix $Prefix
                        $CountOfSCCMMWs = 0 #Set the count to zero since the computer is now a member of no maintenance window collections
                    } #End - If the computer is not in the correct maintenance window collection then remove it from the one that it is in and set the count to 0h
                    else{ #If the computer is in the correct maintenance window collection then huzzah and continue
                        $StatusMessage = $CMDBMWComputer.ComputerName + "`tis in the correct maintenance window collection.`tSCCM collection = " + $CheckSCCMMW.MaintenanceWindow + "`t CMDB Maintenance Window = " + $CMDBMWComputer.MaintenanceWindow
                        Write-Progress -Activity $ActivityMessage -Status $StatusMessage -PercentComplete $progress
                        Write-Output $StatusMessage
                    }  #End - If the computer is in the correct maintenance window collection then huzzah and continue
                } #End - If the computer is in only one maintenance window collection we need to check that it is the correct one
                #If the computer is in no maintenance window collection the put it into the correct maintenance window collection
                if ($CountOfSCCMMWs -eq 0){ 
                    $StatusMessage = $CMDBMWComputer.ComputerName + "`tis not in any maintenance window collection."
                    Write-Progress -Activity $ActivityMessage -Status $StatusMessage -PercentComplete $progress
                    Write-Output $StatusMessage
                    $NewCollectionName = $Prefix  + $CMDBMWComputer.MaintenanceWindow
                    $MWExist = (Get-CMCollection -Name $NewCollectionName)
                    if (-not $MWExist){ 
                        $StatusMessage = "The correct maintenance window does not exist, it should be named " + $Prefix  + $CMDBMWComputer.MaintenanceWindow
                        Write-Progress -Activity $ActivityMessage -Status $StatusMessage -PercentComplete $progress
                        Write-Output $StatusMessage
                        Create-NewSCCMMWCollection -CollectionName $NewCollectionName -TargetFolderID $MWFolderID -LimitingCollectionID $LimitingCollectionID -Schedule $CMDBMWComputer.MaintenanceWindow -MaintDuration $MaintDuration -DeploymentCollectionID $DeploymentCollectionID -SiteCode $SiteCode
                    } #End - If the correct maintenance window collection doesn't exist then create it
                    #Add the computer to the collection
                    $StatusMessage = "Adding " + $CMDBMWComputer.ComputerName + " to " + $NewCollectionName
                    Write-Progress -Activity $ActivityMessage -Status $StatusMessage
                    Write-Output $StatusMessage
                    Add-CMDeviceCollectionDirectMembershipRule -CollectionId (Get-CMCollection -Name $NewCollectionName).CollectionID -ResourceId (Get-CMDevice -Name $CMDBMWComputer.ComputerName).resourceid
                } #End - If the computer is in no maintenance window collection the put it into the correct maintenance window collection
            }#END - only continue if the computer is also in SCCM
            else{ #Computer is not in SCCM
                $StatusMessage = $CMDBMWComputer.ComputerName + "`tis not in SCCM."
                Write-Progress -Activity $ActivityMessage -Status $StatusMessage -PercentComplete $progress
                Write-Output $StatusMessage            
            }#END - Computer is not in SCCM
        }#END - Only continue if there is a maintenance window in CMDB (yeah, that happens too)
        else{ #There was no maintenance window listed in CMDB
            $StatusMessage = $CMDBMWComputer.ComputerName + "`thas no maintenance window listed in CMDB."
            Write-Progress -Activity $ActivityMessage -Status $StatusMessage -PercentComplete $progress
            Write-Output $StatusMessage            
        }#END - Only continue if there is a maintenance window in CMDB (yeah, that happens too)
    } #END - Only continue if there is actually a computername in CMDB (yeah, it happens)
}#End - Check each computer in the CMDB
 
Stop-Transcript #End Logging
-------------------------------------------------------------
-------------------------------------------------------------

Thursday, May 11, 2017

Powershell script to recycle a list of services

This script will stop a set of services in the order given and then start them again in the reverse order.

<#
.Synopsis
    Recycle-Services.ps1 - Stops a list of services and restarts them again

.DESCRIPTION
    This script will stop a given list of services, in the order given,
    then start the services again in the reverse order.

    While stopping the services it will disable them to prevent other automation
    from restarting them prematurely.  When restarting the services it will set
    their startup type to "Automatic".

    Script by Mark Randol - randoltech.blogspot.com

.USAGE
    Change the line that has
        [String[]]$ServiceNames =
    to include whatever services you need restarted. You can add as many as you like
    just be sure to enclose the service names in quotations and seperate them with commas

    Example:
        [String[]]$ServiceNames = "Audiosrv","MyService","SUMDUMSERVICE"

#>

function Wait-ServiceState{
    [CmdletBinding()]
    [OutputType([int])]
    Param(
        [Parameter(Position=0, Mandatory=$true, ValueFromPipelineByPropertyName=$true, ValueFromPipeline=$true)]
        [Alias('Service','ServiceNames')][String[]] $Service2WaitFor,
        [Parameter(Position=0, Mandatory=$true, ValueFromPipelineByPropertyName=$true, ValueFromPipeline=$true)]
        [String[]] $State
    )
    Process{
        $maxRepeat = 120 #Number of half seconds
        do
        {
            $CurrentStatus = (Get-Service $Service2WaitFor).Status
            $maxRepeat--
            sleep -Milliseconds 500
        } until ($CurrentStatus -eq $State -or $maxRepeat -eq 0)
        Return $maxRepeat
    }
} #Waits until service is in specified state or a specified amount of time has elapsed, whichever comes first

function Stop-MyService{
    [CmdletBinding()]
    [OutputType([String])]
    Param(
        [Parameter(Position=0, Mandatory=$true, ValueFromPipelineByPropertyName=$true, ValueFromPipeline=$true)]
        [Alias('Service','ServiceNames')][String] $Service2Stop
    )
    Process{
        If (Get-Service $Service2Stop -ErrorAction Stop){
            If ((Get-Service $Service2Stop).StartType -ne 'Disabled') {Set-Service -Name $Service2Stop -StartupType Disabled -ErrorAction Stop}
            If ((Get-Service $Service2Stop).Status -ne 'Stopped') {Stop-Service -Name $Service2Stop -ErrorAction Continue}
            $StoppedService = (Wait-ServiceState -Service2WaitFor $Service2Stop -State Stopped -ErrorAction Stop)
        }
        If ($StoppedService -ne 0){
            $OutputString = "$Service2Stop successfully stopped."
            Return $OutputString
        }
        else{
            $OutputString = "Unable to stop $Service2Stop"
            Return $OutputString
        }
    }
} #Sets the service to disabled and stops it.

function Start-MyService{
    [CmdletBinding()]
    [OutputType([String])]
    Param(
        [Parameter(Position=0, Mandatory=$true, ValueFromPipelineByPropertyName=$true, ValueFromPipeline=$true)]
        [Alias('Service','ServiceNames')][String] $Service2Start
    )
    Process{
        If (Get-Service $Service2Start -ErrorAction Stop){
            If ((Get-Service $Service2Start).StartType -ne 'Automatic') {Set-Service -Name $Service2Start -StartupType Automatic -ErrorAction Stop}
            If ((Get-Service $Service2Start).Status -ne 'Running') {Start-Service $Service2Start -ErrorAction Continue}
            $StartedService = (Wait-ServiceState -Service2WaitFor $Service2Start -State Running -ErrorAction Stop)
        }
        If ($StartedService -ne 0){
            $OutputString = "$Service2Start successfully started."
            Return $OutputString
        }
        else{
            $OutputString = "Unable to start $Service2Start"
            Return $OutputString
        }
    }
} #Sets the service to automatic and starts it.

[String[]]$ServiceNames = "MyService","SUMDUMSERVICE"
[int]$ServiceNum = 0
[string]$Service = ""

#Stop the services
do{
    $Service = $ServiceNames[$ServiceNum]     Write-Output ("Stopping $Service")
    $Stopped = (Stop-MyService -Service2Stop $Service)
    $SuccessString = "$Service successfully stopped."
    Write-Output $Stopped
    iF ($Stopped -ne $SuccessString){
        Return $Stopped
    }
    $ServiceNum ++
} #Stop the services in the order given
while ($ServiceNum -lt $ServiceNames.Count)


#Start the services
$ServiceNum = $ServiceNames.Count - 1

do{
    $Service = $ServiceNames[$ServiceNum]
    Write-Output ("Starting $Service")
    $Started = (Start-MyService -Service2Start $Service)
    $SuccessString = "$Service successfully started."
    Write-Output $Started
    iF ($Started -ne $SuccessString){
        Return $Started
    }
    $ServiceNum --
} #Start the services in reverse order
while ($ServiceNum -gt -1)

$OutputString = "0"
Return $OutputString


Tuesday, February 28, 2017

Command to set a scheduled task monthly or weekly:

From CMD, not PowerShell (though you can call CMD commands from inside of PowerShell)

schtasks /create /ru SYSTEM /sc monthly /mo first /d SAT /tn Scheduled_Restart /sd "01/01/2016" /ed "12/31/2017" /st "22:00" /tr "shutdown /r /t 30"

schtasks /create /ru SYSTEM /sc WEEKLY /mo 1 /d SAT /tn Scheduled_Restart /sd "01/01/2016" /ed "12/31/2017" /st "22:00" /tr "shutdown /r /t 30"



SCHTASKS /Create [/S system [/U username [/P [password]]]] [/RU username [/RP password]] /SC schedule [/MO modifier] [/D day] [/M months] [/I idletime] /TN taskname /TR taskrun [/ST starttime] [/RI interval] [ {/ET endtime | /DU duration} [/K] [/XML xmlfile] [/V1]] [/SD startdate] [/ED enddate] [/IT | /NP] [/Z] [/F] [/HRESULT] [/?]

 

Description:

                Enables an administrator to create scheduled tasks on a local or remote system.

 

Parameter List:

/S

system

Specifies the remote system to connect to. If omitted the system parameter defaults to the local system.

/U

username

Specifies the user context under which SchTasks.exe should execute.

/P

[password]

Specifies the password for the given user context. Prompts for input if omitted.

/RU

username

Specifies the "run as" user account (user context) under which the task runs. For the system account, valid values are "", "NT AUTHORITY\SYSTEM" or "SYSTEM". For v2 tasks, "NT AUTHORITY\LOCALSERVICE" and "NT AUTHORITY\NETWORKSERVICE" are also available as well as the well known SIDs for all three.

/RP

[password]

Specifies the password for the "run as" user. To prompt for the password, the value must be either "*" or none. This password is ignored for the system account. Must be combined with either /RU or /XML switch.

/SC

schedule

Specifies the schedule frequency. Valid schedule types: MINUTE, HOURLY, DAILY, WEEKLY, MONTHLY, ONCE, ONSTART, ONLOGON, ONIDLE, ONEVENT.

/MO

modifier

Refines the schedule type to allow finer control over schedule recurrence. Valid values are listed in the "Modifiers" section below.

/D

days

Specifies the day of the week to run the task. Valid values: MON, TUE, WED, THU, FRI, SAT, SUN and for MONTHLY schedules 1 - 31 (days of the month). Wildcard "*" specifies all days.

/M

months

Specifies month(s) of the year. Defaults to the first day of the month. Valid values: JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC. Wildcard "*" specifies all months.

/I

idletime

Specifies the amount of idle time to wait before running a scheduled ONIDLE task. Valid range: 1 - 999 minutes.

/TN

taskname

Specifies the string in the form of path\name which uniquely identifies this scheduled task.

/TR

taskrun

Specifies the path and file name of the program to be run at the scheduled time. Example: C:\windows\system32\calc.exe

/ST

starttime

Specifies the start time to run the task. The time format is HH:mm (24 hour time) for example, 14:30 for 2:30 PM. Defaults to current time if /ST is not specified.

/RI

interval

Specifies the repetition interval in minutes. This is not applicable for schedule types: MINUTE, HOURLY, ONSTART, ONLOGON, ONIDLE, ONEVENT. Valid range: 1 - 599940 minutes. If either /ET or /DU is specified, then it defaults to 10 minutes.

/ET

endtime

Specifies the end time to run the task. The time format is HH:mm (24 hour time) for example, 14:50 for 2:50 PM. This is not applicable for schedule types: ONSTART, ONLOGON, ONIDLE, ONEVENT.

/DU

duration

Specifies the duration to run the task. The time format is HH:mm. This is not applicable with /ET and for schedule types: ONSTART, ONLOGON, ONIDLE, ONEVENT. For /V1 tasks, if /RI is specified, duration defaults to 1 hour.

/K

kill

Terminates the task at the endtime or duration time. This is not applicable for schedule types: ONSTART, ONLOGON, ONIDLE, ONEVENT. Either /ET or /DU must be specified.

/SD

startdate

Specifies the first date on which the task runs. The format is mm/dd/yyyy. Defaults to the current date. This is not applicable for schedule types: ONCE, ONSTART, ONLOGON, ONIDLE, ONEVENT.

/ED

enddate

Specifies the last date when the task should run. The format is mm/dd/yyyy. This is not applicable for schedule types: ONCE, ONSTART, ONLOGON, ONIDLE, ONEVENT.

/EC

ChannelName

Specifies the event channel for OnEvent triggers.

/IT

interactive

Enables the task to run interactively only if the /RU user is currently logged on at the time the job runs. This task runs only if the user is logged in.

/NP

No password is stored.

The task runs non-interactively as the given user.

/Z

delete after run

Marks the task for deletion after its final run.

/XML

xmlfile

Creates a task from the task XML specified in a file. Can be combined with /RU and /RP switches, or with /RP alone, when task XML already contains the principal.

/V1

pre-Vista

Creates a task visible to pre-Vista platforms. Not compatible with /XML.

/F

force

Forcefully creates the task and suppresses warnings if the specified task already exists.

/RL

level

Sets the Run Level for the job. Valid values are LIMITED and HIGHEST. The default is LIMITED.

/DELAY delaytime

delay

Specifies the wait time to delay the running of the task after the trigger is fired. The time format is mmmm:ss.

/HRESULT

Result Code

For better diagnosability, the process exit code will be in the HRESULT format.

 

Modifiers:

Valid values for the /MO switch per schedule type:

MINUTE:              1 - 1439 minutes.
HOURLY:              1 - 23 hours.
DAILY:   1 - 365 days.
WEEKLY:               weeks 1 - 52.
ONCE:   No modifiers.
ONSTART: No modifiers.
ONLOGON: No modifiers.
ONEVENT:           XPath event query string.
ONIDLE:               No modifiers.
MONTHLY: 1 - 12, or  FIRST, SECOND, THIRD, FOURTH, LAST, LASTDAY.

 

Examples:

==> Creates a scheduled task "doc" on the remote machine "ABC" which runs notepad.exe every hour under user "runasuser".

SCHTASKS /Create /S ABC /U user /P password /RU runasuser  /RP runaspassword /SC HOURLY /TN doc /TR notepad

 

==> Creates a scheduled task "accountant" on the remote machine  "ABC" to run calc.exe every five minutes from the specified start time to end time between the start date and end date.

SCHTASKS /Create /S ABC /U domain\user /P password /SC MINUTE  /MO 5 /TN accountant /TR calc.exe /ST 12:00 /ET 14:00  /SD 06/06/2006 /ED 06/06/2006 /RU runasuser /RP userpassword

 

==> Creates a scheduled task "gametime" to run freecell on the  first Sunday of every month.

SCHTASKS /Create /SC MONTHLY /MO first /D SUN /TN gametime   /TR c:\windows\system32\freecell

 

==> Creates a scheduled task "report" on remote machine "ABC" to run notepad.exe every week.

SCHTASKS /Create /S ABC /U user /P password /RU runasuser  /RP runaspassword /SC WEEKLY /TN report /TR notepad.exe

 

==> Creates a scheduled task "logtracker" on remote machine "ABC" to run notepad.exe every five minutes starting from the specified start time with no end time. The /RP password will be prompted for.

SCHTASKS /Create /S ABC /U domain\user /P password /SC MINUTE  /MO 5 /TN logtracker   /TR c:\windows\system32\notepad.exe /ST 18:30  /RU runasuser /RP

 

==> Creates a scheduled task "gaming" to run freecell.exe starting at 12:00 and automatically terminating at 14:00 hours every day

SCHTASKS /Create /SC DAILY /TN gaming /TR c:\freecell /ST 12:00  /ET 14:00 /K

 

==> Creates a scheduled task "EventLog" to run wevtvwr.msc starting whenever event 101 is published in the System channel

SCHTASKS /Create /TN EventLog /TR wevtvwr.msc /SC ONEVENT  /EC System /MO *[System/EventID=101]

 

==> Spaces in file paths can be used by using two sets of quotes, one set for CMD.EXE and one for SchTasks.exe. The outer quotes for CMD need to be double quotes; the inner quotes can be single quotes or escaped double quotes:

SCHTASKS /Create   /tr "'c:\program files\internet explorer\iexplorer.exe'   \"c:\log data\today.xml\"" ...