d365bap.tools.psm1
$script:ModuleRoot = $PSScriptRoot $script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\d365bap.tools.psd1").ModuleVersion # Detect whether at some level dotsourcing was enforced $script:doDotSource = Get-PSFConfigValue -FullName d365bap.tools.Import.DoDotSource -Fallback $false if ($d365bap.tools_dotsourcemodule) { $script:doDotSource = $true } <# Note on Resolve-Path: All paths are sent through Resolve-Path/Resolve-PSFPath in order to convert them to the correct path separator. This allows ignoring path separators throughout the import sequence, which could otherwise cause trouble depending on OS. Resolve-Path can only be used for paths that already exist, Resolve-PSFPath can accept that the last leaf my not exist. This is important when testing for paths. #> # Detect whether at some level loading individual module files, rather than the compiled module was enforced $importIndividualFiles = Get-PSFConfigValue -FullName d365bap.tools.Import.IndividualFiles -Fallback $false if ($d365bap.tools_importIndividualFiles) { $importIndividualFiles = $true } if (Test-Path (Resolve-PSFPath -Path "$($script:ModuleRoot)\..\.git" -SingleItem -NewChild)) { $importIndividualFiles = $true } if ("<was compiled>" -eq '<was not compiled>') { $importIndividualFiles = $true } function Import-ModuleFile { <# .SYNOPSIS Loads files into the module on module import. .DESCRIPTION This helper function is used during module initialization. It should always be dotsourced itself, in order to proper function. This provides a central location to react to files being imported, if later desired .PARAMETER Path The path to the file to load .EXAMPLE PS C:\> . Import-ModuleFile -File $function.FullName Imports the file stored in $function according to import policy #> [CmdletBinding()] Param ( [string] $Path ) $resolvedPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($Path).ProviderPath if ($doDotSource) { . $resolvedPath } else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText($resolvedPath))), $null, $null) } } #region Load individual files if ($importIndividualFiles) { # Execute Preimport actions foreach ($path in (& "$ModuleRoot\internal\scripts\preimport.ps1")) { . Import-ModuleFile -Path $path } # Import all internal functions foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) { . Import-ModuleFile -Path $function.FullName } # Import all public functions foreach ($function in (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) { . Import-ModuleFile -Path $function.FullName } # Execute Postimport actions foreach ($path in (& "$ModuleRoot\internal\scripts\postimport.ps1")) { . Import-ModuleFile -Path $path } # End it here, do not load compiled code below return } #endregion Load individual files #region Load compiled code <# This file loads the strings documents from the respective language folders. This allows localizing messages and errors. Load psd1 language files for each language you wish to support. Partial translations are acceptable - when missing a current language message, it will fallback to English or another available language. #> Import-PSFLocalizedString -Path "$($script:ModuleRoot)\en-us\*.psd1" -Module 'd365bap.tools' -Language 'en-US' <# .SYNOPSIS Get language from Environment .DESCRIPTION Fetches all languages from the environment .PARAMETER BaseUri Base Web API URI for the environment Used to construct the correct REST API Url, based on the WebApi / OData endpoint .EXAMPLE PS C:\> Get-EnvironmentLanguage -BaseUri 'https://temp-test.crm4.dynamics.com' This will fetch all languages from the environment. Uses the WebAPI / OData endpoint. .NOTES Author: Mötz Jensen (@Splaxi) #> function Get-EnvironmentLanguage { [CmdletBinding()] param ( [parameter (mandatory = $true)] [string] $BaseUri ) begin { $tokenWebApi = Get-AzAccessToken -ResourceUrl $BaseUri $headersWebApi = @{ "Authorization" = "Bearer $($tokenWebApi.Token)" } $resOrg = Invoke-RestMethod -Method Get -Uri $($baseUri + '/api/data/v9.2/organizations?$select=organizationid,orgdborgsettings,languagecode,localeid,name') -Headers $headersWebApi | Select-Object -ExpandProperty value | Select-Object -First 1 $resLangs = Invoke-RestMethod -Method Get -Uri "$BaseUri/api/data/v9.2/languagelocale" -Headers $headersWebApi | Select-Object -ExpandProperty value } process { foreach ($lanObj in $resLangs) { if ($lanObj.localeid -eq $resOrg.localeid) { # Could also be "languagecode" - maybe we'll get more info later on $lanObj | Add-Member -MemberType NoteProperty -Name "BaseLocaleId" -Value 0 } } $resLangs } } <# .SYNOPSIS Compare environment D365 Apps .DESCRIPTION Enables the user to compare 2 x environments, with one as a source and the other as a destination It will only look for installed D365 Apps on the source, and use this as a baseline against the destination .PARAMETER SourceEnvironmentId Environment Id of the source environment that you want to utilized as the baseline for the compare .PARAMETER DestinationEnvironmentId Environment Id of the destination environment that you want to validate against the baseline (source) .PARAMETER ShowDiffOnly Instruct the cmdlet to only output the differences that are not aligned between the source and destination .PARAMETER GeoRegion Instructs the cmdlet which Geo / Region the environment is located The default value is: "Emea" This is mandatory field from the API specification, we don't have the full list of values at the time of writing .PARAMETER AsExcelOutput Instruct the cmdlet to output all details directly to an Excel file This makes it easier to deep dive into all the details returned from the API, and makes it possible for the user to persist the current state .EXAMPLE PS C:\> Compare-BapEnvironmentD365App -SourceEnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -DestinationEnvironmentId 32c6b196-ef52-4c43-93cf-6ecba51e6aa1 This will get all installed D365 Apps from the Source Environment. It will iterate over all of them, and validate against the Destination Environment. Sample output: PackageId PackageName SourceVersion DestinationVersion AppName --------- ----------- ------------- ------------------ ------- ea8d3b2f-ede2-46b4-900d-ed02c81c44fd AgentProductivityToolsAnchor 9.2.24021.1005 9.2.24012.1005 Agent Prod… 1c0a1237-9408-4b99-9fec-39696d99287b msdyn_AppProfileManagerAnchor 10.1.24021.1005 10.1.24012.1013 appprofile… 6ce2d70e-78bf-4ff6-85ed-1bd63d4ab444 ExportToDataLakeCoreAnchor 1.0.0.1 0.0.0.0 Azure Syna… 42cc1442-194f-462b-a325-ce5b5f18c02d msdyn_EmailAddressValidation 1.0.0.4 1.0.0.4 Data Valid… .EXAMPLE PS C:\> Compare-BapEnvironmentD365App -SourceEnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -DestinationEnvironmentId 32c6b196-ef52-4c43-93cf-6ecba51e6aa1 -ShowDiffOnly This will get all installed D365 Apps from the Source Environment. It will iterate over all of them, and validate against the Destination Environment. It will filter out results, to only include those where the DestinationVersions is different from the SourceVersion. Sample output: PackageId PackageName SourceVersion DestinationVersion AppName --------- ----------- ------------- ------------------ ------- ea8d3b2f-ede2-46b4-900d-ed02c81c44fd AgentProductivityToolsAnchor 9.2.24021.1005 9.2.24012.1005 Agent Prod… 1c0a1237-9408-4b99-9fec-39696d99287b msdyn_AppProfileManagerAnchor 10.1.24021.1005 10.1.24012.1013 appprofile… 6ce2d70e-78bf-4ff6-85ed-1bd63d4ab444 ExportToDataLakeCoreAnchor 1.0.0.1 0.0.0.0 Azure Syna… 7523d261-f1be-46e7-8e68-f3de16eeabbb DualWriteCoreAnchor 1.0.24022.4 1.0.24011.1 Dual-write… .EXAMPLE PS C:\> Compare-BapEnvironmentD365App -SourceEnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -DestinationEnvironmentId 32c6b196-ef52-4c43-93cf-6ecba51e6aa1 -AsExcelOutput This will get all installed D365 Apps from the Source Environment. It will iterate over all of them, and validate against the Destination Environment. Will output all details into an Excel file, that will auto open on your machine. .NOTES Author: Mötz Jensen (@Splaxi) #> function Compare-BapEnvironmentD365App { [CmdletBinding()] param ( [parameter (mandatory = $true)] [string] $SourceEnvironmentId, [parameter (mandatory = $true)] [string] $DestinationEnvironmentId, [switch] $ShowDiffOnly, [string] $GeoRegion = "Emea", [switch] $AsExcelOutput ) begin { # Make sure all *BapEnvironment* cmdlets will validate that the environment exists prior running anything. $envSourceObj = Get-BapEnvironment -EnvironmentId $SourceEnvironmentId | Select-Object -First 1 if ($null -eq $envSourceObj) { $messageString = "The supplied SourceEnvironmentId: <c='em'>$SourceEnvironmentId</c> didn't return any matching environment details. Please verify that the SourceEnvironmentId is correct - try running the <c='em'>Get-BapEnvironment</c> cmdlet." Write-PSFMessage -Level Host -Message $messageString Stop-PSFFunction -Message "Stopping because environment was NOT found based on the id." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) } # Make sure all *BapEnvironment* cmdlets will validate that the environment exists prior running anything. $envDestinationObj = Get-BapEnvironment -EnvironmentId $DestinationEnvironmentId | Select-Object -First 1 if ($null -eq $envDestinationObj) { $messageString = "The supplied DestinationEnvironmentId: <c='em'>$DestinationEnvironmentId</c> didn't return any matching environment details. Please verify that the DestinationEnvironmentId is correct - try running the <c='em'>Get-BapEnvironment</c> cmdlet." Write-PSFMessage -Level Host -Message $messageString Stop-PSFFunction -Message "Stopping because environment was NOT found based on the id." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) } if (Test-PSFFunctionInterrupt) { return } $appsSourceEnvironment = Get-BapEnvironmentD365App -EnvironmentId $SourceEnvironmentId -InstallState Installed -GeoRegion $GeoRegion $appsDestinationEnvironment = Get-BapEnvironmentD365App -EnvironmentId $DestinationEnvironmentId -GeoRegion $GeoRegion } process { if (Test-PSFFunctionInterrupt) { return } $resCol = @(foreach ($sourceApp in $($appsSourceEnvironment | Sort-Object -Property ApplicationName )) { $destinationApp = $appsDestinationEnvironment | Where-Object PackageId -eq $sourceApp.PackageId | Select-Object -First 1 $sourceVersion = if ($sourceApp.InstalledVersion -eq "N/A") { [System.Version]"0.0.0.0" } else { [System.Version]$sourceApp.InstalledVersion } $tmp = [Ordered]@{ PackageId = $sourceApp.PackageId PackageName = $sourceApp.PackageName AppName = $sourceApp.AppName SourceVersion = $sourceVersion DestinationVersion = "Missing" } if (-not ($null -eq $destinationApp)) { $tmp.DestinationVersion = if ($destinationApp.InstalledVersion -eq "N/A") { [System.Version]"0.0.0.0" }else { [System.Version]$destinationApp.InstalledVersion } } ([PSCustomObject]$tmp) | Select-PSFObject -TypeName "D365Bap.Tools.Compare.Package" } ) if ($ShowDiffOnly) { $resCol = $resCol | Where-Object { $_.SourceVersion -ne $_.DestinationVersion } } if ($AsExcelOutput) { $resCol | Export-Excel -NoNumberConversion SourceVersion, DestinationVersion return } $resCol } end { } } <# .SYNOPSIS Compare the environment users .DESCRIPTION Enables the user to compare 2 x environments, with one as a source and the other as a destination It will only look for users on the source, and use this as a baseline against the destination .PARAMETER SourceEnvironmentId Environment Id of the source environment that you want to utilized as the baseline for the compare .PARAMETER DestinationEnvironmentId Environment Id of the destination environment that you want to validate against the baseline (source) .PARAMETER ShowDiffOnly Instruct the cmdlet to only output the differences that are not aligned between the source and destination .PARAMETER IncludeAppIds Instruct the cmdlet to also include the users with the ApplicationId property filled .PARAMETER AsExcelOutput Instruct the cmdlet to output all details directly to an Excel file This makes it easier to deep dive into all the details returned from the API, and makes it possible for the user to persist the current state .EXAMPLE PS C:\> Compare-BapEnvironmentD365App -SourceEnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -DestinationEnvironmentId 32c6b196-ef52-4c43-93cf-6ecba51e6aa1 This will get all system users from the Source Environment. It will iterate over all of them, and validate against the Destination Environment. It will exclude those with ApplicationId filled. Sample output: Email Name AppId SourceId DestinationId ----- ---- ----- -------- ------------- aba@temp.com Austin Baker f85bcd69-ef72-… 5aaac0ec-a91… ade@temp.com Alex Denver 39309a5c-7676-… 1d521227-43b… .EXAMPLE PS C:\> Compare-BapEnvironmentD365App -SourceEnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -DestinationEnvironmentId 32c6b196-ef52-4c43-93cf-6ecba51e6aa1 -IncludeAppIds This will get all system users from the Source Environment. It will iterate over all of them, and validate against the Destination Environment. It will include those with ApplicationId filled. Sample output: Email Name AppId SourceId DestinationId ----- ---- ----- -------- ------------- aba@temp.com Austin Baker f85bcd69-ef72-… 5aaac0ec-a91… ade@temp.com Alex Denver 39309a5c-7676-… 1d521227-43b… AIBuilder_StructuredML_Prod_C… AIBuilder_StructuredML_Prod_C… ff8a1ad8-a415-45c1-… 95dc9ca2-8185-… 328db0cc-14c… AIBuilderProd@onmicrosoft.com AIBuilderProd, # 0a143f2d-2320-4141-… c96f82b8-320f-… 1831f4dc-4c5… .EXAMPLE PS C:\> Compare-BapEnvironmentD365App -SourceEnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -DestinationEnvironmentId 32c6b196-ef52-4c43-93cf-6ecba51e6aa1 -IncludeAppIds -ShowDiffOnly This will get all system users from the Source Environment. It will iterate over all of them, and validate against the Destination Environment. It will include those with ApplicationId filled. It will only output the users that is missing in the destionation environment. Sample output: Email Name AppId SourceId DestinationId ----- ---- ----- -------- ------------- d365-scm-operationdataservice… d365-scm-operationdataservice… 986556ed-a409-4339-… 5e077e6a-a0c9-… Missing d365-scm-operationdataservice… d365-scm-operationdataservice… 14e80222-1878-455d-… 183ec023-9ccb-… Missing def@temp.com Dustin Effect 01e37132-0a44-… Missing .EXAMPLE PS C:\> Compare-BapEnvironmentD365App -SourceEnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -DestinationEnvironmentId 32c6b196-ef52-4c43-93cf-6ecba51e6aa1 -AsExcelOutput This will get all system users from the Source Environment. It will iterate over all of them, and validate against the Destination Environment. It will exclude those with ApplicationId filled. Will output all details into an Excel file, that will auto open on your machine. .NOTES Author: Mötz Jensen (@Splaxi) #> function Compare-BapEnvironmentUser { [CmdletBinding()] param ( [parameter (mandatory = $true)] [string] $SourceEnvironmentId, [parameter (mandatory = $true)] [string] $DestinationEnvironmentId, [switch] $ShowDiffOnly, [switch] $IncludeAppIds, [switch] $AsExcelOutput ) begin { # Make sure all *BapEnvironment* cmdlets will validate that the environment exists prior running anything. $envSourceObj = Get-BapEnvironment -EnvironmentId $SourceEnvironmentId | Select-Object -First 1 if ($null -eq $envSourceObj) { $messageString = "The supplied SourceEnvironmentId: <c='em'>$SourceEnvironmentId</c> didn't return any matching environment details. Please verify that the SourceEnvironmentId is correct - try running the <c='em'>Get-BapEnvironment</c> cmdlet." Write-PSFMessage -Level Host -Message $messageString Stop-PSFFunction -Message "Stopping because environment was NOT found based on the id." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) } # Make sure all *BapEnvironment* cmdlets will validate that the environment exists prior running anything. $envDestinationObj = Get-BapEnvironment -EnvironmentId $DestinationEnvironmentId | Select-Object -First 1 if ($null -eq $envDestinationObj) { $messageString = "The supplied DestinationEnvironmentId: <c='em'>$DestinationEnvironmentId</c> didn't return any matching environment details. Please verify that the DestinationEnvironmentId is correct - try running the <c='em'>Get-BapEnvironment</c> cmdlet." Write-PSFMessage -Level Host -Message $messageString Stop-PSFFunction -Message "Stopping because environment was NOT found based on the id." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) } if (Test-PSFFunctionInterrupt) { return } $usersSourceEnvironment = Get-BapEnvironmentUser -EnvironmentId $SourceEnvironmentId -IncludeAppIds:$IncludeAppIds $usersDestinationEnvironment = Get-BapEnvironmentUser -EnvironmentId $DestinationEnvironmentId -IncludeAppIds:$IncludeAppIds } process { if (Test-PSFFunctionInterrupt) { return } $resCol = @(foreach ($sourceUser in $($usersSourceEnvironment | Sort-Object -Property Email )) { if ([System.String]::IsNullOrEmpty($sourceUser.Email)) { continue } $destinationUser = $usersDestinationEnvironment | Where-Object Email -eq $sourceUser.Email | Select-Object -First 1 $tmp = [Ordered]@{ Email = $sourceUser.Email Name = $sourceUser.Name AppId = $sourceUser.AppId SourceId = $sourceUser.systemuserid DestinationId = "Missing" } if (-not ($null -eq $destinationUser)) { $tmp.DestinationId = $destinationUser.systemuserid } ([PSCustomObject]$tmp) | Select-PSFObject -TypeName "D365Bap.Tools.Compare.User" } ) if ($ShowDiffOnly) { $resCol = $resCol | Where-Object { $_.DestinationId -eq "Missing" } } if ($AsExcelOutput) { $resCol | Export-Excel -NoNumberConversion SourceVersion, DestinationVersion return } $resCol } end { } } <# .SYNOPSIS Compare environment Virtual Entities .DESCRIPTION Enables the user to compare 2 x environments, with one as a source and the other as a destination It will only look for enabled / visible Virtual Entities on the source, and use this as a baseline against the destination .PARAMETER SourceEnvironmentId Environment Id of the source environment that you want to utilized as the baseline for the compare .PARAMETER DestinationEnvironmentId Environment Id of the destination environment that you want to validate against the baseline (source) .PARAMETER ShowDiffOnly Instruct the cmdlet to only output the differences that are not aligned between the source and destination .PARAMETER AsExcelOutput Instruct the cmdlet to output all details directly to an Excel file This makes it easier to deep dive into all the details returned from the API, and makes it possible for the user to persist the current state .EXAMPLE PS C:\> Compare-BapEnvironmentVirtualEntity -SourceEnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -DestinationEnvironmentId 32c6b196-ef52-4c43-93cf-6ecba51e6aa1 This will get all enabled / visible Virtual Entities from the Source Environment. It will iterate over all of them, and validate against the Destination Environment. Sample output: EntityName SourceIsVisible SourceChangeTrackingEnabled Destination DestinationChange IsVisible TrackingEnabled ---------- --------------- --------------------------- ----------- ----------------- AccountantEntity True False True False CurrencyEntity True False False False WMHEOutboundQueueEntity True False False False .EXAMPLE PS C:\> Compare-BapEnvironmentVirtualEntity -SourceEnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -DestinationEnvironmentId 32c6b196-ef52-4c43-93cf-6ecba51e6aa1 -ShowDiffOnly This will get all enabled / visible Virtual Entities from the Source Environment. It will iterate over all of them, and validate against the Destination Environment. It will filter out results, to only include those where the Source is different from the Destination. Sample output: EntityName SourceIsVisible SourceChangeTrackingEnabled Destination DestinationChange IsVisible TrackingEnabled ---------- --------------- --------------------------- ----------- ----------------- CurrencyEntity True False False False WMHEOutboundQueueEntity True False False False .EXAMPLE PS C:\> Compare-BapEnvironmentVirtualEntity -SourceEnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -DestinationEnvironmentId 32c6b196-ef52-4c43-93cf-6ecba51e6aa1 This will get all enabled / visible Virtual Entities from the Source Environment. It will iterate over all of them, and validate against the Destination Environment. Will output all details into an Excel file, that will auto open on your machine. .NOTES Author: Mötz Jensen (@Splaxi) #> function Compare-BapEnvironmentVirtualEntity { [CmdletBinding()] param ( [parameter (mandatory = $true)] [string] $SourceEnvironmentId, [parameter (mandatory = $true)] [string] $DestinationEnvironmentId, [switch] $ShowDiffOnly, [switch] $AsExcelOutput ) begin { # Make sure all *BapEnvironment* cmdlets will validate that the environment exists prior running anything. $envSourceObj = Get-BapEnvironment -EnvironmentId $SourceEnvironmentId | Select-Object -First 1 if ($null -eq $envSourceObj) { $messageString = "The supplied SourceEnvironmentId: <c='em'>$SourceEnvironmentId</c> didn't return any matching environment details. Please verify that the SourceEnvironmentId is correct - try running the <c='em'>Get-BapEnvironment</c> cmdlet." Write-PSFMessage -Level Host -Message $messageString Stop-PSFFunction -Message "Stopping because environment was NOT found based on the id." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) } # Make sure all *BapEnvironment* cmdlets will validate that the environment exists prior running anything. $envDestinationObj = Get-BapEnvironment -EnvironmentId $DestinationEnvironmentId | Select-Object -First 1 if ($null -eq $envDestinationObj) { $messageString = "The supplied DestinationEnvironmentId: <c='em'>$DestinationEnvironmentId</c> didn't return any matching environment details. Please verify that the DestinationEnvironmentId is correct - try running the <c='em'>Get-BapEnvironment</c> cmdlet." Write-PSFMessage -Level Host -Message $messageString Stop-PSFFunction -Message "Stopping because environment was NOT found based on the id." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) } if (Test-PSFFunctionInterrupt) { return } $entitiesSourceEnvironment = @(Get-BapEnvironmentVirtualEntity -EnvironmentId $SourceEnvironmentId -VisibleOnly) $entitiesDestinationEnvironment = @( foreach ($entName in $entitiesSourceEnvironment.EntityName) { Get-BapEnvironmentVirtualEntity -EnvironmentId $DestinationEnvironmentId -Name $entName } ) } process { if (Test-PSFFunctionInterrupt) { return } $resCol = @(foreach ($sourceEntity in $($entitiesSourceEnvironment | Sort-Object -Property EntityName )) { $destinationEntity = $entitiesDestinationEnvironment | Where-Object EntityName -eq $sourceEntity.EntityName | Select-Object -First 1 $tmp = [Ordered]@{ EntityName = $sourceEntity.EntityName SourceIsVisible = $sourceEntity.IsVisible SourceChangeTrackingEnabled = $sourceEntity.ChangeTrackingEnabled SourceEntityGuid = $sourceEntity.EntityGuid DestinationIsVisible = "" DestinationChangeTrackingEnabled = "" DestinationEntityGuid = "" } if (-not ($null -eq $destinationEntity)) { $tmp.DestinationIsVisible = $destinationEntity.IsVisible $tmp.DestinationChangeTrackingEnabled = $destinationEntity.ChangeTrackingEnabled $tmp.DestinationEntityGuid = $destinationEntity.EntityGuid } ([PSCustomObject]$tmp) | Select-PSFObject -TypeName "D365Bap.Tools.Compare.VirtualEntity" } ) if ($ShowDiffOnly) { $resCol = $resCol | Where-Object { ($_.SourceIsVisible -ne $_.DestinationIsVisible) -or ($_.SourceChangeTrackingEnabled -ne $_.DestinationChangeTrackingEnabled) } } if ($AsExcelOutput) { $resCol | Export-Excel -NoNumberConversion SourceVersion, DestinationVersion return } $resCol } end { } } <# .SYNOPSIS Test the integration status .DESCRIPTION Invokes the validation of the PowerPlatform integration, from the Dataverse perspective If it returns an output, the Dataverse is fully connected to the D365FO environment .PARAMETER EnvironmentId The id of the environment that you want to work against .PARAMETER AsExcelOutput Instruct the cmdlet to output all details directly to an Excel file This makes it easier to deep dive into all the details returned from the API, and makes it possible for the user to persist the current state .EXAMPLE PS C:\> Confirm-BapEnvironmentIntegration -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 This will invoke the validation from the Dataverse environment. It will only output details if the environment is fully connected and working. Sample output: LinkedAppLcsEnvId LinkedAppLcsEnvUri IsUnifiedDatabase TenantId ----------------- ------------------ ----------------- -------- 0e52661c-0225-4621-b1b4-804712cf6d9a https://new-test.sandbox.operations.eu.dynamics.c… False 8ccb796b-37b… .EXAMPLE PS C:\> Confirm-BapEnvironmentIntegration -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -AsExcelOutput This will invoke the validation from the Dataverse environment. It will only output details if the environment is fully connected and working. Will output all details into an Excel file, that will auto open on your machine. The excel file will be empty if the integration isn't working. .NOTES Author: Mötz Jensen (@Splaxi) #> function Confirm-BapEnvironmentIntegration { [CmdletBinding()] param ( [parameter (mandatory = $true)] [string] $EnvironmentId, [switch] $AsExcelOutput ) begin { # Make sure all *BapEnvironment* cmdlets will validate that the environment exists prior running anything. $envObj = Get-BapEnvironment -EnvironmentId $EnvironmentId | Select-Object -First 1 if ($null -eq $envObj) { $messageString = "The supplied EnvironmentId: <c='em'>$EnvironmentId</c> didn't return any matching environment details. Please verify that the EnvironmentId is correct - try running the <c='em'>Get-BapEnvironment</c> cmdlet." Write-PSFMessage -Level Host -Message $messageString Stop-PSFFunction -Message "Stopping because environment was NOT found based on the id." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) } if (Test-PSFFunctionInterrupt) { return } $baseUri = $envObj.LinkedMetaPpacEnvUri $tokenWebApi = Get-AzAccessToken -ResourceUrl $baseUri $headersWebApi = @{ "Authorization" = "Bearer $($tokenWebApi.Token)" } } process { if (Test-PSFFunctionInterrupt) { return } $resValidate = Invoke-RestMethod -Method Get -Uri $($baseUri + '/api/data/v9.2/RetrieveFinanceAndOperationsIntegrationDetails') -Headers $headersWebApi $temp = $resValidate | Select-PSFObject -TypeName "D365Bap.Tools.Environment.Integration" -ExcludeProperty "@odata.context" -Property "Id as LinkedAppLcsEnvId", "Url as LinkedAppLcsEnvUri", * if ($AsExcelOutput) { $temp | Export-Excel return } $temp } end { } } <# .SYNOPSIS Export PowerPlatform / Dataverse Solution from the environment .DESCRIPTION Enables the user to export solutions, on a given environment The cmdlet downloads the solution, extracts it and removes unnecessary files .PARAMETER EnvironmentId The id of the environment that you want to work against This can be obtained from the Get-BapEnvironment cmdlet .PARAMETER SolutionId The id of the solution that you want to work against This can be obtained from the Get-BapEnvironmentSolution cmdlet .PARAMETER Path Path to the location that you want the files to be exported to .EXAMPLE PS C:\> Export-BapEnvironmentSolution -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -SolutionId 3ac10775-0808-42e0-bd23-83b6c714972f -Path "C:\Temp\" This will export the solution from the environment. It will extract the files into the "C:\Temp\" location. .NOTES Author: Mötz Jensen (@Splaxi) #> function Export-BapEnvironmentSolution { [CmdletBinding()] param ( [parameter (mandatory = $true)] [string] $EnvironmentId, [parameter (mandatory = $true)] [string] $SolutionId, [parameter (mandatory = $true)] [string] $Path ) begin { # Make sure all *BapEnvironment* cmdlets will validate that the environment exists prior running anything. $envObj = Get-BapEnvironment -EnvironmentId $EnvironmentId | Select-Object -First 1 if ($null -eq $envObj) { $messageString = "The supplied EnvironmentId: <c='em'>$EnvironmentId</c> didn't return any matching environment details. Please verify that the EnvironmentId is correct - try running the <c='em'>Get-BapEnvironment</c> cmdlet." Write-PSFMessage -Level Host -Message $messageString Stop-PSFFunction -Message "Stopping because environment was NOT found based on the id." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) } if (Test-PSFFunctionInterrupt) { return } $solObj = Get-BapEnvironmentSolution -EnvironmentId $EnvironmentId -SolutionId $SolutionId | Select-Object -First 1 if ($null -eq $solObj) { $messageString = "The supplied SolutionId: <c='em'>$SolutionId</c> didn't return any matching solution from the environment. Please verify that the SolutionId is correct - try running the <c='em'>Get-BapEnvironmentSolution</c> cmdlet." Write-PSFMessage -Level Host -Message $messageString Stop-PSFFunction -Message "Stopping because solution was NOT found based on the id." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) } } process { if (Test-PSFFunctionInterrupt) { return } $tmp = pac org list --filter $EnvironmentId Write-PSFMessage -Level Host -Message "<c='em'>$($tmp[0])</c>" $found = $false foreach ($line in $tmp) { $found = $line -like "*$EnvironmentId*" if ($found) { break } } if (-not $found) { $messageString = "It seems that the current pac cli session isn't connected to the correct tenant. Please run the <c='em'>pac auth create --name 'ChangeThis'</c> and make sure to use credentials that have enough privileges." Write-PSFMessage -Level Host -Message $messageString Stop-PSFFunction -Message "Stopping because pac cli session is NOT connected to the correct tenant." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) } if (Test-PSFFunctionInterrupt) { return } # pac cli is connected to the correct tenant $pathDownload = [System.IO.Path]::ChangeExtension([System.IO.Path]::GetTempFileName(), 'zip') $tmp = pac solution export --name $solObj.SystemName --environment $EnvironmentId --path $pathDownload --overwrite if (-not (($tmp | Select-Object -Last 1) -eq "Solution export succeeded.")) { $messageString = "It seems that export of the solution encountered some kind of error. Please run the cmdlet <c='em'>again in a few minutes</c>." Write-PSFMessage -Level Host -Message $messageString Stop-PSFFunction -Message "Stopping because export failed." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) } Expand-Archive -Path $pathDownload -DestinationPath $Path -Force # Give the file system time to persis the extracted files. Start-Sleep -Seconds 2 Remove-Item -LiteralPath $(Join-Path -Path $Path -ChildPath "[Content_Types].xml") -ErrorAction SilentlyContinue -Force } end { } } <# .SYNOPSIS Get environment info .DESCRIPTION Enables the user to query and validate all environments that are available from inside PPAC It utilizes the "https://api.bap.microsoft.com" REST API .PARAMETER EnvironmentId The id of the environment that you want to work against .PARAMETER AsExcelOutput Instruct the cmdlet to output all details directly to an Excel file This makes it easier to deep dive into all the details returned from the API, and makes it possible for the user to persist the current state .EXAMPLE PS C:\> Get-BapEnvironment This will query for ALL available environments. Sample output: PpacEnvId PpacEnvRegion PpacEnvName PpacEnvSku LinkedAppLcsEnvUri --------- ------------- ----------- ---------- ------------------ 32c6b196-ef52-4c43-93cf-6ecba51e6aa1 europe new-uat Sandbox https://new-uat.sandbox.operatio… eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 europe new-test Sandbox https://new-test.sandbox.operati… d45936a7-0408-4b79-94d1-19e4c6e5a52e europe new-golden Sandbox https://new-golden.sandbox.opera… Default-e210bc90-e54b-4544-a9b8-b1f… europe New Customer Default .EXAMPLE PS C:\> Get-BapEnvironment -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 This will query for the specific environment. Sample output: PpacEnvId PpacEnvRegion PpacEnvName PpacEnvSku LinkedAppLcsEnvUri --------- ------------- ----------- ---------- ------------------ eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 europe new-test Sandbox https://new-test.sandbox.operati… .EXAMPLE PS C:\> Get-BapEnvironment -AsExcelOutput This will query for ALL available environments. Will output all details into an Excel file, that will auto open on your machine. .NOTES Author: Mötz Jensen (@Splaxi) #> function Get-BapEnvironment { [CmdletBinding()] [OutputType('System.Object[]')] param ( [string] $EnvironmentId = "*", [switch] $AsExcelOutput ) begin { $tokenBap = Get-AzAccessToken -ResourceUrl "https://service.powerapps.com/" $headers = @{ "Authorization" = "Bearer $($tokenBap.Token)" } $resEnvs = Invoke-RestMethod -Method Get -Uri "https://api.bap.microsoft.com/providers/Microsoft.BusinessAppPlatform/scopes/admin/environments?api-version=2023-06-01" -Headers $headers | Select-Object -ExpandProperty Value } process { $resCol = @( foreach ($envObj in $resEnvs) { if (-not ($envObj.Name -like $EnvironmentId)) { continue } $res = [ordered]@{} $res.Id = $envObj.Name $res.Region = $envObj.Location foreach ($prop in $envObj.Properties.PsObject.Properties) { if ($prop.Value -is [System.Management.Automation.PSCustomObject]) { $res."prop_$($prop.Name)" = @( foreach ($inner in $prop.Value.PsObject.Properties) { "$($inner.Name)=$($inner.Value)" }) -join "`r`n" } else { $res."prop_$($prop.Name)" = $prop.Value } } ([PSCustomObject]$res) | Select-PSFObject -TypeName "D365Bap.Tools.Environment" -Property "Id as PpacEnvId", "Region as PpacEnvRegion", "prop_tenantId as TenantId", "prop_azureRegion as AzureRegion", "prop_displayName as PpacEnvName", @{Name = "DeployedBy"; Expression = { $envObj.Properties.createdBy.userPrincipalName } }, "prop_provisioningState as PpacProvisioningState", "prop_environmentSku as PpacEnvSku", "prop_databaseType as PpacDbType", @{Name = "LinkedAppLcsEnvId"; Expression = { $envObj.Properties.linkedAppMetadata.id } }, @{Name = "LinkedAppLcsEnvUri"; Expression = { $envObj.Properties.linkedAppMetadata.url } }, @{Name = "LinkedMetaPpacOrgId"; Expression = { $envObj.Properties.linkedEnvironmentMetadata.resourceId } }, @{Name = "LinkedMetaPpacUniqueId"; Expression = { $envObj.Properties.linkedEnvironmentMetadata.uniqueName } }, @{Name = "LinkedMetaPpacEnvUri"; Expression = { $envObj.Properties.linkedEnvironmentMetadata.instanceUrl -replace "com/", "com" } }, @{Name = "LinkedMetaPpacEnvApiUri"; Expression = { $envObj.Properties.linkedEnvironmentMetadata.instanceApiUrl -replace "com/", "com" } }, @{Name = "LinkedMetaPpacEnvLanguage"; Expression = { $envObj.Properties.linkedEnvironmentMetadata.baseLanguage } }, @{Name = "PpacClusterIsland"; Expression = { $envObj.Properties.cluster.uriSuffix } }, "*" } ) if ($AsExcelOutput) { $resCol | Export-Excel -NoNumberConversion Version, AvailableVersion, InstalledVersion, crmMinversion, crmMaxVersion, Version return } $resCol } end { } } <# .SYNOPSIS Get application users from environment .DESCRIPTION Enables the user to fetch all application users from the environment Utilizes the built-in "applicationusers" OData entity .PARAMETER EnvironmentId The id of the environment that you want to work against This can be obtained from the Get-BapEnvironment cmdlet .PARAMETER AsExcelOutput Instruct the cmdlet to output all details directly to an Excel file This makes it easier to deep dive into all the details returned from the API, and makes it possible for the user to persist the current state .EXAMPLE PS C:\> Get-BapEnvironmentApplicationUser -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 This will fetch all ApplicationUsers from the environment. Sample output: AppId AppName ApplicationUserId SolutionId ----- ------- ----------------- ---------- b6e52ceb-f771-41ff-bd99-917523b28eaf AIBuilder_StructuredML_Prod_C… 3bafba76-60bf-413d-a4c4-5c49ccabfb12 bf85e0c8-aa47… 21ceaf7c-054c-43f6-8b14-ef6d04b90a21 AIBuilderProd 560c9a6c-4535-4066-a415-480d1493cf98 bf85e0c8-aa47… c76313fd-5c6f-4f1f-9869-c884fa7fe226 AppDeploymentOrchestration d88a3535-ebf0-4b2b-ad23-90e686660a64 99aee001-009e… 29494271-7e38-4433-8bf8-06d335299a17 AriaMdlExporter 8bf8862f-5036-42b0-a4f8-1b638db7896b 99aee001-009e… .EXAMPLE PS C:\> Get-BapEnvironmentApplicationUser -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -AsExcelOutput This will fetch all ApplicationUsers from the environment. Will output all details into an Excel file, that will auto open on your machine. .NOTES Author: Mötz Jensen (@Splaxi) #> function Get-BapEnvironmentApplicationUser { [CmdletBinding()] [OutputType('System.Object[]')] param ( [parameter (mandatory = $true)] [string] $EnvironmentId, [switch] $AsExcelOutput ) begin { # Make sure all *BapEnvironment* cmdlets will validate that the environment exists prior running anything. $envObj = Get-BapEnvironment -EnvironmentId $EnvironmentId | Select-Object -First 1 if ($null -eq $envObj) { $messageString = "The supplied EnvironmentId: <c='em'>$EnvironmentId</c> didn't return any matching environment details. Please verify that the EnvironmentId is correct - try running the <c='em'>Get-BapEnvironment</c> cmdlet." Write-PSFMessage -Level Host -Message $messageString Stop-PSFFunction -Message "Stopping because environment was NOT found based on the id." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) } if (Test-PSFFunctionInterrupt) { return } $baseUri = $envObj.LinkedMetaPpacEnvUri $tokenWebApi = Get-AzAccessToken -ResourceUrl $baseUri $headersWebApi = @{ "Authorization" = "Bearer $($tokenWebApi.Token)" } } process { if (Test-PSFFunctionInterrupt) { return } $resAppUsers = Invoke-RestMethod -Method Get -Uri $($baseUri + '/api/data/v9.2/applicationusers') -Headers $headersWebApi $resCol = @( foreach ($appUsrObj in $($resAppUsers.value | Sort-Object -Property applicationname)) { $appUsrObj | Select-PSFObject -TypeName "D365Bap.Tools.AppUser" -ExcludeProperty "@odata.etag" -Property "applicationid as AppId", "applicationname as AppName", * } ) if ($AsExcelOutput) { $resCol | Export-Excel return } $resCol } end { } } <# .SYNOPSIS Get D365 App from the environment .DESCRIPTION Enables the user to analyze and validate the current D365 Apps and their state, on a given environment It can show all available D365 Apps - including their InstallState It can show only installed D365 Apps It can show only installed D365 Apps, with available updates .PARAMETER EnvironmentId The id of the environment that you want to work against This can be obtained from the Get-BapEnvironment cmdlet .PARAMETER Name Name of the D365 App / Package that you are looking for It supports wildcard searching, which is validated against the following properties: * AppName / ApplicationName * PackageName / UniqueName .PARAMETER InstallState Instruct the cmdlet which install states that you want to have included in the output The default value is: "All" Valid values: * "All" * "Installed" * "None" .PARAMETER GeoRegion Instructs the cmdlet which Geo / Region the environment is located The default value is: "Emea" This is mandatory field from the API specification, we don't have the full list of values at the time of writing .PARAMETER UpdatesOnly Instruct the cmdlet to only output D365 Apps that has an update available Makes it easier to fully automate the update process of a given environment .PARAMETER AsExcelOutput Instruct the cmdlet to output all details directly to an Excel file This makes it easier to deep dive into all the details returned from the API, and makes it possible for the user to persist the current state .EXAMPLE PS C:\> Get-BapEnvironmentD365App -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 This will query the environment for ALL available D365 Apps. It will compare available vs installed D365 Apps, and indicate whether an update is available of not. Sample output: PackageId PackageName AvailableVersion InstalledVersion UpdateAvailable --------- ----------- ---------------- ---------------- --------------- cea6753e-9c74-4aa9-85a1-5869105115d3 msdyn_ExportControlAnchor 1.0.2553.1 N/A ea8d3b2f-ede2-46b4-900d-ed02c81c44fd AgentProductivityToolsAnchor 9.2.24021.1005 9.2.24019.1005 True b1676368-b448-4fbd-a238-9b6ddc36be81 SharePointFormProcessing 202209.5.2901.0 N/A 1c0a1237-9408-4b99-9fec-39696d99287b msdyn_AppProfileManagerAnchor 10.1.24021.1005 10.1.24021.1005 False 9f4c778b-2f0b-416f-8166-e96da680ffb2 mpa_AwardsAndRecognition 1.0.0.32 N/A 6ce2d70e-78bf-4ff6-85ed-1bd63d4ab444 ExportToDataLakeCoreAnchor 1.0.0.1 1.0.0.1 False .EXAMPLE PS C:\> Get-BapEnvironmentD365App -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -InstallState Installed This will query the environment for installed only D365 Apps. It will compare available vs installed D365 Apps, and indicate whether an update is available of not. Sample output: PackageId PackageName AvailableVersion InstalledVersion UpdateAvailable --------- ----------- ---------------- ---------------- --------------- ea8d3b2f-ede2-46b4-900d-ed02c81c44fd AgentProductivityToolsAnchor 9.2.24021.1005 9.2.24019.1005 True 1c0a1237-9408-4b99-9fec-39696d99287b msdyn_AppProfileManagerAnchor 10.1.24021.1005 10.1.24021.1005 False 6ce2d70e-78bf-4ff6-85ed-1bd63d4ab444 ExportToDataLakeCoreAnchor 1.0.0.1 1.0.0.1 False .EXAMPLE PS C:\> Get-BapEnvironmentD365App -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -InstallState None This will query the environment for NON-installed only D365 Apps. It will output all details available for the D365 Apps. Sample output: PackageId PackageName AvailableVersion InstalledVersion UpdateAvailable --------- ----------- ---------------- ---------------- --------------- cea6753e-9c74-4aa9-85a1-5869105115d3 msdyn_ExportControlAnchor 1.0.2553.1 N/A b1676368-b448-4fbd-a238-9b6ddc36be81 SharePointFormProcessing 202209.5.2901.0 N/A 9f4c778b-2f0b-416f-8166-e96da680ffb2 mpa_AwardsAndRecognition 1.0.0.32 N/A .EXAMPLE PS C:\> Get-BapEnvironmentD365App -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -Name "*ProviderAnchor*" This will query the environment for ALL D365 Apps. It will filter the output to only those who match the search pattern "*ProviderAnchor*". It will compare available vs installed D365 Apps, and indicate whether an update is available of not. Sample output: PackageId PackageName AvailableVersion InstalledVersion UpdateAvailable --------- ----------- ---------------- ---------------- --------------- c0cb37fd-d7f4-40f2-8592-64ec71a2c508 msft_ConnectorProviderAnchor 9.0.0.1618 9.0.0.1618 False .EXAMPLE PS C:\> Get-BapEnvironmentD365App -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -UpdatesOnly This will query the environment for ALL available D365 Apps. It will compare available vs installed D365 Apps, and indicate whether an update is available of not. It will filter the output to only containing those who have an update available. Sample output: PackageId PackageName AvailableVersion InstalledVersion UpdateAvailable --------- ----------- ---------------- ---------------- --------------- ea8d3b2f-ede2-46b4-900d-ed02c81c44fd AgentProductivityToolsAnchor 9.2.24021.1005 9.2.24019.1005 True .EXAMPLE PS C:\> $appIds = @(Get-BapEnvironmentD365App -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -InstallState Installed -UpdatesOnly | Select-Object -ExpandProperty PackageId) PS C:\> Invoke-BapEnvironmentInstallD365App -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -PackageId $appIds This will query the environment for installed only D365 Apps. It will filter the output to only containing those who have an update available. It will persist the PackageIds for each D365 App, into an array. It will invoke the installation process using the Invoke-BapEnvironmentInstallD365App cmdlet. .EXAMPLE PS C:\> Get-BapEnvironmentD365App -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -AsExcelOutput This will query the environment for ALL available D365 Apps. It will compare available vs installed D365 Apps, and indicate whether an update is available of not. Will output all details into an Excel file, that will auto open on your machine. .NOTES Author: Mötz Jensen (@Splaxi) #> function Get-BapEnvironmentD365App { [CmdletBinding()] [OutputType('System.Object[]')] param ( [parameter (mandatory = $true)] [string] $EnvironmentId, [string] $Name = "*", [ValidateSet("All", "Installed", "None")] [string] $InstallState = "All", [string] $GeoRegion = "Emea", [switch] $UpdatesOnly, [switch] $AsExcelOutput ) begin { $tenantId = (Get-AzContext).Tenant.Id # Make sure all *BapEnvironment* cmdlets will validate that the environment exists prior running anything. $envObj = Get-BapEnvironment -EnvironmentId $EnvironmentId | Select-Object -First 1 if ($null -eq $envObj) { $messageString = "The supplied EnvironmentId: <c='em'>$EnvironmentId</c> didn't return any matching environment details. Please verify that the EnvironmentId is correct - try running the <c='em'>Get-BapEnvironment</c> cmdlet." Write-PSFMessage -Level Host -Message $messageString Stop-PSFFunction -Message "Stopping because environment was NOT found based on the id." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) } if (Test-PSFFunctionInterrupt) { return } # First we will fetch ALL available apps for the environment $tokenPowerApi = Get-AzAccessToken -ResourceUrl "https://api.powerplatform.com/" $headersPowerApi = @{ "Authorization" = "Bearer $($tokenPowerApi.Token)" } $appsAvailable = Invoke-RestMethod -Method Get -Uri "https://api.powerplatform.com/appmanagement/environments/$EnvironmentId/applicationPackages?api-version=2022-03-01-preview" -Headers $headersPowerApi | Select-Object -ExpandProperty Value # Next we will fetch current installed apps and their details, for the environment $uriSourceEncoded = [System.Web.HttpUtility]::UrlEncode($envObj.LinkedMetaPpacEnvUri) $tokenAdminApi = Get-AzAccessToken -ResourceUrl "065d9450-1e87-434e-ac2f-69af271549ed" $headersAdminApi = @{ "Authorization" = "Bearer $($tokenAdminApi.Token)" } $appsEnvironment = Invoke-RestMethod -Method Get -Uri "https://api.admin.powerplatform.microsoft.com/api/AppManagement/InstancePackages/instanceId/$tenantId`?instanceUrl=$uriSourceEncoded`&geoType=$GeoRegion" -Headers $headersAdminApi } process { if (Test-PSFFunctionInterrupt) { return } $resCol = @( foreach ($appObj in $($appsAvailable | Sort-Object -Property ApplicationName)) { if ((-not ($appObj.ApplicationName -like $Name -or $appObj.ApplicationName -eq $Name)) -and (-not ($appObj.UniqueName -like $Name -or $appObj.UniqueName -eq $Name))) { continue } if ($InstallState -ne "All" -and $appObj.state -ne $InstallState) { continue } $appObj | Add-Member -MemberType NoteProperty -Name CurrentVersion -Value "N/A" $currentApp = $appsEnvironment | Where-Object ApplicationId -eq $appObj.ApplicationId | Select-Object -First 1 if ($currentApp) { $appObj.CurrentVersion = $currentApp.Version $appObj | Add-Member -MemberType NoteProperty -Name IsLatest -Value $($appObj.CurrentVersion -eq $appObj.Version) $appObj | Add-Member -MemberType NoteProperty -Name UpdateAvail -Value $(-not ($appObj.CurrentVersion -eq $appObj.Version)) } $appObj | Select-PSFObject -TypeName "D365Bap.Tools.Package" -Property "Id as PackageId", "UniqueName as PackageName", "Version as AvailableVersion", "CurrentVersion as InstalledVersion", "UpdateAvail as UpdateAvailable", "ApplicationName as AppName", "state as InstallState", *, @{Name = "SupportedCountriesList"; Expression = { $_.supportedCountries -join "," } } } ) if ($UpdatesOnly) { $resCol = @($resCol | Where-Object IsLatest -eq $false) } if ($AsExcelOutput) { $resCol | Export-Excel -NoNumberConversion Version, AvailableVersion, InstalledVersion, crmMinversion, crmMaxVersion, Version return } $resCol } end { } } <# .SYNOPSIS Get PowerPlatform / Dataverse Solution from the environment .DESCRIPTION Enables the user to list solutions and their meta data, on a given environment .PARAMETER EnvironmentId The id of the environment that you want to work against This can be obtained from the Get-BapEnvironment cmdlet .PARAMETER SolutionId The id of the solution that you want to work against Leave blank to get all solutions .PARAMETER IncludeManaged Instruct the cmdlet to include all managed solution .PARAMETER AsExcelOutput Instruct the cmdlet to output all details directly to an Excel file This makes it easier to deep dive into all the details returned from the API, and makes it possible for the user to persist the current state .EXAMPLE PS C:\> Get-BapEnvironmentSolution -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 This will query the specific environment. It will only list Unmanaged / NON-Managed solutions. Sample output: SolutionId Name IsManaged SystemName Description ---------- ---- --------- ---------- ----------- fd140aae-4df4-11dd-bd17-0019b9312238 Active Solution False Active Placeholder solutio… .EXAMPLE PS C:\> Get-BapEnvironmentSolution -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -IncludeManaged This will query the specific environment. It will list all solutions. Sample output: SolutionId Name IsManaged SystemName Description ---------- ---- --------- ---------- ----------- 169edc7d-5f1e-4ee4-8b5c-135b3ba82ea3 Access Team True AccessTeam Access Team solution fd140aae-4df4-11dd-bd17-0019b9312238 Active Solution False Active Placeholder solutio… 458c32fb-4476-4431-97cb-49cfd069c31d Activities True msdynce_Activities Dynamics 365 worklo… 7553bb8a-fc5e-424c-9698-113958c28c98 Activities Patch True msdynce_ActivitiesP… Patch for Dynamics … 3ac10775-0808-42e0-bd23-83b6c714972f ActivitiesInfra Solution Anch… True msft_ActivitiesInfr… ActivitiesInfra Sol… .EXAMPLE PS C:\> Get-BapEnvironmentSolution -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -IncludeManaged This will query the specific environment. It will list all solutions, unmanaged / managed. Sample output: SolutionId Name IsManaged SystemName Description ---------- ---- --------- ---------- ----------- 169edc7d-5f1e-4ee4-8b5c-135b3ba82ea3 Access Team True AccessTeam Access Team solution fd140aae-4df4-11dd-bd17-0019b9312238 Active Solution False Active Placeholder solutio… 458c32fb-4476-4431-97cb-49cfd069c31d Activities True msdynce_Activities Dynamics 365 worklo… 7553bb8a-fc5e-424c-9698-113958c28c98 Activities Patch True msdynce_ActivitiesP… Patch for Dynamics … 3ac10775-0808-42e0-bd23-83b6c714972f ActivitiesInfra Solution Anch… True msft_ActivitiesInfr… ActivitiesInfra Sol… .EXAMPLE PS C:\> Get-BapEnvironmentSolution -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -IncludeManaged -SolutionId 3ac10775-0808-42e0-bd23-83b6c714972f This will query the specific environment. It will list all solutions, unmanaged / managed. It will search for the 3ac10775-0808-42e0-bd23-83b6c714972f solution. Sample output: SolutionId Name IsManaged SystemName Description ---------- ---- --------- ---------- ----------- 3ac10775-0808-42e0-bd23-83b6c714972f ActivitiesInfra Solution Anch… True msft_ActivitiesInfr… ActivitiesInfra Sol… .EXAMPLE PS C:\> Get-BapEnvironmentSolution -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -IncludeManaged -AsExcelOutput This will query the specific environment. It will list all solutions, unmanaged / managed. Will output all details into an Excel file, that will auto open on your machine. .NOTES Author: Mötz Jensen (@Splaxi) #> function Get-BapEnvironmentSolution { [CmdletBinding()] [OutputType('System.Object[]')] param ( [parameter (mandatory = $true)] [string] $EnvironmentId, [string] $SolutionId, [switch] $IncludeManaged, [switch] $AsExcelOutput ) begin { # Make sure all *BapEnvironment* cmdlets will validate that the environment exists prior running anything. $envObj = Get-BapEnvironment -EnvironmentId $EnvironmentId | Select-Object -First 1 if ($null -eq $envObj) { $messageString = "The supplied EnvironmentId: <c='em'>$EnvironmentId</c> didn't return any matching environment details. Please verify that the EnvironmentId is correct - try running the <c='em'>Get-BapEnvironment</c> cmdlet." Write-PSFMessage -Level Host -Message $messageString Stop-PSFFunction -Message "Stopping because environment was NOT found based on the id." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) } if (Test-PSFFunctionInterrupt) { return } $baseUri = $envObj.LinkedMetaPpacEnvUri $tokenWebApi = Get-AzAccessToken -ResourceUrl $baseUri $headersWebApi = @{ "Authorization" = "Bearer $($tokenWebApi.Token)" } } process { if (Test-PSFFunctionInterrupt) { return } $localUri = $baseUri + '/api/data/v9.2/solutions' $search = '?$filter=ismanaged eq false' if (-not $IncludeManaged) { $localUri += $search } $resSolutions = Invoke-RestMethod -Method Get -Uri $localUri -Headers $headersWebApi $resCol = @( foreach ($solObj in $($resSolutions.value | Sort-Object -Property friendlyname)) { if ((-not [System.String]::IsNullOrEmpty($SolutionId)) -and $solObj.SolutionId -ne $SolutionId) { continue } $solObj | Select-PSFObject -TypeName "D365Bap.Tools.Solution" -Property "uniquename as SystemName", "friendlyname as Name", * } ) if ($AsExcelOutput) { $resCol | Export-Excel return } $resCol } end { } } <# .SYNOPSIS Get users from environment .DESCRIPTION Enables the user to fetch all users from the environment Utilizes the built-in "systemusers" OData entity Allows the user to include all users, based on those who has the ApplicationId property filled .PARAMETER EnvironmentId The id of the environment that you want to work against This can be obtained from the Get-BapEnvironment cmdlet .PARAMETER IncludeAppIds Instruct the cmdlet to include all users that are available from the "systemusers" OData Entity Simply includes those who has the ApplicationId property filled .PARAMETER AsExcelOutput Instruct the cmdlet to output all details directly to an Excel file This makes it easier to deep dive into all the details returned from the API, and makes it possible for the user to persist the current state .EXAMPLE PS C:\> Get-BapEnvironmentUser -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 This will fetch all oridinary users from the environment. Sample output: Email Name AppId Systemuserid ----- ---- ----- ------------ SYSTEM 5d2ff978-a74c-4ba4-8cc2-b4c5a23994f7 INTEGRATION baabe592-2860-4d1a-9365-e95317372498 aba@temp.com Austin Baker f85bcd69-ef72-45bd-a338-62670a8cef2a ade@temp.com Alex Denver 39309a5c-7676-4c8a-b702-719fb92c5151 .EXAMPLE PS C:\> Get-BapEnvironmentUser -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 This will fetch all users from the environment. It will include the ones with the ApplicationId property filled. Sample output: Email Name AppId Systemuserid ----- ---- ----- ------------ SYSTEM 5d2ff978-a74c-4ba4-8cc2-b4c5a23994f7 INTEGRATION baabe592-2860-4d1a-9365-e95317372498 aba@temp.com Austin Baker f85bcd69-ef72-45bd-a338-62670a8cef2a AIBuilderProd@onmicrosoft.com AIBuilderProd, # 0a143f2d-2320-4141-… c96f82b8-320f-4c5e-ac84-1831f4dc7d5f .EXAMPLE PS C:\> Get-BapEnvironmentUser -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -AsExcelOutput This will fetch all oridinary users from the environment. Will output all details into an Excel file, that will auto open on your machine. .NOTES Author: Mötz Jensen (@Splaxi) #> function Get-BapEnvironmentUser { [CmdletBinding()] param ( [parameter (mandatory = $true)] [string] $EnvironmentId, [switch] $IncludeAppIds, [switch] $AsExcelOutput ) begin { # Make sure all *BapEnvironment* cmdlets will validate that the environment exists prior running anything. $envObj = Get-BapEnvironment -EnvironmentId $EnvironmentId | Select-Object -First 1 if ($null -eq $envObj) { $messageString = "The supplied EnvironmentId: <c='em'>$EnvironmentId</c> didn't return any matching environment details. Please verify that the EnvironmentId is correct - try running the <c='em'>Get-BapEnvironment</c> cmdlet." Write-PSFMessage -Level Host -Message $messageString Stop-PSFFunction -Message "Stopping because environment was NOT found based on the id." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) } if (Test-PSFFunctionInterrupt) { return } $baseUri = $envObj.LinkedMetaPpacEnvUri $tokenWebApi = Get-AzAccessToken -ResourceUrl $baseUri $headersWebApi = @{ "Authorization" = "Bearer $($tokenWebApi.Token)" } $languages = @(Get-EnvironmentLanguage -BaseUri $baseUri) } process { if (Test-PSFFunctionInterrupt) { return } $resUsers = Invoke-RestMethod -Method Get -Uri $($baseUri + '/api/data/v9.2/systemusers?$select=fullname,internalemailaddress,applicationid&$expand=user_settings($select=uilanguageid)') -Headers $headersWebApi $resCol = @( foreach ($usrObj in $($resUsers.value | Sort-Object -Property internalemailaddress)) { $usrObj | Add-Member -MemberType NoteProperty -Name "lang" -Value $($languages | Where-Object { ($_.localeid -eq $usrObj.user_settings[0].uilanguageid) -or ($_.BaseLocaleId -eq $usrObj.user_settings[0].uilanguageid) } | Select-Object -First 1 -ExpandProperty code) $usrObj | Select-PSFObject -TypeName "D365Bap.Tools.User" -ExcludeProperty "@odata.etag" -Property "internalemailaddress as Email", "fullname as Name", "applicationid as AppId", "lang as Language", * } ) if (-not $IncludeAppIds) { $resCol = $resCol | Where-Object applicationid -eq $null } if ($AsExcelOutput) { $resCol | Export-Excel return } $resCol } end { } } <# .SYNOPSIS Get Virtual Entity from environment .DESCRIPTION Enables the user to query against the Virtual Entities from the D365FO environment This will help determine which Virtual Entities are already enabled / visible and their state, as they are seeing from the Dataverse environment .PARAMETER EnvironmentId The id of the environment that you want to work against .PARAMETER Name The name of the virtual entity that you are looking for The parameter supports wildcards, but will resolve them into a strategy that matches best practice from Microsoft documentation It means that you can only have a single search phrase. E.g. * -Name "*Retail" * -Name "Retail*" * -Name "*Retail*" Multiple search phrases are not going to produce an output, as it will be striped into an invalid search string. E.g. ! -Name "*Retail*Entity*" -> "RetailEntity" ! -Name "Retail*Entity" -> "RetailEntity" ! -Name "*Retail*Entity" -> "RetailEntity" ! -Name "Retail*Entity*" -> "RetailEntity" .PARAMETER VisibleOnly Instruct the cmdlet to only output those virtual entities that are enabled / visible .PARAMETER AsExcelOutput Instruct the cmdlet to output all details directly to an Excel file This makes it easier to deep dive into all the details returned from the API, and makes it possible for the user to persist the current state .EXAMPLE PS C:\> Get-BapEnvironmentVirtualEntity -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 This will fetch all virtual entities from the environment. Sample output: EntityName IsVisible ChangeTrackingEnabled EntityGuid ---------- --------- --------------------- ---------- AadWorkerIntegrationEntity False False 00002893-0000-0000-560a-005001000000 AbbreviationsEntity False False 00002893-0000-0000-5002-005001000000 AccountantEntity False False 00002893-0000-0000-0003-005001000000 AccountingDistributionBiEntity False False 00002893-0000-0000-d914-005001000000 AccountingEventBiEntity False False 00002893-0000-0000-d414-005001000000 .EXAMPLE PS C:\> Get-BapEnvironmentVirtualEntity -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -VisibleOnly This will fetch visble only virtual entities from the environment. Sample output: EntityName IsVisible ChangeTrackingEnabled EntityGuid ---------- --------- --------------------- ---------- CurrencyEntity True False 00002893-0000-0000-c30b-005001000000 WMHEOutboundQueueEntity True False 00002893-0000-0000-f30b-005001000000 .EXAMPLE PS C:\> Get-BapEnvironmentVirtualEntity -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -AsExcelOutput This will fetch all virtual entities from the environment. Will output all details into an Excel file, that will auto open on your machine. .EXAMPLE PS C:\> Get-BapEnvironmentVirtualEntity -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -Name "Retail*" This will fetch all virtual entities that contains the "Retail" text in the name, from the environment. Sample output: EntityName IsVisible ChangeTrackingEnabled EntityGuid ---------- --------- --------------------- ---------- CustHierarchyRetailChannelEnt… False False 00002893-0000-0000-e314-005001000000 DimAttributeRetailChannelEnti… False False 00002893-0000-0000-0804-005001000000 DimAttributeRetailStoreEntity False False 00002893-0000-0000-0f03-005001000000 DimAttributeRetailTerminalEnt… False False 00002893-0000-0000-6e07-005001000000 EcoResRetailProductEntity False False 00002893-0000-0000-ae06-005001000000 .EXAMPLE PS C:\> Get-BapEnvironmentVirtualEntity -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -Name AccountantEntity This will fetch the specific virtual entity from the environment. Sample output: EntityName IsVisible ChangeTrackingEnabled EntityGuid ---------- --------- --------------------- ---------- AccountantEntity False False 00002893-0000-0000-0003-005001000000 .NOTES Author: Mötz Jensen (@Splaxi) #> function Get-BapEnvironmentVirtualEntity { [CmdletBinding()] [OutputType('System.Object[]')] param ( [parameter (mandatory = $true)] [string] $EnvironmentId, [string] $Name = "*", [switch] $VisibleOnly, [switch] $AsExcelOutput ) begin { # Make sure all *BapEnvironment* cmdlets will validate that the environment exists prior running anything. $envObj = Get-BapEnvironment -EnvironmentId $EnvironmentId | Select-Object -First 1 if ($null -eq $envObj) { $messageString = "The supplied EnvironmentId: <c='em'>$EnvironmentId</c> didn't return any matching environment details. Please verify that the EnvironmentId is correct - try running the <c='em'>Get-BapEnvironment</c> cmdlet." Write-PSFMessage -Level Host -Message $messageString Stop-PSFFunction -Message "Stopping because environment was NOT found based on the id." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) } if (Test-PSFFunctionInterrupt) { return } $baseUri = $envObj.LinkedMetaPpacEnvUri $tokenWebApi = Get-AzAccessToken -ResourceUrl $baseUri $headersWebApi = @{ "Authorization" = "Bearer $($tokenWebApi.Token)" } # Fetch all meta data - for all entities in the environment $entitiesMetaRaw = Invoke-RestMethod -Method Get -Uri $($baseUri + '/api/data/v9.2/entities') -Headers $headersWebApi $templateMeta = $entitiesMetaRaw.value[0] # Filter down to only those who are connected to Virtual Entities $entitiesMeta = $entitiesMetaRaw.value | Where-Object { -not [System.String]::IsNullOrEmpty($_.externalname) } } process { if (Test-PSFFunctionInterrupt) { return } $localUri = $($baseUri + '/api/data/v9.2/mserp_financeandoperationsentities') [System.Collections.Generic.List[System.String]]$filters = @() # Is the user search for specific entities? if ($Name -ne "*") { if ($Name.IndexOfAny("*") -gt -1) { # It is a wildcard search $filters.Add("contains(mserp_physicalname, '$($Name.Replace('*',''))')") } else { # It is a full named search $filters.Add("mserp_physicalname eq '$Name'") } } if ($VisibleOnly) { $filters.Add("mserp_hasbeengenerated eq true") } if ($filters.count -gt 0) { # We need to handle multiple filters $localUri += '?$filter=' $localUri += $($filters.ToArray() -join " and ") } $resEntities = Invoke-RestMethod -Method Get -Uri $localUri -Headers $headersWebApi $resCol = @( foreach ($virEntity in $($resEntities.value | Sort-Object -Property mserp_physicalname)) { $tempMeta = $entitiesMeta | Where-Object externalname -eq $virEntity.mserp_physicalname | Select-Object -First 1 if ($null -ne $tempMeta) { # Work against the meta data found foreach ($prop in $tempMeta.PsObject.Properties) { $virEntity | Add-Member -MemberType NoteProperty -Name "meta_$($prop.Name)" -Value $prop.Value } } else { # Create empty properties for those who doesn't have meta data available foreach ($prop in $templateMeta.PsObject.Properties) { $virEntity | Add-Member -MemberType NoteProperty -Name "meta_$($prop.Name)" -Value $null } } $virEntity | Select-PSFObject -TypeName "D365Bap.Tools.VirtualEntity" -Property "mserp_physicalname as EntityName", "mserp_hasbeengenerated as IsVisible", "mserp_changetrackingenabled as ChangeTrackingEnabled", "mserp_financeandoperationsentityid as EntityGuid", * } ) if ($AsExcelOutput) { $resCol | Export-Excel return } $resCol } end { } } <# .SYNOPSIS Invoke the installation of a D365 App in a given environment .DESCRIPTION Enables the invocation of the installation process against the PowerPlatform API (https://api.powerplatform.com) The cmdlet will keep requesting the status of all invoked installations, until they all have a NON "Running" state It will request this status every 60 seconds .PARAMETER EnvironmentId The id of the environment that you want to work against This can be obtained from the Get-BapEnvironment cmdlet .PARAMETER PackageId The id of the package(s) that you want to have Installed It supports id of current packages, with updates available and new D365 apps It support an array as input, so it can invoke multiple D365 App installations .EXAMPLE PS C:\> Invoke-BapEnvironmentInstallD365App -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -PackageId 'be69fc64-7393-4c3c-8908-2a1c2e53aef9','6defa8de-87f9-4478-8f9a-a7d685394e24' This will install the 2 x D365 Apps, based on the Ids supplied. It will run the cmdlet and have it get the status of the installation progress until all D365 Apps have been fully installed. Sample output (Install initialized): status createdDateTime lastActionDateTime error statusMessage operationId ------ --------------- ------------------ ----- ------------- ----------- Running 02/03/2024 13.42.07 02/03/2024 13.42.16 5c80df7f-d89e-42bd-abeb-98e577ae49f4 Running 02/03/2024 13.42.09 02/03/2024 13.42.12 6885e0f4-639f-4ebc-b21e-49ce5d5e920d Sample output (Partly succeeded installation): status createdDateTime lastActionDateTime error statusMessage operationId ------ --------------- ------------------ ----- ------------- ----------- Succeeded 02/03/2024 13.42.07 02/03/2024 13.44.48 5c80df7f-d89e-42bd-abeb-98e577ae49f4 Running 02/03/2024 13.42.09 02/03/2024 13.45.55 6885e0f4-639f-4ebc-b21e-49ce5d5e920d Sample output (Completely succeeded installation): status createdDateTime lastActionDateTime error statusMessage operationId ------ --------------- ------------------ ----- ------------- ----------- Succeeded 02/03/2024 13.42.07 02/03/2024 13.44.48 5c80df7f-d89e-42bd-abeb-98e577ae49f4 Succeeded 02/03/2024 13.42.09 02/03/2024 13.48.26 6885e0f4-639f-4ebc-b21e-49ce5d5e920d .EXAMPLE PS C:\> $appIds = @(Get-BapEnvironmentD365App -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -InstallState Installed -UpdatesOnly | Select-Object -ExpandProperty PackageId) PS C:\> Invoke-BapEnvironmentInstallD365App -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -PackageId $appIds This will find all D365 Apps that has a pending update available. It will gather the Ids into an array. It will run the cmdlet and have it get the status of the installation progress until all D365 Apps have been fully installed. Sample output (Install initialized): status createdDateTime lastActionDateTime error statusMessage operationId ------ --------------- ------------------ ----- ------------- ----------- Running 02/03/2024 13.42.07 02/03/2024 13.42.16 5c80df7f-d89e-42bd-abeb-98e577ae49f4 Running 02/03/2024 13.42.09 02/03/2024 13.42.12 6885e0f4-639f-4ebc-b21e-49ce5d5e920d Sample output (Partly succeeded installation): status createdDateTime lastActionDateTime error statusMessage operationId ------ --------------- ------------------ ----- ------------- ----------- Succeeded 02/03/2024 13.42.07 02/03/2024 13.44.48 5c80df7f-d89e-42bd-abeb-98e577ae49f4 Running 02/03/2024 13.42.09 02/03/2024 13.45.55 6885e0f4-639f-4ebc-b21e-49ce5d5e920d Sample output (Completely succeeded installation): status createdDateTime lastActionDateTime error statusMessage operationId ------ --------------- ------------------ ----- ------------- ----------- Succeeded 02/03/2024 13.42.07 02/03/2024 13.44.48 5c80df7f-d89e-42bd-abeb-98e577ae49f4 Succeeded 02/03/2024 13.42.09 02/03/2024 13.48.26 6885e0f4-639f-4ebc-b21e-49ce5d5e920d .NOTES Author: Mötz Jensen (@Splaxi) #> function Invoke-BapEnvironmentInstallD365App { [CmdletBinding()] param ( [parameter (mandatory = $true)] [string] $EnvironmentId, [parameter (mandatory = $true)] [string[]] $PackageId ) begin { # Make sure all *BapEnvironment* cmdlets will validate that the environment exists prior running anything. $envObj = Get-BapEnvironment -EnvironmentId $EnvironmentId | Select-Object -First 1 if ($null -eq $envObj) { $messageString = "The supplied EnvironmentId: <c='em'>$EnvironmentId</c> didn't return any matching environment details. Please verify that the EnvironmentId is correct - try running the <c='em'>Get-BapEnvironment</c> cmdlet." Write-PSFMessage -Level Host -Message $messageString Stop-PSFFunction -Message "Stopping because environment found based on the id." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) } if (Test-PSFFunctionInterrupt) { return } # First we will fetch ALL available apps for the environment $tokenPowerApi = Get-AzAccessToken -ResourceUrl "https://api.powerplatform.com/" $headersPowerApi = @{ "Authorization" = "Bearer $($tokenPowerApi.Token)" } $appsAvailable = Get-BapEnvironmentD365App -EnvironmentId $EnvironmentId } process { if (Test-PSFFunctionInterrupt) { return } [System.Collections.Generic.List[System.Object]] $arrInstallStarted = @() [System.Collections.Generic.List[System.Object]] $arrStatus = @() $headersPowerApi."Content-Type" = "application/json;charset=utf-8" foreach ($pgkId in $PackageId) { $appToBeInstalled = $appsAvailable | Where-Object Id -eq $pgkId | Select-Object -First 1 if ($null -eq $appToBeInstalled) { $messageString = "The combination of the supplied EnvironmentId: <c='em'>$EnvironmentId</c> and PackageId: <c='em'>$PackageId</c> didn't return any matching D365App. Please verify that the EnvironmentId & PackageId is correct - try running the <c='em'>Get-BapEnvironmentD365App</c> cmdlet." Write-PSFMessage -Level Host -Message $messageString Stop-PSFFunction -Message "Stopping because environment and d365app combination was NOT found based on the supplied parameters." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) } $body = $appToBeInstalled | ConvertTo-Json $resIntall = Invoke-RestMethod -Method Post -Uri "https://api.powerplatform.com/appmanagement/environments/$EnvironmentId/applicationPackages/$($appToBeInstalled.uniqueName)/install?api-version=2022-03-01-preview" -Headers $headersPowerApi -Body $body $arrInstallStarted.Add($resIntall) } do { $tokenPowerApi = Get-AzAccessToken -ResourceUrl "https://api.powerplatform.com/" $headersPowerApi = @{ "Authorization" = "Bearer $($tokenPowerApi.Token)" } Start-Sleep -Seconds 60 # Write-PSFMessage -Level Host -Message "Checking for running operations" $arrStatus = @() foreach ($operation in $arrInstallStarted) { $resInstallOperation = Invoke-RestMethod -Method Get -Uri "https://api.powerplatform.com/appmanagement/environments/$EnvironmentId/operations/$($operation.lastOperation.operationId)?api-version=2022-03-01-preview" -Headers $headersPowerApi $arrStatus.Add($resInstallOperation) } $arrStatus | Format-Table } while ("Running" -in $arrStatus.status) } end { } } <# .SYNOPSIS Set Virtual Entity configuration in environment .DESCRIPTION Enables the user to update the configuration for any given Virtual Entity in the environment The configuration is done against the Dataverse environment, and allows the user to update the Visibility or TrackingChanges, for a given Virtual Entity .PARAMETER EnvironmentId The id of the environment that you want to work against .PARAMETER Name The name of the virtual entity that you are looking for .PARAMETER VisibilityOn Instructs the cmdlet to turn "ON" the Virtual Entity Can be used in combination with TrackingOn / TrackingOff .PARAMETER VisibilityOff Instructs the cmdlet to turn "OFF" the Virtual Entity Can be used in combination with TrackingOn / TrackingOff .PARAMETER TrackingOn Instructs the cmdlet to enable ChangeTracking on the Virtual Entity Can be used in combination with VisibilityOn / VisibilityOff .PARAMETER TrackingOff Instructs the cmdlet to disable ChangeTracking on the Virtual Entity Can be used in combination with VisibilityOn / VisibilityOff .EXAMPLE PS C:\> Set-BapEnvironmentVirtualEntity -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -Name AccountantEntity -VisibilityOn -TrackingOff This will enable the Virtual Entity "AccountantEntity" on the environment. It will turn off the ChangeTracking at the same time. Sample output: EntityName IsVisible ChangeTrackingEnabled EntityGuid ---------- --------- --------------------- ---------- AccountantEntity True False 00002893-0000-0000-0003-005001000000 .EXAMPLE PS C:\> Set-BapEnvironmentVirtualEntity -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -Name AccountantEntity -VisibilityOff This will disable the Virtual Entity "AccountantEntity" on the environment. Sample output: EntityName IsVisible ChangeTrackingEnabled EntityGuid ---------- --------- --------------------- ---------- AccountantEntity False False 00002893-0000-0000-0003-005001000000 .EXAMPLE PS C:\> Set-BapEnvironmentVirtualEntity -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -Name AccountantEntity -TrackingOn This will update the Virtual Entity "AccountantEntity" on the environment. It will enable the ChangeTracking for the entity. Sample output: EntityName IsVisible ChangeTrackingEnabled EntityGuid ---------- --------- --------------------- ---------- AccountantEntity True True 00002893-0000-0000-0003-005001000000 .NOTES Author: Mötz Jensen (@Splaxi) #> function Set-BapEnvironmentVirtualEntity { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [parameter (mandatory = $true)] [string] $EnvironmentId, [parameter (mandatory = $true)] [string] $Name, [switch] $VisibilityOn, [switch] $VisibilityOff, [switch] $TrackingOn, [switch] $TrackingOff ) begin { if (-not($VisibilityOn -or $VisibilityOff -or $TrackingOn -or $TrackingOff)) { $messageString = "You need to select atleast one of the ParameterSets: You have to use either <c='em'>-VisibilityOn</c> / <c='em'>-VisibilityOff</c> or <c='em'>-TrackingOn</c> / <c='em'>-TrackingOff</c>." Write-PSFMessage -Level Host -Message $messageString Stop-PSFFunction -Message "Stopping because intent of the operation is NOT clear." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) } if ($VisibilityOn -and $VisibilityOff) { $messageString = "The supplied parameter combination is not valid. You have to use either <c='em'>-VisibilityOn</c> or <c='em'>-VisibilityOff</c>." Write-PSFMessage -Level Host -Message $messageString Stop-PSFFunction -Message "Stopping because intent of the operation is NOT clear." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) } if ($TrackingOn -and $TrackingOff) { $messageString = "The supplied parameter combination is not valid. You have to use either <c='em'>-TrackingOn</c> or <c='em'>-TrackingOff</c>." Write-PSFMessage -Level Host -Message $messageString Stop-PSFFunction -Message "Stopping because intent of the operation is NOT clear." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) } if (Test-PSFFunctionInterrupt) { return } # Make sure all *BapEnvironment* cmdlets will validate that the environment exists prior running anything. $envObj = Get-BapEnvironment -EnvironmentId $EnvironmentId | Select-Object -First 1 if ($null -eq $envObj) { $messageString = "The supplied EnvironmentId: <c='em'>$EnvironmentId</c> didn't return any matching environment details. Please verify that the EnvironmentId is correct - try running the <c='em'>Get-BapEnvironment</c> cmdlet." Write-PSFMessage -Level Host -Message $messageString Stop-PSFFunction -Message "Stopping because environment was NOT found based on the id." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) } if (Test-PSFFunctionInterrupt) { return } $baseUri = $envObj.LinkedMetaPpacEnvApiUri $tokenWebApi = Get-AzAccessToken -ResourceUrl $baseUri $headersWebApi = @{ "Authorization" = "Bearer $($tokenWebApi.Token)" } $entities = @(Get-BapEnvironmentVirtualEntity -EnvironmentId $EnvironmentId -Name $Name) if ($entities.Count -ne 1) { $messageString = "The supplied EnvironmentId: <c='em'>$EnvironmentId</c> didn't return any matching environment details. Please verify that the EnvironmentId is correct - try running the <c='em'>Get-BapEnvironment</c> cmdlet." Write-PSFMessage -Level Host -Message $messageString Stop-PSFFunction -Message "Stopping because environment was NOT found based on the id." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) } if (Test-PSFFunctionInterrupt) { return } $entity = $entities[0] } process { if (Test-PSFFunctionInterrupt) { return } $localUri = $($baseUri + '/XRMServices/2011/Organization.svc/web?SDKClientVersion=9.2.49.3165' -f $entity.EntityGuid) # Base payload for updating the virtual entity configuration $body = @' <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"> <s:Header> <UserType xmlns="http://schemas.microsoft.com/xrm/2011/Contracts">CrmUser</UserType> <SdkClientVersion xmlns="http://schemas.microsoft.com/xrm/2011/Contracts">9.2.49.3165</SdkClientVersion> <x-ms-client-request-id xmlns="http://schemas.microsoft.com/xrm/2011/Contracts">{0}</x-ms-client-request-id> </s:Header> <s:Body> <Execute xmlns="http://schemas.microsoft.com/xrm/2011/Contracts/Services"> <request i:type="a:UpdateRequest" xmlns:a="http://schemas.microsoft.com/xrm/2011/Contracts" xmlns:i="http://www.w3.org/2001/XMLSchema-instance"> <a:Parameters xmlns:b="http://schemas.datacontract.org/2004/07/System.Collections.Generic"> <a:KeyValuePairOfstringanyType> <b:key>Target</b:key> <b:value i:type="a:Entity"> <a:Attributes> ##INPUT## </a:Attributes> <a:EntityState i:nil="true"/> <a:FormattedValues/> <a:Id>{1}</a:Id> <a:KeyAttributes xmlns:c="http://schemas.microsoft.com/xrm/7.1/Contracts"/> <a:LogicalName>mserp_financeandoperationsentity</a:LogicalName> <a:RelatedEntities/> <a:RowVersion i:nil="true"/> </b:value> </a:KeyValuePairOfstringanyType> </a:Parameters> <a:RequestId>{0}</a:RequestId> <a:RequestName>Update</a:RequestName> </request> </Execute> </s:Body> </s:Envelope> '@ -f $([System.Guid]::NewGuid().Guid), $entity.EntityGuid if ($VisibilityOn -or $VisibilityOff) { # We need to set the Visibility configuration for the virtual entity $visibility = @' <a:KeyValuePairOfstringanyType> <b:key>mserp_hasbeengenerated</b:key> <b:value i:type="c:boolean" xmlns:c="http://www.w3.org/2001/XMLSchema">{0}</b:value> </a:KeyValuePairOfstringanyType> '@ -f $(if ($VisibilityOn) { "true" }else { "false" }) } if ($TrackingOn -or $TrackingOff) { # We need to set the Tracking configuration for the virtual entity $tracking = @' <a:KeyValuePairOfstringanyType> <b:key>mserp_changetrackingenabled</b:key> <b:value i:type="c:boolean" xmlns:c="http://www.w3.org/2001/XMLSchema">{0}</b:value> </a:KeyValuePairOfstringanyType> '@ -f $(if ($TrackingOn) { "true" }else { "false" }) } $body = $body -replace $("##INPUT##", "$visibility`r`n$tracking") $headersWebApi."Content-Type" = "text/xml; charset=utf-8" $headersWebApi.SOAPAction = "http://schemas.microsoft.com/xrm/2011/Contracts/Services/IOrganizationService/Execute" Invoke-RestMethod -Method Post -Uri $localUri -Headers $headersWebApi -Body $body -ContentType "text/xml; charset=utf-8" > $null # We are asking to fast for the meta data to be updated Start-Sleep -Seconds 10 Get-BapEnvironmentVirtualEntity -EnvironmentId $EnvironmentId -Name $Name } end { } } <# .SYNOPSIS Update the meta data for an Virtual Entity in an environment .DESCRIPTION Enables the user to update the metadata for any given Virtual Entity in the environment This is useful when there has been schema changes on the Virtual Entity inside the D365FO environment, after it was made visible / enabled .PARAMETER EnvironmentId The id of the environment that you want to work against .PARAMETER Name The name of the virtual entity that you are looking for .EXAMPLE PS C:\> Update-BapEnvironmentVirtualEntityMetadata -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -Name AccountantEntity This will execute the internal mechanism inside the Dataverse environment. It will halt/stall as long as the operation is running. .NOTES Author: Mötz Jensen (@Splaxi) #> function Update-BapEnvironmentVirtualEntityMetadata { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [parameter (mandatory = $true)] [string] $EnvironmentId, [parameter (mandatory = $true)] [string] $Name ) begin { # Make sure all *BapEnvironment* cmdlets will validate that the environment exists prior running anything. $envObj = Get-BapEnvironment -EnvironmentId $EnvironmentId | Select-Object -First 1 if ($null -eq $envObj) { $messageString = "The supplied EnvironmentId: <c='em'>$EnvironmentId</c> didn't return any matching environment details. Please verify that the EnvironmentId is correct - try running the <c='em'>Get-BapEnvironment</c> cmdlet." Write-PSFMessage -Level Host -Message $messageString Stop-PSFFunction -Message "Stopping because environment was NOT found based on the id." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) } if (Test-PSFFunctionInterrupt) { return } $baseUri = $envObj.LinkedMetaPpacEnvApiUri $tokenWebApi = Get-AzAccessToken -ResourceUrl $baseUri $headersWebApi = @{ "Authorization" = "Bearer $($tokenWebApi.Token)" } $entities = @(Get-BapEnvironmentVirtualEntity -EnvironmentId $EnvironmentId -Name $Name) if ($entities.Count -ne 1) { $messageString = "The supplied EnvironmentId: <c='em'>$EnvironmentId</c> didn't return any matching environment details. Please verify that the EnvironmentId is correct - try running the <c='em'>Get-BapEnvironment</c> cmdlet." Write-PSFMessage -Level Host -Message $messageString Stop-PSFFunction -Message "Stopping because environment was NOT found based on the id." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) } if (Test-PSFFunctionInterrupt) { return } $entity = $entities[0] } process { if (Test-PSFFunctionInterrupt) { return } $localUri = $($baseUri + '/XRMServices/2011/Organization.svc/web?SDKClientVersion=9.2.49.3165' -f $entity.EntityGuid) # Base payload for updating the virtual entity configuration $body = @' <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"> <s:Header> <UserType xmlns="http://schemas.microsoft.com/xrm/2011/Contracts">CrmUser</UserType> <SdkClientVersion xmlns="http://schemas.microsoft.com/xrm/2011/Contracts">9.2.49.3165</SdkClientVersion> <x-ms-client-request-id xmlns="http://schemas.microsoft.com/xrm/2011/Contracts">{0}</x-ms-client-request-id> </s:Header> <s:Body> <Execute xmlns="http://schemas.microsoft.com/xrm/2011/Contracts/Services"> <request i:type="a:UpdateRequest" xmlns:a="http://schemas.microsoft.com/xrm/2011/Contracts" xmlns:i="http://www.w3.org/2001/XMLSchema-instance"> <a:Parameters xmlns:b="http://schemas.datacontract.org/2004/07/System.Collections.Generic"> <a:KeyValuePairOfstringanyType> <b:key>Target</b:key> <b:value i:type="a:Entity"> <a:Attributes> <a:KeyValuePairOfstringanyType> <b:key>mserp_refresh</b:key> <b:value i:type="c:boolean" xmlns:c="http://www.w3.org/2001/XMLSchema">true</b:value> </a:KeyValuePairOfstringanyType> </a:Attributes> <a:EntityState i:nil="true"/> <a:FormattedValues/> <a:Id>{1}</a:Id> <a:KeyAttributes xmlns:c="http://schemas.microsoft.com/xrm/7.1/Contracts"/> <a:LogicalName>mserp_financeandoperationsentity</a:LogicalName> <a:RelatedEntities/> <a:RowVersion i:nil="true"/> </b:value> </a:KeyValuePairOfstringanyType> </a:Parameters> <a:RequestId>{0}</a:RequestId> <a:RequestName>Update</a:RequestName> </request> </Execute> </s:Body> </s:Envelope> '@ -f $([System.Guid]::NewGuid().Guid), $entity.EntityGuid $headersWebApi."Content-Type" = "text/xml; charset=utf-8" $headersWebApi.SOAPAction = "http://schemas.microsoft.com/xrm/2011/Contracts/Services/IOrganizationService/Execute" Invoke-RestMethod -Method Post -Uri $localUri -Headers $headersWebApi -Body $body -ContentType "text/xml; charset=utf-8" > $null } end { } } <# This is an example configuration file By default, it is enough to have a single one of them, however if you have enough configuration settings to justify having multiple copies of it, feel totally free to split them into multiple files. #> <# # Example Configuration Set-PSFConfig -Module 'd365bap.tools' -Name 'Example.Setting' -Value 10 -Initialize -Validation 'integer' -Handler { } -Description "Example configuration setting. Your module can then use the setting using 'Get-PSFConfigValue'" #> Set-PSFConfig -Module 'd365bap.tools' -Name 'Import.DoDotSource' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be dotsourced on import. By default, the files of this module are read as string value and invoked, which is faster but worse on debugging." Set-PSFConfig -Module 'd365bap.tools' -Name 'Import.IndividualFiles' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be imported individually. During the module build, all module code is compiled into few files, which are imported instead by default. Loading the compiled versions is faster, using the individual files is easier for debugging and testing out adjustments." <# Stored scriptblocks are available in [PsfValidateScript()] attributes. This makes it easier to centrally provide the same scriptblock multiple times, without having to maintain it in separate locations. It also prevents lengthy validation scriptblocks from making your parameter block hard to read. Set-PSFScriptblock -Name 'd365bap.tools.ScriptBlockName' -Scriptblock { } #> <# # Example: Register-PSFTeppScriptblock -Name "d365bap.tools.alcohol" -ScriptBlock { 'Beer','Mead','Whiskey','Wine','Vodka','Rum (3y)', 'Rum (5y)', 'Rum (7y)' } #> <# # Example: Register-PSFTeppArgumentCompleter -Command Get-Alcohol -Parameter Type -Name d365bap.tools.alcohol #> New-PSFLicense -Product 'd365bap.tools' -Manufacturer 'MötzJensen' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2024-03-02") -Text @" Copyright (c) 2024 MötzJensen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. "@ #endregion Load compiled code |