# SetSchedule.ps1 # PowerShell script to update the replication schedule # of a specified Active Directory connection object. # # Copyright (c) 2012 Richard L. Mueller # Version 1.0 - November 1, 2018 # # ---------------------------------------------------------------------- # You have a royalty-free right to use, modify, reproduce, and # distribute this script file in any way you find useful, provided that # you agree that the copyright owner above has no warranty, obligations, # or liability for such use. # Specify the supported parameters. The first one (or several) letters of each # parameter can be used as an alias. Param( [String]$Site, [String]$ToServer, [String]$FromServer, [String]$Name, [String]$DN, [String]$CSVFile, [Switch]$Key, [Switch]$Help ) # Script version and date. $Version = "Version 1.0 - November 1, 2018" $Today = Get-Date # Flag any parameters not recognized and abort the script. Any parameters that # do not match the supported parameters (specified by the Param statement # above) will populate the $Args collection, an automatic variable. If all of # the parameters supplied are recognized, then $Args will be empty. $Abort = $False ForEach ($Arg In $Args) { If ($Arg -Like "-*") { Write-Host "Argument not recognized: $Arg" ` -ForegroundColor Red -BackgroundColor Black } Else { Write-Host "Value not recognized: $Arg" ` -ForegroundColor Red -BackgroundColor Black } $Abort = $True } # Breaking out of the above ForEach would not break out of the script. # Breaking out of the If statement will. If ($Abort) { # Display a brief help listing the supported parameters # and their data types. Write-Host "Syntax: SetSchedule.ps1 [[-Site] ]`r`n" ` " [[-ToServer] ] [[-FromServer] ]`r`n" ` " [[-Name] ] [[-DN] ] [[-CSVFile] ]`r`n" ` " [-Key] [-Help]" ` -ForegroundColor Yellow -BackgroundColor Black Write-Host "For help use SetSchedule.ps1 -Help" ` -ForegroundColor yellow -BackgroundColor Black Break } If ($Help) { # User has requested help information. "SetSchedule.ps1" "$Version" "PowerShell script to update the replication schedule of a specified" "Active Directory connection object." "Parameters:" " -Site: The name of the site, where the connection object for" " intra-site replication is located." " -ToServer: The NetBIOS name of the destination server in the site." " -FromServer: The NetBIOS name of the source server in the site." " -Name: The name of a connection object for inter-site" " replication. This is the Relative Distinguished Name." " -DN: Distinguished name of connection object. The value must" " be in quotes. Only needed if you do not use -Site," " -ToServer, -FromServer, or -Name. Useful if you" " have many DN values in a text file." " -CSVFile: CSV file for input, specifying the replication schedule" " as decimal values for each hour of each day of a week." " -Key: Switch to output a table of possible schedule values" " for each hour in decimal, hex, and binary. The table" " indicates what each value means (when replication is" " scheduled during each hour)." " -Help: Switch that outputs this help." "Notes:" " For intra-site replication (between DCs within a site), use the -Site," "-ToServer, and -FromServer parameters." " For inter-site replication (between two sites), use the -Name" "parameter." " The first one or more letters of each parameter can be used as an" "alias." " You can provide -Site, -ToServer, and -FromServer in order without the" "parameter names. See examples below." " The parameter values can be quoted, but quotes are only required if" "the value has spaces or commas." " An input CSV file specifying the schedule to be assigned is required." "The CSV file created by the GetSchedule.ps1 PowerShell script when you" "use the -CSV parameter can be the starting point. Modify this file as" "required, then specify it with the -CSVFile parameter." " If the connection object is for intra-site replication, the script" "clears the first bit of the options attribute of the object, so the KCC" "will not automatically assign the default schedule." "Some usage examples:" ".\SetSchedule.ps1 My-Site1 ServerA ServerB -CSVFile .\MyLink.csv" " Updates the intra-site replication schedule from ServerB to ServerA" " according to the schedule in the comma delimited file ""MyLink.csv""." ".\SetSchedule.ps1 -Name SiteA-SiteB -C .\MySched.csv" " Updates the inter-site replication schedule for the connection object" " ""SiteA-SiteB"" according to the file ""MySched.csv""." ".\SetSchedule.ps1 -Key" " Outputs a table documenting the possible values (in decimal, hex," " and binary) for each hour in the schedule. The table explains how" " each value indicates when replication is scheduled during the hour." Break } If ($Key) { "Key to values in the schedule array, one value for each hour of each day" "of the week. n = no replication, Y = replication scheduled." "For example, ""nnYn"" (decimal 4) means replication is scheduled during" "the third 15 minute interval after the hour." " --- Minutes after hour ---" "Decimal Hex Binary Syncs/Hr 00-14 15-29 30-44 45-59 Display" " 0 0 0000 0 n n n n nnnn" " 1 1 0001 1 Y n n n Ynnn" " 2 2 0010 1 n Y n n nYnn" " 3 3 0011 2 Y Y n n YYnn" " 4 4 0100 1 n n Y n nnYn" " 5 5 0101 2 Y n Y n YnYn" " 6 6 0110 2 n Y Y n nYYn" " 7 7 0111 3 Y Y Y n YYYn" " 8 8 1000 1 n n n Y nnnY" " 9 9 1001 2 Y n n Y YnnY" " 10 A 1010 2 n Y n Y nYnY" " 11 B 1011 3 Y Y n Y YYnY" " 12 C 1100 2 n n Y Y nnYY" " 13 D 1101 3 Y n Y Y YnYY" " 14 E 1110 3 n Y Y Y nYYY" " 15 F 1111 4 Y Y Y Y YYYY" "Note:" "The first bit in each binary value corresponds to the first 15 minute" "interval, from 0 to 14 minutes after the hour. The first bit is the" "far right hand bit of the binary representation of the value." Break } Trap { Write-Host "Error: $_" ` -ForegroundColor Yellow -BackgroundColor Black Break } # Retrieve local Time Zone bias from machine registry in hours. # This bias changes with Daylight Savings Time. $Bias = (Get-ItemProperty -Path ` HKLM:\System\CurrentControlSet\Control\TimeZoneInformation).ActiveTimeBias # Account for negative bias. If ($Bias -gt 10080){$Bias = $Bias - 4294967296} $Bias = [Math]::Round($Bias/60, 0, [MidpointRounding]::AwayFromZero) # Determine Configuration naming context from RootDSE object. $RootDSE = [System.DirectoryServices.DirectoryEntry]([ADSI]"LDAP://RootDSE") $ConfigNC = $RootDSE.Get("configurationNamingContext") # Verify parameters required to identify the connection object. If ((-Not $Name) -And (-Not $DN)) { If (-Not $Site) { $Site = Read-Host "Enter the name of the AD destination site" } # Validate Site. $DestCont = [ADSI]"LDAP://cn=$Site,cn=Sites,$ConfigNC" If (-Not $DestCont.Name) { Write-Host "Site $Site not found." ` -ForegroundColor Red -BackgroundColor Black Write-Host "Script Aborted." ` -ForegroundColor Red -BackgroundColor Black Break } If (-Not $ToServer) { $ToServer = Read-Host "Enter the NetBIOS name of destination server" } # Validate ToServer. $Server = ` [ADSI]"LDAP://cn=$ToServer,cn=Servers,cn=$Site,cn=Sites,$ConfigNC" If (-Not $Server.Name) { Write-Host "Server $ToServer not found in site $Site." ` -ForegroundColor Red -BackgroundColor Black Write-Host "Script Aborted." ` -ForegroundColor Red -BackgroundColor Black Break } If (-Not $FromServer) { $FromServer = Read-Host "Enter the NetBIOS name of source server" If (-Not $FromServer) { Write-Host "Source server required." ` -ForegroundColor Red -BackgroundColor Black Write-Host "Script Aborted." ` -ForegroundColor Red -BackgroundColor Black Break } } # $Site, $ToServer, and $FromServer have been provided. # Use ADSI Searcher object to find any connection objects with the # specified FromServer in the site. $Searcher = New-Object System.DirectoryServices.DirectorySearcher $Searcher.SearchScope = "subtree" $Searcher.PropertiesToLoad.Add("distinguishedName") > $Null # Base of search is the ToServer container in the configuration container. $Searcher.SearchRoot = ` "LDAP://cn=$ToServer,cn=Servers,cn=$Site,cn=Sites,$ConfigNC" # LDAP syntax filter to find the connection object. $Searcher.Filter = "(fromServer=cn=NTDS Settings" ` + ",cn=$FromServer,cn=Servers,cn=$Site,cn=Sites,$ConfigNC)" $Connections = $Searcher.FindAll() $Count = $Connections.Count If ($Count -eq 0) { Write-Host "No connection object found with fromServer $FromServer." ` -ForegroundColor Red -BackgroundColor Black Write-Host "Script Aborted." ` -ForegroundColor Red -BackgroundColor Black Break } ForEach ($Connection In $Connections) { If ($Count -eq 1) { $DN = $Connection.Properties.Item("distinguishedName") } Else { $DN = $Connection.Properties.Item("distinguishedName") Write-Host "More than one connection object found." -NoNewLine Write-Host " Select the DN desired." Write-Host "$DN" $Ans = Read-Host "Is this object DN correct? Reply ""Y"" or ""N""" If (($Ans.Trim().ToLower() -eq "y") ` -Or ($Ans.Trim().ToLower() -eq "yes")) {Break} Else {$DN = $Null} } } } ElseIf (($Name) -And (-Not $DN)) { # Specify the distinguished name of the connection object. $DN = "cn=$Name,cn=IP,cn=Inter-Site Transports,cn=Sites,$ConfigNC" } # Bind to the object, using the distinguished name. $Connection = [ADSI]"LDAP://$DN" # Retrieve the value of the schedule attribute. # This is an array of bytes, datatype OctetString. $Sched = $Connection.schedule.Value # Retrieve the options attribute. $Options = $Connection.options.Value # Check options attribute to see if bit 1 is set. If ($Options -band 1) {$KCC =$True} Else {$KCC = $False} # Validate the DN. If (-Not $Sched) { If ($Connection.Name) { # Schedule attribute not set. # Create an array of 188 bytes. $Sched = New-Object 'Byte[]' 188 } Else { Write-Host "Connection object not found," ` + " or Active Directory not available." ` -ForegroundColor Red -BackgroundColor Black Write-Host "Check the Distinguished Name of the connection object:" Write-Host "$DN" Write-Host "Script Aborted." ` -ForegroundColor Red -BackgroundColor Black Break } } # If $Name has a value, we assume inter-site connection object. If (-Not $Name) { If ($DN -Match "cn=IP,cn=Inter-Site Transports,cn=Sites") { # Inter-Site connection object, assign $Name. $Name = $DN.Split(",")[0].SubString(3) } } If (-Not $CSVFile) { Write-Host "CSV file of schedule values is required." $CSVFile = Read-Host "Enter the CSV file path and name" } If (-Not $CSVFile) { Write-Host "CSV file required" ` -ForegroundColor Red -BackgroundColor Black Write-Host "Script Aborted." -ForegroundColor Red -BackgroundColor Black Break } # Import the specified CSV file. $Days = Import-Csv $CSVFile # Ignore first 20 bytes of schedule array (index 0 through 19). $Start = 20 # Loop through the last 168 bytes, from index 20 to 187. These bytes # represent the 168 hours in a week, Sunday through Saturday, in UTC. # Loop first through each day of the week (index 0 through 6). For ($j = 0; $j -le 6; $j = $j + 1) { # Loop through the 24 hours in the day (index 0 through 23). For ($k = 0; $k -le 23; $k = $k + 1) { # Adjust for time zone bias. The values in the schedule attribute are # in UTC, so they must be adjusted for the local time zone bias. # $m is the index into the byte array $Sched. $m = $start + $k + $Bias # If the time zone adjusted index is less than 20, add 168. If ($m -lt 20) {$m = $m + 168} # If the time zone adjusted index is greater than 187, subtract 168. If ($m -gt 187) {$m = $m - 168} # Each byte indicates when replication is scheduled in the hour. $Sched[$m] = $Days[$j].$k } # End of loop for the 24 hours in a day. # Advance 24 hours (bytes) to the next day. $Start = $Start + 24 } # End of loop for the 7 days in a week. If ($KCC -eq $True) { # Check if the connection object is for intra-site replication. If (-Not $Name) { # Clear the options attribute. $Options = $Options -bxor 1 $Connection.options.Value = $Options $KCC = $False } } # Save the schedule. $Connection.schedule.Value = $Sched # Save the connection object. $Connection.SetInfo() # Output to the console. Write-Host "SetSchedule.ps1" Write-Host "$Version" Write-Host "Date: $Today" If ($Name) { $Cost = $Connection.cost.Value $ReplInterval = $Connection.replInterval.Value Write-Host "Inter-Site Replication Schedule Updated" Write-Host "Name of Connection Object: $Name" Write-Host "Cost of replication: $Cost" Write-Host "Replication interval in minutes: $ReplInterval" } Else { Write-Host "Intra-Site Replication Schedule Updated" Write-Host "Site: $Site" Write-Host "Destination Server: $ToServer" Write-Host "Source Server: $FromServer" } Write-Host "Connection Object:" Write-Host "$DN" Write-Host "Time Zone Bias (in hours): $Bias" If (-Not $Name) {Write-Host "KCC automatically assigns a schedule: $KCC"} Else {Write-Host "Change notification enabled: $KCC"}