commands.ps1
<# .SYNOPSIS Assign D365 Security configuration .DESCRIPTION Assign the same security configuration as the ADMIN user in the D365FO database .PARAMETER sqlCommand The SQL Command object that should be used when assigning the permissions .PARAMETER Id Id of the user inside the D365FO database .EXAMPLE PS C:\> $SqlParams = @{ DatabaseServer = "localhost" DatabaseName = "AXDB" SqlUser = "sqladmin" SqlPwd = "Pass@word1" TrustedConnection = $false } PS C:\> $SqlCommand = Get-SqlCommand @SqlParams PS C:\> Add-AadUserSecurity -SqlCommand $SqlCommand -Id "TestUser" This will create a new Sql Command object using the Get-SqlCommand cmdlet and the $SqlParams hashtable containing all the needed parameters. With the $SqlCommand in place it calls the Add-AadUserSecurity cmdlet and instructs it to update the "TestUser" to have the same security configuration as the ADMIN user. .NOTES Author: Rasmus Andersen (@ITRasmus) Author: M�tz Jensen (@Splaxi) #> function Add-AadUserSecurity { [OutputType('System.Boolean')] param ( [Parameter(Mandatory = $true)] [System.Data.SqlClient.SqlCommand] $SqlCommand, [Parameter(Mandatory = $true)] [string] $Id ) $commandText = (Get-Content "$script:ModuleRoot\internal\sql\Set-AadUserSecurityInD365FO.sql") -join [Environment]::NewLine $sqlCommand.CommandText = $commandText $null = $sqlCommand.Parameters.Add("@Id", $Id) Write-PSFMessage -Level Verbose -Message "Setting security roles in D365FO database" Write-PSFMessage -Level InternalComment -Message "Executing a script against the database." -Target (Get-SqlString $SqlCommand) $differenceBetweenNewUserAndAdmin = $sqlCommand.ExecuteScalar() Write-PSFMessage -Level Verbose -Message "Difference between new user and admin security roles $differenceBetweenNewUserAndAdmin" -Target $differenceBetweenNewUserAndAdmin $SqlCommand.Parameters.Clear() $differenceBetweenNewUserAndAdmin -eq 0 } <# .SYNOPSIS Backup a file .DESCRIPTION Backup a file in the same directory as the original file with a suffix .PARAMETER File Path to the file that you want to backup .PARAMETER Suffix The suffix value that you want to append to the file name when backing it up .EXAMPLE PS C:\> Backup-File -File c:\temp\d365fo.tools\test.txt -Suffix "Original" This will backup the "test.txt" file as "test_Original.txt" inside "c:\temp\d365fo.tools\" .NOTES Author: Rasmus Andersen (@ITRasmus) Author: M�tz Jensen (@Splaxi) #> function Backup-File { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $File, [Parameter(Mandatory = $true)] [string] $Suffix ) $FileBackup = Get-BackupName $File $Suffix Write-PSFMessage -Level Verbose -Message "Backing up $File to $FileBackup" -Target (@($File, $FileBackup)) (Get-Content -Path $File) | Set-Content -path $FileBackup } <# .SYNOPSIS Complete the upload action in LCS .DESCRIPTION Signal to LCS that the upload of the blob has completed .PARAMETER Token The token to be used for the http request against the LCS API .PARAMETER ProjectId The project id for the Dynamics 365 for Finance & Operations project inside LCS .PARAMETER AssetId The unique id of the asset / file that you are trying to upload to LCS .PARAMETER LcsApiUri URI / URL to the LCS API you want to use .EXAMPLE PS C:\> Complete-LcsUpload -Token "Bearer JldjfafLJdfjlfsalfd..." -ProjectId 123456789 -AssetId "958ae597-f089-4811-abbd-c1190917eaae" -LcsApiUri "https://lcsapi.lcs.dynamics.com" This will commit the upload process for the AssetId "958ae597-f089-4811-abbd-c1190917eaae" in the LCS project with Id 123456789. The http request will be using the "Bearer JldjfafLJdfjlfsalfd..." token for authentication against the LCS API. The http request will be going to the LcsApiUri "https://lcsapi.lcs.dynamics.com" (NON-EUROPE). .NOTES Tags: Environment, Url, Config, Configuration, LCS, Upload, Api, AAD, Token Author: M�tz Jensen (@Splaxi) #> function Complete-LcsUpload { [CmdletBinding()] [OutputType()] param( [Parameter(Mandatory = $true)] [string]$Token, [Parameter(Mandatory = $true)] [int]$ProjectId, [Parameter(Mandatory = $true)] [string]$AssetId, [Parameter(Mandatory = $false)] [string]$LcsApiUri ) Invoke-TimeSignal -Start $client = New-Object -TypeName System.Net.Http.HttpClient $client.DefaultRequestHeaders.Clear() $commitFileUri = "$LcsApiUri/box/fileasset/CommitFileAsset/$($ProjectId)?assetId=$AssetId" $request = New-JsonRequest -Uri $commitFileUri -Token $Token Write-PSFMessage -Level Verbose -Message "Sending the commit request against LCS" -Target $request try { $commitResult = Get-AsyncResult -Task $client.SendAsync($request) Write-PSFMessage -Level Verbose -Message "Parsing the commitResult for success" -Target $commitResult if (($commitResult.StatusCode -ne [System.Net.HttpStatusCode]::NoContent) -and ($commitResult.StatusCode -ne [System.Net.HttpStatusCode]::OK)) { Write-PSFMessage -Level Host -Message "The LCS API returned an http error code" -Exception $PSItem.Exception -Target $commitResult Stop-PSFFunction -Message "Stopping because of errors" -StepsUpward 1 } } catch { Write-PSFMessage -Level Host -Message "Something went wrong while working against the LCS API" -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" -StepsUpward 1 return } Invoke-TimeSignal -End $commitResult } <# .SYNOPSIS Convert HashTable into an array .DESCRIPTION Convert HashTable with switches inside into an array of Key:Value .PARAMETER InputObject The HashTable object that you want to work against Shold only contain Key / Vaule, where value is $true or $false .PARAMETER KeyPrefix The prefix that you want to append to the key of the HashTable The default value is "-" .PARAMETER ValuePrefix The prefix that you want to append to the value of the HashTable The default value is ":" .PARAMETER KeepCase Instruct the cmdlet to keep the naming case of the properties from the hashtable Default value is: $true .EXAMPLE PS C:\> $params = @{NoPrompt = $true; CreateParents = $false} PS C:\> $arguments = Convert-HashToArgStringSwitch -Inputs $params This will convert the $params into an array of strings, each with the "-Key:Value" pattern. .EXAMPLE PS C:\> $params = @{NoPrompt = $true; CreateParents = $false} PS C:\> $arguments = Convert-HashToArgStringSwitch -InputObject $params -KeyPrefix "&" -ValuePrefix "=" This will convert the $params into an array of strings, each with the "&Key=Value" pattern. .NOTES Tags: HashTable, Arguments Author: M�tz Jensen (@Splaxi) #> function Convert-HashToArgStringSwitch { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidDefaultValueSwitchParameter", "")] [CmdletBinding()] [OutputType([System.String])] param ( [HashTable] $InputObject, [string] $KeyPrefix = "-", [string] $ValuePrefix = ":", [switch] $KeepCase = $true ) foreach ($key in $InputObject.Keys) { $value = "{0}" -f $InputObject.Item($key).ToString() if (-not $KeepCase) {$value = $value.ToLower()} "$KeyPrefix$($key)$ValuePrefix$($value)" } } <# .SYNOPSIS Convert an object to boolean .DESCRIPTION Convert an object to boolean or default it to the specified boolean value .PARAMETER Object Input object that you want to work against .PARAMETER Default The default boolean value you want returned if the convert / cast fails .EXAMPLE PS C:\> ConvertTo-BooleanOrDefault -Object "1" -Default $true This will try and convert the "1" value to a boolean value. If the convert would fail, it would return the default value $true. .NOTES Author: M�tz Jensen (@Splaxi) #> function ConvertTo-BooleanOrDefault { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingEmptyCatchBlock', '')] [CmdletBinding()] [OutputType('System.Boolean')] param ( [Object] $Object, [Boolean] $Default ) [boolean] $result = $Default; $stringTrue = @("yes", "true", "ok", "y") $stringFalse = @( "no", "false", "n") try { if (-not ($null -eq $Object) ) { switch ($Object.ToString().ToLower()) { {$stringTrue -contains $_} { $result = $true break } {$stringFalse -contains $_} { $result = $false break } default { $result = [System.Boolean]::Parser($Object.ToString()) break } } } } catch { } $result } <# .SYNOPSIS Convert an object into a HashTable .DESCRIPTION Convert an object into a HashTable, can be used with json objects to create a HashTable .PARAMETER InputObject The object you want to convert .EXAMPLE PS C:\> $jsonString = '{"Test1": "Test1","Test2": "Test2"}' PS C:\> $jsonString | ConvertFrom-Json | ConvertTo-Hashtable .NOTES Author: M�tz Jensen (@Splaxi) Original Author: Adam Bertram (@techsnips_io) Original blog post with the function explained: https://4sysops.com/archives/convert-json-to-a-powershell-hash-table/ #> function ConvertTo-Hashtable { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseOutputTypeCorrectly', '')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseCmdletCorrectly', '')] [CmdletBinding()] param ( [Parameter(ValueFromPipeline)] $InputObject ) process { ## Return null if the input is null. This can happen when calling the function ## recursively and a property is null if ($null -eq $InputObject) { return $null } ## Check if the input is an array or collection. If so, we also need to convert ## those types into hash tables as well. This function will convert all child ## objects into hash tables (if applicable) if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) { $collection = @( foreach ($object in $InputObject) { ConvertTo-Hashtable -InputObject $object } ) ## Return the array but don't enumerate it because the object may be pretty complex Write-Output -NoEnumerate $collection } elseif ($InputObject -is [psobject]) { ## If the object has properties that need enumeration ## Convert it to its own hash table and return it $hash = @{} foreach ($property in $InputObject.PSObject.Properties) { $hash[$property.Name] = ConvertTo-Hashtable -InputObject $property.Value } $hash } else { ## If the object isn't an array, collection, or other object, it's already a hash table ## So just return it. $InputObject } } } <# .SYNOPSIS Convert a Hashtable into a PSCustomObject .DESCRIPTION Convert a Hashtable into a PSCustomObject .PARAMETER InputObject The hashtable you want to convert .EXAMPLE PS C:\> $params = @{SqlUser = ""; SqlPwd = ""} PS C:\> $params | ConvertTo-PsCustomObject This will create a hashtable with 2 properties. It will convert the hashtable into a PSCustomObject .NOTES Author: M�tz Jensen (@Splaxi) Original blog post with the function explained: https://blogs.msdn.microsoft.com/timid/2013/03/05/converting-pscustomobject-tofrom-hashtables/ #> function ConvertTo-PsCustomObject { [OutputType('[PsCustomObject]')] param ( [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [object[]] $InputObject ) begin { $i = 0 } process { foreach ($myHashtable in $InputObject) { if ($myHashtable.GetType().Name -eq 'hashtable') { $output = New-Object -TypeName PsObject Add-Member -InputObject $output -MemberType ScriptMethod -Name AddNote -Value { Add-Member -InputObject $this -MemberType NoteProperty -Name $args[0] -Value $args[1] } $myHashtable.Keys | Sort-Object | ForEach-Object { $output.AddNote($_, $myHashtable.$_) } $output } elseif ($myHashtable.GetType().Name -eq 'OrderedDictionary') { $output = New-Object -TypeName PsObject Add-Member -InputObject $output -MemberType ScriptMethod -Name AddNote -Value { Add-Member -InputObject $this -MemberType NoteProperty -Name $args[0] -Value $args[1] } $myHashtable.Keys | ForEach-Object { $output.AddNote($_, $myHashtable.$_) } $output } else { Write-PSFMessage -Level Warning -Message "Index `$i is not of type [hashtable]" -Target $i } $i += 1 } } } <# .SYNOPSIS Copy local file to Azure Blob Storage .DESCRIPTION Copy local file to Azure Blob Storage that is used by LCS .PARAMETER FilePath Path to the file you want to upload to the Azure Blob storage .PARAMETER FullUri The full URI, including SAS token and Policy Permissions to the blob .EXAMPLE PS C:\> Copy-FileToLcsBlob -FilePath "C:\temp\d365fo.tools\GOLDEN.bacpac" -FullUri "https://uswedpl1catalog.blob.core.windows.net/...." This will upload the "C:\temp\d365fo.tools\GOLDEN.bacpac" to the "https://uswedpl1catalog.blob.core.windows.net/...." Blob Storage location. It is required that the FullUri contains all the needed SAS tokens and Policy Permissions for the upload to succeed. .NOTES Tags: Azure Blob, LCS, Upload Author: M�tz Jensen (@Splaxi) #> function Copy-FileToLcsBlob { [CmdletBinding()] [OutputType()] param( [Parameter(Mandatory = $true)] [string]$FilePath, [Parameter(Mandatory = $true)] [System.Uri]$FullUri ) Invoke-TimeSignal -Start Write-PSFMessage -Level Verbose -Message "Initializing the needed .net objects to work against Azure Blob." -Target $FullUri $cloudblob = New-Object -TypeName Microsoft.WindowsAzure.Storage.Blob.CloudBlockBlob -ArgumentList @($FullUri) try { $uploadResult = Get-AsyncResult -Task $cloudblob.UploadFromFileAsync([System.String]$FilePath) } catch { Write-PSFMessage -Level Host -Message "Something went wrong while uploading the desired file to Azure Blob." -Exception $PSItem.Exception -Target $FullUri Stop-PSFFunction -Message "Stopping because of errors" return } Invoke-TimeSignal -End $uploadResult } <# .SYNOPSIS Load all necessary information about the D365 instance .DESCRIPTION Load all servicing dll files from the D365 instance into memory .EXAMPLE PS C:\> Get-ApplicationEnvironment This will load all the different dll files into memory. .NOTES Author: M�tz Jensen (@Splaxi) #> function Get-ApplicationEnvironment { [System.Collections.ArrayList] $Files2Process = New-Object -TypeName "System.Collections.ArrayList" $AOSPath = Join-Path $script:ServiceDrive "\AOSService\webroot\bin" Write-PSFMessage -Level Verbose -Message "Testing if we are running on a AOS server or not." if (-not (Test-Path -Path $AOSPath -PathType Container)) { Write-PSFMessage -Level Verbose -Message "The machine is NOT an AOS server." $MRPath = Join-Path $script:ServiceDrive "MRProcessService\MRInstallDirectory\Server\Services" Write-PSFMessage -Level Verbose -Message "Testing if we are running on a BI / MR server or not." if (-not (Test-Path -Path $MRPath -PathType Container)) { Write-PSFMessage -Level Verbose -Message "It seems that you ran this cmdlet on a machine that doesn't have the assemblies needed to obtain system details. Most likely you ran it on a <c='em'>personal workstation / personal computer</c>." return } else { Write-PSFMessage -Level Verbose -Message "The machine is a BI / MR server." $BasePath = $MRPath $null = $Files2Process.Add((Join-Path $script:ServiceDrive "Monitoring\Instrumentation\Microsoft.Dynamics.AX.Authentication.Instrumentation.dll")) } } else { Write-PSFMessage -Level Verbose -Message "The machine is an AOS server." $BasePath = $AOSPath $null = $Files2Process.Add((Join-Path $BasePath "Microsoft.Dynamics.AX.Authentication.Instrumentation.dll")) } Write-PSFMessage -Level Verbose -Message "Shadow cloning all relevant assemblies to the Microsoft.Dynamics.ApplicationPlatform.Environment.dll to avoid locking issues. This enables us to install updates while having d365fo.tools loaded" $null = $Files2Process.Add((Join-Path $BasePath "Microsoft.Dynamics.AX.Configuration.Base.dll")) $null = $Files2Process.Add((Join-Path $BasePath "Microsoft.Dynamics.BusinessPlatform.SharedTypes.dll")) $null = $Files2Process.Add((Join-Path $BasePath "Microsoft.Dynamics.AX.Framework.EncryptionEngine.dll")) $null = $Files2Process.Add((Join-Path $BasePath "Microsoft.Dynamics.AX.Security.Instrumentation.dll")) $null = $Files2Process.Add((Join-Path $BasePath "Microsoft.Dynamics.ApplicationPlatform.Environment.dll")) Import-AssemblyFileIntoMemory -Path $($Files2Process.ToArray()) if (Test-PSFFunctionInterrupt) { return } Write-PSFMessage -Level Verbose -Message "All assemblies loaded. Getting environment details." $environment = [Microsoft.Dynamics.ApplicationPlatform.Environment.EnvironmentFactory]::GetApplicationEnvironment() $environment } <# .SYNOPSIS Simple abstraction to handle asynchronous executions .DESCRIPTION Simple abstraction to handle asynchronous executions for several other cmdlets .PARAMETER Task The task you want to work / wait for to complete .EXAMPLE PS C:\> $client = New-Object -TypeName System.Net.Http.HttpClient PS C:\> Get-AsyncResult -Task $client.SendAsync($request) This will take the client (http) and have it send a request using the asynchronous pattern. .NOTES Tags: Async, Waiter, Wait Author: M�tz Jensen (@Splaxi) #> function Get-AsyncResult { [CmdletBinding()] [OutputType('Object')] param ( [Parameter(Mandatory = $true, Position = 1)] [object] $Task ) Write-PSFMessage -Level Verbose -Message "Building the Task Waiter and start waiting." -Target $Task $Task.GetAwaiter().GetResult() } <# .SYNOPSIS Get the Azure Service Objectives .DESCRIPTION Get the current tiering details from the Azure SQL Database instance .PARAMETER DatabaseServer The name of the database server If on-premises or classic SQL Server, use either short name og Fully Qualified Domain Name (FQDN). If Azure use the full address to the database server, e.g. server.database.windows.net .PARAMETER DatabaseName The name of the database .PARAMETER SqlUser The login name for the SQL Server instance .PARAMETER SqlPwd The password for the SQL Server user. .EXAMPLE PS C:\> Get-AzureServiceObjective -DatabaseServer dbserver1.database.windows.net -DatabaseName AxDB -SqlUser User123 -SqlPwd "Password123" This will get the Azure service objective details from the Azure SQL Database instance located at "dbserver1.database.windows.net" .NOTES Author: Rasmus Andersen (@ITRasmus) Author: M�tz Jensen (@Splaxi) #> function Get-AzureServiceObjective { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $DatabaseServer, [Parameter(Mandatory = $true)] [string] $DatabaseName, [Parameter(Mandatory = $true)] [string] $SqlUser, [Parameter(Mandatory = $true)] [string] $SqlPwd ) $sqlCommand = Get-SqlCommand @PsBoundParameters -TrustedConnection $false $commandText = (Get-Content "$script:ModuleRoot\internal\sql\get-azureserviceobjective.sql") -join [Environment]::NewLine $sqlCommand.CommandText = $commandText try { Write-PSFMessage -Level InternalComment -Message "Executing a script against the database." -Target (Get-SqlString $SqlCommand) $sqlCommand.Connection.Open() $reader = $sqlCommand.ExecuteReader() if ($reader.Read() -eq $true) { Write-PSFMessage -Level Verbose "Extracting details from the result retrieved from the Azure DB instance" $edition = $reader.GetString(1) $serviceObjective = $reader.GetString(2) $reader.close() $sqlCommand.Connection.Close() $sqlCommand.Dispose() [PSCustomObject]@{ DatabaseEdition = $edition DatabaseServiceObjective = $serviceObjective } } else { Write-PSFMessage -Level Host -Message "The query to detect <c='em'>edition</c> and <c='em'>service objectives</c> from the Azure DB instance <c='em'>failed</c>." Stop-PSFFunction -Message "Stopping because of missing parameters" return } } catch { Write-PSFMessage -Level Host -Message "Something went wrong while working against the database" -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" return } } <# .SYNOPSIS Get a backup name for the file .DESCRIPTION Generate a backup name for the file parsed .PARAMETER File Path to the file that you want a backup name for .PARAMETER Suffix The name that you want to put into the new backup file name .EXAMPLE PS C:\> Get-BackupName -File "C:\temp\d365do.tools\Test.txt" -Suffix "Original" The function will return "C:\temp\d365do.tools\Test_Original.txt" .NOTES Author: Rasmus Andersen (@ITRasmus) Author: M�tz Jensen (@Splaxi) #> function Get-BackupName { [CmdletBinding()] [OutputType([System.String])] param ( [Parameter(Mandatory = $true)] [string] $File, [Parameter(Mandatory = $true)] [string] $Suffix ) Write-PSFMessage -Level Verbose -Message "Getting backup name for file: $File" -Tag $File $FileInfo = [System.IO.FileInfo]::new($File) $BackupName = "{0}{1}_{2}{3}" -f $FileInfo.Directory, $FileInfo.BaseName, $Suffix, $FileInfo.Extension Write-PSFMessage -Level Verbose -Message "Backup name for the file will be $BackupName" -Tag $BackupName $BackupName } <# .SYNOPSIS Load the Canonical Identity Provider .DESCRIPTION Load the necessary dll files from the D365 instance to get the Canonical Identity Provider object .EXAMPLE PS C:\> Get-CanonicalIdentityProvider This will get the Canonical Identity Provider from the D365 instance .NOTES Author: Rasmus Andersen (@ITRasmus) Author: M�tz Jensen (@Splaxi) #> function Get-CanonicalIdentityProvider { [CmdletBinding()] param () try { Write-PSFMessage -Level Verbose "Loading dll files to do some work against the CanonicalIdentityProvider." Add-Type -Path "$Script:AOSPath\bin\Microsoft.Dynamics.AX.Framework.EncryptionEngine.dll" Add-Type -Path "$Script:AOSPath\bin\Microsoft.Dynamics.AX.Security.AuthenticationCommon.dll" Write-PSFMessage -Level Verbose "Executing the CanonicalIdentityProvider lookup logic." $Identity = [Microsoft.Dynamics.AX.Security.AuthenticationCommon.AadHelper]::GetIdentityProvider() $Provider = [Microsoft.Dynamics.AX.Security.AuthenticationCommon.AadHelper]::GetCanonicalIdentityProvider($Identity) Write-PSFMessage -Level Verbose "CanonicalIdentityProvider is: $Provider" -Tag $Provider return $Provider } catch { Write-PSFMessage -Level Host -Message "Something went wrong while working against the CanonicalIdentityProvider." -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" return } } <# .SYNOPSIS Clone a hashtable .DESCRIPTION Create a deep clone of a hashtable for you to work on it without updating the original object .PARAMETER InputObject The hashtable you want to clone .EXAMPLE PS C:\> Get-DeepClone -InputObject $HashTable This will clone the $HashTable variable into a new object and return it to you. .NOTES Author: M�tz Jensen (@Splaxi) #> function Get-DeepClone { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseOutputTypeCorrectly', '')] [CmdletBinding()] param( [parameter(Mandatory = $true)] $InputObject ) process { if($InputObject -is [hashtable]) { $clone = @{} foreach($key in $InputObject.keys) { $clone[$key] = Get-DeepClone $InputObject[$key] } $clone } else { $InputObject } } } <# .SYNOPSIS Get the file version details .DESCRIPTION Get the file version details for any given file .PARAMETER Path Path to the file that you want to extract the file version details from .EXAMPLE PS C:\> Get-FileVersion -Path "C:\Program Files\Microsoft Dynamics AX\60\Server\MicrosoftDynamicsAX\Bin\AxServ32.exe" This will get the file version details for the AX AOS executable (AxServ32.exe). .NOTES Author: M�tz Jensen (@Splaxi) Inspired by https://blogs.technet.microsoft.com/askpfeplat/2014/12/07/how-to-correctly-check-file-versions-with-powershell/ #> function Get-FileVersion { [CmdletBinding()] Param( [Parameter(Mandatory = $true)] [string] $Path ) if (-not (Test-PathExists -Path $Path -Type Leaf)) { return } Write-PSFMessage -Level Verbose -Message "Extracting the file properties for: $Path" -Target $Path $Filepath = Get-Item -Path $Path [PSCustomObject]@{ FileVersion = $Filepath.VersionInfo.FileVersion ProductVersion = $Filepath.VersionInfo.ProductVersion FileVersionUpdated = "$($Filepath.VersionInfo.FileMajorPart).$($Filepath.VersionInfo.FileMinorPart).$($Filepath.VersionInfo.FileBuildPart).$($Filepath.VersionInfo.FilePrivatePart)" ProductVersionUpdated = "$($Filepath.VersionInfo.ProductMajorPart).$($Filepath.VersionInfo.ProductMinorPart).$($Filepath.VersionInfo.ProductBuildPart).$($Filepath.VersionInfo.ProductPrivatePart)" } } <# .SYNOPSIS Get the identity provider .DESCRIPTION Execute a web request to get the identity provider for the given email address .PARAMETER Email Email address on the account that you want to get the Identity Provider details about .EXAMPLE PS C:\> Get-IdentityProvider -Email "Claire@contoso.com" This will get the Identity Provider details for the user account with the email address "Claire@contoso.com" .NOTES Author : Rasmus Andersen (@ITRasmus) Author : M�tz Jensen (@splaxi) #> function Get-IdentityProvider { [CmdletBinding()] param( [Parameter(Mandatory = $true, Position = 1)] [string]$Email ) $tenant = Get-TenantFromEmail $Email try { $webRequest = New-WebRequest "https://login.windows.net/$tenant/.well-known/openid-configuration" $null "GET" $response = $WebRequest.GetResponse() if ($response.StatusCode -eq [System.Net.HttpStatusCode]::Ok) { $stream = $response.GetResponseStream() $streamReader = New-Object System.IO.StreamReader($stream); $openIdConfig = $streamReader.ReadToEnd() $streamReader.Close(); } else { $statusDescription = $response.StatusDescription throw "Https status code : $statusDescription" } $openIdConfigJSON = ConvertFrom-Json $openIdConfig $openIdConfigJSON.issuer } catch { Write-PSFMessage -Level Host -Message "Something went wrong while executing the web request" -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" return } } <# .SYNOPSIS Get the instance provider from the D365FO instance .DESCRIPTION Get the instance provider from the dll files used for encryption and authentication for D365FO .EXAMPLE PS C:\> Get-InstanceIdentityProvider This will return the Instance Identity Provider based on the D365FO instance. .NOTES Author : Rasmus Andersen (@ITRasmus) Author : M�tz Jensen (@splaxi) #> function Get-InstanceIdentityProvider { [CmdletBinding()] [OutputType([System.String])] param() $files = @("$Script:AOSPath\bin\Microsoft.Dynamics.AX.Framework.EncryptionEngine.dll", "$Script:AOSPath\bin\Microsoft.Dynamics.AX.Security.AuthenticationCommon.dll") if (-not (Test-PathExists -Path $files -Type Leaf)) { return } try { Add-Type -Path $files $Identity = [Microsoft.Dynamics.AX.Security.AuthenticationCommon.AadHelper]::GetIdentityProvider() Write-PSFMessage -Level Verbose -Message "The found instance identity provider is: $Identity" -Target $Identity $Identity } catch { Write-PSFMessage -Level Host -Message "Something went wrong while working against the Identity provider" -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" return } } <# .SYNOPSIS Get the Azure Database instance values .DESCRIPTION Extract the PlanId, TenantId and PlanCapability from the Azure Database instance .PARAMETER DatabaseServer The name of the database server If on-premises or classic SQL Server, use either short name og Fully Qualified Domain Name (FQDN). If Azure use the full address to the database server, e.g. server.database.windows.net .PARAMETER DatabaseName The name of the database .PARAMETER SqlUser The login name for the SQL Server instance .PARAMETER SqlPwd The password for the SQL Server user. .PARAMETER TrustedConnection Should the connection use a Trusted Connection or not .EXAMPLE PS C:\> Get-InstanceValues -DatabaseServer SQLServer -DatabaseName AXDB -SqlUser "SqlAdmin" -SqlPwd "Pass@word1" This will extract the PlanId, TenantId and PlanCapability from the AXDB on the SQLServer, using the "SqlAdmin" credentials to do so. .NOTES Author: Rasmus Andersen (@ITRasmus) Author: M�tz Jensen (@Splaxi) #> function Get-InstanceValues { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [CmdletBinding()] [OutputType('System.Collections.Hashtable')] param ( [Parameter(Mandatory = $true)] [string] $DatabaseServer, [Parameter(Mandatory = $true)] [string] $DatabaseName, [Parameter(Mandatory = $false)] [string] $SqlUser, [Parameter(Mandatory = $false)] [string] $SqlPwd, [Parameter(Mandatory = $false)] [boolean] $TrustedConnection ) $sqlCommand = Get-SqlCommand @PsBoundParameters $commandText = (Get-Content "$script:ModuleRoot\internal\sql\get-instancevalues.sql") -join [Environment]::NewLine $sqlCommand.CommandText = $commandText try { Write-PSFMessage -Level InternalComment -Message "Executing a script against the database." -Target (Get-SqlString $SqlCommand) $sqlCommand.Connection.Open() $reader = $sqlCommand.ExecuteReader() if ($reader.Read() -eq $true) { Write-PSFMessage -Level Verbose "Extracting details from the result retrieved from the DB instance" $tenantId = $reader.GetString(0) $planId = $reader.GetGuid(1) $planCapability = $reader.GetString(2) @{ TenantId = $tenantId PlanId = $planId PlanCapability = $planCapability } } else { Write-PSFMessage -Level Host -Message "The query to detect <c='em'>TenantId</c>, <c='em'>PlanId</c> and <c='em'>PlanCapability</c> from the database <c='em'>failed</c>." Stop-PSFFunction -Message "Stopping because of missing parameters" return } } catch { Write-PSFMessage -Level Host -Message "Something went wrong while working against the database" -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" return } finally { $reader.close() $sqlCommand.Connection.Close() $sqlCommand.Dispose() } } <# .SYNOPSIS Get the login name from the e-mail address .DESCRIPTION Extract the login name from the e-mail address by substring everything before the @ character .PARAMETER Email The e-mail address that you want to get the login name from .EXAMPLE PS C:\> Get-LoginFromEmail -Email Claire@contoso.com This will substring the e-mail address and return "Claire" as the result .NOTES Author: Rasmus Andersen (@ITRasmus) Author: M�tz Jensen (@Splaxi) #> function Get-LoginFromEmail { [CmdletBinding()] [OutputType('System.String')] param ( [string]$Email ) $email.Substring(0, $Email.LastIndexOf('@')).Trim() } <# .SYNOPSIS Get the network domain from the e-mail .DESCRIPTION Get the network domain provider (Azure) for the e-mail / user .PARAMETER Email The e-mail that you want to retrieve the provider for .EXAMPLE PS C:\> Get-NetworkDomain -Email "Claire@contoso.com" This will return the provider registered with the "Claire@contoso.com" e-mail address. .NOTES Author: Rasmus Andersen (@ITRasmus) Author: M�tz Jensen (@Splaxi) #> function Get-NetworkDomain { [CmdletBinding()] [OutputType('System.String')] param( [Parameter(Mandatory = $true, Position = 1)] [string]$Email ) $tenant = Get-TenantFromEmail $Email $provider = Get-InstanceIdentityProvider $canonicalIdentityProvider = Get-CanonicalIdentityProvider if ($Provider.ToLower().Contains($Tenant.ToLower()) -eq $True) { $canonicalIdentityProvider } else { "$canonicalIdentityProvider$Tenant" } } <# .SYNOPSIS Get the product information .DESCRIPTION Get the product information object from the environment .EXAMPLE PS C:\> Get-ProductInfoProvider This will get the product information object and return it .NOTES Author: Rasmus Andersen (@ITRasmus) Author: M�tz Jensen (@Splaxi) #> function Get-ProductInfoProvider { Add-Type -Path "$Script:AOSPath\bin\Microsoft.Dynamics.BusinessPlatform.ProductInformation.Provider.dll" [Microsoft.Dynamics.BusinessPlatform.ProductInformation.Provider.ProductInfoProvider]::get_Provider() } <# .SYNOPSIS Get the list of Dynamics 365 services .DESCRIPTION Get the list of Dynamics 365 service names based on the parameters .PARAMETER All Switch to instruct the cmdlet to output all service names .PARAMETER Aos Switch to instruct the cmdlet to output the aos service name .PARAMETER Batch Switch to instruct the cmdlet to output the batch service name .PARAMETER FinancialReporter Switch to instruct the cmdlet to output the financial reporter service name .PARAMETER DMF Switch to instruct the cmdlet to output the data management service name .EXAMPLE PS C:\> Get-ServiceList -All This will return all services for an D365 environment .NOTES Author: M�tz Jensen (@Splaxi) #> Function Get-ServiceList { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidDefaultValueSwitchParameter", "")] [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 2 )] [switch] $All = $true, [Parameter(Mandatory = $false, ParameterSetName = 'Specific', Position = 2 )] [switch] $Aos, [Parameter(Mandatory = $false, ParameterSetName = 'Specific', Position = 3 )] [switch] $Batch, [Parameter(Mandatory = $false, ParameterSetName = 'Specific', Position = 4 )] [switch] $FinancialReporter, [Parameter(Mandatory = $false, ParameterSetName = 'Specific', Position = 5 )] [switch] $DMF ) if ($PSCmdlet.ParameterSetName -eq "Specific") { $All = $false } Write-PSFMessage -Level Verbose -Message "The PSBoundParameters was" -Target $PSBoundParameters $aosname = "w3svc" $batchname = "DynamicsAxBatch" $financialname = "MR2012ProcessService" $dmfname = "Microsoft.Dynamics.AX.Framework.Tools.DMF.SSISHelperService.exe" [System.Collections.ArrayList]$Services = New-Object -TypeName "System.Collections.ArrayList" if ($All) { $null = $Services.AddRange(@($aosname, $batchname, $financialname, $dmfname)) } else { if ($Aos) { $null = $Services.Add($aosname) } if ($Batch) { $null = $Services.Add($batchname) } if ($FinancialReporter) { $null = $Services.Add($financialname) } if ($DMF) { $null = $Services.Add($dmfname) } } $Services.ToArray() } <# .SYNOPSIS Get a SqlCommand object .DESCRIPTION Get a SqlCommand object initialized with the passed parameters .PARAMETER DatabaseServer The name of the database server If on-premises or classic SQL Server, use either short name og Fully Qualified Domain Name (FQDN). If Azure use the full address to the database server, e.g. server.database.windows.net .PARAMETER DatabaseName The name of the database .PARAMETER SqlUser The login name for the SQL Server instance .PARAMETER SqlPwd The password for the SQL Server user. .PARAMETER TrustedConnection Should the connection use a Trusted Connection or not .EXAMPLE PS C:\> Get-SqlCommand -DatabaseServer localhost -DatabaseName AxDB -SqlUser User123 -SqlPwd "Password123" -TrustedConnection $true This will initialize a new SqlCommand object (.NET type) with localhost as the server name, AxDB as the database and the User123 sql credentials. .NOTES Author: Rasmus Andersen (@ITRasmus) Author: M�tz Jensen (@Splaxi) #> function Get-SQLCommand { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $DatabaseServer, [Parameter(Mandatory = $true)] [string] $DatabaseName, [Parameter(Mandatory = $false)] [string] $SqlUser, [Parameter(Mandatory = $false)] [string] $SqlPwd, [Parameter(Mandatory = $false)] [boolean] $TrustedConnection ) Write-PSFMessage -Level Debug -Message "Writing the bound parameters" -Target $PsBoundParameters [System.Collections.ArrayList]$Params = New-Object -TypeName "System.Collections.ArrayList" $null = $Params.Add("Server='$DatabaseServer';") $null = $Params.Add("Database='$DatabaseName';") if ($null -eq $TrustedConnection -or (-not $TrustedConnection)) { $null = $Params.Add("User='$SqlUser';") $null = $Params.Add("Password='$SqlPwd';") } else { $null = $Params.Add("Integrated Security='SSPI';") } $null = $Params.Add("Application Name='d365fo.tools'") Write-PSFMessage -Level Verbose -Message "Building the SQL connection string." -Target ($Params -join ",") $sqlConnection = New-Object System.Data.SqlClient.SqlConnection try { $sqlConnection.ConnectionString = ($Params -join "") $sqlCommand = New-Object System.Data.SqlClient.SqlCommand $sqlCommand.Connection = $sqlConnection $sqlCommand.CommandTimeout = 0 } catch { Write-PSFMessage -Level Host -Message "Something went wrong while working with the sql server connection objects" -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" return } $sqlCommand } <# .SYNOPSIS Get the size from the parameter .DESCRIPTION Get the size from the parameter based on its datatype and value .PARAMETER SqlParameter The SqlParameter object that you want to get the size from .EXAMPLE PS C:\> $SqlCmd = New-Object System.Data.SqlClient.SqlCommand PS C:\> $SqlCmd.Parameters.AddWithValue("@Parm1", "1234") PS C:\> Get-SqlParameterSize -SqlParameter $SqlCmd.Parameters[0] This will extract the size from the first parameter from the SqlCommand object and return it as a formatted string. .NOTES Author: M�tz Jensen (@Splaxi) #> function Get-SqlParameterSize { [CmdletBinding()] [OutputType('System.String')] param ( [System.Data.SqlClient.SqlParameter] $SqlParameter ) $res = "" $stringSizeTypes = @( [System.Data.SqlDbType]::Char, [System.Data.SqlDbType]::NChar, [System.Data.SqlDbType]::NText, [System.Data.SqlDbType]::NVarChar, [System.Data.SqlDbType]::Text, [System.Data.SqlDbType]::VarChar ) if ( $stringSizeTypes -contains $SqlParameter.SqlDbType) { $res = "($($SqlParameter.Size))" } $res } <# .SYNOPSIS Get the value from the parameter .DESCRIPTION Get the value that is assigned to the SqlParameter object .PARAMETER SqlParameter The SqlParameter object that you want to work against .EXAMPLE PS C:\> $SqlCmd = New-Object System.Data.SqlClient.SqlCommand PS C:\> $SqlCmd.Parameters.AddWithValue("@Parm1", "1234") PS C:\> Get-SqlParameterValue -SqlParameter $SqlCmd.Parameters[0] This will extract the value from the first parameter from the SqlCommand object. .NOTES Author: M�tz Jensen (@Splaxi) #> function Get-SqlParameterValue { [CmdletBinding()] [OutputType('System.String')] param ( [System.Data.SqlClient.SqlParameter] $SqlParameter ) $result = $null $stringEscaped = @( [System.Data.SqlDbType]::Char, [System.Data.SqlDbType]::DateTime, [System.Data.SqlDbType]::NChar, [System.Data.SqlDbType]::NText, [System.Data.SqlDbType]::NVarChar, [System.Data.SqlDbType]::Text, [System.Data.SqlDbType]::VarChar, [System.Data.SqlDbType]::Xml, [System.Data.SqlDbType]::Date, [System.Data.SqlDbType]::Time, [System.Data.SqlDbType]::DateTime2, [System.Data.SqlDbType]::DateTimeOffset ) $stringNumbers = @([System.Data.SqlDbType]::Float, [System.Data.SqlDbType]::Decimal) switch ($SqlParameter.SqlDbType) { { $stringEscaped -contains $_ } { $result = "'{0}'" -f $SqlParameter.Value.ToString().Replace("'", "''") break } { [System.Data.SqlDbType]::Bit } { if ((ConvertTo-BooleanOrDefault -Object $SqlParameter.Value.ToString() -Default $true)) { $result = '1' } else { $result = '0' } break } { $stringNumbers -contains $_ } { $SqlParameter.Value $result = ([System.Double]$SqlParameter.Value).ToString([System.Globalization.CultureInfo]::InvariantCulture).Replace("'", "''") break } default { $result = $SqlParameter.Value.ToString().Replace("'", "''") break } } $result } <# .SYNOPSIS Get an executable string from a SqlCommand object .DESCRIPTION Get an formatted and valid string from a SqlCommand object that contains all variables .PARAMETER SqlCommand The SqlCommand object that you want to retrieve the string from .EXAMPLE PS C:\> $SqlCmd = New-Object System.Data.SqlClient.SqlCommand PS C:\> $SqlCmd.CommandText = "SELECT * FROM Table WHERE Column = @Parm1" PS C:\> $SqlCmd.Parameters.AddWithValue("@Parm1", "1234") PS C:\> Get-SqlString -SqlCommand $SqlCmd .NOTES Author: M�tz Jensen (@Splaxi) #> function Get-SqlString { [CmdletBinding()] [OutputType('System.String')] param ( [System.Data.SqlClient.SqlCommand] $SqlCommand ) $sbDeclare = [System.Text.StringBuilder]::new() $sbAssignment = [System.Text.StringBuilder]::new() $sbRes = [System.Text.StringBuilder]::new() if ($SqlCommand.CommandType -eq [System.Data.CommandType]::Text) { if (-not ($null -eq $SqlCommand.Connection)) { $null = $sbDeclare.Append("USE [").Append($SqlCommand.Connection.Database).AppendLine("]") } foreach ($parameter in $SqlCommand.Parameters) { if ($parameter.Direction -eq [System.Data.ParameterDirection]::Input) { $null = $sbDeclare.Append("DECLARE ").Append($parameter.ParameterName).Append("`t") $null = $sbDeclare.Append($parameter.SqlDbType.ToString().ToUpper()) $null = $sbDeclare.AppendLine((Get-SqlParameterSize -SqlParameter $parameter)) $null = $sbAssignment.Append("SET ").Append($parameter.ParameterName).Append(" = ").AppendLine((Get-SqlParameterValue -SqlParameter $parameter)) } } $null = $sbRes.AppendLine($sbDeclare.ToString()) $null = $sbRes.AppendLine($sbAssignment.ToString()) $null = $sbRes.AppendLine($SqlCommand.CommandText) } $sbRes.ToString() } <# .SYNOPSIS Get the tenant from e-mail address .DESCRIPTION Get the tenant (domain) from an e-mail address .PARAMETER Email The e-mail address you want to get the tenant from .EXAMPLE PS C:\> Get-TenantFromEmail -Email "Claire@contoso.com" This will return the tenant (domain) from the "Claire@contoso.com" e-mail address. .NOTES Author: Rasmus Andersen (@ITRasmus) Author: M�tz Jensen (@Splaxi) #> function Get-TenantFromEmail { [CmdletBinding()] [OutputType('System.String')] param ( [string] $email ) $email.Substring($email.LastIndexOf('@') + 1).Trim(); } <# .SYNOPSIS Get time zone .DESCRIPTION Extract the time zone object from the supplied parameter Uses regex to determine whether or not the parameter is the ID or the DisplayName of a time zone .PARAMETER InputObject String value that you want converted into a time zone object .EXAMPLE PS C:\> Get-TimeZone -InputObject "UTC" This will return the time zone object based on the UTC id. .NOTES Tag: Time, TimeZone, Author: M�tz Jensen (@Splaxi) #> function Get-TimeZone { [CmdletBinding()] [OutputType('System.TimeZoneInfo')] param ( [Parameter(Mandatory = $true, Position = 1)] [string] $InputObject ) if ($InputObject -match "\s\-\s\[") { $search = [regex]::Split($InputObject, "\s\-\s\[")[0] [System.TimeZoneInfo]::GetSystemTimeZones() | Where-Object {$PSItem.DisplayName -eq $search} | Select-Object -First 1 } else { try { [System.TimeZoneInfo]::FindSystemTimeZoneById($InputObject) } catch { Write-PSFMessage -Level Host -Message "Unable to translate the <c='em'>$InputObject</c> to a known .NET timezone value. Please make sure you filled in a valid timezone." Stop-PSFFunction -Message "Stopping because timezone wasn't found." -StepsUpward 1 return } } } <# .SYNOPSIS Get the SID from an Azure Active Directory (AAD) user .DESCRIPTION Get the generated SID that an Azure Active Directory (AAD) user will get in relation to Dynamics 365 Finance & Operations environment .PARAMETER SignInName The sign in name (email address) for the user that you want the SID from .PARAMETER Provider The provider connected to the sign in name .EXAMPLE PS C:\> Get-UserSIDFromAad -SignInName "Claire@contoso.com" -Provider "ZXY" This will get the SID for Azure Active Directory user "Claire@contoso.com" .NOTES Author: Rasmus Andersen (@ITRasmus) Author: M�tz Jensen (@Splaxi) #> function Get-UserSIDFromAad { [CmdletBinding()] [OutputType('System.String')] param ( [string] $SignInName, [string] $Provider ) try { Add-Type -Path "$Script:AOSPath\bin\Microsoft.Dynamics.BusinessPlatform.SharedTypes.dll" Add-Type -Path "$Script:AOSPath\bin\Microsoft.Dynamics.ApplicationPlatform.PerformanceCounters.dll" Add-Type -Path "$Script:AOSPath\bin\Microsoft.Dynamics.ApplicationPlatform.XppServices.Instrumentation.dll" Add-Type -Path "$Script:AOSPath\bin\Microsoft.Dynamics.AX.Security.SidGenerator.dll" $SID = [Microsoft.Dynamics.Ax.Security.SidGenerator]::Generate($SignInName, $Provider) Write-PSFMessage -Level Verbose -Message "Generated SID: $SID" -Target $SID $SID } catch { Write-PSFMessage -Level Host -Message "Something went wrong while working against the database" -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" return } } <# .SYNOPSIS Import an Azure Active Directory (AAD) user .DESCRIPTION Import an Azure Active Directory (AAD) user into a Dynamics 365 for Finance & Operations environment .PARAMETER SqlCommand The SQL Command object that should be used when importing the AAD user .PARAMETER SignInName The sign in name (email address) for the user that you want to import .PARAMETER Name The name that the imported user should have inside the D365FO environment .PARAMETER Id The ID that the imported user should have inside the D365FO environment .PARAMETER SID The SID that correlates to the imported user inside the D365FO environment .PARAMETER StartUpCompany The default company (legal entity) for the imported user .PARAMETER IdentityProvider The provider for the imported to validated against .PARAMETER NetworkDomain The network domain of the imported user .PARAMETER ObjectId The Azure Active Directory object id for the imported user .EXAMPLE PS C:\> $SqlCommand = Get-SqlCommand -DatabaseServer localhost -DatabaseName AxDB -SqlUser User123 -SqlPwd "Password123" PS C:\> Import-AadUserIntoD365FO -SqlCommand $SqlCommand -SignInName "Claire@contoso.com" -Name "Claire" -Id "claire" -SID "123XYZ" -StartupCompany "DAT" -IdentityProvider "XYZ" -NetworkDomain "Contoso.com" -ObjectId "123XYZ" This will get a SqlCommand object that will connect to the localhost server and the AXDB database, with the sql credential "User123". The SqlCommand object is passed to the Import-AadUserIntoD365FO along with all the necessary details for importing Claire@contoso.com as an user into the D365FO environment. .NOTES Author: Rasmus Andersen (@ITRasmus) Author: M�tz Jensen (@Splaxi) #> function Import-AadUserIntoD365FO { [CmdletBinding()] param ( [System.Data.SqlClient.SqlCommand] $SqlCommand, [string] $SignInName, [string] $Name, [string] $Id, [string] $SID, [string] $StartUpCompany, [string] $IdentityProvider, [string] $NetworkDomain, [string] $ObjectId ) Write-PSFMessage -Level Verbose -Message "Testing the Email $signInName" -Target $signInName $UserFound = Test-AadUserInD365FO $sqlCommand $SignInName if ($UserFound -eq $false) { Write-PSFMessage -Level Verbose -Message "Testing the userid $Id" -Target $Id $idTaken = Test-AadUserIdInD365FO $sqlCommand $id if (Test-PSFFunctionInterrupt) { return } if ($idTaken -eq $false) { $userAdded = New-D365FOUser $sqlCommand $SignInName $Name $Id $Sid $StartUpCompany $IdentityProvider $NetworkDomain $ObjectId if ($userAdded -eq $true) { $securityAdded = Add-AadUserSecurity $sqlCommand $Id Write-PSFMessage -Level Host -Message "User $SignInName Imported" if ($securityAdded -eq $false) { Write-PSFMessage -Level Host -Message "User $SignInName did not get securityRoles" #Stop-PSFFunction -Message "Stopping because of errors" -StepsUpward 1 #return } } else { Write-PSFMessage -Level Host -Message "User $SignInName, not added to D365FO" #Stop-PSFFunction -Message "Stopping because of errors" -StepsUpward 1 #return } } else { Write-PSFMessage -Level Host -Message "An User with ID = '$ID' already exists" #Stop-PSFFunction -Message "Stopping because of errors" -StepsUpward 1 #return } } else { Write-PSFMessage -Level Host -Message "An User with Email $SignInName already exists in D365FO" #Stop-PSFFunction -Message "Stopping because of errors" -StepsUpward 1 #return } } <# .SYNOPSIS Imports a .NET dll file into memory .DESCRIPTION Imports a .NET dll file into memory, by creating a copy (temporary file) and imports it using reflection .PARAMETER Path Path to the dll file you want to import Accepts an array of strings .EXAMPLE PS C:\> Import-AssemblyFileIntoMemory -Path "C:\AOSService\PackagesLocalDirectory\Bin\Microsoft.Dynamics.BusinessPlatform.ProductInformation.Framework.dll" This will create an new file named "C:\AOSService\PackagesLocalDirectory\Bin\Microsoft.Dynamics.BusinessPlatform.ProductInformation.Framework.dll_shawdow.dll" The new file is then imported into memory using .NET Reflection. After the file has been imported, it will be deleted from disk. .NOTES Author: M�tz Jensen (@Splaxi) #> function Import-AssemblyFileIntoMemory { [CmdletBinding()] [OutputType()] param ( [Parameter(Mandatory = $true, Position = 1)] [string[]] $Path ) if (-not (Test-PathExists -Path $Path -Type Leaf)) { Stop-PSFFunction -Message "Stopping because unable to locate file." -StepsUpward 1 return } Invoke-TimeSignal -Start foreach ($itemPath in $Path) { $shadowClonePath = "$itemPath`_shadow.dll" try { Write-PSFMessage -Level Verbose -Message "Cloning $itemPath to $shadowClonePath" Copy-Item -Path $itemPath -Destination $shadowClonePath -Force Write-PSFMessage -Level Verbose -Message "Loading $shadowClonePath into memory" $null = [AppDomain]::CurrentDomain.Load(([System.IO.File]::ReadAllBytes($shadowClonePath))) } catch { Write-PSFMessage -Level Host -Message "Something went wrong while working against the database" -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" return } finally { Write-PSFMessage -Level Verbose -Message "Removing $shadowClonePath" Remove-Item -Path $shadowClonePath -Force -ErrorAction SilentlyContinue } } Invoke-TimeSignal -End } <# .SYNOPSIS Authenticate against Azure Active Directory (AAD) .DESCRIPTION Authenticate against Azure Active Directory (AAD) and retrieve a token .PARAMETER Resource The resource / URL you want the authentication to be valid for .PARAMETER GrantType The type of grant you want the authentication request to be Valid options (non-validated): authorization_code refresh_token password client_credentials .PARAMETER ClientId The Azure Registered Application Id / Client Id obtained while creating a Registered App inside the Azure Portal .PARAMETER ClientSecret The secret obtained when you created a secret in relation to the Registered Application from the Azure Portal .PARAMETER Username The username of the account that you want to impersonate .PARAMETER Password The password of the account that you want to impersonate .PARAMETER Scope The scope value to apply to the authentication request .PARAMETER AuthProviderUri The URI / URL for the Authentication Provider you want to authenticate against Default value is "https://login.microsoftonline.com/common/oauth2" .EXAMPLE PS C:\> Invoke-AadAuthentication -Resource "https://lcsapi.lcs.dynamics.com" -GrantType "password" -ClientId "9b4f4503-b970-4ade-abc6-2c086e4c4929" -Username claire@contoso.com -Password "pass@word1" -Scope openid This will create a http authentication request against the default AuthProviderUri ("https://login.microsoftonline.com/common/oauth2"). The request will be for the Resource "https://lcsapi.lcs.dynamics.com". The GrantType will be "password". The ClientId will "9b4f4503-b970-4ade-abc6-2c086e4c4929". The Username is claire@contoso.com, and the Password is "pass@word1". The Scope is "openid" .NOTES Tags: Authentication, AAD, Azure Active Directory, Grant, ClientId Author: M�tz Jensen (@Splaxi) #> function Invoke-AadAuthentication { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingUserNameAndPassWordParams", "")] [CmdletBinding()] [OutputType('System.String')] param ( [Parameter(Mandatory = $true, Position = 1)] [string] $Resource, [Parameter(Mandatory = $true, Position = 2)] [string] $GrantType, [Parameter(Mandatory = $false, Position = 3)] [string] $ClientId, [Parameter(Mandatory = $false, Position = 4)] [string] $ClientSecret, [Parameter(Mandatory = $false, Position = 5)] [string] $Username, [Parameter(Mandatory = $false, Position = 6)] [string] $Password, [Parameter(Mandatory = $false, Position = 7)] [string] $Scope, [Parameter(Mandatory = $false, Position = 8)] [string] $AuthProviderUri = "https://login.microsoftonline.com/common/oauth2/token" ) Invoke-TimeSignal -Start $parms = @{} $parms.resource = [System.Web.HttpUtility]::UrlEncode($Resource) $parms.grant_type = [System.Web.HttpUtility]::UrlEncode($GrantType) if (-not ($ClientId -eq "")) {$parms.client_id = [System.Web.HttpUtility]::UrlEncode($ClientId)} if (-not ($ClientSecret -eq "")) {$parms.client_secret = [System.Web.HttpUtility]::UrlEncode($ClientSecret)} if (-not ($Username -eq "")) {$parms.username = [System.Web.HttpUtility]::UrlEncode($Username)} if (-not ($Password -eq "")) {$parms.password = [System.Web.HttpUtility]::UrlEncode($Password)} if (-not ($Scope -eq "")) {$parms.scope = [System.Web.HttpUtility]::UrlEncode($Scope)} $body = (Convert-HashToArgStringSwitch -InputObject $parms -KeyPrefix "&" -ValuePrefix "=") -join "" $body = $body.Substring(1) Write-PSFMessage -Level Verbose -Message "Authenticating against Azure Active Directory (AAD)." -Target $body try { $requestParams = @{Method = "Post"; ContentType = "application/x-www-form-urlencoded"; Body = $body} $Authorization = Invoke-RestMethod $AuthProviderUri @requestParams } catch { Write-PSFMessage -Level Host -Message "Something went wrong while working against Azure Active Directory (AAD)" -Exception $PSItem.Exception -Target $body Stop-PSFFunction -Message "Stopping because of errors" -StepsUpward 1 return } $Authorization.access_token } <# .SYNOPSIS Create a database copy in Azure SQL Database instance .DESCRIPTION Create a new database by cloning a database in Azure SQL Database instance .PARAMETER DatabaseServer The name of the database server If on-premises or classic SQL Server, use either short name og Fully Qualified Domain Name (FQDN) If Azure use the full address to the database server, e.g. server.database.windows.net .PARAMETER DatabaseName The name of the database .PARAMETER SqlUser The login name for the SQL Server instance .PARAMETER SqlPwd The password for the SQL Server user .PARAMETER NewDatabaseName Name of the new / cloned database in the Azure SQL Database instance .EXAMPLE PS C:\> Invoke-AzureBackupRestore -DatabaseServer TestServer.database.windows.net -DatabaseName AxDB -SqlUser User123 -SqlPwd "Password123" -NewDatabaseName ExportClone This will create a database named "ExportClone" in the "TestServer.database.windows.net" Azure SQL Database instance. It uses the SQL credential "User123" to preform the needed actions. .NOTES Author: Rasmus Andersen (@ITRasmus) Author: M�tz Jensen (@Splaxi) #> Function Invoke-AzureBackupRestore { [CmdletBinding()] [OutputType('System.Boolean')] param ( [Parameter(Mandatory = $true)] [string] $DatabaseServer, [Parameter(Mandatory = $true)] [string] $DatabaseName, [Parameter(Mandatory = $true)] [string] $SqlUser, [Parameter(Mandatory = $true)] [string] $SqlPwd, [Parameter(Mandatory = $true)] [string] $NewDatabaseName ) Invoke-TimeSignal -Start $StartTime = Get-Date $SqlConParams = @{DatabaseServer = $DatabaseServer; SqlUser = $SqlUser; SqlPwd = $SqlPwd; TrustedConnection = $false} $sqlCommand = Get-SqlCommand @SqlConParams -DatabaseName $DatabaseName $commandText = (Get-Content "$script:ModuleRoot\internal\sql\newazuredbfromcopy.sql") -join [Environment]::NewLine $commandText = $commandText.Replace('@CurrentDatabase', $DatabaseName) $commandText = $commandText.Replace('@NewName', $NewDatabaseName) $sqlCommand.CommandText = $commandText try { Write-PSFMessage -Level InternalComment -Message "Executing a script against the database." -Target (Get-SqlString $SqlCommand) $sqlCommand.Connection.Open() $null = $sqlCommand.ExecuteNonQuery() } catch { Write-PSFMessage -Level Host -Message "Something went wrong while creating the copy of the Azure DB" -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" return } finally { if ($sqlCommand.Connection.State -ne [System.Data.ConnectionState]::Closed) { $sqlCommand.Connection.Close() } $sqlCommand.Dispose() } $sqlCommand = Get-SqlCommand @SqlConParams -DatabaseName "master" $commandText = (Get-Content "$script:ModuleRoot\internal\sql\checkfornewazuredb.sql") -join [Environment]::NewLine $sqlCommand.CommandText = $commandText $null = $sqlCommand.Parameters.Add("@NewName", $NewDatabaseName) $null = $sqlCommand.Parameters.Add("@Time", $StartTime) try { Write-PSFMessage -Level InternalComment -Message "Executing a script against the database." -Target (Get-SqlString $SqlCommand) $sqlCommand.Connection.Open() $operation_row_count = 0 #Loop every minute until we get a row, if we get a row copy is done while ($operation_row_count -eq 0) { Write-PSFMessage -Level Verbose -Message "Waiting for the creation of the copy." $Reader = $sqlCommand.ExecuteReader() $Datatable = New-Object System.Data.DataTable $Datatable.Load($Reader) $operation_row_count = $Datatable.Rows.Count Start-Sleep -s 60 } $true } catch { Write-PSFMessage -Level Host -Message "Something went wrong while checking for the new copy of the Azure DB" -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" -StepsUpward 1 return } finally { $Reader.close() if ($sqlCommand.Connection.State -ne [System.Data.ConnectionState]::Closed) { $sqlCommand.Connection.Close() } $sqlCommand.Dispose() $Datatable.Dispose() } Invoke-TimeSignal -End } <# .SYNOPSIS Clear Azure SQL Database specific objects .DESCRIPTION Clears all the objects that can only exists inside an Azure SQL Database instance or disable things that will require rebuilding on the receiving system .PARAMETER DatabaseServer The name of the database server If on-premises or classic SQL Server, use either short name og Fully Qualified Domain Name (FQDN) If Azure use the full address to the database server, e.g. server.database.windows.net .PARAMETER DatabaseName The name of the database .PARAMETER SqlUser The login name for the SQL Server instance .PARAMETER SqlPwd The password for the SQL Server user .EXAMPLE PS C:\> Invoke-ClearAzureSpecificObjects -DatabaseServer TestServer.database.windows.net -DatabaseName ExportClone -SqlUser User123 -SqlPwd "Password123" This will execute all necessary scripts against the "ExportClone" database that exists in the "TestServer.database.windows.net" Azure SQL Database instance. It uses the SQL credential "User123" to preform the needed actions. .NOTES Author: Rasmus Andersen (@ITRasmus) Author: M�tz Jensen (@Splaxi) #> Function Invoke-ClearAzureSpecificObjects { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter(Mandatory = $true)] [string] $DatabaseServer, [Parameter(Mandatory = $true)] [string] $DatabaseName, [Parameter(Mandatory = $true)] [string] $SqlUser, [Parameter(Mandatory = $true)] [string] $SqlPwd ) $sqlCommand = Get-SQLCommand @PsBoundParameters -TrustedConnection $false $commandText = (Get-Content "$script:ModuleRoot\internal\sql\clear-azurebacpacdatabase.sql") -join [Environment]::NewLine $commandText = $commandText.Replace("@NewDatabase", $DatabaseName) $sqlCommand.CommandText = $commandText try { Write-PSFMessage -Level InternalComment -Message "Executing a script against the database." -Target (Get-SqlString $SqlCommand) $sqlCommand.Connection.Open() $null = $sqlCommand.ExecuteNonQuery() $true } catch { Write-PSFMessage -Level Host -Message "Something went wrong while clearing the Azure specific objects from the Azure DB" -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" -StepsUpward 1 return } finally { if ($sqlCommand.Connection.State -ne [System.Data.ConnectionState]::Closed) { $sqlCommand.Connection.Close() } $sqlCommand.Dispose() } } <# .SYNOPSIS Clear SQL Server (on-premises) specific objects .DESCRIPTION Clears all the objects that can only exists inside a SQL Server (on-premises) instance or disable things that will require rebuilding on the receiving system .PARAMETER DatabaseServer The name of the database server If on-premises or classic SQL Server, use either short name og Fully Qualified Domain Name (FQDN) If Azure use the full address to the database server, e.g. server.database.windows.net .PARAMETER DatabaseName The name of the database .PARAMETER SqlUser The login name for the SQL Server instance .PARAMETER SqlPwd The password for the SQL Server user .PARAMETER TrustedConnection Should the connection use a Trusted Connection or not .EXAMPLE PS C:\> Invoke-ClearSqlSpecificObjects -DatabaseServer localhost -DatabaseName ExportClone -SqlUser User123 -SqlPwd "Password123" This will execute all necessary scripts against the "ExportClone" database that exists in the localhost SQL Server instance. It uses the SQL credential "User123" to preform the needed actions. .NOTES Author: Rasmus Andersen (@ITRasmus) Author: M�tz Jensen (@Splaxi) #> Function Invoke-ClearSqlSpecificObjects { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter(Mandatory = $true)] [string] $DatabaseServer, [Parameter(Mandatory = $true)] [string] $DatabaseName, [Parameter(Mandatory = $false)] [string] $SqlUser, [Parameter(Mandatory = $false)] [string] $SqlPwd, [Parameter(Mandatory = $false)] [boolean] $TrustedConnection ) $sqlCommand = Get-SQLCommand @PsBoundParameters $commandText = (Get-Content "$script:ModuleRoot\internal\sql\clear-sqlbacpacdatabase.sql") -join [Environment]::NewLine $sqlCommand.CommandText = $commandText try { Write-PSFMessage -Level InternalComment -Message "Executing a script against the database." -Target (Get-SqlString $SqlCommand) $sqlCommand.Connection.Open() $null = $sqlCommand.ExecuteNonQuery() $true } catch { Write-PSFMessage -Level Host -Message "Something went wrong while working against the database" -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" -StepsUpward 1 return } finally { if ($sqlCommand.Connection.State -ne [System.Data.ConnectionState]::Closed) { $sqlCommand.Connection.Close() } $sqlCommand.Dispose() } } <# .SYNOPSIS Invoke the ModelUtil.exe .DESCRIPTION A cmdlet that wraps some of the cumbersome work into a streamlined process .PARAMETER Command Instruct the cmdlet to what process you want to execute against the ModelUtil tool Valid options: Import Export Delete Replace .PARAMETER Path Used for import to point where to import from Used for export to point where to export the model to The cmdlet only supports an already extracted ".axmodel" file .PARAMETER Model Name of the model that you want to work against Used for export to select the model that you want to export Used for delete to select the model that you want to delete .PARAMETER BinDir The path to the bin directory for the environment Default path is the same as the AOS service PackagesLocalDirectory\bin Default value is fetched from the current configuration on the machine .PARAMETER MetaDataDir The path to the meta data directory for the environment Default path is the same as the aos service PackagesLocalDirectory .EXAMPLE PS C:\> Invoke-ModelUtil -Command Import -Path "c:\temp\d365fo.tools\CustomModel.axmodel" This will execute the import functionality of ModelUtil.exe and have it import the "CustomModel.axmodel" file. .EXAMPLE PS C:\> Invoke-ModelUtil -Command Export -Path "c:\temp\d365fo.tools" -Model CustomModel This will execute the export functionality of ModelUtil.exe and have it export the "CustomModel" model. The file will be placed in "c:\temp\d365fo.tools". .EXAMPLE PS C:\> Invoke-ModelUtil -Command Delete -Model CustomModel This will execute the delete functionality of ModelUtil.exe and have it delete the "CustomModel" model. The folders in PackagesLocalDirectory for the "CustomModel" will NOT be deleted .EXAMPLE PS C:\> Invoke-ModelUtil -Command Replace -Path "c:\temp\d365fo.tools\CustomModel.axmodel" This will execute the replace functionality of ModelUtil.exe and have it replace the "CustomModel" model. .NOTES Tags: AXModel, Model, ModelUtil, Servicing, Import, Export, Delete, Replace Author: M�tz Jensen (@Splaxi) #> function Invoke-ModelUtil { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidDefaultValueSwitchParameter", "")] [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory = $true, Position = 1 )] [ValidateSet('Import', 'Export', 'Delete', 'Replace')] [string] $Command, [Parameter(Mandatory = $True, ParameterSetName = 'Import', Position = 1 )] [Parameter(Mandatory = $True, ParameterSetName = 'Export', Position = 1 )] [Alias('File')] [string] $Path, [Parameter(Mandatory = $True, ParameterSetName = 'Export', Position = 2 )] [Parameter(Mandatory = $True, ParameterSetName = 'Delete', Position = 1 )] [string] $Model, [Parameter(Mandatory = $false)] [string] $BinDir = "$Script:PackageDirectory\bin", [Parameter(Mandatory = $false)] [string] $MetaDataDir = "$Script:MetaDataDir" ) Invoke-TimeSignal -Start if (-not (Test-PathExists -Path $MetaDataDir, $BinDir -Type Container)) { Stop-PSFFunction -Message "Stopping because of missing paths." -StepsUpward 1 } $executable = Join-Path $BinDir "ModelUtil.exe" if (-not (Test-PathExists -Path $executable -Type Leaf)) { Stop-PSFFunction -Message "Stopping because of missing paths." -StepsUpward 1 } [System.Collections.ArrayList] $params = New-Object -TypeName "System.Collections.ArrayList" Write-PSFMessage -Level Verbose -Message "Building the parameter options." switch ($Command.ToLowerInvariant()) { 'import' { if (-not (Test-PathExists -Path $Path -Type Leaf)) { Stop-PSFFunction -Message "Stopping because of missing paths." -StepsUpward 1 } $null = $params.Add("-import") $null = $params.Add("-metadatastorepath=`"$MetaDataDir`"") $null = $params.Add("-file=`"$Path`"") } 'export' { $null = $params.Add("-export") $null = $params.Add("-metadatastorepath=`"$MetaDataDir`"") $null = $params.Add("-outputpath=`"$Path`"") $null = $params.Add("-modelname=`"$Model`"") } 'delete' { $null = $params.Add("-delete") $null = $params.Add("-metadatastorepath=`"$MetaDataDir`"") $null = $params.Add("-modelname=`"$Model`"") } 'replace' { if (-not (Test-PathExists -Path $Path -Type Leaf)) { Stop-PSFFunction -Message "Stopping because of missing paths." -StepsUpward 1 } $null = $params.Add("-replace") $null = $params.Add("-metadatastorepath=`"$MetaDataDir`"") $null = $params.Add("-file=`"$Path`"") } } Write-PSFMessage -Level Verbose -Message "Starting the $executable with the parameter options." -Target $($params.ToArray() -join " ") Start-Process -FilePath $executable -ArgumentList ($($params.ToArray() -join " ")) -NoNewWindow -Wait Invoke-TimeSignal -End } <# .SYNOPSIS Invoke a process .DESCRIPTION Invoke a process and pass the needed parameters to it .PARAMETER Path Path to the program / executable that you want to start .PARAMETER Params Array of string parameters that you want to pass to the executable .PARAMETER ShowOriginalProgress Instruct the cmdlet to show the standard output in the console Default is $false which will silence the standard output .EXAMPLE PS C:\> Invoke-Process -Path "C:\AOSService\PackagesLocalDirectory\Bin\xppc.exe" -Params "-metadata=`"C:\AOSService\PackagesLocalDirectory\Bin`"", "-modelmodule=`"ApplicationSuite`"", "-output=`"C:\AOSService\PackagesLocalDirectory\Bin`"", "-referencefolder=`"C:\AOSService\PackagesLocalDirectory\Bin`"", "-log=`"C:\temp\d365fo.tools\ApplicationSuite\Dynamics.AX.$Module.xppc.log`"", "-xmlLog=`"C:\temp\d365fo.tools\ApplicationSuite\Dynamics.AX.ApplicationSuite.xppc.xml`"", "-verbose" This will invoke the "C:\AOSService\PackagesLocalDirectory\Bin\xppc.exe" executable. All parameters will be passed to it. The standard output will be redirected to a local variable. The error output will be redirected to a local variable. The standard output will be written to the verbose stream before exiting. If an error should occur, both the standard output and error output will be written to the console / host. .EXAMPLE PS C:\> Invoke-Process -ShowOriginalProgress -Path "C:\AOSService\PackagesLocalDirectory\Bin\xppc.exe" -Params "-metadata=`"C:\AOSService\PackagesLocalDirectory\Bin`"", "-modelmodule=`"ApplicationSuite`"", "-output=`"C:\AOSService\PackagesLocalDirectory\Bin`"", "-referencefolder=`"C:\AOSService\PackagesLocalDirectory\Bin`"", "-log=`"C:\temp\d365fo.tools\ApplicationSuite\Dynamics.AX.$Module.xppc.log`"", "-xmlLog=`"C:\temp\d365fo.tools\ApplicationSuite\Dynamics.AX.ApplicationSuite.xppc.xml`"", "-verbose" This will invoke the "C:\AOSService\PackagesLocalDirectory\Bin\xppc.exe" executable. All parameters will be passed to it. The standard output will be outputted directly to the console / host. The error output will be outputted directly to the console / host. .NOTES Author: M�tz Jensen (@Splaxi) #> function Invoke-Process { [CmdletBinding()] [OutputType()] param ( [Parameter(Mandatory = $true, Position = 1)] [Alias('Executable')] [string] $Path, [Parameter(Mandatory = $true, Position = 2)] [string[]] $Params, [Parameter(Mandatory = $False, Position = 3 )] [switch] $ShowOriginalProgress ) Invoke-TimeSignal -Start if (-not (Test-PathExists -Path $Path -Type Leaf)) {return} if (Test-PSFFunctionInterrupt) { return } $tool = Split-Path -Path $Path -Leaf $pinfo = New-Object System.Diagnostics.ProcessStartInfo $pinfo.FileName = "$Path" if (-not $ShowOriginalProgress) { Write-PSFMessage -Level Verbose "Output and Error streams will be redirected (silence mode)" $pinfo.RedirectStandardError = $true $pinfo.RedirectStandardOutput = $true } $pinfo.UseShellExecute = $false $pinfo.Arguments = "$($Params -join " ")" $p = New-Object System.Diagnostics.Process $p.StartInfo = $pinfo Write-PSFMessage -Level Verbose "Starting the $tool" -Target "$($params -join " ")" $p.Start() | Out-Null if (-not $ShowOriginalProgress) { $stdout = $p.StandardOutput.ReadToEnd() $stderr = $p.StandardError.ReadToEnd() } Write-PSFMessage -Level Verbose "Waiting for the $tool to complete" $p.WaitForExit() if ($p.ExitCode -ne 0 -and (-not $ShowOriginalProgress)) { Write-PSFMessage -Level Host "Exit code from $tool indicated an error happened. Will output both standard stream and error stream." Write-PSFMessage -Level Host "Standard output was: \r\n $stdout" Write-PSFMessage -Level Host "Error output was: \r\n $stderr" Stop-PSFFunction -Message "Stopping because an Exit Code from $tool wasn't 0 (zero) like expected." -StepsUpward 1 return } else { Write-PSFMessage -Level Verbose "Standard output was: \r\n $stdout" } Invoke-TimeSignal -End } <# .SYNOPSIS Backup & Restore SQL Server database .DESCRIPTION Backup a database and restore it back into the SQL Server .PARAMETER DatabaseServer The name of the database server If on-premises or classic SQL Server, use either short name og Fully Qualified Domain Name (FQDN). If Azure use the full address to the database server, e.g. server.database.windows.net .PARAMETER DatabaseName The name of the database .PARAMETER SqlUser The login name for the SQL Server instance .PARAMETER SqlPwd The password for the SQL Server user. .PARAMETER TrustedConnection Should the connection use a Trusted Connection or not .PARAMETER NewDatabaseName Name of the new (restored) database .PARAMETER BackupDirectory Path to a directory that can store the backup file .EXAMPLE PS C:\> Invoke-SqlBackupRestore -DatabaseServer localhost -DatabaseName AxDB -SqlUser User123 -SqlPwd "Password123" -NewDatabaseName "ExportClone" -BackupDirectory "C:\temp\d365fo.tools\sqlbackup" This will backup the AxDB database and place the backup file inside the "c:\temp\d365fo.tools\sqlbackup" directory. The backup file will the be used to restore into a new database named "ExportClone". .NOTES Author: Rasmus Andersen (@ITRasmus) Author: M�tz Jensen (@Splaxi) #> Function Invoke-SqlBackupRestore { [CmdletBinding()] [OutputType('System.Boolean')] param ( [Parameter(Mandatory = $true)] [string] $DatabaseServer, [Parameter(Mandatory = $true)] [string] $DatabaseName, [Parameter(Mandatory = $false)] [string] $SqlUser, [Parameter(Mandatory = $false)] [string] $SqlPwd, [Parameter(Mandatory = $false)] [boolean] $TrustedConnection, [Parameter(Mandatory = $true)] [string] $NewDatabaseName, [Parameter(Mandatory = $true)] [string] $BackupDirectory ) Invoke-TimeSignal -Start $Params = @{DatabaseServer = $DatabaseServer; DatabaseName = $DatabaseName; SqlUser = $SqlUser; SqlPwd = $SqlPwd; TrustedConnection = $TrustedConnection; } $sqlCommand = Get-SQLCommand @Params $sqlCommand.CommandText = (Get-Content "$script:ModuleRoot\internal\sql\backuprestoredb.sql") -join [Environment]::NewLine $null = $sqlCommand.Parameters.Add("@CurrentDatabase", $DatabaseName) $null = $sqlCommand.Parameters.Add("@NewName", $NewDatabaseName) $null = $sqlCommand.Parameters.Add("@BackupDirectory", $BackupDirectory) try { Write-PSFMessage -Level InternalComment -Message "Executing a script against the database." -Target (Get-SqlString $SqlCommand) $sqlCommand.Connection.Open() $null = $sqlCommand.ExecuteNonQuery() $true } catch { Write-PSFMessage -Level Host -Message "Something went wrong while working against the database" -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" -StepsUpward 1 return } finally { $sqlCommand.Connection.Close() $sqlCommand.Dispose() } Invoke-TimeSignal -End } <# .SYNOPSIS Invoke the sqlpackage executable .DESCRIPTION Invoke the sqlpackage executable and pass the necessary parameters to it .PARAMETER Action Can either be import or export .PARAMETER DatabaseServer The name of the database server If on-premises or classic SQL Server, use either short name og Fully Qualified Domain Name (FQDN). If Azure use the full address to the database server, e.g. server.database.windows.net .PARAMETER DatabaseName The name of the database .PARAMETER SqlUser The login name for the SQL Server instance .PARAMETER SqlPwd The password for the SQL Server user. .PARAMETER TrustedConnection Should the sqlpackage work with TrustedConnection or not .PARAMETER FilePath Path to the file, used for either import or export .PARAMETER Properties Array of all the properties that needs to be parsed to the sqlpackage.exe .EXAMPLE PS C:\> $BaseParams = @{ DatabaseServer = $DatabaseServer DatabaseName = $DatabaseName SqlUser = $SqlUser SqlPwd = $SqlPwd } PS C:\> $ImportParams = @{ Action = "import" FilePath = $BacpacFile } PS C:\> Invoke-SqlPackage @BaseParams @ImportParams This will start the sqlpackage.exe file and pass all the needed parameters. .NOTES Author: M�tz Jensen (@splaxi) #> function Invoke-SqlPackage { [CmdletBinding()] [OutputType([System.Boolean])] param ( [ValidateSet('Import', 'Export')] [string]$Action, [string]$DatabaseServer, [string]$DatabaseName, [string]$SqlUser, [string]$SqlPwd, [string]$TrustedConnection, [string]$FilePath, [string[]]$Properties ) $executable = $Script:SqlPackage Invoke-TimeSignal -Start if (!(Test-PathExists -Path $executable -Type Leaf)) {return} Write-PSFMessage -Level Verbose -Message "Starting to prepare the parameters for sqlpackage.exe" [System.Collections.ArrayList]$Params = New-Object -TypeName "System.Collections.ArrayList" if ($Action -eq "export") { $null = $Params.Add("/Action:export") $null = $Params.Add("/SourceServerName:$DatabaseServer") $null = $Params.Add("/SourceDatabaseName:$DatabaseName") $null = $Params.Add("/TargetFile:`"$FilePath`"") $null = $Params.Add("/Properties:CommandTimeout=1200") if (!$UseTrustedConnection) { $null = $Params.Add("/SourceUser:$SqlUser") $null = $Params.Add("/SourcePassword:$SqlPwd") } Remove-Item -Path $FilePath -ErrorAction SilentlyContinue -Force } else { $null = $Params.Add("/Action:import") $null = $Params.Add("/TargetServerName:$DatabaseServer") $null = $Params.Add("/TargetDatabaseName:$DatabaseName") $null = $Params.Add("/SourceFile:`"$FilePath`"") $null = $Params.Add("/Properties:CommandTimeout=1200") if (!$UseTrustedConnection) { $null = $Params.Add("/TargetUser:$SqlUser") $null = $Params.Add("/TargetPassword:$SqlPwd") } } foreach ($item in $Properties) { $null = $Params.Add("/Properties:$item") } Write-PSFMessage -Level Verbose "Start sqlpackage.exe with parameters" -Target $Params #! We should consider to redirect the standard output & error like this: https://stackoverflow.com/questions/8761888/capturing-standard-out-and-error-with-start-process Start-Process -FilePath $executable -ArgumentList ($Params -join " ") -NoNewWindow -Wait Invoke-TimeSignal -End $true } <# .SYNOPSIS Handle time measurement .DESCRIPTION Handle time measurement from when a cmdlet / function starts and ends Will write the output to the verbose stream (Write-PSFMessage -Level Verbose) .PARAMETER Start Switch to instruct the cmdlet that a start time registration needs to take place .PARAMETER End Switch to instruct the cmdlet that a time registration has come to its end and it needs to do the calculation .EXAMPLE PS C:\> Invoke-TimeSignal -Start This will start the time measurement for any given cmdlet / function .EXAMPLE PS C:\> Invoke-TimeSignal -End This will end the time measurement for any given cmdlet / function. The output will go into the verbose stream. .NOTES Author: M�tz Jensen (@Splaxi) #> function Invoke-TimeSignal { [CmdletBinding(DefaultParameterSetName = 'Start')] param ( [Parameter(Mandatory = $true, ParameterSetName = 'Start', Position = 1 )] [switch] $Start, [Parameter(Mandatory = $True, ParameterSetName = 'End', Position = 2 )] [switch] $End ) $Time = (Get-Date) $Command = (Get-PSCallStack)[1].Command if ($Start) { if ($Script:TimeSignals.ContainsKey($Command)) { Write-PSFMessage -Level Verbose -Message "The command '$Command' was already taking part in time measurement. The entry has been update with current date and time." $Script:TimeSignals[$Command] = $Time } else { $Script:TimeSignals.Add($Command, $Time) } } else { if ($Script:TimeSignals.ContainsKey($Command)) { $TimeSpan = New-TimeSpan -End $Time -Start (($Script:TimeSignals)[$Command]) Write-PSFMessage -Level Verbose -Message "Total time spent inside the function was $TimeSpan" -Target $TimeSpan -FunctionName $Command -Tag "TimeSignal" $null = $Script:TimeSignals.Remove($Command) } else { Write-PSFMessage -Level Verbose -Message "The command '$Command' was never started to take part in time measurement." } } } <# .SYNOPSIS Create a new authorization header .DESCRIPTION Get a new authorization header by acquiring a token from the authority web service .PARAMETER Authority The authority that you want to work against .PARAMETER ClientId The client id that you have registered for getting access to the web resource that you want to work against .PARAMETER ClientSecret The client secret that enables you to prove that you have privileges to get an authorization header .PARAMETER D365FO The URL to the Dynamics 365 for Finance & Operations that you want to work against .EXAMPLE PS C:\> New-AuthorizationHeader -Authority "XYZ" -ClientId "123" -ClientSecret "TopSecretId" -D365FO "https://usnconeboxax1aos.cloud.onebox.dynamics.com" This will retrieve a new authorization header from the D365FO instance located at "https://usnconeboxax1aos.cloud.onebox.dynamics.com". .NOTES Author: Rasmus Andersen (@ITRasmus) Author: M�tz Jensen (@Splaxi) #> function New-AuthorizationHeader { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] Param ( [string] $Authority, [string] $ClientId, [string] $ClientSecret, [string] $D365FO ) $authContext = new-Object Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext ($Authority, $false) $clientCred = New-Object Microsoft.IdentityModel.Clients.ActiveDirectory.ClientCredential($ClientId, $ClientSecret) $task = $authContext.AcquireTokenAsync($D365FO, $clientCred) $taskStatus = $task.Wait(1000) Write-PSFMessage -Level Verbose -Message "Status $TaskStatus" $authorizationHeader = $task.Result Write-PSFMessage -Level Verbose -Message "AuthorizationHeader $authorizationHeader" $authorizationHeader } <# .SYNOPSIS Creates a new user .DESCRIPTION Creates a new user in a Dynamics 365 for Finance & Operations instance .PARAMETER sqlCommand The SQL Command object that should be used when creating the new user .PARAMETER SignInName The sign in name (email address) for the user that you want the SID from .PARAMETER Name The name that the imported user should have inside the D365FO environment .PARAMETER Id The ID that the imported user should have inside the D365FO environment .PARAMETER SID The SID that correlates to the imported user inside the D365FO environment .PARAMETER StartUpCompany The default company (legal entity) for the imported user .PARAMETER IdentityProvider The provider for the imported to validated against .PARAMETER NetworkDomain The network domain of the imported user .PARAMETER ObjectId The Azure Active Directory object id for the imported user .EXAMPLE PS C:\> $SqlCommand = Get-SqlCommand -DatabaseServer localhost -DatabaseName AxDB -SqlUser User123 -SqlPwd "Password123" PS C:\> New-D365FOUser -SqlCommand $SqlCommand -SignInName "Claire@contoso.com" -Name "Claire" -Id "claire" -SID "123XYZ" -StartupCompany "DAT" -IdentityProvider "XYZ" -NetworkDomain "Contoso.com" -ObjectId "123XYZ" This will get a SqlCommand object that will connect to the localhost server and the AXDB databae, with the sql credential "User123". The SqlCommand object is passed to the Import-AadUserIntoD365FO along with all the necessary details for importing Claire@contoso.com as an user into the D365FO environment. .NOTES Author: Rasmus Andersen (@ITRasmus) Author: Rasmus Andersen (@ITRasmus) #> function New-D365FOUser { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] Param ( [System.Data.SqlClient.SqlCommand] $SqlCommand, [string] $SignInName, [string] $Name, [string] $Id, [string] $SID, [string] $StartUpCompany, [string] $IdentityProvider, [string] $NetworkDomain, [string] $ObjectId ) $sqlCommand.CommandText = (Get-Content "$script:ModuleRoot\internal\sql\Add-AadUserIntoD365FO.sql") -join [Environment]::NewLine Write-PSFMessage -Level Verbose -Message "Adding User : $SignInName,$Name,$Id,$SID,$StartUpCompany,$IdentityProvider,$NetworkDomain" $null = $sqlCommand.Parameters.Add("@SignInName", $SignInName) $null = $sqlCommand.Parameters.Add("@Name", $Name) $null = $sqlCommand.Parameters.Add("@SID", $SID) $null = $sqlCommand.Parameters.Add("@NetworkDomain", $NetworkDomain) $null = $sqlCommand.Parameters.Add("@IdentityProvider", $IdentityProvider) $null = $sqlCommand.Parameters.Add("@StartUpCompany", $StartUpCompany) $null = $sqlCommand.Parameters.Add("@Id", $Id) $null = $sqlCommand.Parameters.Add("@ObjectId", $ObjectId) Write-PSFMessage -Level Verbose -Message "Creating the user in database" Write-PSFMessage -Level InternalComment -Message "Executing a script against the database." -Target (Get-SqlString $SqlCommand) $rowsCreated = $sqlCommand.ExecuteScalar() Write-PSFMessage -Level Verbose -Message "Rows inserted $rowsCreated for user $SignInName" $SqlCommand.Parameters.Clear() $rowsCreated -eq 1 } <# .SYNOPSIS Create a new self signed certificate .DESCRIPTION Create a new self signed certificate and have it password protected .PARAMETER CertificateFileName Path to the location where you want to store the CER file for the certificate .PARAMETER PrivateKeyFileName Path to the location where you want to store the PFX file for the certificate .PARAMETER Password The password that you want to use to protect your different certificates with .EXAMPLE PS C:\> New-D365SelfSignedCertificate -CertificateFileName "C:\temp\d365fo.tools\TestAuth.cer" -PrivateKeyFileName "C:\temp\d365fo.tools\TestAuth.pfx" -Password (ConvertTo-SecureString -String "pass@word1" -Force -AsPlainText) This will generate a new CER certificate that is stored at "C:\temp\d365fo.tools\TestAuth.cer". This will generate a new PFX certificate that is stored at "C:\temp\d365fo.tools\TestAuth.pfx". Both certificates will be password protected with "pass@word1". .NOTES Author: Kenny Saelen (@kennysaelen) Author: M�tz Jensen (@Splaxi) #> function New-D365SelfSignedCertificate { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [Parameter(Mandatory = $false, Position = 1)] [string] $CertificateFileName = (Join-Path $env:TEMP "TestAuthCert.cer"), [Parameter(Mandatory = $false, Position = 2)] [string] $PrivateKeyFileName = (Join-Path $env:TEMP "TestAuthCert.pfx"), [Parameter(Mandatory = $false, Position = 3)] [Security.SecureString] $Password = (ConvertTo-SecureString -String "Password1" -Force -AsPlainText) ) try { # First generate a self-signed certificate and place it in the local store on the machine $certificate = New-SelfSignedCertificate -dnsname 127.0.0.1 -CertStoreLocation cert:\LocalMachine\My -FriendlyName "D365 Automated testing certificate" -Provider "Microsoft Strong Cryptographic Provider" $certificatePath = 'cert:\localMachine\my\' + $certificate.Thumbprint # Export the private key Export-PfxCertificate -cert $certificatePath -FilePath $PrivateKeyFileName -Password $Password # Import the certificate into the local machine's trusted root certificates store $importedCertificate = Import-PfxCertificate -FilePath $PrivateKeyFileName -CertStoreLocation Cert:\LocalMachine\Root -Password $Password } catch { Write-PSFMessage -Level Host -Message "Something went wrong while generating the self-signed certificate and installing it into the local machine's trusted root certificates store." -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" -StepsUpward 1 return } return $importedCertificate } <# .SYNOPSIS Decrypt web.config file .DESCRIPTION Utilize the built in encryptor utility to decrypt the web.config file from inside the AOS .PARAMETER File Path to the file that you want to work against Please be careful not to point to the original file from inside the AOS directory .PARAMETER DropPath Path to the directory where you want save the file after decryption is completed .EXAMPLE PS C:\> New-DecryptedFile -File "C:\temp\d365fo.tools\web.config" -DropPath "c:\temp\d365fo.tools\decrypted.config" This will take the "C:\temp\d365fo.tools\web.config" and decrypt it. After decryption the output file will be stored in "c:\temp\d365fo.tools\decrypted.config". .NOTES Author: Rasmus Andersen (@ITRasmus) Author: M�tz Jensen (@Splaxi) #> function New-DecryptedFile { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] Param ( [string] $File, [string] $DropPath ) $Decrypter = Join-Path $AosServiceWebRootPath -ChildPath "bin\Microsoft.Dynamics.AX.Framework.ConfigEncryptor.exe" if (-not (Test-PathExists -Path $Decrypter -Type Leaf)) { return } $fileInfo = [System.IO.FileInfo]::new($File) $DropFile = Join-Path $DropPath $FileInfo.Name Write-PSFMessage -Level Verbose -Message "Extracted file path is: $DropFile" -Target $DropFile Copy-Item $File $DropFile -Force -ErrorAction Stop if (-not (Test-PathExists -Path $DropFile -Type Leaf)) { return } & $Decrypter -decrypt $DropFile } <# .SYNOPSIS Create a new Json HttpRequestMessage .DESCRIPTION Create a new HttpRequestMessage with the ContentType = application/json .PARAMETER Uri The URI / URL for the web site you want to work against .PARAMETER Token The token that contains the needed authorization permission .PARAMETER Content The content that you want to include in the HttpRequestMessage .EXAMPLE PS C:\> New-JsonRequest -Token "Bearer JldjfafLJdfjlfsalfd..." -Uri "https://lcsapi.lcs.dynamics.com/box/fileasset/CommitFileAsset/123456789?assetId=958ae597-f089-4811-abbd-c1190917eaae" This will create a new HttpRequestMessage what will work against the "https://lcsapi.lcs.dynamics.com/box/fileasset/CommitFileAsset/123456789?assetId=958ae597-f089-4811-abbd-c1190917eaae". It attaches the Token "Bearer JldjfafLJdfjlfsalfd..." to the request. .NOTES Tags: Json, Http, HttpRequestMessage, POST Author: M�tz Jensen (@Splaxi) #> function New-JsonRequest { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] [OutputType()] param ( [Parameter(Mandatory = $true, Position = 1)] [string]$Uri, [Parameter(Mandatory = $true, Position = 2)] [string]$Token, [Parameter(Mandatory = $false, Position = 3)] [string]$Content ) Write-PSFMessage -Level Verbose -Message "Building a HttpRequestMessage." -Target $Uri $request = New-Object -TypeName System.Net.Http.HttpRequestMessage -ArgumentList @([System.Net.Http.HttpMethod]::Post, $Uri) if (-not ($Content -eq "")) { Write-PSFMessage -Level Verbose -Message "Adding content to the HttpRequestMessage." -Target $Content $request.Content = New-Object -TypeName System.Net.Http.StringContent -ArgumentList @($Content, [System.Text.Encoding]::UTF8, "application/json") } Write-PSFMessage -Level Verbose -Message "Adding Authorization token to the HttpRequestMessage." -Target $Token $request.Headers.Authorization = $Token $request } <# .SYNOPSIS Get a web request object .DESCRIPTION Get a prepared web request object with all necessary headers and tokens in place .PARAMETER RequestUrl The URL you want to work against .PARAMETER AuthorizationHeader The Authorization Header object that you want to use for you web request .PARAMETER Action The HTTP action you want to preform .EXAMPLE PS C:\> New-WebRequest -RequestUrl "https://login.windows.net/contoso/.well-known/openid-configuration" -AuthorizationHeader $null -Action GET This will create a new web request object that will work against the "https://login.windows.net/contoso/.well-known/openid-configuration" URL. The HTTP action is GET and in this case we don't need an Authorization Header in place. .NOTES Author: Rasmus Andersen (@ITRasmus) Author: M�tz Jensen (@Splaxi) #> function New-WebRequest { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] param ( $RequestUrl, $AuthorizationHeader, $Action ) Write-PSFMessage -Level Verbose -Message "New Request $RequestUrl, $Action" $request = [System.Net.WebRequest]::Create($RequestUrl) if ($null -ne $AuthorizationHeader) { $request.Headers["Authorization"] = $AuthorizationHeader.CreateAuthorizationHeader() } $request.Method = $Action $request } <# .SYNOPSIS Rename the value in the web.config file .DESCRIPTION Replace the old value with the new value inside a web.config file .PARAMETER File Path to the file that you want to update/rename/replace .PARAMETER NewValue The new value that replaces the old value .PARAMETER OldValue The old value that needs to be replaced .EXAMPLE PS C:\> Rename-ConfigValue -File "C:\temp\d365fo.tools\web.config" -NewValue "Demo-8.1" -OldValue "usnconeboxax1aos" This will open the "C:\temp\d365fo.tools\web.config" file and replace all "usnconeboxax1aos" entries with "Demo-8.1" .NOTES Author: Rasmus Andersen (@ITRasmus) Author: M�tz Jensen (@Splaxi) #> function Rename-ConfigValue { param ( [string] $File, [string] $NewValue, [string] $OldValue ) Write-PSFMessage -Level Verbose -Message "Replace content from $File. Old value is $OldValue. New value is $NewValue." -Target (@($File, $OldValue, $NewValue)) (Get-Content $File).replace($OldValue, $NewValue) | Set-Content $File } <# .SYNOPSIS Short description .DESCRIPTION Long description .PARAMETER InputObject Parameter description .PARAMETER Property Parameter description .PARAMETER ExcludeProperty Parameter description .PARAMETER TypeName Parameter description .EXAMPLE PS C:\> Select-DefaultView -InputObject $result -Property CommandName, Synopsis This will help you do it right. .NOTES Author: M�tz Jensen (@Splaxi) #> function Select-DefaultView { <# This command enables us to send full on objects to the pipeline without the user seeing it a lot of this is from boe, thanks boe! https://learn-powershell.net/2013/08/03/quick-hits-set-the-default-property-display-in-powershell-on-custom-objects/ TypeName creates a new type so that we can use ps1xml to modify the output #> [CmdletBinding()] param ( [parameter(ValueFromPipeline)] [object] $InputObject, [string[]] $Property, [string[]] $ExcludeProperty, [string] $TypeName ) process { if ($null -eq $InputObject) { return } if ($TypeName) { $InputObject.PSObject.TypeNames.Insert(0, "d365fo.tools.$TypeName") } if ($ExcludeProperty) { if ($InputObject.GetType().Name.ToString() -eq 'DataRow') { $ExcludeProperty += 'Item', 'RowError', 'RowState', 'Table', 'ItemArray', 'HasErrors' } $props = ($InputObject | Get-Member | Where-Object MemberType -in 'Property', 'NoteProperty', 'AliasProperty' | Where-Object { $_.Name -notin $ExcludeProperty }).Name $defaultset = New-Object System.Management.Automation.PSPropertySet('DefaultDisplayPropertySet', [string[]]$props) } else { # property needs to be string if ("$property" -like "* as *") { $newproperty = @() foreach ($p in $property) { if ($p -like "* as *") { $old, $new = $p -isplit " as " # Do not be tempted to not pipe here $inputobject | Add-Member -Force -MemberType AliasProperty -Name $new -Value $old -ErrorAction SilentlyContinue $newproperty += $new } else { $newproperty += $p } } $property = $newproperty } $defaultset = New-Object System.Management.Automation.PSPropertySet('DefaultDisplayPropertySet', [string[]]$Property) } $standardmembers = [System.Management.Automation.PSMemberInfo[]]@($defaultset) # Do not be tempted to not pipe here $inputobject | Add-Member -Force -MemberType MemberSet -Name PSStandardMembers -Value $standardmembers -ErrorAction SilentlyContinue $inputobject } } <# .SYNOPSIS Provision an user to be the administrator of a Dynamics 365 for Finance & Operations environment .DESCRIPTION Provision an user to be the administrator by using the supplied tools from Microsoft (AdminUserProvisioning.exe) .PARAMETER SignInName The sign in name (email address) for the user that you want to be the administrator .PARAMETER DatabaseServer The name of the database server If on-premises or classic SQL Server, use either short name og Fully Qualified Domain Name (FQDN). If Azure use the full address to the database server, e.g. server.database.windows.net .PARAMETER DatabaseName The name of the database .PARAMETER SqlUser The login name for the SQL Server instance .PARAMETER SqlPwd The password for the SQL Server user. .EXAMPLE PS C:\> Set-AdminUser -SignInName "Claire@contoso.com" -DatabaseServer localhost -DatabaseName AxDB -SqlUser User123 -SqlPwd "Password123" This will provision the user with the e-mail "Claire@contoso.com" to be the administrator of the D365 for Finance & Operations instance. It will handle if the tenant is switching also, and update the necessary details. .NOTES Author: Rasmus Andersen (@ITRasmus) Author: M�tz Jensen (@Splaxi) #> function Set-AdminUser { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] Param ( [string] $SignInName, [string] $DatabaseServer, [string] $DatabaseName, [string] $SqlUser, [string] $SqlPwd ) $WebConfigFile = Join-Path $Script:AOSPath $Script:WebConfig $MetaDataNode = Select-Xml -XPath "/configuration/appSettings/add[@key='Aos.MetadataDirectory']/@value" -Path $WebConfigFile $MetaDataNodeDirectory = $MetaDataNode.Node.Value Write-PSFMessage -Level Verbose -Message "MetaDataDirectory: $MetaDataNodeDirectory" -Target $MetaDataNodeDirectory $AdminFile = "$MetaDataNodeDirectory\Bin\AdminUserProvisioning.exe" $TempFileName = New-TemporaryFile $TempFileName = $TempFileName.BaseName $AdminDll = "$env:TEMP\$TempFileName.dll" copy-item -Path $AdminFile -Destination $AdminDll $adminAssembly = [System.Reflection.Assembly]::LoadFile($AdminDll) $AdminUserUpdater = $adminAssembly.GetType("Microsoft.Dynamics.AdminUserProvisioning.AdminUserUpdater") $PublicBinding = [System.Reflection.BindingFlags]::Public $StaticBinding = [System.Reflection.BindingFlags]::Static $CombinedBinding = $PublicBinding -bor $StaticBinding $UpdateAdminUser = $AdminUserUpdater.GetMethod("UpdateAdminUser", $CombinedBinding) Write-PSFMessage -Level Verbose -Message "Updating Admin using the values $SignInName, $DatabaseServer, $DatabaseName, $SqlUser, $SqlPwd" $params = $SignInName, $null, $null, $DatabaseServer, $DatabaseName, $SqlUser, $SqlPwd $UpdateAdminUser.Invoke($null, $params) } <# .SYNOPSIS Change the different Azure SQL Database details .DESCRIPTION When preparing an Azure SQL Database to be the new database for an Tier 2+ environment you need to set different details .PARAMETER DatabaseServer The name of the database server If on-premises or classic SQL Server, use either short name og Fully Qualified Domain Name (FQDN). If Azure use the full address to the database server, e.g. server.database.windows.net .PARAMETER DatabaseName The name of the database .PARAMETER SqlUser The login name for the SQL Server instance .PARAMETER SqlPwd The password for the SQL Server user .PARAMETER AxDeployExtUserPwd Password obtained from LCS .PARAMETER AxDbAdminPwd Password obtained from LCS .PARAMETER AxRuntimeUserPwd Password obtained from LCS .PARAMETER AxMrRuntimeUserPwd Password obtained from LCS .PARAMETER AxRetailRuntimeUserPwd Password obtained from LCS .PARAMETER AxRetailDataSyncUserPwd Password obtained from LCS .PARAMETER AxDbReadonlyUserPwd Password obtained from LCS .PARAMETER TenantId The ID of tenant that the Azure SQL Database instance is going to be run under .PARAMETER PlanId The ID of the type of plan that the Azure SQL Database is going to be using .PARAMETER PlanCapability The capabilities that the Azure SQL Database instance will be running with .EXAMPLE PS C:\> Set-AzureBacpacValues -DatabaseServer dbserver1.database.windows.net -DatabaseName Import -SqlUser User123 -SqlPwd "Password123" -AxDeployExtUserPwd "Password123" -AxDbAdminPwd "Password123" -AxRuntimeUserPwd "Password123" -AxMrRuntimeUserPwd "Password123" -AxRetailRuntimeUserPwd "Password123" -AxRetailDataSyncUserPwd "Password123" -AxDbReadonlyUserPwd "Password123" -TenantId "TenantIdFromAzure" -PlanId "PlanIdFromAzure" -PlanCapability "Capabilities" This will set all the needed details inside the "Import" database that is located in the "dbserver1.database.windows.net" Azure SQL Database instance. All service accounts and their passwords will be updated accordingly. .NOTES Author: Rasmus Andersen (@ITRasmus) Author: M�tz Jensen (@Splaxi) #> function Set-AzureBacpacValues { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter(Mandatory = $true)] [string] $DatabaseServer, [Parameter(Mandatory = $true)] [string] $DatabaseName, [Parameter(Mandatory = $true)] [string] $SqlUser, [Parameter(Mandatory = $true)] [string] $SqlPwd, [Parameter(Mandatory = $true)] [string]$AxDeployExtUserPwd, [Parameter(Mandatory = $true)] [string]$AxDbAdminPwd, [Parameter(Mandatory = $true)] [string]$AxRuntimeUserPwd, [Parameter(Mandatory = $true)] [string]$AxMrRuntimeUserPwd, [Parameter(Mandatory = $true)] [string]$AxRetailRuntimeUserPwd, [Parameter(Mandatory = $true)] [string]$AxRetailDataSyncUserPwd, [Parameter(Mandatory = $true)] [string]$AxDbReadonlyUserPwd, [Parameter(Mandatory = $true)] [string]$TenantId, [Parameter(Mandatory = $true)] [string]$PlanId, [Parameter(Mandatory = $true)] [string]$PlanCapability ) $sqlCommand = Get-SQLCommand -DatabaseServer $DatabaseServer -DatabaseName $DatabaseName -SqlUser $SqlUser -SqlPwd $SqlPwd -TrustedConnection $false $commandText = (Get-Content "$script:ModuleRoot\internal\sql\set-bacpacvaluesazure.sql") -join [Environment]::NewLine $commandText = $commandText.Replace('@axdeployextuser', $AxDeployExtUserPwd) $commandText = $commandText.Replace('@axdbadmin', $AxDbAdminPwd) $commandText = $commandText.Replace('@axruntimeuser', $AxRuntimeUserPwd) $commandText = $commandText.Replace('@axmrruntimeuser', $AxMrRuntimeUserPwd) $commandText = $commandText.Replace('@axretailruntimeuser', $AxRetailRuntimeUserPwd) $commandText = $commandText.Replace('@axretaildatasyncuser', $AxRetailDataSyncUserPwd) $commandText = $commandText.Replace('@axdbreadonlyuser', $AxDbReadonlyUserPwd) $sqlCommand.CommandText = $commandText $null = $sqlCommand.Parameters.Add("@TenantId", $TenantId) $null = $sqlCommand.Parameters.Add("@PlanId", $PlanId) $null = $sqlCommand.Parameters.Add("@PlanCapability ", $PlanCapability) try { Write-PSFMessage -Level InternalComment -Message "Executing a script against the database." -Target (Get-SqlString $SqlCommand) $sqlCommand.Connection.Open() $null = $sqlCommand.ExecuteNonQuery() $true } catch { Write-PSFMessage -Level Host -Message "Something went wrong while working against the database" -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" return } finally { if ($sqlCommand.Connection.State -ne [System.Data.ConnectionState]::Closed) { $sqlCommand.Connection.Close() } $sqlCommand.Dispose() } } <# .SYNOPSIS Set the SQL Server specific values .DESCRIPTION Set the SQL Server specific values when restoring a bacpac file .PARAMETER DatabaseServer The name of the database server If on-premises or classic SQL Server, use either short name og Fully Qualified Domain Name (FQDN). If Azure use the full address to the database server, e.g. server.database.windows.net .PARAMETER DatabaseName The name of the database .PARAMETER SqlUser The login name for the SQL Server instance .PARAMETER SqlPwd The password for the SQL Server user .PARAMETER TrustedConnection Should the connection use a Trusted Connection or not .EXAMPLE PS C:\> Set-SqlBacpacValues -DatabaseServer localhost -DatabaseName "AxDB" -SqlUser "User123" -SqlPwd "Password123" This will connect to the "AXDB" database that is available in the SQL Server instance running on the localhost. It will use the "User123" SQL Server credentials to connect to the SQL Server instance. This will set all the necessary SQL Server database options and create the needed objects in side the "AxDB" database. .NOTES Author: Rasmus Andersen (@ITRasmus) Author: M�tz Jensen (@Splaxi) #> function Set-SqlBacpacValues { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [CmdletBinding()] [OutputType('System.Boolean')] param ( [Parameter(Mandatory = $true)] [string] $DatabaseServer, [Parameter(Mandatory = $true)] [string] $DatabaseName, [Parameter(Mandatory = $false)] [string] $SqlUser, [Parameter(Mandatory = $false)] [string] $SqlPwd, [Parameter(Mandatory = $false)] [bool] $TrustedConnection ) $Params = @{DatabaseServer = $DatabaseServer; DatabaseName = $DatabaseName; SqlUser = $SqlUser; SqlPwd = $SqlPwd; TrustedConnection = $TrustedConnection; } $sqlCommand = Get-SQLCommand @Params $commandText = (Get-Content "$script:ModuleRoot\internal\sql\set-bacpacvaluessql.sql") -join [Environment]::NewLine $commandText = $commandText.Replace('@DATABASENAME', $DatabaseName) $sqlCommand.CommandText = $commandText try { Write-PSFMessage -Level InternalComment -Message "Executing a script against the database." -Target (Get-SqlString $SqlCommand) $sqlCommand.Connection.Open() $sqlCommand.ExecuteNonQuery() $true } catch { Write-PSFMessage -Level Critical -Message "Something went wrong while working against the database" -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" return } finally { if ($sqlCommand.Connection.State -ne [System.Data.ConnectionState]::Closed) { $sqlCommand.Connection.Close() } $sqlCommand.Dispose() } } <# .SYNOPSIS Start the upload process to LCS .DESCRIPTION Start the flow of actions to upload a file to LCS .PARAMETER Token The token to be used for the http request against the LCS API .PARAMETER ProjectId The project id for the Dynamics 365 for Finance & Operations project inside LCS .PARAMETER FileType Type of file you want to upload Valid options: "DeployablePackage" "DatabaseBackup" .PARAMETER Name Name to be assigned / shown on LCS .PARAMETER Description Description to be assigned / shown on LCS .PARAMETER LcsApiUri URI / URL to the LCS API you want to use Depending on whether your LCS project is located in europe or not, there is 2 valid URI's / URL's Valid options: "https://lcsapi.lcs.dynamics.com" "https://lcsapi.eu.lcs.dynamics.com" .EXAMPLE PS C:\> Start-LcsUpload -Token "Bearer JldjfafLJdfjlfsalfd..." -ProjectId 123456789 -FileType "DatabaseBackup" -Name "ReadyForTesting" -Description "Contains all customers & vendors" -LcsApiUri "https://lcsapi.lcs.dynamics.com" This will contact the NON-EUROPE LCS API and instruct it that we want to upload a new file to the Asset Library. The token "Bearer JldjfafLJdfjlfsalfd..." is used to the authorize against the LCS API. The ProjectId is 123456789 and FileType is "DatabaseBackup". The file will be named "ReadyForTesting" and the Description will be "Contains all customers & vendors". .NOTES Tags: Url, LCS, Upload, Api, Token Author: M�tz Jensen (@Splaxi) #> function Start-LcsUpload { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [Cmdletbinding()] param( [Parameter(Mandatory = $true)] [string]$Token, [Parameter(Mandatory = $true)] [int]$ProjectId, [Parameter(Mandatory = $true)] [ValidateSet('DeployablePackage', 'DatabaseBackup')] [string]$FileType, [Parameter(Mandatory = $false)] [string]$Name, [Parameter(Mandatory = $false)] [string]$Description, [Parameter(Mandatory = $false)] [string]$LcsApiUri ) Invoke-TimeSignal -Start if ($Description -eq "") { $jsonDescription = "null" } else { $jsonDescription = "`"$Description`"" } $fileTypeValue = 0 switch ($FileType) { "DeployablePackage" { $fileTypeValue = 10 } "DatabaseBackup" { $fileTypeValue = 17 } } $jsonFile = "{ `"Name`": `"$Name`", `"FileName`": `"$fileName`", `"FileDescription`": $jsonDescription, `"SizeByte`": 0, `"FileType`": $fileTypeValue }" Write-PSFMessage -Level Verbose -Message "Json payload for LCS generated." -Target $jsonFile $client = New-Object -TypeName System.Net.Http.HttpClient $client.DefaultRequestHeaders.Clear() $createUri = "$LcsApiUri/box/fileasset/CreateFileAsset/$ProjectId" $request = New-JsonRequest -Uri $createUri -Content $jsonFile -Token $Token try { Write-PSFMessage -Level Verbose -Message "Invoke LCS request." $result = Get-AsyncResult -task $client.SendAsync($request) Write-PSFMessage -Level Verbose -Message "Extracting the response received from LCS." $responseString = Get-AsyncResult -task $result.Content.ReadAsStringAsync() $asset = ConvertFrom-Json -InputObject $responseString -ErrorAction SilentlyContinue Write-PSFMessage -Level Verbose -Message "Extracting the response received from LCS." if (-not ($result.StatusCode -eq [System.Net.HttpStatusCode]::OK)) { if (($asset) -and ($asset.Message)) { Write-PSFMessage -Level Host -Message "Error creating new file asset." -Target $($asset.Message) Stop-PSFFunction -Message "Stopping because of errors" } else { Write-PSFMessage -Level Host -Message "API Call returned $($result.StatusCode)." -Target $($result.ReasonPhrase) Stop-PSFFunction -Message "Stopping because of errors" } } if (-not ($asset.Id)) { if ($asset.Message) { Write-PSFMessage -Level Host -Message "Error creating new file asset." -Target $($asset.Message) Stop-PSFFunction -Message "Stopping because of errors" } else { Write-PSFMessage -Level Host -Message "Unknown error creating new file asset." -Target $asset Stop-PSFFunction -Message "Stopping because of errors" } } } catch { Write-PSFMessage -Level Host -Message "Something went wrong while working against the LCS API." -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" return } Invoke-TimeSignal -End $asset } <# .SYNOPSIS Test to see if a given user ID exists .DESCRIPTION Test to see if a given user ID exists in the Dynamics 365 for Finance & Operations instance .PARAMETER SqlCommand The SQL Command object that should be used when testing the user ID .PARAMETER Id Id of the user that you want to test exists or not .EXAMPLE PS C:\> $SqlCommand = Get-SqlCommand -DatabaseServer localhost -DatabaseName AxDB -SqlUser User123 -SqlPwd "Password123" PS C:\> Test-AadUserIdInD365FO -SqlCommand $SqlCommand -Id "TestUser" This will get a SqlCommand object that will connect to the localhost server and the AXDB database, with the sql credential "User123". It will query the the database for any user with the Id "TestUser". .NOTES Author: Rasmus Andersen (@ITRasmus) Author: M�tz Jensen (@Splaxi) #> function Test-AadUserIdInD365FO { param ( [System.Data.SqlClient.SqlCommand] $SqlCommand, [string] $Id ) $commandText = (Get-Content "$script:ModuleRoot\internal\sql\test-aaduseridind365fo.sql") -join [Environment]::NewLine $sqlCommand.CommandText = $commandText $null = $sqlCommand.Parameters.Add("@Id", $Id) Write-PSFMessage -Level InternalComment -Message "Executing a script against the database." -Target (Get-SqlString $SqlCommand) $NumFound = $sqlCommand.ExecuteScalar() Write-PSFMessage -Level Verbose -Message "Number of user rows found in database $NumFound" -Target $NumFound $SqlCommand.Parameters.Clear() $NumFound -ne 0 } <# .SYNOPSIS Test to see if a given user already exists .DESCRIPTION Test to see if a given user already exists in the Dynamics 365 for Finance & Operations instance .PARAMETER SqlCommand The SQL Command object that should be used when testing the user .PARAMETER SignInName The sign in name (email address) for the user that you want test .EXAMPLE PS C:\> $SqlCommand = Get-SqlCommand -DatabaseServer localhost -DatabaseName AxDB -SqlUser User123 -SqlPwd "Password123" PS C:\> Test-AadUserInD365FO -SqlCommand $SqlCommand -SignInName "Claire@contoso.com" This will get a SqlCommand object that will connect to the localhost server and the AXDB database, with the sql credential "User123". It will query the the database for the user with the e-mail address "Claire@contoso.com". .NOTES Author: Rasmus Andersen (@ITRasmus) Author: M�tz Jensen (@Splaxi) #> function Test-AadUserInD365FO { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.Data.SqlClient.SqlCommand] $SqlCommand, [Parameter(Mandatory = $true)] [string] $SignInName ) $sqlCommand.CommandText = (Get-Content "$script:ModuleRoot\internal\sql\test-aaduserind365fo.sql") -join [Environment]::NewLine $null = $sqlCommand.Parameters.Add("@Email", $SignInName) try { Write-PSFMessage -Level InternalComment -Message "Executing a script against the database." -Target (Get-SqlString $SqlCommand) $NumFound = $sqlCommand.ExecuteScalar() Write-PSFMessage -Level Verbose -Message "Number of user rows found in database $NumFound" -Target $NumFound } catch { Write-PSFMessage -Level Host -Message "Something went wrong while working against the database" -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" -StepsUpward 1 return } finally { $SqlCommand.Parameters.Clear() } $NumFound -ne 0 } <# .SYNOPSIS Test if any D365 assemblies are loaded .DESCRIPTION Test if any D365 assemblies are loaded into memory and will be a blocking issue .EXAMPLE PS C:\> Test-AssembliesLoaded This will test in any D365 specific assemblies are loaded into memory. If is, a Stop-PSFFunction test will state that we should stop execution. .NOTES Author: M�tz Jensen (@Splaxi) #> function Test-AssembliesLoaded { [CmdletBinding()] [OutputType()] param ( ) Invoke-TimeSignal -Start $assembliesLoaded = [System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object Location -ne $null $assembliesBlocking = $assembliesLoaded.location -match "AOSService|Dynamics|PackagesLocalDirectory" if ($assembliesBlocking.Count -gt 0) { Stop-PSFFunction -Message "Stopping because some assembly (DLL) files seems to be loaded into memory." -StepsUpward 1 return } Invoke-TimeSignal -End } <# .SYNOPSIS Test accessible to the configuration storage .DESCRIPTION Test if the desired configuration storage is accessible with the current user context .PARAMETER ConfigStorageLocation Parameter used to instruct where to store the configuration objects The default value is "User" and this will store all configuration for the active user Valid options are: "User" "System" "System" will store the configuration so all users can access the configuration objects .EXAMPLE PS C:\> Test-ConfigStorageLocation -ConfigStorageLocation "System" This will test if the current executing user has enough privileges to save to the system wide configuration storage. The system wide configuration storage requires administrator rights. .NOTES Author: M�tz Jensen (@Splaxi) #> function Test-ConfigStorageLocation { [CmdletBinding()] [OutputType('System.String')] param ( [ValidateSet('User', 'System')] [string] $ConfigStorageLocation = "User" ) $configScope = "UserDefault" if ($ConfigStorageLocation -eq "System") { if ($Script:IsAdminRuntime) { $configScope = "SystemDefault" } else { Write-PSFMessage -Level Host -Message "Unable to locate save the <c='em'>configuration objects</c> in the <c='em'>system wide configuration store</c> on the machine. Please start an elevated session and run the cmdlet again." Stop-PSFFunction -Message "Elevated permissions needed. Please start an elevated session and run the cmdlet again." -StepsUpward 1 return } } $configScope } <# .SYNOPSIS The multiple paths .DESCRIPTION Easy way to test multiple paths for public functions and have the same error handling .PARAMETER Path Array of paths you want to test They have to be the same type, either file/leaf or folder/container .PARAMETER Type Type of path you want to test Either 'Leaf' or 'Container' .PARAMETER Create Instruct the cmdlet to create the directory if it doesn't exist .PARAMETER ShouldNotExist Instruct the cmdlet to return true if the file doesn't exists .PARAMETER DontBreak Instruct the cmdlet NOT to break execution whenever the test condition normally should .EXAMPLE PS C:\> Test-PathExists "c:\temp","c:\temp\dir" -Type Container This will test if the mentioned paths (folders) exists and the current context has enough permission. .NOTES Author: M�tz Jensen (@splaxi) #> function Test-PathExists { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter(Mandatory = $True, Position = 1 )] [string[]] $Path, [ValidateSet('Leaf', 'Container')] [Parameter(Mandatory = $True, Position = 2 )] [string] $Type, [switch] $Create, [switch] $ShouldNotExist, [switch] $DontBreak ) $res = $false $arrList = New-Object -TypeName "System.Collections.ArrayList" foreach ($item in $Path) { Write-PSFMessage -Level Verbose -Message "Testing the path: $item" -Target $item $temp = Test-Path -Path $item -Type $Type if ((-not $temp) -and ($Create) -and ($Type -eq "Container")) { Write-PSFMessage -Level Verbose -Message "Creating the path: $item" -Target $item $null = New-Item -Path $item -ItemType Directory -Force -ErrorAction Stop $temp = $true } elseif ($ShouldNotExist) { Write-PSFMessage -Level Verbose -Message "The should NOT exists: $item" -Target $item } elseif (-not $temp ) { Write-PSFMessage -Level Host -Message "The <c='em'>$item</c> path wasn't found. Please ensure the path <c='em'>exists</c> and you have enough <c='em'>permission</c> to access the path." } $null = $arrList.Add($temp) } if ($arrList.Contains($false) -and (-not $ShouldNotExist)) { if (-not $DontBreak) { Stop-PSFFunction -Message "Stopping because of missing paths." -StepsUpward 1 } } elseif ($arrList.Contains($true) -and $ShouldNotExist) { if (-not $DontBreak) { Stop-PSFFunction -Message "Stopping because file exists." -StepsUpward 1 } } else { $res = $true } $res } <# .SYNOPSIS Test if a given registry key exists or not .DESCRIPTION Test if a given registry key exists in the path specified .PARAMETER Path Path to the registry hive and sub directories you want to work against .PARAMETER Name Name of the registry key that you want to test for .EXAMPLE PS C:\> Test-RegistryValue -Path "HKLM:\SOFTWARE\Microsoft\Dynamics\Deployment\" -Name "InstallationInfoDirectory" This will query the LocalMachine hive and the sub directories "HKLM:\SOFTWARE\Microsoft\Dynamics\Deployment\" for a registry key with the name of "InstallationInfoDirectory". .NOTES Author: M�tz Jensen (@Splaxi) #> Function Test-RegistryValue { [OutputType('System.Boolean')] param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] [string]$Name ) if (Test-Path -Path $Path -PathType Any) { $null -ne (Get-ItemProperty $Path).$Name } else { $false } } <# .SYNOPSIS Test PSBoundParameters whether or not to support TrustedConnection .DESCRIPTION Test callers PSBoundParameters (HashTable) for details that determines whether or not a SQL Server connection should support TrustedConnection or not .PARAMETER Inputs HashTable ($PSBoundParameters) with the parameters from the callers invocation .EXAMPLE PS C:\> $UseTrustedConnection = Test-TrustedConnection $PSBoundParameters This will send the entire HashTable from the callers invocation, containing all explicit defined parameters to be analyzed whether or not the SQL Server connection should support TrustedConnection or not. .NOTES Author: M�tz Jensen (@splaxi) #> function Test-TrustedConnection { [CmdletBinding()] [OutputType([System.Boolean])] param ( [HashTable] $Inputs ) if (($Inputs.ContainsKey("ImportModeTier2")) -or ($Inputs.ContainsKey("ExportModeTier2"))){ Write-PSFMessage -Level Verbose -Message "Not capable of using Trusted Connection based on Tier validation." $false } elseif (($Inputs.ContainsKey("SqlUser")) -or ($Inputs.ContainsKey("SqlPwd"))) { Write-PSFMessage -Level Verbose -Message "Not capable of using Trusted Connection based on supplied SQL login details." $false } elseif ($Inputs.ContainsKey("TrustedConnection")) { Write-PSFMessage -Level Verbose -Message "The script was calling with TrustedConnection directly. This overrides all other logic in respect that the caller should know what it is doing. Value was: $($Inputs.TrustedConnection)" -Tag $Inputs.TrustedConnection $Inputs.TrustedConnection } else { Write-PSFMessage -Level Verbose -Message "Capabilities based on the centralized logic in the psm1 file." -Target $Script:CanUseTrustedConnection $Script:CanUseTrustedConnection } } <# .SYNOPSIS Update the broadcast message config variables .DESCRIPTION Update the active broadcast message config variables that the module will use as default values .EXAMPLE PS C:\> Update-BroadcastVariables This will update the broadcast variables. .NOTES Author: M�tz Jensen (@Splaxi) #> function Update-BroadcastVariables { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [CmdletBinding()] [OutputType()] param ( ) $configName = (Get-PSFConfig -FullName "d365fo.tools.active.broadcast.message.config.name").Value.ToString().ToLower() if (-not ($configName -eq "")) { $broadcastHash = Get-D365ActiveBroadcastMessageConfig -OutputAsHashtable foreach ($item in $broadcastHash.Keys) { if ($item -eq "name") { continue } $name = "Broadcast" + (Get-Culture).TextInfo.ToTitleCase($item) Write-PSFMessage -Level Verbose -Message "$name - $($broadcastHash[$item])" -Target $broadcastHash[$item] Set-Variable -Name $name -Value $broadcastHash[$item] -Scope Script } } } <# .SYNOPSIS Update the topology file .DESCRIPTION Update the topology file based on the already installed list of services on the machine .PARAMETER Path Path to the folder where the topology XML file that you want to work against is placed Should only contain a path to a folder, not a file .EXAMPLE PS C:\> Update-TopologyFile -Path "c:\temp\d365fo.tools\DefaultTopologyData.xml" This will update the "c:\temp\d365fo.tools\DefaultTopologyData.xml" file with all the installed services on the machine. .NOTES # Credit http://dev.goshoom.net/en/2016/11/installing-deployable-packages-with-powershell/ Author: Tommy Skaue (@Skaue) Author: M�tz Jensen (@Splaxi) #> function Update-TopologyFile { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter(Mandatory = $true)] [string]$Path ) $topologyFile = Join-Path $Path 'DefaultTopologyData.xml' Write-PSFMessage -Level Verbose "Creating topology file: $topologyFile" [xml]$xml = Get-Content $topologyFile $machine = $xml.TopologyData.MachineList.Machine $machine.Name = $env:computername $serviceModelList = $machine.ServiceModelList $null = $serviceModelList.RemoveAll() [System.Collections.ArrayList] $Files2Process = New-Object -TypeName "System.Collections.ArrayList" $null = $Files2Process.Add((Join-Path $Path 'Microsoft.Dynamics.AX.AXInstallationInfo.dll')) Import-AssemblyFileIntoMemory -Path $($Files2Process.ToArray()) $models = [Microsoft.Dynamics.AX.AXInstallationInfo.AXInstallationInfo]::GetInstalledServiceModel() foreach ($name in $models.Name) { $element = $xml.CreateElement('string') $element.InnerText = $name $serviceModelList.AppendChild($element) } $xml.Save($topologyFile) $true } <# .SYNOPSIS Save an Azure Storage Account config .DESCRIPTION Adds an Azure Storage Account config to the configuration store .PARAMETER Name The logical name of the Azure Storage Account you are about to registered in the configuration store .PARAMETER AccountId The account id for the Azure Storage Account you want to register in the configuration store .PARAMETER AccessToken The access token for the Azure Storage Account you want to register in the configuration store .PARAMETER SAS The SAS key that you have created for the storage account or blob container .PARAMETER Container The name of the blob container inside the Azure Storage Account you want to register in the configuration store .PARAMETER Force Switch to instruct the cmdlet to overwrite already registered Azure Storage Account entry .EXAMPLE PS C:\> Add-D365AzureStorageConfig -Name "UAT-Exports" -AccountId "1234" -AccessToken "dafdfasdfasdf" -Container "testblob" This will add an entry into the list of Azure Storage Accounts that is stored with the name "UAT-Exports" with AccountId "1234", AccessToken "dafdfasdfasdf" and blob container "testblob". .EXAMPLE PS C:\> Add-D365AzureStorageConfig -Name UAT-Exports -SAS "sv2018-03-28&siunlisted&src&sigAUOpdsfpoWE976ASDhfjkasdf(5678sdfhk" -AccountId "1234" -Container "testblob" This will add an entry into the list of Azure Storage Accounts that is stored with the name "UAT-Exports" with AccountId "1234", SAS "sv=2018-03-28&si=unlisted&sr=c&sig=AUOpdsfpoWE976ASDhfjkasdf(5678sdfhk" and blob container "testblob". The SAS key enables you to provide explicit access to a given blob container inside an Azure Storage Account. The SAS key can easily be revoked and that way you have control over the access to the container and its content. .NOTES Tags: Azure, Azure Storage, Config, Configuration, Token, Blob, Container Author: M�tz Jensen (@Splaxi) #> function Add-D365AzureStorageConfig { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Name, [Parameter(Mandatory = $true)] [string] $AccountId, [Parameter(Mandatory = $true, ParameterSetName = "AccessToken")] [string] $AccessToken, [Parameter(Mandatory = $true, ParameterSetName = "SAS")] [string] $SAS, [Parameter(Mandatory = $true)] [Alias('Blob')] [Alias('Blobname')] [string] $Container, [switch] $Force ) $Details = @{AccountId = $AccountId.ToLower(); Container = $Container.ToLower(); } if ($PSCmdlet.ParameterSetName -eq "AccessToken") { $Details.AccessToken = $AccessToken } if ($PSCmdlet.ParameterSetName -eq "SAS") { if ($SAS.StartsWith("?")) { $SAS = $SAS.Substring(1) } $Details.SAS = $SAS } $Accounts = [hashtable](Get-PSFConfigValue -FullName "d365fo.tools.azure.storage.accounts") if ($Accounts.ContainsKey($Name)) { if ($Force) { $Accounts[$Name] = $Details Set-PSFConfig -FullName "d365fo.tools.azure.storage.accounts" -Value $Accounts Register-PSFConfig -FullName "d365fo.tools.azure.storage.accounts" } else { Write-PSFMessage -Level Host -Message "An Azure Storage Account with that name <c='em'>already exists</c>. If you want to <c='em'>overwrite</c> the already registered details please supply the <c='em'>-Force</c> parameter." Stop-PSFFunction -Message "Stopping because an Azure Storage Account already exists with that name." return } } else { $null = $Accounts.Add($Name, $Details) Set-PSFConfig -FullName "d365fo.tools.azure.storage.accounts" -Value $Accounts Register-PSFConfig -FullName "d365fo.tools.azure.storage.accounts" } } <# .SYNOPSIS Save a broadcast message config .DESCRIPTION Adds a broadcast message config to the configuration store .PARAMETER Name The logical name of the broadcast configuration you are about to register in the configuration store .PARAMETER Tenant Azure Active Directory (AAD) tenant id (Guid) that the D365FO environment is connected to, that you want to send a message to .PARAMETER URL URL / URI for the D365FO environment you want to send a message to .PARAMETER ClientId The ClientId obtained from the Azure Portal when you created a Registered Application .PARAMETER ClientSecret The ClientSecret obtained from the Azure Portal when you created a Registered Application .PARAMETER TimeZone Id of the Time Zone your environment is running in You might experience that the local VM running the D365FO is running another Time Zone than the computer you are running this cmdlet from All available .NET Time Zones can be traversed with tab for this parameter The default value is "UTC" .PARAMETER EndingInMinutes Specify how many minutes into the future you want this message / maintenance window to last Default value is 60 minutes The specified StartTime will always be based on local Time Zone. If you specify a different Time Zone than the local computer is running, the start and end time will be calculated based on your selection. .PARAMETER Temporary Instruct the cmdlet to only temporarily add the broadcast message configuration in the configuration store .PARAMETER Force Instruct the cmdlet to overwrite the broadcast message configuration with the same name .EXAMPLE PS C:\> Add-D365BroadcastMessageConfig -Name "UAT" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" -URL "https://usnconeboxax1aos.cloud.onebox.dynamics.com" -ClientId "dea8d7a9-1602-4429-b138-111111111111" -ClientSecret "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522" This will create a new broadcast message configuration with the name "UAT". It will save "e674da86-7ee5-40a7-b777-1111111111111" as the Azure Active Directory guid. It will save "https://usnconeboxax1aos.cloud.onebox.dynamics.com" as the D365FO environment. It will save "dea8d7a9-1602-4429-b138-111111111111" as the ClientId. It will save "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522" as ClientSecret. It will use the default value "UTC" Time Zone for converting the different time and dates. It will use the default end time which is 60 minutes. .NOTES Tags: Servicing, Broadcast, Message, Users, Environment, Config, Configuration, ClientId, ClientSecret Author: M�tz Jensen (@Splaxi) .LINK Clear-D365ActiveBroadcastMessageConfig .LINK Get-D365ActiveBroadcastMessageConfig .LINK Get-D365BroadcastMessageConfig .LINK Remove-D365BroadcastMessageConfig .LINK Send-D365BroadcastMessage .LINK Set-D365ActiveBroadcastMessageConfig #> function Add-D365BroadcastMessageConfig { [CmdletBinding()] param ( [Parameter(Mandatory = $true, Position = 0)] [string] $Name, [Parameter(Mandatory = $false, Position = 1)] [Alias('$AADGuid')] [string] $Tenant, [Parameter(Mandatory = $false, Position = 2)] [Alias('URI')] [string] $URL, [Parameter(Mandatory = $false, Position = 3)] [string] $ClientId, [Parameter(Mandatory = $false, Position = 4)] [string] $ClientSecret, [Parameter(Mandatory = $false, Position = 5)] [string] $TimeZone = "UTC", [Parameter(Mandatory = $false, Position = 6)] [int] $EndingInMinutes = 60, [switch] $Temporary, [switch] $Force ) if (((Get-PSFConfig -FullName "d365fo.tools.broadcast.*.name").Value -contains $Name) -and (-not $Force)) { Write-PSFMessage -Level Host -Message "A broadcast message configuration with <c='em'>$Name</c> as name <c='em'>already exists</c>. If you want to <c='em'>overwrite</c> the current configuration, please supply the <c='em'>-Force</c> parameter." Stop-PSFFunction -Message "Stopping because a broadcast message configuration already exists with that name." return } $configName = "" #The ':keys' label is used to have a continue inside the switch statement itself :keys foreach ($key in $PSBoundParameters.Keys) { $configurationValue = $PSBoundParameters.Item($key) $configurationName = $key.ToLower() $fullConfigName = "" Write-PSFMessage -Level Verbose -Message "Working on $key with $configurationValue" -Target $configurationValue switch ($key) { "Name" { $configName = $Name.ToLower() $fullConfigName = "d365fo.tools.broadcast.$configName.name" } {"Temporary","Force" -contains $_} { continue keys } "TimeZone" { $timeZoneFound = Get-TimeZone -InputObject $TimeZone if (Test-PSFFunctionInterrupt) { return } $fullConfigName = "d365fo.tools.broadcast.$configName.$configurationName" $configurationValue = $timeZoneFound.Id } Default { $fullConfigName = "d365fo.tools.broadcast.$configName.$configurationName" } } Write-PSFMessage -Level Verbose -Message "Setting $fullConfigName to $configurationValue" -Target $configurationValue Set-PSFConfig -FullName $fullConfigName -Value $configurationValue if (-not $Temporary) { Register-PSFConfig -FullName $fullConfigName -Scope UserDefault } } } <# .SYNOPSIS Save an environment config .DESCRIPTION Adds an environment config to the configuration store .PARAMETER Name The logical name of the environment you are about to registered in the configuration .PARAMETER URL The URL to the environment you want the module to use when possible .PARAMETER SqlUser The login name for the SQL Server instance .PARAMETER SqlPwd The password for the SQL Server user .PARAMETER Company The company you want to work against when calling any browser based cmdlets The default value is "DAT" .PARAMETER TfsUri The URI for the TFS / VSTS account that you are working against. .PARAMETER ConfigStorageLocation Parameter used to instruct where to store the configuration objects The default value is "User" and this will store all configuration for the active user Valid options are: "User" "System" "System" will store the configuration so all users can access the configuration objects .PARAMETER Force Switch to instruct the cmdlet to overwrite already registered environment entry .EXAMPLE PS C:\> Add-D365EnvironmentConfig -Name "Customer-UAT" -URL "https://usnconeboxax1aos.cloud.onebox.dynamics.com/?cmp=USMF" -Company "DAT" This will add an entry into the list of environments that is stored with the name "Customer-UAT" and with the URL "https://usnconeboxax1aos.cloud.onebox.dynamics.com/?cmp=USMF". The company is registered "DAT". .EXAMPLE PS C:\> Add-D365EnvironmentConfig -Name "Customer-UAT" -URL "https://usnconeboxax1aos.cloud.onebox.dynamics.com/?cmp=USMF" -Company "DAT" -SqlUser "SqlAdmin" -SqlPwd "Pass@word1" This will add an entry into the list of environments that is stored with the name "Customer-UAT" and with the URL "https://usnconeboxax1aos.cloud.onebox.dynamics.com/?cmp=USMF". It will register the SqlUser as "SqlAdmin" and the SqlPassword to "Pass@word1". This it useful for working on Tier 2 environments where the SqlUser and SqlPassword cannot be extracted from the environment itself. .NOTES Tags: Environment, Url, Config, Configuration, Tfs, Vsts, Sql, SqlUser, SqlPwd Author: M�tz Jensen (@Splaxi) #> function Add-D365EnvironmentConfig { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Name, [string] $URL, [string] $SqlUser = "sqladmin", [string] $SqlPwd, [string] $Company = "DAT", [string] $TfsUri, [switch] $Force ) $Details = @{URL = $URL; Company = $Company; SqlUser = $SqlUser; SqlPwd = $SqlPwd; TfsUri = $TfsUri; } $Environments = [hashtable](Get-PSFConfigValue -FullName "d365fo.tools.environments") if ($Environments.ContainsKey($Name)) { if ($Force) { $Environments[$Name] = $Details Set-PSFConfig -FullName "d365fo.tools.environments" -Value $Environments Register-PSFConfig -FullName "d365fo.tools.environments" } else { Write-PSFMessage -Level Host -Message "An environment with that name <c='em'>already exists</c>. You want to <c='em'>overwrite</c> the already registered details please supply the <c='em'>-Force</c> parameter." Stop-PSFFunction -Message "Stopping because an environment already exists with that name." return } } else { $null = $Environments.Add($Name, $Details) Set-PSFConfig -FullName "d365fo.tools.environments" -Value $Environments Register-PSFConfig -FullName "d365fo.tools.environments" } } <# .SYNOPSIS Add a certificate thumbprint to the wif.config. .DESCRIPTION Register a certificate thumbprint in the wif.config file. This can be useful for example when configuring RSAT on a local machine and add the used certificate thumbprint to that AOS.s .PARAMETER CertificateThumbprint The thumbprint value of the certificate that you want to register in the wif.config file .EXAMPLE PS C:\> Add-D365WIFConfigAuthorityThumbprint -CertificateThumbprint "12312323r424" This will open the wif.config file and insert the "12312323r424" thumbprint value into the file. .NOTES Author: Kenny Saelen (@kennysaelen) #> function Add-D365WIFConfigAuthorityThumbprint { [CmdletBinding()] param ( [Parameter(Mandatory = $true, Position = 1)] [string]$CertificateThumbprint ) try { $wifConfigFile = Join-Path $script:ServiceDrive "\AOSService\webroot\wif.config" if($true -eq (Test-Path -Path $wifConfigFile)) { [xml]$wifXml = Get-Content $wifConfigFile $authorities = $wifXml.SelectNodes('//system.identityModel//identityConfiguration//securityTokenHandlers//securityTokenHandlerConfiguration//issuerNameRegistry//authority[@name="https://fakeacs.accesscontrol.windows.net/"]') if($authorities.Count -lt 1) { Write-PSFMessage -Level Critical -Message "Only one authority should be found with the name https://fakeacs.accesscontrol.windows.net/" Stop-PSFFunction -Message "Stopping because an invalid authority structure was found in the wif.config file." return } else { foreach ($authority in $authorities) { $addElem = $wifXml.CreateElement("add") $addAtt = $wifXml.CreateAttribute("thumbprint") $addAtt.Value = $CertificateThumbprint $addElem.Attributes.Append($addAtt) $authority.FirstChild.AppendChild($addElem) $wifXml.Save($wifConfigFile) } } } else { Write-PSFMessage -Level Critical -Message "The wif.config file would not be located on the system." Stop-PSFFunction -Message "Stopping because the wif.config file could not be located." return } } catch { Write-PSFMessage -Level Host -Message "Something went wrong while configuring the certificates and the Windows Identity Foundation configuration for the AOS" -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" -StepsUpward 1 return } } <# .SYNOPSIS Create a backup of the Metadata directory .DESCRIPTION Creates a backup of all the files and folders from the Metadata directory .PARAMETER MetaDataDir Path to the Metadata directory Default value is the PackagesLocalDirectory .PARAMETER BackupDir Path where you want the backup to be place .EXAMPLE PS C:\> Backup-D365MetaDataDir This will backup the PackagesLocalDirectory and create an PackagesLocalDirectory_backup next to it .NOTES Tags: PackagesLocalDirectory, MetaData, MetaDataDir, MeteDataDirectory, Backup, Development Author: M�tz Jensen (@Splaxi) #> function Backup-D365MetaDataDir { [CmdletBinding()] param ( [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 1 )] [string] $MetaDataDir = "$Script:MetaDataDir", [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 1 )] [string] $BackupDir = "$($Script:MetaDataDir)_backup" ) if(!(Test-Path -Path $MetaDataDir -Type Container)) { Write-PSFMessage -Level Host -Message "The <c='em'>$MetaDataDir</c> path wasn't found. Please ensure the path <c='em'>exists </c> and you have enough <c='em'>permission/c> to access the directory." Stop-PSFFunction -Message "Stopping because the path is missing." return } Invoke-TimeSignal -Start $Params = @($MetaDataDir, $BackupDir, "/MT:4", "/E", "/NFL", "/NDL", "/NJH", "/NC", "/NS", "/NP") Start-Process -FilePath "Robocopy.exe" -ArgumentList $Params -NoNewWindow -Wait Invoke-TimeSignal -End } <# .SYNOPSIS Backup a runbook file .DESCRIPTION Backup a runbook file for you to persist it for later analysis .PARAMETER File Path to the file you want to backup .PARAMETER DestinationPath Path to the folder where you want the backup file to be placed .PARAMETER Force Instructs the cmdlet to overwrite the destination file if it already exists .EXAMPLE PS C:\> Backup-D365Runbook -File "C:\DynamicsAX\InstallationRecords\Runbooks\Runbook_20190327.xml" This will backup the "C:\DynamicsAX\InstallationRecords\Runbooks\Runbook_20190327.xml". The default destination folder is used, "c:\temp\d365fo.tools\runbookbackups\". .EXAMPLE PS C:\> Backup-D365Runbook -File "C:\DynamicsAX\InstallationRecords\Runbooks\Runbook_20190327.xml" -Force This will backup the "C:\DynamicsAX\InstallationRecords\Runbooks\Runbook_20190327.xml". The default destination folder is used, "c:\temp\d365fo.tools\runbookbackups\". If the file already exists in the destination folder, it will be overwritten. .EXAMPLE PS C:\> Get-D365Runbook | Backup-D365Runbook This will backup all runbook files found with the "Get-D365Runbook" cmdlet. The default destination folder is used, "c:\temp\d365fo.tools\runbookbackups\". .NOTES Tags: Runbook, Backup, Analysis Author: M�tz Jensen (@Splaxi) #> function Backup-D365Runbook { [CmdletBinding()] [OutputType()] param ( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [Alias('Path')] [string] $File, [Parameter(Mandatory = $false)] [string] $DestinationPath = $(Join-Path $Script:DefaultTempPath "RunbookBackups"), [switch] $Force ) begin { if (-not (Test-PathExists -Path $DestinationPath -Type Container -Create)) { return } } process { if (-not (Test-PathExists -Path $File -Type Leaf)) { return } if (Test-PSFFunctionInterrupt) { return } $fileName = Split-Path -Path $File -Leaf $destinationFile = $(Join-Path $DestinationPath $fileName) if (-not $Force) { if ((-not (Test-PathExists -Path $destinationFile -Type Leaf -ShouldNotExist -DontBreak))) { Write-PSFMessage -Level Host -Message "The <c='em'>$destinationFile</c> already exists. Consider changing the <c='em'>destination</c> path or set the <c='em'>Force</c> parameter to overwrite the file." return } } Write-PSFMessage -Level Verbose -Message "Copying from: $File" -Target $item Copy-Item -Path $File -Destination $destinationFile -Force:$Force -PassThru | Select-PSFObject "Name as Filename", "LastWriteTime as LastModified", "Fullname as File" } } <# .SYNOPSIS Clear the active broadcast message config .DESCRIPTION Clear the active broadcast message config from the configuration store .PARAMETER Temporary Instruct the cmdlet to only temporarily clear the active broadcast message configuration in the configuration store .EXAMPLE PS C:\> Clear-D365ActiveBroadcastMessageConfig This will clear the active broadcast message configuration from the configuration store. .NOTES Tags: Servicing, Broadcast, Message, Users, Environment, Config, Configuration, ClientId, ClientSecret Author: M�tz Jensen (@Splaxi) .LINK Add-D365BroadcastMessageConfig .LINK Get-D365ActiveBroadcastMessageConfig .LINK Get-D365BroadcastMessageConfig .LINK Remove-D365BroadcastMessageConfig .LINK Send-D365BroadcastMessage .LINK Set-D365ActiveBroadcastMessageConfig #> function Clear-D365ActiveBroadcastMessageConfig { [CmdletBinding()] [OutputType()] param ( [switch] $Temporary ) $configurationName = "d365fo.tools.active.broadcast.message.config.name" Reset-PSFConfig -FullName $configurationName if (-not $Temporary) { Register-PSFConfig -FullName $configurationName -Scope UserDefault } } <# .SYNOPSIS Clear the monitoring data from a Dynamics 365 for Finance & Operations machine .DESCRIPTION Clear the monitoring data that is filling up the service drive on a Dynamics 365 for Finance & Operations .PARAMETER Path The path to where the monitoring data is located The default value is the "ServiceDrive" (j:\ | k:\) and the \MonAgentData\SingleAgent\Tables folder structure .EXAMPLE PS C:\> Clear-D365MonitorData This will delete all the files that are located in the default path on the machine. Some files might be locked by a process, but the cmdlet will attemp to delete all files. .NOTES Tags: Monitor, MonitorData, MonitorAgent, CleanUp, Servicing Author: M�tz Jensen (@Splaxi) #> function Clear-D365MonitorData { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [Parameter(Position = 1, ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true)] [string] $Path = (Join-Path $script:ServiceDrive "\MonAgentData\SingleAgent\Tables") ) Get-ChildItem -Path $Path | Remove-Item -Force -ErrorAction SilentlyContinue } <# .SYNOPSIS Sets the environment back into operating state .DESCRIPTION Sets the Dynamics 365 environment back into operating / running state after been in maintenance mode .PARAMETER MetaDataDir The path to the meta data directory for the environment Default path is the same as the aos service PackagesLocalDirectory .PARAMETER BinDir The path to the bin directory for the environment Default path is the same as the aos service PackagesLocalDirectory\bin .PARAMETER DatabaseServer The name of the database server If on-premises or classic SQL Server, use either short name og Fully Qualified Domain Name (FQDN). If Azure use the full address to the database server, e.g. server.database.windows.net .PARAMETER DatabaseName The name of the database .PARAMETER SqlUser The login name for the SQL Server instance .PARAMETER SqlPwd The password for the SQL Server user. .PARAMETER ShowOriginalProgress Instruct the cmdlet to show the standard output in the console Default is $false which will silence the standard output .EXAMPLE PS C:\> Disable-D365MaintenanceMode This will execute the Microsoft.Dynamics.AX.Deployment.Setup.exe with the default values that was pulled from the environment and put the environment into the operate / running state. .EXAMPLE PS C:\> Disable-D365MaintenanceMode -ShowOriginalProgress This will execute the Microsoft.Dynamics.AX.Deployment.Setup.exe with the default values that was pulled from the environment and put the environment into the operate / running state. The output from stopping the services will be written to the console / host. The output from the "deployment" process will be written to the console / host. The output from starting the services will be written to the console / host. .NOTES Tags: MaintenanceMode, Maintenance, License, Configuration, Servicing Author: M�tz Jensen (@splaxi) Author: Tommy Skaue (@skaue) With administrator privileges: The cmdlet wraps the execution of Microsoft.Dynamics.AX.Deployment.Setup.exe and parses the parameters needed. Without administrator privileges: Will stop all services, execute a Sql script and start all services. .LINK Enable-D365MaintenanceMode .LINK Get-D365MaintenanceMode #> function Disable-D365MaintenanceMode { [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 1 )] [string] $MetaDataDir = "$Script:MetaDataDir", [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 2 )] [string] $BinDir = "$Script:BinDir", [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 3 )] [string] $DatabaseServer = $Script:DatabaseServer, [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 4 )] [string] $DatabaseName = $Script:DatabaseName, [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 5 )] [string] $SqlUser = $Script:DatabaseUserName, [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 6 )] [string] $SqlPwd = $Script:DatabaseUserPassword, [Parameter(Mandatory = $False)] [switch] $ShowOriginalProgress ) if ((Get-Process -Name "devenv" -ErrorAction SilentlyContinue).Count -gt 0) { Write-PSFMessage -Level Host -Message "It seems that you have a <c='em'>Visual Studio</c> running. Please <c='em'>exit</c> Visual Studio and run the cmdlet again." Stop-PSFFunction -Message "Stopping because of running Visual Studio." return } Stop-D365Environment -All -ShowOriginalProgress:$ShowOriginalProgress | Format-Table if(-not ($Script:IsAdminRuntime)) { Write-PSFMessage -Level Verbose -Message "Setting Maintenance Mode without using executable (which requires local admin)." $UseTrustedConnection = Test-TrustedConnection $PSBoundParameters $Params = @{ DatabaseServer = $DatabaseServer DatabaseName = $DatabaseName SqlUser = $SqlUser SqlPwd = $SqlPwd } Invoke-D365SqlScript @Params -FilePath $("$script:ModuleRoot\internal\sql\disable-maintenancemode.sql") -TrustedConnection $UseTrustedConnection } else { Write-PSFMessage -Level Verbose -Message "Setting Maintenance Mode using executable." $executable = Join-Path $BinDir "bin\Microsoft.Dynamics.AX.Deployment.Setup.exe" if (-not (Test-PathExists -Path $MetaDataDir,$BinDir -Type Container)) { return } if (-not (Test-PathExists -Path $executable -Type Leaf)) { return } $params = @("-isemulated", "true", "-sqluser", "$SqlUser", "-sqlpwd", "$SqlPwd", "-sqlserver", "$DatabaseServer", "-sqldatabase", "$DatabaseName", "-metadatadir", "$MetaDataDir", "-bindir", "$BinDir", "-setupmode", "maintenancemode", "-isinmaintenancemode", "false") Invoke-Process -Executable $executable -Params $params -ShowOriginalProgress:$ShowOriginalProgress } Start-D365Environment -All -ShowOriginalProgress:$ShowOriginalProgress | Format-Table } <# .SYNOPSIS Disables the user in D365FO .DESCRIPTION Sets the enabled to 0 in the userinfo table. .PARAMETER DatabaseServer The name of the database server If on-premises or classic SQL Server, use either short name og Fully Qualified Domain Name (FQDN). If Azure use the full address to the database server, e.g. server.database.windows.net .PARAMETER DatabaseName The name of the database .PARAMETER SqlUser The login name for the SQL Server instance .PARAMETER SqlPwd The password for the SQL Server user. .PARAMETER Email The search string to select which user(s) should be disabled. The parameter supports wildcards. E.g. -Email "*@contoso.com*" .EXAMPLE PS C:\> Disable-D365User This will Disable all users for the environment .EXAMPLE PS C:\> Disable-D365User -Email "claire@contoso.com" This will Disable the user with the email address "claire@contoso.com" .EXAMPLE PS C:\> Disable-D365User -Email "*contoso.com" This will Disable all users that matches the search "*contoso.com" in their email address .NOTES Tags: User, Users, Security, Configuration, Permission Author: M�tz Jensen (@Splaxi) #> function Disable-D365User { [CmdletBinding()] param ( [Parameter(Mandatory = $false, Position = 1)] [string]$DatabaseServer = $Script:DatabaseServer, [Parameter(Mandatory = $false, Position = 2)] [string]$DatabaseName = $Script:DatabaseName, [Parameter(Mandatory = $false, Position = 3)] [string]$SqlUser = $Script:DatabaseUserName, [Parameter(Mandatory = $false, Position = 4)] [string]$SqlPwd = $Script:DatabaseUserPassword, [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true, Position = 5)] [string]$Email = "*" ) begin { Invoke-TimeSignal -Start $UseTrustedConnection = Test-TrustedConnection $PSBoundParameters $SqlParams = @{ DatabaseServer = $DatabaseServer; DatabaseName = $DatabaseName; SqlUser = $SqlUser; SqlPwd = $SqlPwd } $SqlCommand = Get-SqlCommand @SqlParams -TrustedConnection $UseTrustedConnection try { $sqlCommand.Connection.Open() } catch { Write-PSFMessage -Level Host -Message "Something went wrong while working against the database" -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" return } } process { if (Test-PSFFunctionInterrupt) { return } $sqlCommand.CommandText = (Get-Content "$script:ModuleRoot\internal\sql\disable-user.sql") -join [Environment]::NewLine $null = $sqlCommand.Parameters.AddWithValue('@Email', $Email.Replace("*", "%")) try { Write-PSFMessage -Level InternalComment -Message "Executing a script against the database." -Target (Get-SqlString $SqlCommand) $reader = $sqlCommand.ExecuteReader() $NumAffected = 0 while ($reader.Read() -eq $true) { Write-PSFMessage -Level Verbose -Message "User $($reader.GetString(0)), $($reader.GetString(1)), $($reader.GetString(2)) Updated" $NumAffected++ } $reader.Close() Write-PSFMessage -Level Verbose -Message "Users updated : $NumAffected" } catch { Write-PSFMessage -Level Host -Message "Something went wrong while working against the database" -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" return } finally { $reader.close() $sqlCommand.Parameters.Clear() } } end { if ($sqlCommand.Connection.State -ne [System.Data.ConnectionState]::Closed) { $sqlCommand.Connection.Close() } $sqlCommand.Dispose() Invoke-TimeSignal -End } } <# .SYNOPSIS Sets the environment into maintenance mode .DESCRIPTION Sets the Dynamics 365 environment into maintenance mode to enable the user to update the license configuration .PARAMETER MetaDataDir The path to the meta data directory for the environment Default path is the same as the aos service PackagesLocalDirectory .PARAMETER BinDir The path to the bin directory for the environment Default path is the same as the aos service PackagesLocalDirectory\bin .PARAMETER DatabaseServer The name of the database server If on-premises or classic SQL Server, use either short name og Fully Qualified Domain Name (FQDN). If Azure use the full address to the database server, e.g. server.database.windows.net .PARAMETER DatabaseName The name of the database .PARAMETER SqlUser The login name for the SQL Server instance .PARAMETER SqlPwd The password for the SQL Server user. .PARAMETER ShowOriginalProgress Instruct the cmdlet to show the standard output in the console Default is $false which will silence the standard output .EXAMPLE PS C:\> Enable-D365MaintenanceMode This will execute the Microsoft.Dynamics.AX.Deployment.Setup.exe with the default values that was pulled from the environment and put the environment into the operate / running state .EXAMPLE PS C:\> Enable-D365MaintenanceMode -ShowOriginalProgress This will execute the Microsoft.Dynamics.AX.Deployment.Setup.exe with the default values that was pulled from the environment and put the environment into the operate / running state The output from stopping the services will be written to the console / host. The output from the "deployment" process will be written to the console / host. The output from starting the services will be written to the console / host. .NOTES Tags: MaintenanceMode, Maintenance, License, Configuration, Servicing Author: M�tz Jensen (@splaxi) Author: Tommy Skaue (@skaue) With administrator privileges: The cmdlet wraps the execution of Microsoft.Dynamics.AX.Deployment.Setup.exe and parses the parameters needed. Without administrator privileges: Will stop all services, execute a Sql script and start all services. .LINK Get-D365MaintenanceMode .LINK Disable-D365MaintenanceMode #> function Enable-D365MaintenanceMode { [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 1 )] [string] $MetaDataDir = "$Script:MetaDataDir", [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 2 )] [string] $BinDir = "$Script:BinDir", [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 3 )] [string] $DatabaseServer = $Script:DatabaseServer, [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 4 )] [string] $DatabaseName = $Script:DatabaseName, [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 5 )] [string] $SqlUser = $Script:DatabaseUserName, [Parameter(Mandatory = $false, ParameterSetName =  |