tools/AduCmdlets/AduUpdate.psm1 (325 lines of code) (raw):

# # Device Update for IoT Hub # PowerShell module for creating import manifest for Device Update for IoT Hub (ADU). # Copyright (c) Microsoft Corporation. # #Requires -Version 5.0 # ------------------------------------------------- # CLASSES # ------------------------------------------------- class UpdateId { [string] $Provider [string] $Name [string] $Version UpdateId($provider, $name, $version) { $this.Provider = $provider $this.Name = $name $this.Version = $version } } # ------------------------------------------------- # INTERNAL METHODS # ------------------------------------------------- function Get-UpdateId { Param( [ValidateNotNullOrEmpty()] [string] $Provider = $(throw "'Provider' parameter is required."), [ValidateNotNullOrEmpty()] [string] $Name = $(throw "'Name' parameter is required."), [ValidateNotNullOrEmpty()] [version] $Version = $(throw "'Version' parameter is required.") ) # Server will accept any order; preserving order for aesthetics only. $updateId = [ordered] @{ 'provider' = $Provider 'name' = $Name 'version' = "$Version" } return $updateId } function Get-FileMetadatas { Param( [ValidateCount(0, 5)] [string[]] $FilePaths = $(throw "'FilePaths' parameter is required.") ) $files = @() foreach ($filePath in $FilePaths) { if (!(Test-Path $filePath)) { throw "$filePath could not be found." } $file = Get-Item $filePath $fileHashes = Get-AduFileHashes $filePath # Server will accept any order; preserving order for aesthetics only. $fileMap = [ordered] @{ 'filename' = $file.Name 'sizeInBytes' = $file.Length 'hashes' = $fileHashes } $files += $fileMap } return $files } # ------------------------------------------------- # EXPORTED METHODS # ------------------------------------------------- function Get-AduFileHashes { <# .SYNOPSIS Get file hashes in a format required by ADU. .EXAMPLE PS > Get-AduFileHashes -FilePath .\payload.bin #> [CmdletBinding()] Param( # Full path to the file. [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string] $FilePath ) if (!(Test-Path $filePath)) { throw "$FilePath could not be found." } $FilePath = Resolve-Path $FilePath $fs = [System.IO.File]::OpenRead($FilePath) $sha256 = New-Object System.Security.Cryptography.SHA256Managed $bytes = $sha256.ComputeHash($fs) $sha256.Dispose() $fs.Close() $fileHash = [System.Convert]::ToBase64String($bytes) $hashes = [pscustomobject]@{ 'sha256' = $fileHash } return $hashes } function New-AduUpdateId { <# .SYNOPSIS Create a new ADU update identity. .EXAMPLE PS > New-AduUpdateId -Provider Contoso -Name Toaster -Version 1.0 #> [CmdletBinding()] Param( # Update provider. [Parameter(Mandatory=$true)] [ValidateLength(1, 64)] [ValidatePattern("^[a-zA-Z0-9.-]+$")] [string] $Provider, # Update name. [Parameter(Mandatory=$true)] [ValidateLength(1, 64)] [ValidatePattern("^[a-zA-Z0-9.-]+$")] [string] $Name, # Update version. [Parameter(Mandatory=$true)] [version] $Version ) return [UpdateId]::New($Provider, $Name, $Version) } function New-AduUpdateCompatibility { <# .SYNOPSIS Create a new ADU update compatibility info. .EXAMPLE PS > New-AduUpdateCompatibility -Manufacturer Contoso -Model Toaster #> [CmdletBinding()] Param( # Compatibility properties. [Parameter(ParameterSetName='CustomProperties', Mandatory=$true)] [hashtable] $Properties, # Device manufacturer. [Parameter(ParameterSetName='BackwardCompat', Mandatory=$true)] [ValidateLength(1, 64)] [string] $Manufacturer, # Device model. [Parameter(ParameterSetName='BackwardCompat', Mandatory=$true)] [ValidateLength(1, 64)] [string] $Model ) switch ($PSCmdlet.ParameterSetName) { 'CustomProperties' { return $Properties } 'BackwardCompat' { return @{ manufacturer = $Manufacturer model = $Model } } } } function New-AduInstallationStep { <# .SYNOPSIS Create a new ADU installation step. .EXAMPLE PS > New-AduInstallationStep -Handler 'microsoft/swupdate:1' -Files '.\file1.json', '.\file2.zip' #> [CmdLetBinding()] Param( # Step handler type in form of "{provider}/{handlerType}:{handlerTypeVersion}". # This parameter is forwarded to client device during deployment. # For example, reference ADU agent uses the following: # - "microsoft/swupdate:1" for SwUpdate image-based installation step. # - "microsoft/apt:1" for APT package-based installation step. [Parameter(ParameterSetName = 'inline', Mandatory=$true)] [ValidateLength(1, 32)] [ValidatePattern("^\S+\/\S+:\d{1,5}$")] [string] $Handler, # The payload filenames used for this step. [Parameter(ParameterSetName = 'inline', Mandatory=$true)] [ValidateCount(1, 10)] [string[]] $Files, # Optional JSON object argument to step handler. [Parameter(ParameterSetName = 'inline')] [hashtable] $HandlerProperties, # Identity of child update to install for this step. [Parameter(ParameterSetName = 'reference', Mandatory=$true)] [UpdateId] $UpdateId, # Optional step description. [ValidateLength(0, 64)] [string] $Description ) switch ($PsCmdlet.ParameterSetName) { 'inline' { $step = [ordered] @{ type = 'inline' } if ($Description.Length -gt 0) { $step.description = $Description } $step.handler = $Handler $step.files = $Files if ($HandlerProperties -ne $null) { $step.handlerProperties = $HandlerProperties } return $step } 'reference' { $step = [ordered] @{ type = 'reference' } if ($Description.Length -gt 0) { $step.description = $Description } $step.updateId = Get-UpdateId -Provider $UpdateId.Provider -Name $UpdateId.Name -Version $UpdateId.Version return $step } } } function New-AduImportManifest { <# .SYNOPSIS Create a new ADU update import manifest. .EXAMPLE PS > $updateId = New-AduUpdateId -Provider Fabrikam -Name Toaster -Version 2.0 PS > $compatInfo1 = New-AduUpdateCompatibility -Manufacturer Fabrikam -Model Toaster PS > $compatInfo2 = New-AduUpdateCompatibility -Properties @{ OS = "Linux"; Manufacturer = "Fabrikam" } PS > $step = New-AduInstallationStep -Handler 'microsoft/swupdate:1' -Files '.\file1.json', '.\file2.zip' PS > PS > New-AduImportManifest -UpdateId $updateId -Compatibility $compatInfo1, $compatInfo2 -InstallationSteps $step #> [CmdletBinding()] Param( # Update identity created using New-AduUpdateId. [Parameter(ParameterSetName='UpdateId', Mandatory=$true)] [ValidateNotNull()] [UpdateId] $UpdateId, # Update provider. [Parameter(ParameterSetName='BackwardCompat', Mandatory=$true)] [ValidateLength(1, 64)] [ValidatePattern("^[a-zA-Z0-9.-]+$")] [string] $Provider, # Update name. [Parameter(ParameterSetName='BackwardCompat', Mandatory=$true)] [ValidateLength(1, 64)] [ValidatePattern("^[a-zA-Z0-9.-]+$")] [string] $Name, # Update version. [Parameter(ParameterSetName='BackwardCompat', Mandatory=$true)] [version] $Version, # Optional friendly update description [ValidateLength(0, 512)] [string] $Description, # List of compatibility information of devices this update is compatible with, created using New-AduUpdateCompatibility. [Parameter(Mandatory=$true)] [ValidateCount(1, 10)] [hashtable[]] $Compatibility, # List of update installation steps. [Parameter(Mandatory=$true)] [ValidateCount(1, 10)] [System.Collections.Specialized.OrderedDictionary[]] $InstallationSteps, # Whether the update can be deployed on its own to a device. Must be false for a referenced update. [bool] $IsDeployable = $true ) switch ($PSCmdlet.ParameterSetName) { 'UpdateId' { $id = Get-UpdateId -Provider $UpdateId.Provider -Name $UpdateId.Name -Version $UpdateId.Version } 'BackwardCompat' { $id = Get-UpdateId -Provider $Provider -Name $Name -Version $Version } } $fileMetadatas = @() foreach ($step in $InstallationSteps) { if ($step.type -eq 'inline') { for ($iFile = 0; $iFile -lt $step.files.Count; $iFile++) { $meta = Get-FileMetadatas $step.files[$iFile] # in case multiple steps are sharing a payload file. if ($fileMetadatas.filename -notcontains $meta.filename) { $fileMetadatas += $meta } # inline step requires only payload file name, convert full path to filename. $step.files[$iFile] = $meta.filename } } } if ($fileMetadatas.Count -gt 10) { Write-Error 'Update cannot have more than 10 payload files.' } # Server will accept any order; preserving order for aesthetics only. $importManifest = [ordered] @{ updateId = $id } if ($Description.Length -gt 0) { $importManifest.description = $Description } $importManifest.isDeployable = $IsDeployable $importManifest.compatibility = [array] $Compatibility $importManifest.instructions = [ordered] @{ steps = [array] $InstallationSteps } if ($fileMetadatas.Length -gt 0) { $importManifest.files = [array] $fileMetadatas } $importManifest.createdDateTime = (Get-Date).ToUniversalTime().ToString('o') # ISO8601 $importManifest.manifestVersion = '4.0' ConvertTo-Json -InputObject $importManifest -Depth 20 } Export-ModuleMember -Function New-AduImportManifest, New-AduUpdateId, New-AduUpdateCompatibility, New-AduInstallationStep, Get-AduFileHashes