Update #42: AD Migration
Refactor to target specific Domain Controllers, with failover.
This commit is contained in:
@@ -0,0 +1,383 @@
|
||||
using Disco.Services.Logging;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.DirectoryServices;
|
||||
using System.DirectoryServices.ActiveDirectory;
|
||||
using System.Linq;
|
||||
using System.Security.Principal;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Disco.Services.Interop.ActiveDirectory
|
||||
{
|
||||
public class ADDomain
|
||||
{
|
||||
private const int DomainMaintanceIntervalMinutes = 15;
|
||||
private const int SearchExceptionRetryMax = 4;
|
||||
|
||||
private ActiveDirectoryContext context;
|
||||
private ConcurrentStack<ADDomainController> domainControllers;
|
||||
|
||||
private int domainControllerRoundRobin = -1;
|
||||
private object domainMaintainLock = new object();
|
||||
private DateTime domainMaintenanceNext;
|
||||
|
||||
public Domain Domain { get; private set; }
|
||||
public IEnumerable<ADDomainController> DomainControllers
|
||||
{
|
||||
get
|
||||
{
|
||||
return domainControllers.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public string Name { get; private set; }
|
||||
public string NetBiosName { get; private set; }
|
||||
public string DistinguishedName { get; private set; }
|
||||
public string ConfigurationNamingContext { get; private set; }
|
||||
public SecurityIdentifier SecurityIdentifier { get; private set; }
|
||||
|
||||
public List<string> SearchContainers { get; private set; }
|
||||
|
||||
public ADDomain(ActiveDirectoryContext Context, Domain Domain)
|
||||
{
|
||||
this.context = Context;
|
||||
|
||||
this.Domain = Domain;
|
||||
this.SearchContainers = null;
|
||||
this.domainControllers = null;
|
||||
this.domainMaintenanceNext = DateTime.Now.AddMinutes(DomainMaintanceIntervalMinutes);
|
||||
|
||||
this.Initialize();
|
||||
}
|
||||
|
||||
private void Initialize()
|
||||
{
|
||||
this.Name = Domain.Name;
|
||||
|
||||
var dc = Domain.FindDomainController();
|
||||
|
||||
string ldapPath = string.Format("LDAP://{0}/", dc.Name);
|
||||
|
||||
using (var adRootDSE = new DirectoryEntry(ldapPath + "RootDSE"))
|
||||
{
|
||||
this.DistinguishedName = adRootDSE.Properties["defaultNamingContext"][0].ToString();
|
||||
this.ConfigurationNamingContext = adRootDSE.Properties["configurationNamingContext"][0].ToString();
|
||||
}
|
||||
|
||||
using (var adDomainRoot = new DirectoryEntry(ldapPath + this.DistinguishedName))
|
||||
{
|
||||
this.SecurityIdentifier = new SecurityIdentifier((byte[])(adDomainRoot.Properties["objectSid"][0]), 0);
|
||||
}
|
||||
|
||||
using (var configSearchRoot = new DirectoryEntry(ldapPath + "CN=Partitions," + this.ConfigurationNamingContext))
|
||||
{
|
||||
var configSearchFilter = string.Format("(&(objectcategory=Crossref)(dnsRoot={0})(netBIOSName=*))", this.Name);
|
||||
|
||||
using (var configSearcher = new DirectorySearcher(configSearchRoot, configSearchFilter, new string[] { "NetBIOSName" }, System.DirectoryServices.SearchScope.OneLevel))
|
||||
{
|
||||
SearchResult configResult = configSearcher.FindOne();
|
||||
if (configResult != null)
|
||||
this.NetBiosName = configResult.Properties["NetBIOSName"][0].ToString();
|
||||
else
|
||||
this.NetBiosName = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region Domain Controllers
|
||||
|
||||
public IEnumerable<ADDomainController> GetAllReachableDomainControllers()
|
||||
{
|
||||
return this.Domain.FindAllDomainControllers().WhereReachable().Select(dc => new ADDomainController(this.context, dc, this, dc.SiteName == this.context.Site.Name, false));
|
||||
}
|
||||
|
||||
public IEnumerable<ADDomainController> GetReachableSiteDomainControllers()
|
||||
{
|
||||
return this.DomainControllers.Where(dc => dc.IsSiteServer && dc.DomainController.IsReachable());
|
||||
}
|
||||
|
||||
public ADDomainController GetAvailableDomainController(bool RequireWritable = false)
|
||||
{
|
||||
if (this.domainMaintenanceNext < DateTime.Now)
|
||||
MaintainDomainControllers();
|
||||
|
||||
IEnumerable<ADDomainController> availableServers;
|
||||
|
||||
// Try Site Servers first
|
||||
availableServers = AvilableDomainControllers(RequireSiteServer: true, RequireWritable: RequireWritable);
|
||||
|
||||
if (!availableServers.Any())
|
||||
{
|
||||
// No Site Servers available - try all
|
||||
availableServers = AvilableDomainControllers(RequireSiteServer: false, RequireWritable: RequireWritable);
|
||||
|
||||
if (!availableServers.Any())
|
||||
{
|
||||
lock (domainMaintainLock)
|
||||
{
|
||||
availableServers = AvilableDomainControllers(RequireSiteServer: false, RequireWritable: RequireWritable);
|
||||
|
||||
if (!availableServers.Any())
|
||||
return DiscoverAvailableDomainController(RequireWritable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (availableServers.Count())
|
||||
{
|
||||
case 1:
|
||||
// 1 Available DC
|
||||
return availableServers.First();
|
||||
default:
|
||||
// Multiple DCs Available - Round Robin
|
||||
int drr = Interlocked.Increment(ref domainControllerRoundRobin);
|
||||
int dcrrValue = drr % availableServers.Count();
|
||||
|
||||
if (drr > availableServers.Count())
|
||||
domainControllerRoundRobin = -1;
|
||||
|
||||
return availableServers.ElementAt(dcrrValue);
|
||||
}
|
||||
}
|
||||
private IEnumerable<ADDomainController> AvilableDomainControllers(bool RequireSiteServer, bool RequireWritable)
|
||||
{
|
||||
IEnumerable<ADDomainController> query = this.DomainControllers.Where(dc => dc.IsAvailable);
|
||||
if (RequireSiteServer)
|
||||
query = query.Where(dc => dc.IsSiteServer);
|
||||
if (RequireWritable)
|
||||
query = query.Where(dc => dc.IsWritable);
|
||||
|
||||
return query;
|
||||
}
|
||||
private ADDomainController DiscoverAvailableDomainController(bool RequireWritable)
|
||||
{
|
||||
LocatorOptions locatorOptions;
|
||||
if (RequireWritable)
|
||||
locatorOptions = LocatorOptions.ForceRediscovery | LocatorOptions.WriteableRequired;
|
||||
else
|
||||
locatorOptions = LocatorOptions.ForceRediscovery;
|
||||
|
||||
var dc = this.Domain.FindDomainController(locatorOptions);
|
||||
|
||||
var dcName = dc.Name;
|
||||
|
||||
var existingDC = this.DomainControllers.FirstOrDefault(edc => edc.Name == dcName);
|
||||
|
||||
if (existingDC != null)
|
||||
{
|
||||
// DC already in scope
|
||||
|
||||
// Native API indicates writable
|
||||
if (RequireWritable)
|
||||
existingDC.IsWritable = true;
|
||||
|
||||
// Native API indicates it is available
|
||||
existingDC.IsAvailable = true;
|
||||
|
||||
return existingDC;
|
||||
}
|
||||
else
|
||||
{
|
||||
// New DC discovered
|
||||
|
||||
var adDC = new ADDomainController(this.context, dc, this, dc.SiteName == this.context.Site.Name, RequireWritable);
|
||||
|
||||
// Add DC to Available Servers
|
||||
this.domainControllers.Push(adDC);
|
||||
|
||||
return adDC;
|
||||
}
|
||||
}
|
||||
|
||||
private void MaintainDomainControllers()
|
||||
{
|
||||
lock (domainMaintainLock)
|
||||
{
|
||||
var servers = this.domainControllers.ToList();
|
||||
|
||||
var nonSiteServersPresent = servers.Any(s => !s.IsSiteServer);
|
||||
|
||||
if (nonSiteServersPresent)
|
||||
{
|
||||
var siteServersAvailable = servers.Any(s => s.IsSiteServer && s.IsAvailable);
|
||||
var nonSiteServersUnavailable = servers.Any(s => !s.IsSiteServer && !s.IsAvailable);
|
||||
|
||||
if (siteServersAvailable)
|
||||
{
|
||||
// Remove non-site servers
|
||||
UpdateDomainControllers(servers.Where(s => s.IsSiteServer));
|
||||
}
|
||||
else if (nonSiteServersUnavailable)
|
||||
{
|
||||
// Remove unavailable non-site servers
|
||||
UpdateDomainControllers(servers.Where(s => s.IsSiteServer || s.IsAvailable));
|
||||
}
|
||||
}
|
||||
this.domainMaintenanceNext = DateTime.Now.AddMinutes(DomainMaintanceIntervalMinutes);
|
||||
}
|
||||
}
|
||||
|
||||
internal void UpdateDomainControllers(IEnumerable<ADDomainController> DomainControllers)
|
||||
{
|
||||
this.domainControllers = new ConcurrentStack<ADDomainController>(DomainControllers);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public ADDirectoryEntry RetrieveDirectoryEntry(string DistinguishedName, string[] LoadProperties = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(DistinguishedName))
|
||||
throw new ArgumentNullException("DistinguishedName");
|
||||
|
||||
if (!DistinguishedName.EndsWith(this.DistinguishedName, StringComparison.OrdinalIgnoreCase))
|
||||
throw new ArgumentException(string.Format("The Distinguished Name ({0}) isn't a member of this domain [{1}]", DistinguishedName, this.Name), "DistinguishedName");
|
||||
|
||||
var dc = GetAvailableDomainController();
|
||||
|
||||
return dc.RetrieveDirectoryEntry(DistinguishedName, LoadProperties);
|
||||
}
|
||||
|
||||
#region Searching
|
||||
|
||||
public IEnumerable<ADSearchResult> SearchEntireDomain(string LdapFilter, string[] LoadProperties, int? ResultLimit = null)
|
||||
{
|
||||
return SearchInternal(this.DistinguishedName, LdapFilter, LoadProperties, ResultLimit);
|
||||
}
|
||||
|
||||
public IEnumerable<ADSearchResult> SearchScope(string LdapFilter, string[] LoadProperties, int? ResultLimit = null)
|
||||
{
|
||||
var searchScope = this.SearchContainers;
|
||||
|
||||
// No scope set, search entire domain
|
||||
if (searchScope == null)
|
||||
return SearchEntireDomain(LdapFilter, LoadProperties, ResultLimit);
|
||||
|
||||
switch (searchScope.Count)
|
||||
{
|
||||
case 0: // Ignore domain
|
||||
return Enumerable.Empty<ADSearchResult>();
|
||||
|
||||
case 1: // Single-search
|
||||
return SearchInternal(searchScope[0], LdapFilter, LoadProperties, ResultLimit);
|
||||
|
||||
default: // Multi-search - Parallelize
|
||||
|
||||
var queryTasks = searchScope.Select(scope =>
|
||||
Task<IEnumerable<ADSearchResult>>.Factory.StartNew(() =>
|
||||
SearchInternal(scope, LdapFilter, LoadProperties, ResultLimit))).ToArray();
|
||||
|
||||
// Block
|
||||
Task.WaitAll(queryTasks);
|
||||
|
||||
var results = queryTasks.SelectMany(t => t.Result);
|
||||
if (ResultLimit.HasValue)
|
||||
results = results.Take(ResultLimit.Value);
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
internal IEnumerable<ADSearchResult> SearchInternal(string SearchRoot, string LdapFilter, string[] LoadProperties, int? ResultLimit)
|
||||
{
|
||||
if (string.IsNullOrEmpty(SearchRoot))
|
||||
throw new ArgumentNullException("SearchRoot");
|
||||
if (string.IsNullOrEmpty(LdapFilter))
|
||||
throw new ArgumentNullException("LdapFilter");
|
||||
if (ResultLimit.HasValue && ResultLimit.Value < 1)
|
||||
throw new ArgumentOutOfRangeException("ResultLimit", "The ResultLimit must be 1 or greater");
|
||||
|
||||
// Search with recovery
|
||||
var exceptionCount = 0;
|
||||
Queue<Exception> exceptions = null;
|
||||
do
|
||||
{
|
||||
var domainController = GetAvailableDomainController();
|
||||
|
||||
try
|
||||
{
|
||||
return domainController.SearchInternal(SearchRoot, LdapFilter, LoadProperties, ResultLimit);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (exceptions == null)
|
||||
exceptions = new Queue<Exception>(SearchExceptionRetryMax);
|
||||
|
||||
exceptions.Enqueue(ex);
|
||||
exceptionCount++;
|
||||
|
||||
// Set offline for DomainControllerUnavailableMinutes
|
||||
domainController.IsAvailable = false;
|
||||
SystemLog.LogWarning(string.Format("A domain controller [{0}] is offline. It will be retried after {1}. Error: {2} [{3}]", domainController.Name, domainController.AvailableWhen.Value.ToShortTimeString(), ex.Message, ex.GetType().Name));
|
||||
}
|
||||
} while (exceptionCount < SearchExceptionRetryMax);
|
||||
|
||||
throw new AggregateException(
|
||||
new Exception[] { new Exception(string.Format("Unable to perform Active Directory Search after {0} attempts", exceptionCount)) }
|
||||
.Concat(exceptions));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
internal void UpdateSearchContainers(List<string> Containers)
|
||||
{
|
||||
this.SearchContainers = Containers ?? new List<string>();
|
||||
}
|
||||
internal void UpdateSearchEntireDomain()
|
||||
{
|
||||
this.SearchContainers = null;
|
||||
}
|
||||
|
||||
#region Helpers
|
||||
|
||||
public string DefaultComputerContainer
|
||||
{
|
||||
get
|
||||
{
|
||||
return string.Format("CN=Computers,{0}", this.DistinguishedName);
|
||||
}
|
||||
}
|
||||
|
||||
public string FriendlyDistinguishedNamePath(string DistinguishedName)
|
||||
{
|
||||
if (!DistinguishedName.EndsWith(this.DistinguishedName, StringComparison.OrdinalIgnoreCase))
|
||||
throw new ArgumentException(string.Format("The Distinguished Name [{0}] doesn't exist within this domain [{1}]", DistinguishedName, this.DistinguishedName));
|
||||
|
||||
StringBuilder name = new StringBuilder();
|
||||
|
||||
name.Append('[').Append(this.NetBiosName).Append(']');
|
||||
|
||||
var subDN = DistinguishedName.Substring(0, DistinguishedName.Length - this.DistinguishedName.Length);
|
||||
var subDNComponents = subDN.Split(',');
|
||||
|
||||
subDNComponents
|
||||
.Where(c => !string.IsNullOrWhiteSpace(c))
|
||||
.Reverse()
|
||||
.Select(c => c.Substring(c.IndexOf('=') + 1))
|
||||
.ToList()
|
||||
.ForEach(c => name.Append(" > ").Append(c));
|
||||
|
||||
return name.ToString();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("{0} [{1}]", this.Name, this.NetBiosName);
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (obj == null || !(obj is ADDomain))
|
||||
return false;
|
||||
else
|
||||
return this.DistinguishedName == ((ADDomain)obj).DistinguishedName;
|
||||
}
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(this.DistinguishedName);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user