functions/Remove-UdeEnvironmentModel.ps1
|
<# .SYNOPSIS Remove models from a unified environment. .DESCRIPTION Removes one or more models from a unified environment (UDE, USE and others). Implements Way 2 from Plan-UdeModelDelete-Way2: downloads the last uploaded deploy zip for each model as a seed, rebuilds it as a Delete package and deploys it with BuildType=Delete. -Model accepts an array of names. Each name is validated against Get-UdeEnvironmentModel. Unknown names stop the cmdlet. Dependency linked models are supported: descriptor files (Descriptor/<model>.xml) from all installed models are read from their seed packages. If a model outside the removal list references a model inside it, the cmdlet stops. If a model inside the removal list references another installed model, the referenced model is added to the removal list, so the environment stays in a working state. Without -Force the cmdlet never calls DELETE msprov_fnomodules. That record-only fallback requires -Force. .PARAMETER EnvironmentId The id of the environment that you want to work against .PARAMETER Model The names of the models that you want to remove. Each name is validated against Get-UdeEnvironmentModel. If the module table is empty, names fall back to the package names from Get-UnifiedEnvironmentPackage. .PARAMETER WorkFolder The folder where seed and delete packages are stored. Defaults to "C:\Temp\d365bap.tools\RemoveUdeModel". Files are organized in subfolders for each environment and model. .PARAMETER WaitForCompletion Instructs the cmdlet to wait until the delete deployment has completed. .PARAMETER DownloadLog Instructs the cmdlet to download the operation logs for the delete deployment. .PARAMETER Force Instructs the cmdlet to allow the record-only fallback. Without Force the cmdlet never calls DELETE msprov_fnomodules. .EXAMPLE PS C:\> Remove-UdeEnvironmentModel -EnvironmentId "env-123" -Model "B" This will remove the model B from the specified environment id. It will wait for the delete deployment to complete. .EXAMPLE PS C:\> Remove-UdeEnvironmentModel -EnvironmentId "env-123" -Model "A" -DownloadLog This will remove the model A and all models that depend on it from the specified environment id. It will download the operation logs to the work folder. .NOTES Author: Mötz Jensen (@Splaxi) #> function Remove-UdeEnvironmentModel { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [CmdletBinding()] [OutputType('System.Object[]')] param ( [Parameter (Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [Alias("PpacEnvId")] [string] $EnvironmentId, [Parameter (Mandatory = $true)] [Alias("Name")] [string[]] $Model, [string] $WorkFolder = "C:\Temp\d365bap.tools\RemoveUdeModel", [switch] $WaitForCompletion, [switch] $DownloadLog, [switch] $Force ) begin { Add-Type -AssemblyName System.IO.Compression.FileSystem } process { if (Test-PSFFunctionInterrupt) { return } $envObj = Get-UnifiedEnvironment -EnvironmentId $EnvironmentId -SkipVersionDetails | Select-Object -First 1 if ($null -eq $envObj) { $messageString = "Could not find environment with Id <c='em'>$EnvironmentId</c>. Please verify the Id and try again, or list available environments using <c='em'>Get-UnifiedEnvironment</c>. Consider using wildcards if needed." Write-PSFMessage -Level Important -Message $messageString Stop-PSFFunction -Message "Stopping because environment was NOT found based on the id." ` -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) return } $baseUri = $envObj.PpacEnvUri + "/" #! Very important to have the trailing slash $secureToken = (Get-AzAccessToken -ResourceUrl $baseUri -AsSecureString).Token $tokenWebApiValue = ConvertFrom-SecureString -AsPlainText -SecureString $secureToken $headers = @{ "Authorization" = "Bearer $($tokenWebApiValue)" "Accept" = "application/json;odata.metadata=minimal" # minimal || full "OData-MaxVersion" = "4.0" "OData-Version" = "4.0" "Prefer" = "odata.include-annotations=*" } $colInstalledModels = @(Get-UdeEnvironmentModel -EnvironmentId $EnvironmentId) $colInstalledPackages = @(Get-UnifiedEnvironmentPackage -EnvironmentId $EnvironmentId) if ($colInstalledModels.Count -eq 0 -and $colInstalledPackages.Count -eq 0) { $messageString = "No models were found on the environment <c='em'>$($envObj.PpacEnvName)</c>. Nothing to remove." Write-PSFMessage -Level Important -Message $messageString Stop-PSFFunction -Message "Stopping because no models were found on the environment." ` -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) return } $colInstalledNames = @($colInstalledModels | Select-Object -ExpandProperty Name) if ($colInstalledNames.Count -eq 0) { $colInstalledNames = @($colInstalledPackages | ForEach-Object { "$($_.Name)".Replace("_1_0_0_1_managed.zip", "") } | Where-Object { $_ -notlike "*_Delete_*" } | Select-Object -Unique) Write-PSFMessage -Level Important -Message "The msprov_fnomodules table is empty on <c='em'>$($envObj.PpacEnvName)</c>. Falling back to $($colInstalledNames.Count) model name(s) from the package table: $($colInstalledNames -join ', ')." } foreach ($modelName in $Model) { if ($modelName -notin $colInstalledNames) { $messageString = "The model <c='em'>$modelName</c> was not found on the environment <c='em'>$($envObj.PpacEnvName)</c>. Installed models are: $($colInstalledNames -join ', ')." Write-PSFMessage -Level Important -Message $messageString Stop-PSFFunction -Message "Stopping because a requested model was NOT found on the environment." ` -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) return } } $colRemoveNames = @($Model | Select-Object -Unique) $hashSeedZipByModel = @{} $hashReferencesByModel = @{} foreach ($installedName in $colInstalledNames) { $seedInfo = Get-ModelSeedPackage -BaseUri $baseUri ` -Headers $headers ` -ModelName $installedName if ($null -eq $seedInfo) { $messageString = "No prior Full upload (seed package) was found for model <c='em'>$installedName</c>. Way 2 needs a seed for every installed model to resolve dependencies. Run a Full deploy of <c='em'>$installedName</c> first, then try again." Write-PSFMessage -Level Important -Message $messageString Stop-PSFFunction -Message "Stopping because a seed package was NOT found." ` -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) return } $seedZipPath = Save-ModelSeedPackage -BaseUri $baseUri ` -Headers $headers ` -Seed $seedInfo ` -ModelName $installedName ` -WorkFolder $WorkFolder ` -EnvironmentName $envObj.PpacEnvName if ($null -eq $seedZipPath) { return } $hashSeedZipByModel[$installedName] = $seedZipPath $hashReferencesByModel[$installedName] = @(Get-ModelModuleReference -SeedZipPath $seedZipPath -ModelName $installedName) } $colExpandedNames = Expand-ModelRemovalList -RequestedNames $colRemoveNames ` -InstalledNames $colInstalledNames ` -ReferencesByModel $hashReferencesByModel if ($null -eq $colExpandedNames) { return } $colOutsideNames = @($colInstalledNames | Where-Object { $_ -notin $colExpandedNames }) foreach ($outsideName in $colOutsideNames) { $outsideRefs = @($hashReferencesByModel[$outsideName] | Where-Object { $_ -in $colExpandedNames }) if ($outsideRefs.Count -gt 0) { $messageString = "Cannot remove model(s) $($colExpandedNames -join ', ') because installed model <c='em'>$outsideName</c> depends on $($outsideRefs -join ', '). Add <c='em'>$outsideName</c> to -Model, or remove the dependency first." Write-PSFMessage -Level Important -Message $messageString Stop-PSFFunction -Message "Stopping because an installed model outside the removal list depends on a model to remove." ` -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) return } } $resCol = @( foreach ($removeName in $colExpandedNames) { $deleteResult = Invoke-ModelDeletePackage -BaseUri $baseUri ` -Headers $headers ` -Environment $envObj ` -ModelName $removeName ` -SeedZipPath $hashSeedZipByModel[$removeName] ` -WorkFolder $WorkFolder ` -WaitForCompletion:$WaitForCompletion ` -DownloadLog:$DownloadLog ` -Force:$Force if ($null -eq $deleteResult) { Stop-PSFFunction -Message "Stopping because the delete deployment failed." ` -Exception $([System.Exception]::new("The delete deployment failed. See previous messages for details.")) return } $deleteResult } ) $resCol } end { } } function Get-ModelSeedPackage { [CmdletBinding()] [OutputType([psobject])] param ( [Parameter(Mandatory = $true)] [string] $BaseUri, [Parameter(Mandatory = $true)] [hashtable] $Headers, [Parameter(Mandatory = $true)] [string] $ModelName ) $filterValue = [System.Uri]::EscapeDataString("'$ModelName'") $filterValue = $filterValue.Replace('%2527', '%27') $localUri = $BaseUri + "api/data/v9.0/msprov_fnopackages?`$filter=contains(msprov_name,$filterValue)&`$orderby=modifiedon desc&`$top=10" $colPackages = Invoke-RestMethod -Uri $localUri ` -Method Get ` -Headers $Headers | ` Select-Object -ExpandProperty value foreach ($packageObj in $colPackages) { $seedZipPath = Join-Path ([System.IO.Path]::GetTempPath()) "$([guid]::NewGuid().ToString()).zip" try { Invoke-WebRequest -Uri ($BaseUri + "api/data/v9.0/msprov_fnopackages($($packageObj.msprov_fnopackageid))/msprov_packagepayload/`$value") ` -Method Get ` -Headers $Headers ` -OutFile $seedZipPath ` -SkipHttpErrorCheck 4> $null } catch { if ([System.IO.File]::Exists($seedZipPath)) { Remove-Item -Path $seedZipPath -Force -ErrorAction SilentlyContinue } continue } if (-not [System.IO.File]::Exists($seedZipPath)) { continue } try { $zipObj = [IO.Compression.ZipFile]::OpenRead($seedZipPath) $definitionEntry = $zipObj.Entries | Where-Object FullName -eq 'fnomoduledefinition.json' | Select-Object -First 1 if ($null -eq $definitionEntry) { $zipObj.Dispose() [GC]::Collect() [GC]::WaitForPendingFinalizers() Remove-Item -Path $seedZipPath -Force -ErrorAction SilentlyContinue continue } $reader = New-Object System.IO.StreamReader($definitionEntry.Open()) $definitionText = $reader.ReadToEnd() $reader.Close() $zipObj.Dispose() [GC]::Collect() [GC]::WaitForPendingFinalizers() $definitionJson = $definitionText | ConvertFrom-Json -ErrorAction SilentlyContinue $definitionModuleName = $null if ($null -ne $definitionJson.Module -and $null -ne $definitionJson.Module.Name) { $definitionModuleName = "$($definitionJson.Module.Name)" } elseif ($null -ne $definitionJson.Modules) { $definitionModuleName = "$($definitionJson.Modules[0].Name)" } elseif ($null -ne $definitionJson.Name) { $definitionModuleName = "$($definitionJson.Name)" } if ($definitionModuleName -eq $ModelName) { Remove-Item -Path $seedZipPath -Force -ErrorAction SilentlyContinue [PsCustomObject]@{ PackageId = "$($packageObj.msprov_fnopackageid)" PackageName = "$($packageObj.msprov_name)" } return } } catch { [GC]::Collect() [GC]::WaitForPendingFinalizers() } Remove-Item -Path $seedZipPath -Force -ErrorAction SilentlyContinue } $null } function Save-ModelSeedPackage { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] [OutputType([string])] param ( [Parameter(Mandatory = $true)] [string] $BaseUri, [Parameter(Mandatory = $true)] [hashtable] $Headers, [Parameter(Mandatory = $true)] [psobject] $Seed, [Parameter(Mandatory = $true)] [string] $ModelName, [Parameter(Mandatory = $true)] [string] $WorkFolder, [Parameter(Mandatory = $true)] [string] $EnvironmentName ) $modelDir = Join-Path $WorkFolder "$EnvironmentName\$ModelName" $seedDir = Join-Path $modelDir "seed" $outDir = Join-Path $modelDir "out" foreach ($dirPath in @($seedDir, $outDir)) { New-Item -Path $dirPath ` -ItemType Directory ` -Force ` -WarningAction SilentlyContinue > $null } $seedZipPath = Join-Path $seedDir "$($ModelName)_seed.zip" try { Invoke-WebRequest -Uri ($BaseUri + "api/data/v9.0/msprov_fnopackages($($Seed.PackageId))/msprov_packagepayload/`$value") ` -Method Get ` -Headers $Headers ` -OutFile $seedZipPath ` -SkipHttpErrorCheck 4> $null } catch { $messageString = "Could not download the seed package for model <c='em'>$ModelName</c>. Please verify network access and try again." Write-PSFMessage -Level Important -Message $messageString -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because the seed package could NOT be downloaded." ` -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) -StepsUpward 1 return } if (-not [System.IO.File]::Exists($seedZipPath)) { $messageString = "Could not download the seed package for model <c='em'>$ModelName</c>. The payload was empty." Write-PSFMessage -Level Important -Message $messageString Stop-PSFFunction -Message "Stopping because the seed package could NOT be downloaded." ` -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) -StepsUpward 1 return } $seedZipPath } function Get-ModelModuleReference { [CmdletBinding()] [OutputType([string[]])] param ( [Parameter(Mandatory = $true)] [string] $SeedZipPath, [Parameter(Mandatory = $true)] [string] $ModelName ) $descriptorPath = "Descriptor/$ModelName.xml" try { $zipObj = [IO.Compression.ZipFile]::OpenRead($SeedZipPath) $descriptorEntry = $zipObj.Entries | Where-Object FullName -eq $descriptorPath | Select-Object -First 1 if ($null -eq $descriptorEntry) { $zipObj.Dispose() [GC]::Collect() [GC]::WaitForPendingFinalizers() return [string[]]@() } $reader = New-Object System.IO.StreamReader($descriptorEntry.Open()) $descriptorText = $reader.ReadToEnd() $reader.Close() $zipObj.Dispose() [GC]::Collect() [GC]::WaitForPendingFinalizers() $descriptorXml = [xml]$descriptorText return [string[]]@($descriptorXml.AxModelInfo.ModuleReferences.string | Where-Object { -not [System.String]::IsNullOrWhiteSpace($_) }) } catch { [GC]::Collect() [GC]::WaitForPendingFinalizers() return [string[]]@() } } function Expand-ModelRemovalList { [CmdletBinding()] [OutputType([string[]])] param ( [Parameter(Mandatory = $true)] [string[]] $RequestedNames, [Parameter(Mandatory = $true)] [string[]] $InstalledNames, [Parameter(Mandatory = $true)] [hashtable] $ReferencesByModel ) $colExpanded = [System.Collections.Generic.List[string]]::new($RequestedNames) $changed = $true while ($changed) { $changed = $false foreach ($installedName in $InstalledNames) { if ($installedName -in $colExpanded) { continue } $dependsOnRemove = @($ReferencesByModel[$installedName] | Where-Object { $_ -in $colExpanded }) if ($dependsOnRemove.Count -gt 0) { Write-PSFMessage -Level Important -Message "Model <c='em'>$installedName</c> depends on $($dependsOnRemove -join ', '). It will also be removed, so the environment stays in a working state." $colExpanded.Add($installedName) $changed = $true } } } return [string[]]@($colExpanded) } function Invoke-ModelDeletePackage { [CmdletBinding()] [OutputType([psobject])] param ( [Parameter(Mandatory = $true)] [string] $BaseUri, [Parameter(Mandatory = $true)] [hashtable] $Headers, [Parameter(Mandatory = $true)] [psobject] $Environment, [Parameter(Mandatory = $true)] [string] $ModelName, [Parameter(Mandatory = $true)] [string] $SeedZipPath, [Parameter(Mandatory = $true)] [string] $WorkFolder, [switch] $WaitForCompletion, [switch] $DownloadLog, [switch] $Force ) $modelDir = Join-Path $WorkFolder "$($Environment.PpacEnvName)\$ModelName" $seedDir = Join-Path $modelDir "seed" $outDir = Join-Path $modelDir "out" foreach ($dirPath in @($seedDir, $outDir)) { New-Item -Path $dirPath ` -ItemType Directory ` -Force ` -WarningAction SilentlyContinue > $null } if ([System.IO.Directory]::Exists($seedDir)) { Get-ChildItem -Path $seedDir -Directory | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue } [IO.Compression.ZipFile]::ExtractToDirectory($SeedZipPath, $seedDir, $true) $definitionPath = Join-Path $seedDir "fnomoduledefinition.json" if (-not [System.IO.File]::Exists($definitionPath)) { $messageString = "The seed package for model <c='em'>$ModelName</c> does not contain fnomoduledefinition.json at the top level. Way 2 needs the seed definition to build the delete package." if ($Force) { Write-PSFMessage -Level Important -Message $messageString Write-PSFMessage -Level Important -Message "Record-only fallback with -Force is not implemented yet. Stopping instead of calling DELETE msprov_fnomodules." } else { Write-PSFMessage -Level Important -Message $messageString } Stop-PSFFunction -Message "Stopping because the seed package is missing fnomoduledefinition.json." ` -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) -StepsUpward 1 return } $correlationId = [guid]::NewGuid().ToString() $definitionJson = Get-Content -Path $definitionPath -Raw | ConvertFrom-Json -ErrorAction SilentlyContinue if ($null -eq $definitionJson) { $messageString = "Could not read fnomoduledefinition.json for model <c='em'>$ModelName</c>. Way 2 needs the seed definition to build the delete package." Write-PSFMessage -Level Important -Message $messageString Stop-PSFFunction -Message "Stopping because fnomoduledefinition.json could NOT be read." ` -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) -StepsUpward 1 return } Set-ModelDeleteDefinition -Definition $definitionJson -CorrelationId $correlationId $definitionJson | ConvertTo-Json -Depth 10 | Set-Content -Path $definitionPath -Encoding utf8 $deleteZipName = "$($ModelName)_1_0_0_1_managed.zip" $deleteZipPath = Join-Path $outDir $deleteZipName if ([System.IO.File]::Exists($deleteZipPath)) { Remove-Item -Path $deleteZipPath -Force -ErrorAction SilentlyContinue } Compress-Archive -Path (Join-Path $seedDir "*") -DestinationPath $deleteZipPath -Force $packageName = "$($ModelName)_Delete_$(Get-Date -Format 'yyyyMMddHHmmss')_$($correlationId.Substring(0, 8))" $packageBody = @{ "msprov_name" = $packageName "msprov_buildtype" = 2 "msprov_packagetype" = 0 "msprov_dbsyncoptions" = 0 } | ConvertTo-Json -Depth 3 $packageHeaders = $Headers.Clone() $packageHeaders["Prefer"] = "return=representation" $packageResponse = Invoke-RestMethod -Uri ($BaseUri + "api/data/v9.0/msprov_fnopackages") ` -Method Post ` -Headers $packageHeaders ` -Body $packageBody ` -ContentType "application/json" ` -ResponseHeadersVariable packageRespHeaders $packageId = "$($packageResponse.msprov_fnopackageid)" if ([System.String]::IsNullOrWhiteSpace($packageId) -and $null -ne $packageRespHeaders) { $entityId = @($packageRespHeaders["OData-EntityId"] | Select-Object -First 1) if ($entityId -match 'msprov_fnopackages\(([^)]+)\)') { $packageId = $Matches[1] } } if ([System.String]::IsNullOrWhiteSpace($packageId)) { $messageString = "Could not create the delete package row for model <c='em'>$ModelName</c>. The server did not return a package id." Write-PSFMessage -Level Important -Message $messageString Stop-PSFFunction -Message "Stopping because the delete package row could NOT be created." ` -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) -StepsUpward 1 return } $uploadOk = Send-ModelPackagePayload -BaseUri $BaseUri ` -Headers $Headers ` -PackageId $packageId ` -ZipPath $deleteZipPath ` -ZipName $deleteZipName if (-not $uploadOk) { return } $moduleUri = $BaseUri + "api/data/v9.0/msprov_fnomodules" $moduleResponse = Invoke-RestMethod -Uri ($moduleUri + "?`$filter=msprov_name eq '$ModelName'&`$select=msprov_fnomoduleid,msprov_name") ` -Method Get ` -Headers $Headers | ` Select-Object -ExpandProperty value $moduleRow = @($moduleResponse) | Select-Object -First 1 if ($null -ne $moduleRow) { $moduleBody = @{ "msprov_name" = $ModelName } | ConvertTo-Json -Depth 3 Invoke-RestMethod -Uri ($BaseUri + "api/data/v9.0/msprov_fnomodules($($moduleRow.msprov_fnomoduleid))") ` -Method Patch ` -Headers $Headers ` -Body $moduleBody ` -ContentType "application/json" > $null } else { $moduleBody = @{ "msprov_name" = $ModelName } | ConvertTo-Json -Depth 3 Invoke-RestMethod -Uri $moduleUri ` -Method Post ` -Headers $Headers ` -Body $moduleBody ` -ContentType "application/json" > $null } $deployBody = @{ "finopspackages" = @( @{ "@odata.type" = "Microsoft.Dynamics.CRM.msprov_fnopackage" "msprov_fnopackageid" = $packageId } ) "packagetype" = 0 "buildtype" = 2 } | ConvertTo-Json -Depth 5 $deployResponse = Invoke-RestMethod -Uri ($BaseUri + "api/data/v9.2/msprov_deploypackagetofinopsasync") ` -Method Post ` -Headers $Headers ` -Body $deployBody ` -ContentType "application/json" $asyncOperationId = "$($deployResponse.asyncoperationid)" if ([System.String]::IsNullOrWhiteSpace($asyncOperationId)) { $asyncOperationId = "$($deployResponse.AsyncOperationId)" } $operationResult = [PsCustomObject][ordered]@{ Environment = "$($Environment.PpacEnvName)" Model = $ModelName PackageId = $packageId PackageName = $packageName CorrelationId = $correlationId AsyncOperation = $asyncOperationId ZipPath = $deleteZipPath Status = "Started" PSTypeName = "D365Bap.Tools.UdeEnvironmentModelDelete" } if (-not $WaitForCompletion) { $operationResult return } $terminalState = Wait-ModelAsyncOperation -BaseUri $BaseUri ` -Headers $Headers ` -AsyncOperationId $asyncOperationId if ($null -eq $terminalState) { return } $operationResult | Add-Member -NotePropertyName "OperationState" -NotePropertyValue "$($terminalState.statecode)" -Force $operationResult | Add-Member -NotePropertyName "OperationStatus" -NotePropertyValue "$($terminalState.statuscode)" -Force $operationResult | Add-Member -NotePropertyName "OperationMessage" -NotePropertyValue "$($terminalState.message)" -Force $operationResult.Status = "Completed" if ($DownloadLog) { $logDir = Join-Path $modelDir "logs" New-Item -Path $logDir ` -ItemType Directory ` -Force ` -WarningAction SilentlyContinue > $null Save-ModelOperationLog -BaseUri $BaseUri ` -Headers $Headers ` -CorrelationId $correlationId ` -LogDir $logDir $operationResult | Add-Member -NotePropertyName "LogDir" -NotePropertyValue $logDir -Force } $verifyModels = @(Get-UdeEnvironmentModel -EnvironmentId $Environment.PpacEnvId | Select-Object -ExpandProperty Name) if ($ModelName -in $verifyModels) { Write-PSFMessage -Level Important -Message "Model <c='em'>$ModelName</c> is still reported on the environment. It may take a while before the delete is visible." $operationResult.Status = "VerifyPending" } else { Write-PSFMessage -Level Important -Message "Model <c='em'>$ModelName</c> is gone from the environment. Remaining models: $($verifyModels -join ', ')." } $operationResult | Select-PSFObject -TypeName "D365Bap.Tools.UdeEnvironmentModelDelete" -Property * } function Set-ModelDeleteDefinition { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] [OutputType()] param ( [Parameter(Mandatory = $true)] [psobject] $Definition, [Parameter(Mandatory = $true)] [string] $CorrelationId ) if ($null -ne $Definition.Module) { $Definition.Module | Add-Member -NotePropertyName "BuildType" -NotePropertyValue "Delete" -Force } else { $Definition | Add-Member -NotePropertyName "BuildType" -NotePropertyValue "Delete" -Force } $Definition | Add-Member -NotePropertyName "BuildType" -NotePropertyValue "Delete" -Force $Definition | Add-Member -NotePropertyName "PackageType" -NotePropertyValue "Dev" -Force $Definition | Add-Member -NotePropertyName "PackageVersion" -NotePropertyValue "1.0.0.1" -Force $Definition | Add-Member -NotePropertyName "CorrelationID" -NotePropertyValue $CorrelationId -Force $Definition | Add-Member -NotePropertyName "TimestampUtc" -NotePropertyValue ([datetime]::UtcNow.ToString("o")) -Force if ($null -ne $Definition.DBSync) { $Definition.DBSync | Add-Member -NotePropertyName "SyncKind" -NotePropertyValue "None" -Force $Definition.DBSync | Add-Member -NotePropertyName "Arguments" -NotePropertyValue "" -Force } else { $Definition | Add-Member -NotePropertyName "DBSync" -NotePropertyValue ([PsCustomObject]@{ "SyncKind" = "None" "Arguments" = "" }) -Force } } function Send-ModelPackagePayload { [CmdletBinding()] [OutputType([bool])] param ( [Parameter(Mandatory = $true)] [string] $BaseUri, [Parameter(Mandatory = $true)] [hashtable] $Headers, [Parameter(Mandatory = $true)] [string] $PackageId, [Parameter(Mandatory = $true)] [string] $ZipPath, [Parameter(Mandatory = $true)] [string] $ZipName ) $initBody = @{ "Target" = "msprov_fnopackages($PackageId)" "FileAttributeName" = "msprov_packagepayload" "FileName" = $ZipName } | ConvertTo-Json -Depth 3 $initResponse = Invoke-RestMethod -Uri ($BaseUri + "api/data/v9.2/InitializeFileBlocksUpload") ` -Method Post ` -Headers $Headers ` -Body $initBody ` -ContentType "application/json" $continuationToken = "$($initResponse.FileContinuationToken)" if ([System.String]::IsNullOrWhiteSpace($continuationToken)) { $messageString = "Could not initialize the file upload for package <c='em'>$PackageId</c>. The server did not return a continuation token." Write-PSFMessage -Level Important -Message $messageString Stop-PSFFunction -Message "Stopping because the file upload could NOT be initialized." ` -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) -StepsUpward 1 return $false } $blockSize = 4MB $zipBytes = [System.IO.File]::ReadAllBytes($ZipPath) $blockCount = [Math]::Ceiling($zipBytes.Length / $blockSize) $blockIds = @() for ($blockIndex = 0; $blockIndex -lt $blockCount; $blockIndex++) { $offset = $blockIndex * $blockSize $length = [Math]::Min($blockSize, $zipBytes.Length - $offset) $blockBytes = New-Object byte[] $length [System.Array]::Copy($zipBytes, $offset, $blockBytes, 0, $length) $blockId = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes("block-{0:D7}" -f $blockIndex)) $blockIds += $blockId $blockBody = @{ "BlockId" = $blockId "BlockData" = [Convert]::ToBase64String($blockBytes) "FileContinuationToken" = $continuationToken } | ConvertTo-Json -Depth 3 Invoke-RestMethod -Uri ($BaseUri + "api/data/v9.2/UploadBlock") ` -Method Post ` -Headers $Headers ` -Body $blockBody ` -ContentType "application/json" > $null } $commitBody = @{ "FileName" = $ZipName "MimeType" = "application/zip" "BlockList" = $blockIds "FileContinuationToken" = $continuationToken } | ConvertTo-Json -Depth 5 Invoke-RestMethod -Uri ($BaseUri + "api/data/v9.2/CommitFileBlocksUpload") ` -Method Post ` -Headers $Headers ` -Body $commitBody ` -ContentType "application/json" > $null $true } function Wait-ModelAsyncOperation { [CmdletBinding()] [OutputType([psobject])] param ( [Parameter(Mandatory = $true)] [string] $BaseUri, [Parameter(Mandatory = $true)] [hashtable] $Headers, [Parameter(Mandatory = $true)] [string] $AsyncOperationId ) if ([System.String]::IsNullOrWhiteSpace($AsyncOperationId)) { $messageString = "The server did not return an async operation id. Cannot poll for completion." Write-PSFMessage -Level Important -Message $messageString Stop-PSFFunction -Message "Stopping because the async operation id is missing." ` -Exception $([System.Exception]::new($messageString)) -StepsUpward 1 return } do { Start-Sleep -Seconds 20 $asyncState = Invoke-RestMethod -Uri ($BaseUri + "api/data/v9.0/asyncoperations($AsyncOperationId)?`$select=statuscode,statecode,message,friendlymessage,errorcode") ` -Method Get ` -Headers $Headers $stateValue = "$($asyncState.statecode)" Write-PSFMessage -Level Verbose -Message "Delete deployment state: $stateValue. Status: $($asyncState.statuscode). Message: $($asyncState.message)" } while ($stateValue -notin @("Completed", "Failed", "Canceled", "3", "30", "31", "32")) $asyncState } function Save-ModelOperationLog { [CmdletBinding()] [OutputType()] param ( [Parameter(Mandatory = $true)] [string] $BaseUri, [Parameter(Mandatory = $true)] [hashtable] $Headers, [Parameter(Mandatory = $true)] [string] $CorrelationId, [Parameter(Mandatory = $true)] [string] $LogDir ) $historyUri = $BaseUri + "api/data/v9.0/msprov_operationhistories?`$filter=msprov_correlationid eq '$CorrelationId'&`$orderby=modifiedon desc" $colHistory = Invoke-RestMethod -Uri $historyUri ` -Method Get ` -Headers $Headers | ` Select-Object -ExpandProperty value foreach ($historyObj in @($colHistory)) { $logFilePath = Join-Path $LogDir "$($historyObj.msprov_operationhistoryid).log" try { Invoke-WebRequest -Uri ($BaseUri + "api/data/v9.0/msprov_operationhistories($($historyObj.msprov_operationhistoryid))/msprov_logs/`$value") ` -Method Get ` -Headers $Headers ` -OutFile $logFilePath ` -SkipHttpErrorCheck 4> $null } catch { Write-PSFMessage -Level Important -Message "Could not download the log for operation <c='em'>$($historyObj.msprov_operationhistoryid)</c>." -Exception $PSItem.Exception } } } |