Private/Sites/Engine.ps1
|
#requires -Version 5.1 [CmdletBinding(DefaultParameterSetName='Live')] param( [Parameter(Mandatory=$true,ParameterSetName='Live')][ValidatePattern('^(?=.{1,253}$)[A-Za-z0-9][A-Za-z0-9.-]*[A-Za-z0-9]$')][string]$Domain, [Parameter(ParameterSetName='Live')][pscredential]$Credential, [Parameter(ParameterSetName='Live')][ValidateRange(10,600)][int]$TimeoutSeconds=120, [Parameter(Mandatory=$true,ParameterSetName='Sample')][switch]$SampleData, [ValidateNotNullOrEmpty()][string]$OutputPath='C:\Temp\Reports', [switch]$Open ) Set-StrictMode -Version 2.0;$ErrorActionPreference='Stop' foreach($file in @('Helpers.ps1','Collector.ps1','Model.ps1','Export.ps1')){. (Join-Path $PSScriptRoot $file)} $moduleRoot=Split-Path -Parent (Split-Path -Parent $PSScriptRoot) $started=[datetimeoffset]::Now;$runId=[guid]::NewGuid().ToString('N').Substring(0,8) $folder=Join-Path $OutputPath ('PSInsightInventory_Sites_'+$started.ToString('yyyyMMdd_HHmmss_fff')+'_'+$runId) $reports=Join-Path $folder 'Reports';$logs=Join-Path $folder 'Logs';$null=New-Item -ItemType Directory -Path $reports,$logs -Force $logPath=Join-Path $logs ('SitesInventory_'+$started.ToString('yyyyMMdd_HHmmss')+'.jsonl') $required=@('sites','subnets','links','bridges','servers','settings','transports','ntds') $context=$null;$envelopes=New-Object collections.ArrayList;$seen=@{};$job=$null try{ Write-PSISitesLog $logPath $runId 'Run' 'Start' 'Started' $Domain ('Forest-wide inventory; SampleData='+[bool]$SampleData) if($SampleData){ $fixture=[IO.File]::ReadAllText((Join-Path $moduleRoot 'Assets/SitesAndServices.sample.json'))|ConvertFrom-Json $context=$fixture.context foreach($key in $required){[void]$envelopes.Add([pscustomobject]@{Key=$key;Status='Succeeded';Records=@($fixture.raw.$key);Method='Synthetic fixture / Configuration partition';CollectedAt=$fixture.collectedAt;DurationMs=0;ErrorId=$null;ErrorMessage=$null})} }else{ if($env:OS -ne 'Windows_NT'){throw 'Live Sites inventory requires Windows and the RSAT ActiveDirectory module.'} if($Domain -notmatch '\.'){throw 'Specify a DNS domain name. It selects the connection domain, not a domain-only report scope.'} if(-not (Get-Module -ListAvailable ActiveDirectory)){throw 'RSAT ActiveDirectory module is required on the collection host.'} if(-not (Get-Command Test-PSIInventoryQuiet -ErrorAction SilentlyContinue) -or -not (Test-PSIInventoryQuiet)){Write-Information "Reading forest-wide Sites configuration using $Domain..." -InformationAction Continue} $collected=Invoke-PSISitesCollection -Domain $Domain -Credential $Credential -TimeoutSeconds $TimeoutSeconds $context=$collected.Context foreach($entry in $collected.Envelopes){[void]$envelopes.Add($entry)} } if(Get-Command Update-PSIInventoryDisplay -ErrorAction SilentlyContinue){Update-PSIInventoryDisplay -Stage 'Preparing report data' -Force} $model=ConvertTo-PSISitesModel $context $envelopes.ToArray() foreach($entry in $model.Data.collection){Write-PSISitesLog $logPath $runId 'Collection' $entry.Name $entry.Status $context.Server $entry.Message ([long]$entry.'Duration ms') ([string]$entry.'Error ID')} $now=[datetimeoffset]::Now $meta=[ordered]@{moduleName='PSInsightInventory';version='1.0.0';inventoryId='ADSitesAndServices';title='Sites & Services Inventory';run=$runId;dataSource=if($SampleData){'Sample'}else{'Live'};scope='Forest-wide Configuration partition';forest=$context.Forest;connectionDomain=$context.Domain;sourceDC=$context.Server;configurationNC=$context.ConfigurationNC;started=$started.ToString('o');generated=$now.ToString('o');generatedDisplay=$now.ToString('dd MMM yyyy, HH:mm:ss zzz');timeZone=[timezoneinfo]::Local.Id;notice=if($SampleData){'SAMPLE DATA. Fictional forest. No directory or server was contacted.'}else{'Read-only configuration snapshot from one DC. Domain selects the connection; Sites configuration is forest-wide.'};limitations=@('Directory objects and settings, not server availability or replication health.','Successful queries cannot prove ACL-filtered object/attribute visibility. Null values can mean not set or not returned.','Stored schedules use UTC; they are not effective replication schedules. Transport options and combined paths can affect behavior.','DC objects are observed NTDS Settings objects beneath server objects, not a live DC census.','No WinRM, remote registry, service probes or infrastructure changes are used by this command.');timeoutSeconds=if($SampleData){$null}else{$TimeoutSeconds}} if(Get-Command Receive-PSIInventoryDisplay -ErrorAction SilentlyContinue){Receive-PSIInventoryDisplay -Items @($envelopes);Complete-PSIInventoryCollectionDisplay -Collection $model.Data.collection;Update-PSIInventoryDisplay -Stage 'Saving reports' -Force} $paths=Export-PSISitesReport $model $meta $moduleRoot $reports Write-PSISitesLog $logPath $runId 'Export' 'Report' 'Succeeded' $context.Server $paths.HtmlReport $completed=@($model.Data.collection | Where-Object{$_.Status -eq 'Succeeded'}).Count if($Open){try{Invoke-Item -LiteralPath $paths.HtmlReport}catch{Write-Warning "Report saved, but could not open automatically: $($_.Exception.Message)"}} [pscustomobject][ordered]@{ModuleName='PSInsightInventory';ModuleVersion='1.0.0';InventoryId='ADSitesAndServices';Scope='Forest-wide';Forest=$context.Forest;ConnectionDomain=$context.Domain;SourceDC=$context.Server;DataSource=$meta.dataSource;QueriesCollected=$completed;QueriesUnavailable=8-$completed;Sites=if($model.Coverage.sites){$model.Data.sites.Count}else{$null};SiteLinks=if($model.Coverage.links){$model.Data.links.Count}else{$null};HtmlReport=$paths.HtmlReport;JsonSnapshot=$paths.JsonSnapshot;CsvFolder=$paths.CsvFolder;LogFile=$logPath;OutputFolder=$folder} }catch{ Write-PSISitesLog $logPath $runId 'Run' 'Complete' 'Failed' $Domain $_.Exception.Message 0 $_.FullyQualifiedErrorId throw }finally{if($null -ne $job){if($job.State -in @('Running','NotStarted','Blocked')){Stop-Job $job -ErrorAction SilentlyContinue};Remove-Job $job -Force -ErrorAction SilentlyContinue}} |