<# .NOTES Written by Ian Cammarata http://ian.cammarata.us Get-LenovoProductInfo.ps1 v0.3 Will this only work for products sold in the U.S.? To Do: -Generate a proper object (this will allow tab completion and easier programmatic use) SNs for testing: LRNMV31 | 0301-24U | ThinkPad Edge 15 R9002F1V | 20A8Z05QUS | ThinkPad X1 Carbon .SYNOPSIS Query by SN to retreive Lenovo warranty, product name and type, and parts. .EXAMPLE PS> Get-LenovoProductInfo MJDGZE4 #> param( #The serial number to query. [Parameter( Mandatory = $True, Position = 0, ValueFromPipeline= $True )] [Alias( "SN" )] [String[]] $SerialNumber, #Search string for parts listing. [Parameter( Position = 1 )] [Alias( "Part" )] $PartQuery, #Output an object instead of an plain text. [Switch] [Alias( "Object" )] $OutputObject, #Purge cash for supplied SN(s) and fetch fresh data [Switch] [Alias( "Purge" )] $Refresh ) BEGIN { Set-StrictMode -Version Latest if ( $Script:PSBoundParameters.ContainsKey("Debug") ) { $DebugPreference = "Continue" } ### For some reason I was getting first 3 debug lines output in the Process block even with using -Debug, but none afterwards ###### Set Constants #<# ### uncomment this line for testing in ISE after initial run Set-Variable productObjectVersion -option Constant -value 0.4 Set-Variable lenovoWarrantyUrl -option Constant -value "https://csp.lenovo.com/ibapp/il/WarrantyStatus.jsp" Set-Variable lenovoProductDescriptionUrl -option Constant -value "http://support.lenovo.com/services/us/en/advancedsearch/getsearchresult/1c2d5342-a15b-4828-9095-13e5d52f9df6?dataSource=aca55143-0507-4b4d-a559-65147c1dec9a&SearchKey=" Set-Variable lenovoPartsUrl -option Constant -value "http://support.lenovo.com/templatedata/Web%20Content/JSP/partsLookup.jsp" Set-Variable cacheDir -option Constant -value (Join-Path $env:APPDATA "LenovoProductInfo_Cache") Set-Variable maxCacheAge -option Constant -value 90 #In Days #> ###### Prep if ( !( Test-Path $cacheDir ) ){ $null = New-Item -Type "Directory" -Path $cacheDir } <###### Default display property set $objectTemplateFile = Join-Path $cacheDir "LenovoProductObjectTemplate_v$($productObjectVersion).ps1xml" if ( !( Test-Path $objectTemplateFile ) ) { $template = "" $template += "System.Management.Automation.PSCustomObject" $template += "PSStandardMembersDefaultDisplayPropertySet" $template += "ProductNumber" $template += "SerialNumber" $template += "Description" $template += "WarrantyOnsite" $template += "Parts" $template += "" Out-File $objectTemplateFile -Encoding "UTF8" -InputObject $template -Force } if ( $PSVersionTable.PSVersion.Major -gt 2 ) { Update-TypeData $objectTemplateFile } elseif ( ! ( $host.Runspace.RunspaceConfiguration.Types | ? { $_.FileName -eq $objectTemplateFile } ) ) { Update-TypeData $objectTemplateFile }#> } PROCESS { Function CleanHtml($htData) { Return $htData -replace " ", "" -replace "(\r|\f|\n)*", "" -replace "\t", " " -replace " ", "" -replace "> <", "><" -replace "]*>", "" -replace "", "" } Foreach ($SerialNumber in $SerialNumber) { $SerialNumber = $SerialNumber.Trim().ToUpper() Write-Debug ">> Check cache for XML copy of data" $useCache = $false if ( $cacheHit = ls $cacheDir | ? { $_.Name -like "$($SerialNumber)_*" } ) { Write-Debug "Cache Hit!!! ($cacheHit)" $useCache = $true if ( $Refresh ) { $useCache = $false } elseif ( $cacheHit -notlike "*_v$($productObjectVersion).xml" ) { Write-Debug "Version Mismatch; Deleting... :-(" $useCache = $false } elseif ( ( Get-Date ) -gt $cacheHit.CreationTime.AddDays(90) ) { Write-Debug "Stale Cache; Deleting... :-(" $useCache = $false } if ( ! $useCache ) { Remove-Item ( Join-Path $cacheDir $cacheHit ) } } Write-Debug "Using Cache: $($useCache)" Write-Debug "<< End Check Cache" function downloadProductData () { Write-Debug "/!\Begin critical section/!\ Any errors here break the entire script." $ErrorActionPreference = "Stop" Write-Debug "Set up httpWebRequest for Warranty lookup" [net.httpWebRequest] $webReq = [net.webRequest]::create($lenovoWarrantyUrl) $webReq.Method = "POST" $postData = "type=&serial=$($SerialNumber)&selLanguage=EN&country=897&iws=off&sitestyle=lenovo" $postBuffer = [Text.Encoding]::Ascii.GetBytes($postData) $webReq.ContentLength = $postBuffer.Length $webReq.ContentType = "application/x-www-form-urlencoded" $reqStr = $webReq.GetRequestStream() $reqStr.Write( $postBuffer, 0, $postBuffer.Length) $reqStr.Flush() $reqStr.Close() [net.httpWebResponse] $webResp = $webReq.GetResponse() $strRdr = New-Object IO.StreamReader($webResp.GetResponseStream()) $htData = CleanHtml( $strRdr.ReadToEnd() ) $webResp.Close() $warrantyHtData = $htData -replace ".*", "" -replace "Excluding:.*", "" -replace "End Date:", "" $skuData = (Select-String "]*>([^<:]+?)" -InputObject $warrantyHtData -AllMatches).Matches | % {$_.Groups[1].Value} Write-Debug "Create psObject representing the product" $lenovoProduct = New-Object psObject Write-Debug "Load generic sku data into the object" $lenovoProduct | Add-Member NoteProperty ProductID $skuData[0] $lenovoProduct | Add-Member NoteProperty Type ($skuData[1] -split "-")[0] $lenovoProduct | Add-Member NoteProperty Model ($skuData[1] -split "-")[1] $lenovoProduct | Add-Member NoteProperty SerialNumber $skuData[2] $lenovoProduct | Add-Member NoteProperty Location $skuData[3] $lenovoProduct | Add-Member NoteProperty WarrantyExpiration $skuData[4] $ErrorActionPreference = "Continue" Write-Debug "/!\End critical section/!\" Write-Debug "Filter parts table rows out of the HTML." $null = $htData -match "]*PartsTable.*?" $trData = (Select-String "]*>[^<]*]*>([^-<]*)]*>([^<]*)" -InputObject $Matches[0] -AllMatches).Matches Write-Debug "Add FRUs to the product object." $lenovoProduct | Add-Member NoteProperty FRUs (New-Object psobject) foreach ( $tr in $trData ){ $partObject = New-Object psObject $partObject | Add-Member NoteProperty FRU $tr.Groups[1].Value $partObject | Add-Member NoteProperty Description $tr.Groups[2].Value $lenovoProduct.FRUs | Add-Member NoteProperty $partObject.FRU $partObject -ErrorAction "SilentlyContinue" } Write-Debug "Create WebClient to query ProductName" $htClient = New-Object System.Net.Webclient #Query by product type $htData = $htClient.DownloadString("$($lenovoProductDescriptionUrl)$($lenovoProduct.ProductID)") #Navigate to result page $null = $htData -match '' $htData = cleanHtml( $htClient.DownloadString("http://support.lenovo.com/$($Matches[1])") ) $null = $htData -match 'class="product-detail-title">(.*?)<' $lenovoProduct | Add-Member NoteProperty ProductName ($Matches[1] -replace ".*>" -replace " $($lenovoProduct.ProductID)" -replace " \(.*?\)") $lenovoProduct | Add-Member NoteProperty __QueryDate (Get-Date) $lenovoProduct | Add-Member NoteProperty __productObjectVersion $productObjectVersion $lenovoProduct | Export-Clixml (Join-Path $cacheDir "$($lenovoProduct.SerialNumber)_$($lenovoProduct.ProductID)_v$($productObjectVersion).xml") Return } #Import line is potentially error prone, make it more specific later on if ( -not $useCache ) { downloadProductData } $lenovoProduct = Import-Clixml ( Join-Path $cacheDir "$($SerialNumber)_*" ) if ( $OutputObject ) { $lenovoProduct } Else { $lineLen = 75 "█" * $lineLen "Serial Number:`t$($lenovoProduct.SerialNumber)" "Product ID:`t$($lenovoProduct.ProductID )" "Product Name:`t$($lenovoProduct.ProductName)" "Warranty End:`t$($lenovoProduct.WarrantyExpiration)" "" $parts = $lenovoProduct.FRUs | get-member -MemberType "NoteProperty" $partsTxt = @() foreach ( $part in $parts | % { $_.Name } ) { if ( $PartQuery ) { if ( $lenovoProduct.FRUs.$part.Description -match ".*$($PartQuery).*" ) { $partsTxt += "$($lenovoProduct.FRUs.$part.FRU) - $($lenovoProduct.FRUs.$part.Description)" } } Else { $partsTxt += "$($lenovoProduct.FRUs.$part.FRU) - $($lenovoProduct.FRUs.$part.Description)" } } "FRUs$(if ( $PartQuery ) {" matching query '$($PartQuery)'"}): $($partsTxt.Length)" "─" * $lineLen $partsTxt "█"*$lineLen } } }