Public/Get-UTCMSnapshot.ps1
|
function Get-UTCMSnapshot { <# .SYNOPSIS Retrieves a UTCM snapshot job by ID. .DESCRIPTION Fetches snapshot job metadata from GET /beta/admin/configurationManagement/configurationSnapshotJobs/{id}. Optionally includes errorDetails/resourceLocation (-IncludeDetails) or downloads the full configuration payload and attaches it as configurationItems (-IncludeItems). .PARAMETER SnapshotId The GUID of the snapshot job to retrieve. .PARAMETER IncludeDetails Include resourceLocation and errorDetails in the response. .PARAMETER IncludeItems Download the snapshot artifact from resourceLocation and attach configurationItems. .PARAMETER AsJson Return the result as a JSON string. .OUTPUTS PSObject representing the snapshot job, optionally with configurationItems attached. .EXAMPLE Get-UTCMSnapshot -SnapshotId 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' .EXAMPLE Get-UTCMSnapshot -SnapshotId $id -IncludeItems -AsJson .EXAMPLE # Inspect errors on a partiallySuccessful job $job = Get-UTCMSnapshot -SnapshotId $id -IncludeDetails $job.errorDetails | ForEach-Object { "---"; $_ } #> [CmdletBinding()] param( # Validate at bind time; throw a friendly message if invalid [Parameter(Mandatory)] [ValidateScript({ if (-not (Validate-Guid $_)) { throw "SnapshotId '$_' is not a valid GUID." } $true })] [string] $SnapshotId, # Include resourceLocation + errorDetails in the server-side projection [switch] $IncludeDetails, # Download the artifact from resourceLocation and attach configurationItems [switch] $IncludeItems, # Return JSON instead of an object [switch] $AsJson ) # Ensure we have a Graph token (uses the module helper if present) if (Get-Command -Name Ensure-GraphConnection -ErrorAction SilentlyContinue) { Ensure-GraphConnection } # 1) Build $select to minimize payload from Graph $select = @('id','displayName','description','createdDateTime','completedDateTime','status','createdBy','resources','tenantId') if ($IncludeDetails -or $IncludeItems) { $select += @('resourceLocation','errorDetails') } $qs = "`$select=" + [System.Uri]::EscapeDataString(($select -join ',')) # 2) GET the job $uri = "$($script:SnapshotJobsUri)/${SnapshotId}?${qs}" $job = Invoke-GraphRequestWithRetry -Method 'GET' -Uri $uri # 3) If the caller wants items, download from resourceLocation (for completed jobs) if ($IncludeItems) { # Validate job status first if ($job.status -eq 'failed') { $err = ($job.PSObject.Properties.Name -contains 'errorDetails') ? ($job.errorDetails -join '; ') : 'n/a' throw ("Snapshot job '{0}' failed. Errors: {1}" -f $job.id, $err) } if ($job.status -notin @('succeeded','partiallySuccessful')) { throw ("Snapshot job '{0}' is not completed yet. Current status: '{1}'." -f $job.id, $job.status) } if (-not $job.resourceLocation -or [string]::IsNullOrWhiteSpace($job.resourceLocation)) { throw "Snapshot job '$($job.id)' does not expose resourceLocation; cannot retrieve items." } # Download to temp, parse as JSON, attach configurationItems $tmp = [System.IO.Path]::GetTempFileName() try { Invoke-WebRequest -Uri $job.resourceLocation -OutFile $tmp -UseBasicParsing -ErrorAction Stop $raw = Get-Content -LiteralPath $tmp -Raw $json = $null try { $json = $raw | ConvertFrom-Json -ErrorAction Stop } catch { $json = $null } if ($null -eq $json) { throw "Downloaded artifact is not JSON; cannot extract items." } $items = $null if ($json.PSObject.Properties.Name -contains 'configurationItems') { $items = $json.configurationItems } elseif ($json.PSObject.Properties.Name -contains 'resources') { $items = $json.resources } elseif ($json -is [System.Collections.IEnumerable]) { $items = $json } else { throw "Artifact JSON does not contain 'configurationItems'/'resources' and is not an array\u2014cannot extract items." } # Normalize items so downstream consumers (Compare-UTCMConfiguration, # New-UTCMDriftReport, Export-UTCMSnapshot) get a consistent shape with # id/displayName/type/data regardless of the raw payload variant. $normalizedItems = foreach ($it in $items) { $idVal = $null if ($it.PSObject.Properties.Name -contains 'id' -and $it.id) { $idVal = [string]$it.id } elseif ($it.PSObject.Properties.Name -contains 'resourceInstanceIdentifier' -and $it.resourceInstanceIdentifier) { $idVal = [string]$it.resourceInstanceIdentifier } elseif ($it.PSObject.Properties.Name -contains 'properties' -and $it.properties) { foreach ($p in 'Id','Identity','Guid','ObjectId') { if ($it.properties.PSObject.Properties.Name -contains $p -and $it.properties.$p) { $idVal = [string]$it.properties.$p; break } } } $typeVal = $null if ($it.PSObject.Properties.Name -contains 'resourceType' -and $it.resourceType) { $typeVal = [string]$it.resourceType } elseif ($it.PSObject.Properties.Name -contains 'type' -and $it.type) { $typeVal = [string]$it.type } $dataVal = $null if ($it.PSObject.Properties.Name -contains 'properties') { $dataVal = $it.properties } elseif ($it.PSObject.Properties.Name -contains 'data') { $dataVal = $it.data } $displayVal = $null if ($it.PSObject.Properties.Name -contains 'displayName') { $displayVal = [string]$it.displayName } [pscustomobject]@{ id = $idVal displayName = $displayVal type = $typeVal data = $dataVal } } # Attach both the normalized items and the raw payload for power users if ($job.PSObject.Properties.Name -contains 'configurationItems') { $job.configurationItems = $normalizedItems } else { Add-Member -InputObject $job -NotePropertyName configurationItems -NotePropertyValue $normalizedItems -Force } Add-Member -InputObject $job -NotePropertyName rawConfiguration -NotePropertyValue $json -Force } finally { if (Test-Path -LiteralPath $tmp) { Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue } } } # 4) Output if ($AsJson) { return ($job | ConvertTo-Json -Depth 99) } # If caller didn’t ask for details/items, return a concise view (back-compat) if (-not $IncludeDetails -and -not $IncludeItems) { return $job | Select-Object id, displayName, createdDateTime, status, createdBy, resources, tenantId } # If details requested, pass through the richer object return $job } |