Private/Sites/Collector.ps1
|
# This worker is invoked in a bounded local job. Every directory query is pinned to one DC. $script:PSISitesCollector={ param([string]$Domain,[pscredential]$Credential) $ErrorActionPreference='Stop' Import-Module ActiveDirectory -ErrorAction Stop $ad=@{Server=$Domain;ErrorAction='Stop'};if($null -ne $Credential){$ad.Credential=$Credential} $root=Get-ADRootDSE @ad $server=[string]$root.dnsHostName if([string]::IsNullOrWhiteSpace($server)){throw 'Discovery did not return a DC DNS host name.'} $ad.Server=$server $root=Get-ADRootDSE @ad $domainInfo=Get-ADDomain @ad if([string]$domainInfo.DNSRoot -ine $Domain){throw 'The supplied connection domain does not match the discovered domain.'} $forest=Get-ADForest @ad $config=[string]$root.configurationNamingContext if([string]::IsNullOrWhiteSpace($config)){throw 'Configuration naming context is unavailable.'} $base='CN=Sites,'+$config [pscustomobject]@{Kind='Context';Forest=[string]$forest.Name;Domain=[string]$domainInfo.DNSRoot;Server=$server;ConfigurationNC=$config;CollectedAt=[datetimeoffset]::Now.ToString('o')} $queries=@( @{Key='sites';Filter='(objectClass=site)';Properties=@('description','location','whenCreated','whenChanged')}, @{Key='subnets';Filter='(objectClass=subnet)';Properties=@('siteObject','description','location')}, @{Key='links';Filter='(objectClass=siteLink)';Properties=@('cost','replInterval','siteList','options','schedule','description')}, @{Key='bridges';Filter='(objectClass=siteLinkBridge)';Properties=@('siteLinkList','description')}, @{Key='servers';Filter='(objectClass=server)';Properties=@('dNSHostName','serverReference','bridgeheadTransportList')}, @{Key='settings';Filter='(objectClass=nTDSSiteSettings)';Properties=@('interSiteTopologyGenerator','options','msDS-Preferred-GC-Site','schedule')}, @{Key='transports';Filter='(objectClass=interSiteTransport)';Properties=@('options','transportAddressAttribute')}, @{Key='ntds';Filter='(objectClass=nTDSDSA)';Properties=@('objectGUID')} ) foreach($query in $queries){ $watch=[diagnostics.stopwatch]::StartNew();$records=@();$status='Succeeded';$id=$null;$message=$null try{ $records=@(Get-ADObject -SearchBase $base -SearchScope Subtree -LDAPFilter $query.Filter -Properties $query.Properties @ad | ForEach-Object{ # Plain property bags avoid losing optional LDAP attributes during job deserialization. $row=[ordered]@{Name=[string]$_.Name;DistinguishedName=[string]$_.DistinguishedName} foreach($property in $query.Properties){ $v=$_.PSObject.Properties[$property] if($null -eq $v){$row[$property]=$null} elseif($property -eq 'schedule'){$row[$property]=if($null -ne $v.Value -and @($v.Value).Count){[convert]::ToBase64String([byte[]]$v.Value)}else{$null}} elseif($v.Value -is [datetime]){$row[$property]=([datetimeoffset]$v.Value).ToString('o')} else{$row[$property]=@($v.Value | ForEach-Object{if($null -ne $_){[string]$_}})} } [pscustomobject]$row }) }catch{$status='Failed';$id=$_.FullyQualifiedErrorId;$message=$_.Exception.Message} $watch.Stop() [pscustomobject]@{Kind='Section';Key=$query.Key;Status=$status;Records=$records;Method='ADWS / '+$query.Filter;CollectedAt=[datetimeoffset]::Now.ToString('o');DurationMs=$watch.ElapsedMilliseconds;ErrorId=$id;ErrorMessage=$message} } } function Invoke-PSISitesCollection { [CmdletBinding()] param([string]$Domain,[pscredential]$Credential,[ValidateRange(1,600)][int]$TimeoutSeconds=120,[scriptblock]$Worker=$script:PSISitesCollector) $required=@('sites','subnets','links','bridges','servers','settings','transports','ntds') $context=$null;$envelopes=New-Object collections.ArrayList;$seen=@{};$job=$null try{ $job=Start-Job -ScriptBlock $Worker -ArgumentList $Domain,$Credential $timer=[diagnostics.stopwatch]::StartNew();$lastError='';$timedOut=$false while($true){ $receiveErrors=@();$items=@(Receive-Job $job -ErrorAction SilentlyContinue -ErrorVariable receiveErrors) if(Get-Command Receive-PSIInventoryDisplay -ErrorAction SilentlyContinue){Receive-PSIInventoryDisplay -Items $items -Total $required.Count} if($receiveErrors.Count){$lastError=($receiveErrors|ForEach-Object{$_.Exception.Message}) -join '; '} foreach($item in $items){ if($item.Kind -eq 'Context'){if($null -ne $context){throw 'Duplicate forest context.'};$context=$item} elseif($item.Kind -eq 'Section'){ if($item.Key -notin $required -or $seen.ContainsKey($item.Key)){throw 'Unknown or duplicate collector result.'} [void]$envelopes.Add($item);$seen[$item.Key]=$true }else{throw 'Unexpected collector output.'} } if($job.State -in @('Completed','Failed','Stopped')){break} if($timer.Elapsed.TotalSeconds -ge $TimeoutSeconds){$timedOut=$true;Stop-Job $job -ErrorAction SilentlyContinue;break} Start-Sleep -Milliseconds 200 } # Drain the race between receiving the stream and worker completion/cancellation. $receiveErrors=@();$items=@(Receive-Job $job -ErrorAction SilentlyContinue -ErrorVariable receiveErrors) if(Get-Command Receive-PSIInventoryDisplay -ErrorAction SilentlyContinue){Receive-PSIInventoryDisplay -Items $items -Total $required.Count} if($receiveErrors.Count){$lastError=($receiveErrors|ForEach-Object{$_.Exception.Message}) -join '; '} foreach($item in $items){ if($item.Kind -eq 'Context' -and $null -eq $context){$context=$item} elseif($item.Kind -eq 'Section' -and -not $seen.ContainsKey($item.Key)){[void]$envelopes.Add($item);$seen[$item.Key]=$true} } if($null -eq $context){throw "Forest context could not be established. $lastError"} foreach($key in $required){if(-not $seen.ContainsKey($key)){ [void]$envelopes.Add([pscustomobject]@{Key=$key;Status=if($timedOut){'TimedOut'}else{'Failed'};Records=@();Method='Collector / incomplete query';CollectedAt=[datetimeoffset]::Now.ToString('o');DurationMs=$timer.ElapsedMilliseconds;ErrorId='PSInsightInventory.SitesQueryIncomplete';ErrorMessage=if($timedOut){"Overall collector budget of $TimeoutSeconds seconds expired."}elseif($lastError){$lastError}else{'Collector did not return this query.'}}) }} return [pscustomobject]@{Context=$context;Envelopes=@($envelopes.ToArray())} }finally{if($null -ne $job){if($job.State -in @('Running','NotStarted','Blocked')){Stop-Job $job -ErrorAction SilentlyContinue};Remove-Job $job -Force -ErrorAction SilentlyContinue}} } |