DellCatalogMirror.psm1
function Import-DellCatalogXml { <# .SYNOPSIS Import a Dell catalog and return an XML object .DESCRIPTION Import a Dell catalog and return an XML object. By default fetches the catalog from downloads.dell.com. #> [CmdletBinding( DefaultParameterSetName='Uri' )] [OutputType([xml])] param( [Parameter( ParameterSetName='Uri' )] [uri] $CatalogUri = 'https://downloads.dell.com/catalog/Catalog.xml.gz', [Parameter( ParameterSetName='File', Mandatory )] [string] $Path ) if ( $PSCmdlet.ParameterSetName -eq 'Uri' ) { Write-Verbose 'Downloading catalog from Dell...' Write-Verbose ( 'Download URI: {0}' -f $CatalogUri ) $CatalogDownload = Invoke-WebRequest -UseBasicParsing -Uri $CatalogUri if ( $CatalogDownload.Headers.'Content-Type' -eq 'application/x-gzip' ) { Write-Verbose 'Decompressing the catalog...' # decompress catalog $MemoryStream = [System.IO.MemoryStream]::new( $CatalogDownload.Content ) $GZipStream = [System.IO.Compression.GZipStream]::new( $MemoryStream, [System.IO.Compression.CompressionMode]::Decompress ) $ResultStream = [System.IO.MemoryStream]::new() $GZipStream.CopyTo($ResultStream) $MemoryStream.Close() $GZipStream.Close() $ResultStream.Close() # convert to byte array $ResultBytes = $ResultStream.ToArray() # convert to string $CatalogContent = [Text.Encoding]::Unicode.GetString( $ResultBytes[ 2 .. $ResultBytes.Count ] ) } else { Write-Verbose 'Downloading catalog from Dell...' Write-Verbose ( 'Download URI: {0}' -f $CatalogUri ) $CatalogContent = $CatalogDownload.Content } } else { $CatalogContent = Get-Content -Path $Path } Write-Verbose 'Loading XML...' try { [xml]$CatalogXml = $CatalogContent } catch { throw ( 'Failed to process XML. {0}' -f $_.Exception.Message ) } return $CatalogXml } class DellModelInfo { [string] $Brand [string] $Model [string] $SystemId [string] $Type DellModelInfo() {} DellModelInfo( [string]$Model ) { $this.Model = $Model } [string] ToString() { return $this.Model } } function Get-DellCatalogModels { <# .SYNOPSIS Parses supported models from the Dell update catalog. #> [CmdletBinding()] [OutputType([DellModelInfo])] param( [Parameter( Mandatory, ValueFromPipeline )] [xml] $CatalogXml ) foreach ( $Brand in $CatalogXml.SelectNodes('/Manifest/SoftwareBundle').TargetSystems.Brand ) { foreach ( $Model in $Brand.Model ) { [DellModelInfo]@{ Brand = $Brand.Display.InnerText Model = $Model.Display.InnerText SystemId = $Model.systemId Type = $Model.systemIdType } } } } function Optimize-DellCatalogXml { <# .SYNOPSIS Optimize the catalog for specific models #> [CmdletBinding( DefaultParameterSetName = 'NoReturn' )] [OutputType( [xml], ParameterSetName='PassThru' )] [OutputType( [void], ParameterSetName='NoReturn' )] param( [Parameter( Mandatory, ValueFromPipeline )] [xml] $CatalogXml, [Parameter( Mandatory )] [string[]] $Models, [Parameter( Mandatory, ParameterSetName='PassThru')] [switch] $PassThru ) Write-Verbose ( 'Optimizing Catalog for systems: {0}' -f ( $Models -join ', ' ) ) Write-Verbose 'Removing unwanted software bundles...' Write-Verbose ( 'Before processing there are {0} bundles.' -f $CatalogXml.SelectNodes('/Manifest/SoftwareBundle').Count ) # remove unwanted elements $CatalogXml.SelectNodes('/Manifest/SoftwareBundle').ForEach({ if ( $_.TargetSystems.Brand.Model.Display.InnerText.Where({ $_ -in $Models }) ) { Write-Verbose ( '[KEEP]: {0} ({1})' -f $_.bundleId, ($_.TargetSystems.Brand.Model.Display.InnerText -join ', ') ) } else { Write-Verbose ( '[REMOVE] {0}' -f $_.bundleId ) $_.ParentNode.RemoveChild($_) > $null } }) Write-Verbose ( 'After processing there are {0} bundles.' -f $CatalogXml.SelectNodes('/Manifest/SoftwareBundle').Count ) Write-Verbose 'Removing unwanted software components...' Write-Verbose ( 'Before processing there are {0} software components.' -f $CatalogXml.SelectNodes('/Manifest/SoftwareComponent').Count ) $PackagePaths = $CatalogXml.SelectNodes('//SoftwareBundle//Package').Path $CatalogXml.SelectNodes('/Manifest/SoftwareComponent').ForEach({ if ( $_.Path.Split('/')[-1] -notin $PackagePaths ) { Write-Verbose ( '[REMOVE] {0}' -f $_.Name.Display.InnerText ) $_.ParentNode.RemoveChild($_) > $null } else { Write-Verbose ( '[KEEP] {0}' -f $_.Name.Display.InnerText ) } }) Write-Verbose ( 'After processing there are {0} software components.' -f $CatalogXml.SelectNodes('/Manifest/SoftwareComponent').Count ) if ( $PassThru ) { return $CatalogXml } } function Update-DellCatalogMirror { <# .SYNOPSIS Update a local mirror .DESCRIPTION Update a local mirror by downloading the updates specified in the catalog. Skips existing files. #> [CmdletBinding( SupportsShouldProcess, ConfirmImpact='Low' )] param( [Parameter( Mandatory, ValueFromPipeline )] [xml] $CatalogXml, [string] $Path = $PSScriptRoot ) $ErrorActionPreference = 'Stop' $DownloadProtocol = $CatalogXml.Manifest.baseLocationAccessProtocols.ToLower() | Where-Object { $_ -in @( 'http', 'https' ) } | Select-Object -First 1 $DownloadHost = $CatalogXml.Manifest.baseLocation.ToLower() $BaseDownloadUri = '{0}://{1}' -f $DownloadProtocol, $DownloadHost $Path = Resolve-Path -Path $Path | Convert-Path Write-Verbose ( 'Repo Path: {0}' -f $Path ) Write-Verbose 'Looking for existing downloads...' # find existing files [string[]]$ExistingItems = Get-ChildItem -Path $Path -File -Recurse | Where-Object { $_.Directory.FullName -ne $Path } | Select-Object -ExpandProperty FullName Write-Verbose ( 'Before processing there are {0} downloaded software components.' -f $ExistingItems.Count ) $UpdatedCatalog = $false # download the components [string[]]$DownloadedItems = $CatalogXml.SelectNodes('/Manifest/SoftwareComponent').ForEach({ $SoftwareComponent = $_ Write-Host ( 'Processing: {0}' -f $SoftwareComponent.SelectNodes("./Name/Display[@lang='en']").InnerText ) $DestinationPath = Join-Path $Path $SoftwareComponent.Path.Replace('/','\') if ( $DestinationPath -in $ExistingItems ) { if ( $SoftwareComponent.hashMD5 -eq (Get-FileHash $DestinationPath -Algorithm MD5).Hash ) { Write-Verbose ( 'Skipping existing file: {0}' -f $DestinationPath ) return $DestinationPath } else { Write-Warning ( 'Existing file hash missmatch!' ) } } New-Item -Path ( Split-Path $DestinationPath -Parent ) -ItemType Directory -Force > $null $DownloadUri = $BaseDownloadUri, $SoftwareComponent.Path -join '/' #Write-Verbose ( 'Downloading: {0}' -f $DownloadUri ) if ( $PSCmdlet.ShouldProcess($DownloadUri, 'Download Software Package') ) { try { Invoke-WebRequest -UseBasicParsing -Uri $DownloadUri -OutFile $DestinationPath -ErrorVariable DownloadError -ErrorAction Stop > $null } catch { Write-Warning ''.PadRight(40,'-') Write-Warning 'DOWNLOAD FAILED' Write-Warning ( 'Error: {0}' -f $_.Exception.Message ) $SoftwareComponent.SelectNodes('.//*[@URL]').URL | ForEach-Object { Write-Warning ( 'Link: {0}' -f $_ ) } Write-Warning ( 'Destination: {0}' -f $DestinationPath ) Write-Warning ''.PadRight(40,'-') return } if ( $SoftwareComponent.hashMD5 -ne (Get-FileHash $DestinationPath -Algorithm MD5).Hash ) { Write-Warning 'Hash of downloaded file does not match manifest!' } Write-Verbose ( 'New file: {0}' -f $DestinationPath ) $UpdatedCatalog = $true return $DestinationPath } }) Write-Verbose ( 'Processed {0} new software components.' -f $DownloadedItems.Count ) if ( $UpdatedCatalog ) { Write-Verbose 'Exporting Catalog...' # clean up the manifest and save $CatalogXml.Manifest.RemoveAttribute('baseLocationAccessProtocols') $CatalogXml.Manifest.baseLocation = '' $CatalogXml.Save( "$Path\Catalog.xml" ) Write-Verbose 'Cleaning up old software components...' # remove old downloaded items if ( $ExtraItems = $ExistingItems | Where-Object { $_ -notin $DownloadedItems } ) { Remove-Item -Path $ExtraItems -Confirm:$false -Force } } } |