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 = '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\enable-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", "true") Invoke-Process -Executable $executable -Params $params -ShowOriginalProgress:$ShowOriginalProgress } Start-D365Environment -Aos -ShowOriginalProgress:$ShowOriginalProgress | Format-Table } <# .SYNOPSIS Enables the user in D365FO .DESCRIPTION Sets the enabled to 1 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 enabled The parameter supports wildcards. E.g. -Email "*@contoso.com*" Default value is "*" to update all users .EXAMPLE PS C:\> Enable-D365User This will enable all users for the environment .EXAMPLE PS C:\> Enable-D365User -Email "claire@contoso.com" This will enable the user with the email address "claire@contoso.com" .EXAMPLE PS C:\> Enable-D365User -Email "*contoso.com" This will enable all users that matches the search "*contoso.com" in their email address .NOTES Tags: User, Users, Security, Configuration, Permission Author: M�tz Jensen #> function Enable-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\enable-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 Export a model from Dynamics 365 for Finance & Operations .DESCRIPTION Export a model from a Dynamics 365 for Finance & Operations environment .PARAMETER Path Path to the folder where you want to save the model file .PARAMETER Model Name of the model that you want to work against .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:\> Export-D365Model -Path c:\temp\d365fo.tools -Model CustomModelName This will export the "CustomModelName" model from the default PackagesLocalDirectory path. It export the model to the "c:\temp\d365fo.tools" location. .NOTES Tags: ModelUtil, Axmodel, Model, Export Author: M�tz Jensen (@Splaxi) #> function Export-D365Model { # [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidDefaultValueSwitchParameter", "")] [CmdletBinding()] param ( [Parameter(Mandatory = $True, Position = 1 )] [Alias('File')] [string] $Path, [Parameter(Mandatory = $True, Position = 2 )] [string] $Model, [Parameter(Mandatory = $false, Position = 3 )] [string] $BinDir = "$Script:PackageDirectory\bin", [Parameter(Mandatory = $false, Position = 4 )] [string] $MetaDataDir = "$Script:MetaDataDir" ) Invoke-TimeSignal -Start if($Path.EndsWith("\")) { $Path = $Path.Substring(0, $Path.Length - 1) } Invoke-ModelUtil -Command "Export" -Path $Path -BinDir $BinDir -MetaDataDir $MetaDataDir -Model $Model Invoke-TimeSignal -End } <# .SYNOPSIS Extract details from a User Interface Security file .DESCRIPTION Extracts and partitions the security details from an User Interface Security file into the same structure as AOT security files .PARAMETER FilePath Path to the User Interface Security XML file you want to work against .PARAMETER OutputDirectory Path to the folder where the cmdlet will output and structure the details from the file. The cmdlet will create a sub folder named like the input file. Default value is: "C:\temp\d365fo.tools\security-extraction" .EXAMPLE PS C:\> Export-D365SecurityDetails -FilePath C:\temp\d365fo.tools\SecurityDatabaseCustomizations.xml This will grab all the details inside the "C:\temp\d365fo.tools\SecurityDatabaseCustomizations.xml" file and extract that into the default path "C:\temp\d365fo.tools\security-extraction" .NOTES Tags: Security, Configuration, Permission, Development Author: M�tz Jensen (@splaxi) The work and design of this cmdlet is based on the findings by Alex Meyer (@alexmeyer_ITGuy). He wrote about his findings on his blog: https://alexdmeyer.com/2018/09/26/converting-d365fo-user-interface-security-customizations-export-to-aot-security-xml-files/ He published a github repository: https://github.com/ameyer505/D365FOSecurityConverter All credits goes to Alex Meyer #> function Export-D365SecurityDetails { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [Alias('Path')] [string]$FilePath, [Parameter(Mandatory = $false)] [Alias('Output')] [string]$OutputDirectory = "C:\temp\d365fo.tools\security-extraction" ) begin { } process { if (-not (Test-PathExists -Path $FilePath -Type Leaf)) { return } if (-not (Test-PathExists -Path $OutputDirectory -Type Container)) { return } [xml] $xdoc = Get-Content $FilePath $fileName = [System.IO.Path]::GetFileNameWithoutExtension($FilePath) $OutputDirectory = Join-Path $OutputDirectory $fileName Write-PSFMessage -Level Verbose -Message "Creating the output directory for the extraction" -Target $OutputDirectory $null = New-Item -Path $OutputDirectory -ItemType Directory -Force -ErrorAction SilentlyContinue Write-PSFMessage -Level Verbose -Message "Getting all the security objects." $secObjects = $xdoc.SelectNodes("/*/*/*/*/*[starts-with(name(),'AxSec')]") if ($secObjects.Count -gt 0) { Write-PSFMessage -Level Verbose -Message "Looping through all the security objects we found" foreach ( $secObject in $secObjects) { $secPath = Join-Path $OutputDirectory $secObject.LocalName $null = New-Item -Path $secPath -ItemType Directory -Force -ErrorAction SilentlyContinue $secObjectName = $secObject.Name if (-not ([string]::IsNullOrEmpty($secObjectName))) { $filePathOut = Join-Path $secPath $secObjectName $filePathOut += ".xml" Write-PSFMessage -Level Verbose -Message "Generating the output file: $filePathOut" -Target $filePathOut $secObject.OuterXml | Out-File $filePathOut } } } } end { } } #ValidationTags#Messaging,FlowControl,Pipeline,CodeStyle# function Find-D365Command { <# .SYNOPSIS Finds d365fo.tools commands searching through the inline help text .DESCRIPTION Finds d365fo.tools commands searching through the inline help text, building a consolidated json index and querying it because Get-Help is too slow .PARAMETER Tag Finds all commands tagged with this auto-populated tag .PARAMETER Author Finds all commands tagged with this author .PARAMETER MinimumVersion Finds all commands tagged with this auto-populated minimum version .PARAMETER MaximumVersion Finds all commands tagged with this auto-populated maximum version .PARAMETER Rebuild Rebuilds the index .PARAMETER Pattern Searches help for all commands in d365fo.tools for the specified pattern and displays all results .PARAMETER Confirm Confirms overwrite of index .PARAMETER WhatIf Displays what would happen if the command is run .PARAMETER EnableException By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. .EXAMPLE PS C:\> Find-D365Command "snapshot" For lazy typers: finds all commands searching the entire help for "snapshot" .EXAMPLE PS C:\> Find-D365Command -Pattern "snapshot" For rigorous typers: finds all commands searching the entire help for "snapshot" .EXAMPLE PS C:\> Find-D365Command -Tag copy Finds all commands tagged with "copy" .EXAMPLE PS C:\> Find-D365Command -Tag copy,user Finds all commands tagged with BOTH "copy" and "user" .EXAMPLE PS C:\> Find-D365Command -Author M�tz Finds every command whose author contains "M�tz" .EXAMPLE PS C:\> Find-D365Command -Author M�tz -Tag copy Finds every command whose author contains "M�tz" and it tagged as "copy" .EXAMPLE PS C:\> Find-D365Command -Pattern snapshot -Rebuild Finds all commands searching the entire help for "snapshot", rebuilding the index (good for developers) .NOTES Tags: Find, Help, Command Author: M�tz Jensen (@Splaxi) License: MIT https://opensource.org/licenses/MIT This cmdlet / function is copy & paste implementation based on the Find-DbaCommand from the dbatools.io project Original author: Simone Bizzotto (@niphold) #> [CmdletBinding(SupportsShouldProcess = $true)] param ( [String]$Pattern, [String[]]$Tag, [String]$Author, [String]$MinimumVersion, [String]$MaximumVersion, [switch]$Rebuild, [Alias('Silent')] [switch]$EnableException ) begin { function Get-D365TrimmedString($Text) { return $Text.Trim() -replace '(\r\n){2,}', "`n" } $tagsRex = ([regex]'(?m)^[\s]{0,15}Tags:(.*)$') $authorRex = ([regex]'(?m)^[\s]{0,15}Author:(.*)$') $minverRex = ([regex]'(?m)^[\s]{0,15}MinimumVersion:(.*)$') $maxverRex = ([regex]'(?m)^[\s]{0,15}MaximumVersion:(.*)$') function Get-D365Help([String]$commandName) { $thishelp = Get-Help $commandName -Full $thebase = @{ } $thebase.CommandName = $commandName $thebase.Name = $thishelp.Name $alias = Get-Alias -Definition $commandName -ErrorAction SilentlyContinue $thebase.Alias = $alias.Name -Join ',' ## fetch the description $thebase.Description = $thishelp.Description.Text ## fetch examples $thebase.Examples = Get-D365TrimmedString -Text ($thishelp.Examples | Out-String -Width 200) ## fetch help link $thebase.Links = ($thishelp.relatedLinks).NavigationLink.Uri ## fetch the synopsis $thebase.Synopsis = $thishelp.Synopsis ## fetch the syntax $thebase.Syntax = Get-D365TrimmedString -Text ($thishelp.Syntax | Out-String -Width 600) ## store notes $as = $thishelp.AlertSet | Out-String -Width 600 ## fetch the tags $tags = $tagsrex.Match($as).Groups[1].Value if ($tags) { $thebase.Tags = $tags.Split(',').Trim() } ## fetch the author $author = $authorRex.Match($as).Groups[1].Value if ($author) { $thebase.Author = $author.Trim() } ## fetch MinimumVersion $MinimumVersion = $minverRex.Match($as).Groups[1].Value if ($MinimumVersion) { $thebase.MinimumVersion = $MinimumVersion.Trim() } ## fetch MaximumVersion $MaximumVersion = $maxverRex.Match($as).Groups[1].Value if ($MaximumVersion) { $thebase.MaximumVersion = $MaximumVersion.Trim() } ## fetch Parameters $parameters = $thishelp.parameters.parameter $command = Get-Command $commandName $params = @() foreach($p in $parameters) { $paramAlias = $command.parameters[$p.Name].Aliases $paramDescr = Get-D365TrimmedString -Text ($p.Description | Out-String -Width 200) $params += , @($p.Name, $paramDescr, ($paramAlias -Join ','), ($p.Required -eq $true), $p.PipelineInput, $p.DefaultValue) } $thebase.Params = $params [pscustomobject]$thebase } function Get-D365Index() { if ($Pscmdlet.ShouldProcess($dest, "Recreating index")) { $dbamodule = Get-Module -Name d365fo.tools $allCommands = $dbamodule.ExportedCommands.Values | Where-Object CommandType -EQ 'Function' $helpcoll = New-Object System.Collections.Generic.List[System.Object] foreach ($command in $allCommands) { $x = Get-D365Help "$command" $helpcoll.Add($x) } # $dest = Get-DbatoolsConfigValue -Name 'Path.TagCache' -Fallback "$(Resolve-Path $PSScriptRoot\..)\dbatools-index.json" $dest = "$moduleDirectory\bin\d365fo.tools-index.json" $helpcoll | ConvertTo-Json -Depth 4 | Out-File $dest -Encoding UTF8 } } $moduleDirectory = (Get-Module -Name d365fo.tools).ModuleBase } process { $Pattern = $Pattern.TrimEnd("s") $idxFile = "$moduleDirectory\bin\d365fo.tools-index.json" if (!(Test-Path $idxFile) -or $Rebuild) { Write-PSFMessage -Level Verbose -Message "Rebuilding index into $idxFile" $swRebuild = [system.diagnostics.stopwatch]::StartNew() Get-D365Index Write-PSFMessage -Level Verbose -Message "Rebuild done in $($swRebuild.ElapsedMilliseconds)ms" } $consolidated = Get-Content -Raw $idxFile | ConvertFrom-Json $result = $consolidated if ($Pattern.Length -gt 0) { $result = $result | Where-Object { $_.PsObject.Properties.Value -like "*$Pattern*" } } if ($Tag.Length -gt 0) { foreach ($t in $Tag) { $result = $result | Where-Object Tags -Contains $t } } if ($Author.Length -gt 0) { $result = $result | Where-Object Author -Like "*$Author*" } if ($MinimumVersion.Length -gt 0) { $result = $result | Where-Object MinimumVersion -GE $MinimumVersion } if ($MaximumVersion.Length -gt 0) { $result = $result | Where-Object MaximumVersion -LE $MaximumVersion } Select-DefaultView -InputObject $result -Property CommandName, Synopsis } } <# .SYNOPSIS Get active Azure Storage Account configuration .DESCRIPTION Get active Azure Storage Account configuration object from the configuration store .EXAMPLE PS C:\> Get-D365ActiveAzureStorageConfig This will get the active Azure Storage configuration .NOTES Tags: Azure, Azure Storage, Config, Configuration, Token, Blob, Container Author: M�tz Jensen (@Splaxi) #> function Get-D365ActiveAzureStorageConfig { [CmdletBinding()] param () Get-PSFConfigValue -FullName "d365fo.tools.active.azure.storage.account" } <# .SYNOPSIS Get active broadcast message configuration .DESCRIPTION Get active broadcast message configuration from the configuration store .PARAMETER OutputAsHashtable Instruct the cmdlet to return a hastable object .EXAMPLE PS C:\> Get-D365ActiveBroadcastMessageConfig This will get the active broadcast message configuration. .NOTES Tags: Servicing, Message, Users, Environment, Config, Configuration, ClientId, ClientSecret Author: M�tz Jensen (@Splaxi) .LINK Add-D365BroadcastMessageConfig .LINK Clear-D365ActiveBroadcastMessageConfig .LINK Get-D365BroadcastMessageConfig .LINK Remove-D365BroadcastMessageConfig .LINK Send-D365BroadcastMessage .LINK Set-D365ActiveBroadcastMessageConfig #> function Get-D365ActiveBroadcastMessageConfig { [CmdletBinding()] [OutputType()] param ( [switch] $OutputAsHashtable ) $configName = (Get-PSFConfig -FullName "d365fo.tools.active.broadcast.message.config.name").Value if ($configName -eq "") { Write-PSFMessage -Level Host -Message "It looks like there <c='em'>isn't configured</c> an active broadcast message configuration." Stop-PSFFunction -Message "Stopping because an active broadcast message configuration wasn't found." return } Get-D365BroadcastMessageConfig -Name $configName -OutputAsHashtable:$OutputAsHashtable } <# .SYNOPSIS Get active environment configuration .DESCRIPTION Get active environment configuration object from the configuration store .EXAMPLE PS C:\> Get-D365ActiveEnvironmentConfig This will get the active environment configuration .EXAMPLE PS C:\> $params = @{} PS C:\> $params.SqlUser = (Get-D365ActiveEnvironmentConfig).SqlUser PS C:\> $params.SqlPwd = (Get-D365ActiveEnvironmentConfig).SqlPwd This gives you a hashtable with the SqlUser and SqlPwd values from the active environment. This enables you to use the $params as splatting for other cmdlets. .NOTES Tags: Environment, Url, Config, Configuration, Tfs, Vsts, Sql, SqlUser, SqlPwd Author: M�tz Jensen (@Splaxi) #> function Get-D365ActiveEnvironmentConfig { [CmdletBinding()] param () (Get-PSFConfigValue -FullName "d365fo.tools.active.environment") } <# .SYNOPSIS Search for AOT object .DESCRIPTION Enables you to search for different AOT objects .PARAMETER Path Path to the package that you want to work against .PARAMETER ObjectType The type of AOT object you're searching for .PARAMETER Name Name of the object that you're looking for Accepts wildcards for searching. E.g. -Name "Work*status" Default value is "*" which will search for all objects .PARAMETER SearchInPackages Switch to instruct the cmdlet to search in packages directly instead of searching in the XppMetaData directory under a given package .PARAMETER IncludePath Switch to instruct the cmdlet to include the path for the object found .EXAMPLE PS C:\> Get-D365AOTObject -Name *flush* -ObjectType AxClass -Path "C:\AOSService\PackagesLocalDirectory\ApplicationFoundation" This will search inside the ApplicationFoundation package for all AxClasses that matches the search *flush*. .EXAMPLE PS C:\> Get-D365AOTObject -Name *flush* -ObjectType AxClass -IncludePath -Path "C:\AOSService\PackagesLocalDirectory\ApplicationFoundation" This will search inside the ApplicationFoundation package for all AxClasses that matches the search *flush* and include the full path to the files. .EXAMPLE PS C:\> Get-D365InstalledPackage -Name Application* | Get-D365AOTObject -Name *flush* -ObjectType AxClass This searches for all packages that matches Application* and pipes them into Get-D365AOTObject which will search for all AxClasses that matches the search *flush*. .EXAMPLE This is an advanced example and shouldn't be something you resolve to every time. PS C:\> Get-D365AOTObject -Path "C:\AOSService\PackagesLocalDirectory\*" -Name *flush* -ObjectType AxClass -SearchInPackages This will search across all packages and will look for the all AxClasses that matches the search *flush*. It will NOT search in the XppMetaData directory for each package. This can stress your system. .NOTES Author: M�tz Jensen (@Splaxi) #> function Get-D365AOTObject { [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, Position = 1)] [Alias('PackageDirectory')] [string] $Path, [Parameter(Mandatory = $false, Position = 2)] [ValidateSet('AxAggregateDataEntity', 'AxClass', 'AxCompositeDataEntityView', 'AxDataEntityView', 'AxForm', 'AxMap', 'AxQuery', 'AxTable', 'AxView')] [Alias('Type')] [string[]] $ObjectType = @("AxClass"), [Parameter(Mandatory = $false, Position = 3)] [string] $Name = "*", [Parameter(Mandatory = $false, Position = 4)] [switch] $SearchInPackages, [Parameter(Mandatory = $false, Position = 5)] [switch] $IncludePath ) begin { } process { $SearchList = New-Object -TypeName "System.Collections.ArrayList" foreach ($item in $ObjectType) { if ($SearchInPackages) { $SearchParent = Split-Path $Path -Leaf $null = $SearchList.Add((Join-Path "$Path" "\$SearchParent\$item\*.xml")) $SearchParent = $item #* Hack to make the logic when selecting the output work as expected } else { $SearchParent = "XppMetadata" $null = $SearchList.Add((Join-Path "$Path" "\$SearchParent\*\$item\*.xml")) } } #* We are searching files - so the last character has to be a * if($Name.Substring($Name.Length -1, 1) -ne "*") {$Name = "$Name*"} $Files = Get-ChildItem -Path ($SearchList.ToArray()) -Filter $Name if($IncludePath) { $Files | Select-PSFObject -TypeName "D365FO.TOOLS.AotObject" "BaseName as Name", @{Name = "AotType"; Expression = {Split-Path(Split-Path -Path $_.Fullname -Parent) -leaf }}, @{Name = "Model"; Expression = {Split-Path(($_.Fullname -Split $SearchParent)[0] ) -leaf }}, "Fullname as Path" } else { $Files | Select-PSFObject -TypeName "D365FO.TOOLS.AotObject" "BaseName as Name", @{Name = "AotType"; Expression = {Split-Path(Split-Path -Path $_.Fullname -Parent) -leaf }}, @{Name = "Model"; Expression = {Split-Path(($_.Fullname -Split $SearchParent)[0] ) -leaf }} } } end { } } <# .SYNOPSIS Get Azure Storage Account configs .DESCRIPTION Get all Azure Storage Account configuration objects from the configuration store .PARAMETER Name The name of the Azure Storage Account you are looking for Default value is "*" to display all Azure Storage Account configs .EXAMPLE PS C:\> Get-D365AzureStorageConfig This will show all Azure Storage Account configs .NOTES Tags: Azure, Azure Storage, Config, Configuration, Token, Blob, Container Author: M�tz Jensen (@Splaxi) #> function Get-D365AzureStorageConfig { [CmdletBinding()] param ( [string] $Name = "*" ) $Environments = [hashtable](Get-PSFConfigValue -FullName "d365fo.tools.azure.storage.accounts") foreach ($item in $Environments.Keys) { if ($item -NotLike $Name) { continue } $temp = [ordered]@{Name = $item} $temp += $Environments[$item] [PSCustomObject]$temp } } <# .SYNOPSIS Get a file from Azure .DESCRIPTION Get all files from an Azure Storage Account .PARAMETER AccountId Storage Account Name / Storage Account Id where you want to look for files .PARAMETER AccessToken The token that has the needed permissions for the search action .PARAMETER SAS The SAS key that you have created for the storage account or blob container .PARAMETER Container Name of the blob container inside the storage account you want to look for files .PARAMETER Name Name of the file you are looking for Accepts wildcards for searching. E.g. -Name "Application*Adaptor" Default value is "*" which will search for all packages .PARAMETER Latest Instruct the cmdlet to only fetch the latest file from the Azure Storage Account .EXAMPLE PS C:\> Get-D365AzureStorageFile -AccountId "miscfiles" -AccessToken "xx508xx63817x752xx74004x30705xx92x58349x5x78f5xx34xxxxx51" -Container "backupfiles" This will get all files in the blob container "backupfiles". It will use the AccessToken "xx508xx63817x752xx74004x30705xx92x58349x5x78f5xx34xxxxx51" to gain access. .EXAMPLE PS C:\> Get-D365AzureStorageFile -AccountId "miscfiles" -AccessToken "xx508xx63817x752xx74004x30705xx92x58349x5x78f5xx34xxxxx51" -Container "backupfiles" -Latest This will get the latest (newest) file from the blob container "backupfiles". It will use the AccessToken "xx508xx63817x752xx74004x30705xx92x58349x5x78f5xx34xxxxx51" to gain access to the container. .EXAMPLE PS C:\> Get-D365AzureStorageFile -AccountId "miscfiles" -AccessToken "xx508xx63817x752xx74004x30705xx92x58349x5x78f5xx34xxxxx51" -Container "backupfiles" -Name "*UAT*" This will get all files in the blob container "backupfiles" that fits the "*UAT*" search value. It will use the AccessToken "xx508xx63817x752xx74004x30705xx92x58349x5x78f5xx34xxxxx51" to gain access to the container. .EXAMPLE PS C:\> Get-D365AzureStorageFile -AccountId "miscfiles" -SAS "sv2018-03-28&siunlisted&src&sigAUOpdsfpoWE976ASDhfjkasdf(5678sdfhk" -Container "backupfiles" -Latest This will get the latest (newest) file from the blob container "backupfiles". It will use the SAS key "sv2018-03-28&siunlisted&src&sigAUOpdsfpoWE976ASDhfjkasdf(5678sdfhk" to gain access to the container. .NOTES Tags: Azure, Azure Storage, Token, Blob, File, Container Author: M�tz Jensen (@Splaxi) #> function Get-D365AzureStorageFile { [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory = $false)] [string] $AccountId = $Script:AccountId, [Parameter(Mandatory = $false)] [string] $AccessToken = $Script:AccessToken, [Parameter(Mandatory = $false)] [string] $SAS = $Script:SAS, [Parameter(Mandatory = $false)] [Alias('Blob')] [Alias('Blobname')] [string] $Container = $Script:Container, [Parameter(Mandatory = $false, ParameterSetName = 'Default')] [Alias('FileName')] [string] $Name = "*", [Parameter(Mandatory = $true, ParameterSetName = 'Latest')] [Alias('GetLatest')] [switch] $Latest ) if (([string]::IsNullOrEmpty($AccountId) -eq $true) -or ([string]::IsNullOrEmpty($Container)) -or (([string]::IsNullOrEmpty($AccessToken)) -and ([string]::IsNullOrEmpty($SAS)))) { Write-PSFMessage -Level Host -Message "It seems that you are missing some of the parameters. Please make sure that you either supplied them or have the right configuration saved." Stop-PSFFunction -Message "Stopping because of missing parameters" return } Invoke-TimeSignal -Start if ([string]::IsNullOrEmpty($SAS)) { Write-PSFMessage -Level Verbose -Message "Working against Azure Storage Account with AccessToken" $storageContext = new-AzureStorageContext -StorageAccountName $AccountId.ToLower() -StorageAccountKey $AccessToken } else { Write-PSFMessage -Level Verbose -Message "Working against Azure Storage Account with SAS" $conString = $("BlobEndpoint=https://{0}.blob.core.windows.net/;QueueEndpoint=https://{0}.queue.core.windows.net/;FileEndpoint=https://{0}.file.core.windows.net/;TableEndpoint=https://{0}.table.core.windows.net/;SharedAccessSignature={1}" -f $AccountId.ToLower(), $SAS) $storageContext = new-AzureStorageContext -ConnectionString $conString } $cloudStorageAccount = [Microsoft.WindowsAzure.Storage.CloudStorageAccount]::Parse($storageContext.ConnectionString) $blobClient = $cloudStorageAccount.CreateCloudBlobClient() $blobcontainer = $blobClient.GetContainerReference($Container); try { $files = $blobcontainer.ListBlobs() | Sort-Object -Descending { $_.Properties.LastModified } if ($Latest) { $files | Select-Object -First 1 | Select-PSFObject -TypeName D365FO.TOOLS.Azure.Blob "name", @{Name = "Size"; Expression = {[PSFSize]$_.Properties.Length}}, "IsDeleted", @{Name = "LastModified"; Expression = {[Datetime]::Parse($_.Properties.LastModified)}} } else { foreach ($obj in $files) { if ($obj.Name -NotLike $Name) { continue } $obj | Select-PSFObject -TypeName D365FO.TOOLS.Azure.Blob "name", @{Name = "Size"; Expression = {[PSFSize]$_.Properties.Length}}, "IsDeleted", @{Name = "LastModified"; Expression = {[Datetime]::Parse($_.Properties.LastModified)}} } } } catch { Write-PSFMessage -Level Warning -Message "Something broke" -ErrorRecord $_ } } <# .SYNOPSIS Get broadcast message from the D365FO environment .DESCRIPTION Get broadcast message from the D365FO environment by looking into the database 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 ExcludeExpired Exclude all the records that has already expired .EXAMPLE PS C:\> Get-D365BroadcastMessage This will display all the broadcast message records from the SysBroadcastMessage table. .EXAMPLE PS C:\> Get-D365BroadcastMessage -ExcludeExpired This will display all active the broadcast message records from the SysBroadcastMessage table. .NOTES Tags: Broadcast, Message, SysBroadcastMessage, Servicing, Message, Users, Environment Author: M�tz Jensen (@Splaxi) #> function Get-D365BroadcastMessage { [CmdletBinding()] [OutputType()] 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, [switch] $ExcludeExpired ) $UseTrustedConnection = Test-TrustedConnection $PSBoundParameters $SqlParams = @{ DatabaseServer = $DatabaseServer; DatabaseName = $DatabaseName; SqlUser = $SqlUser; SqlPwd = $SqlPwd } $SqlCommand = Get-SqlCommand @SqlParams -TrustedConnection $UseTrustedConnection if ($ExcludeExpired) { $sqlCommand.CommandText = (Get-Content "$script:ModuleRoot\internal\sql\get-broadcastmessageactive.sql") -join [Environment]::NewLine } else { $sqlCommand.CommandText = (Get-Content "$script:ModuleRoot\internal\sql\get-broadcastmessage.sql") -join [Environment]::NewLine } try { Write-PSFMessage -Level InternalComment -Message "Executing a script against the database." -Target (Get-SqlString $SqlCommand) $sqlCommand.Connection.Open() $reader = $sqlCommand.ExecuteReader() while ($reader.Read() -eq $true) { [PSCustomObject]@{ StartTime = [System.TimeZoneInfo]::ConvertTimeFromUtc($($reader.GetDateTime($($reader.GetOrdinal("FROMDATETIME")))), [System.TimeZoneInfo]::Local) EndTime = [System.TimeZoneInfo]::ConvertTimeFromUtc($($reader.GetDateTime($($reader.GetOrdinal("TODATETIME")))), [System.TimeZoneInfo]::Local) StartTimeUtc = [System.TimeZoneInfo]::ConvertTimeFromUtc($($reader.GetDateTime($($reader.GetOrdinal("TODATETIME")))), [System.TimeZoneInfo]::Utc) EndTimeUtc = [System.TimeZoneInfo]::ConvertTimeFromUtc($($reader.GetDateTime($($reader.GetOrdinal("TODATETIME")))), [System.TimeZoneInfo]::Utc) AOSId = "$($reader.GetString($($reader.GetOrdinal("AOSID"))))" } } } 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() if ($sqlCommand.Connection.State -ne [System.Data.ConnectionState]::Closed) { $sqlCommand.Connection.Close() } $sqlCommand.Dispose() } } <# .SYNOPSIS Get broadcast message configs .DESCRIPTION Get all broadcast message configuration objects from the configuration store .PARAMETER Name The name of the broadcast message configuration you are looking for Default value is "*" to display all broadcast message configs .PARAMETER OutputAsHashtable Instruct the cmdlet to return a hastable object .EXAMPLE PS C:\> Get-D365BroadcastMessageConfig This will display all broadcast message configurations on the machine. .EXAMPLE PS C:\> Get-D365BroadcastMessageConfig -OutputAsHashtable This will display all broadcast message configurations on the machine. Every object will be output as a hashtable, for you to utilize as parameters for other cmdlets. .EXAMPLE PS C:\> Get-D365BroadcastMessageConfig -Name "UAT" This will display the broadcast message configuration that is saved with the name "UAT" on the machine. .NOTES Tags: Servicing, Message, Users, Environment, Config, Configuration, ClientId, ClientSecret Author: M�tz Jensen (@Splaxi) .LINK Add-D365BroadcastMessageConfig .LINK Clear-D365ActiveBroadcastMessageConfig .LINK Get-D365ActiveBroadcastMessageConfig .LINK Remove-D365BroadcastMessageConfig .LINK Send-D365BroadcastMessage .LINK Set-D365ActiveBroadcastMessageConfig #> function Get-D365BroadcastMessageConfig { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseOutputTypeCorrectly', '')] [CmdletBinding()] [OutputType('PSCustomObject')] param ( [string] $Name = "*", [switch] $OutputAsHashtable ) Write-PSFMessage -Level Verbose -Message "Fetch all configurations based on $Name" -Target $Name $Name = $Name.ToLower() $configurations = Get-PSFConfig -FullName "d365fo.tools.broadcast.$Name.name" foreach ($configName in $configurations.Value.ToLower()) { Write-PSFMessage -Level Verbose -Message "Working against the $configName configuration" -Target $configName $res = @{} $configName = $configName.ToLower() foreach ($config in Get-PSFConfig -FullName "d365fo.tools.broadcast.$configName.*") { $propertyName = $config.FullName.ToString().Replace("d365fo.tools.broadcast.$configName.", "") $res.$propertyName = $config.Value } if($OutputAsHashtable) { $res } else { [PSCustomObject]$res } } } <# .SYNOPSIS Get the ClickOnce configuration .DESCRIPTION Creates the needed registry keys and values for ClickOnce to work on the machine .EXAMPLE PS C:\> Get-D365ClickOnceTrustPrompt This will get the current ClickOnce configuration .NOTES Tags: ClickOnce, Registry, TrustPrompt Author: M�tz Jensen (@Splaxi) #> function Get-D365ClickOnceTrustPrompt { [CmdletBinding()] param ( ) begin { } process { Write-PSFMessage -Level Verbose -Message "Testing if the registry key exists or not" if ((Test-Path -Path "HKLM:\SOFTWARE\MICROSOFT\.NETFramework\Security\TrustManager\PromptingLevel") -eq $false) { Write-PSFMessage -Level Host -Message "It looks like ClickOnce trust prompt has never been configured on this machine. Run Set-D365ClickOnceTrustPrompt to fix that" } else { Write-PSFMessage -Level Verbose -Message "Gathering the details from registry" [PSCustomObject]@{ UntrustedSites = (Get-ItemProperty -Path "HKLM:\SOFTWARE\MICROSOFT\.NETFramework\Security\TrustManager\PromptingLevel" "UntrustedSites").UntrustedSites Internet = (Get-ItemProperty -Path "HKLM:\SOFTWARE\MICROSOFT\.NETFramework\Security\TrustManager\PromptingLevel" "Internet").Internet MyComputer = (Get-ItemProperty -Path "HKLM:\SOFTWARE\MICROSOFT\.NETFramework\Security\TrustManager\PromptingLevel" "MyComputer").MyComputer LocalIntranet = (Get-ItemProperty -Path "HKLM:\SOFTWARE\MICROSOFT\.NETFramework\Security\TrustManager\PromptingLevel" "LocalIntranet").LocalIntranet TrustedSites = (Get-ItemProperty -Path "HKLM:\SOFTWARE\MICROSOFT\.NETFramework\Security\TrustManager\PromptingLevel" "TrustedSites").TrustedSites } } } end { } } <# .SYNOPSIS Get databases from the server .DESCRIPTION Get the names of databases on either SQL Server or in Azure SQL Database instance .PARAMETER Name Name of the database that you are looking for Default value is "*" which will show all databases .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-D365Database This will show all databases on the default SQL Server / Azure SQL Database instance. .EXAMPLE PS C:\> Get-D365Database -Name AXDB_ORIGINAL This will show if the AXDB_ORIGINAL database exists on the default SQL Server / Azure SQL Database instance. .NOTES Tags: Database, DB, Servicing Author: M�tz Jensen (@Splaxi) #> function Get-D365Database { [CmdletBinding(DefaultParameterSetName = 'Default')] [OutputType('[PsCustomObject]')] param ( [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 1 )] [string[]] $Name = "*", [Parameter(Mandatory = $false, Position = 2 )] [string] $DatabaseServer = $Script:DatabaseServer, [Parameter(Mandatory = $false, Position = 3 )] [string] $DatabaseName = $Script:DatabaseName, [Parameter(Mandatory = $false, Position = 4 )] [string] $SqlUser = $Script:DatabaseUserName, [Parameter(Mandatory = $false, Position = 5 )] [string] $SqlPwd = $Script:DatabaseUserPassword ) $UseTrustedConnection = Test-TrustedConnection $PSBoundParameters $SqlParams = @{ DatabaseServer = $DatabaseServer; DatabaseName = "master"; SqlUser = $SqlUser; SqlPwd = $SqlPwd } $sqlCommand = Get-SqlCommand @SqlParams -TrustedConnection $UseTrustedConnection $sqlCommand.CommandText = (Get-Content "$script:ModuleRoot\internal\sql\get-database.sql") -join [Environment]::NewLine try { $sqlCommand.Connection.Open() $reader = $sqlCommand.ExecuteReader() while ($reader.Read() -eq $true) { $res = [PSCustomObject]@{ Name = "$($reader.GetString($($reader.GetOrdinal("NAME"))))" } if ($res.Name -NotLike $Name) { continue } $res } } 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() if ($sqlCommand.Connection.State -ne [System.Data.ConnectionState]::Closed) { $sqlCommand.Connection.Close() } $sqlCommand.Dispose() } } <# .SYNOPSIS Shows the Database Access information for the D365 Environment .DESCRIPTION Gets all database information from the D365 environment .EXAMPLE PS C:\> Get-D365DatabaseAccess This will get all relevant details, including connection details, for the database configured for the environment .NOTES Tags: Database, Connection, Sql, SqlUser, SqlPwd Author: Rasmus Andersen (@ITRasmus) The cmdlet wraps the call against a dll file that is shipped with Dynamics 365 for Finance & Operations. The call to the dll file gets all relevant connections details for the database server. #> function Get-D365DatabaseAccess { [CmdletBinding()] param () $environment = Get-ApplicationEnvironment return $environment.DataAccess } <# .SYNOPSIS Decrypts the AOS config file .DESCRIPTION Function used for decrypting the config file used by the D365 Finance & Operations AOS service .PARAMETER DropPath Place where the decrypted files should be placed .PARAMETER AosServiceWebRootPath Location of the D365 webroot folder .EXAMPLE PS C:\> Get-D365DecryptedConfigFile -DropPath "c:\temp\d365fo.tools" This will get the config file from the instance, decrypt it and save it to "c:\temp\d365fo.tools" .NOTES Tags: Configuration, Service Account, Sql, SqlUser, SqlPwd, WebConfig, Web.Config, Decryption Author : Rasmus Andersen (@ITRasmus) Author : M�tz Jensen (@splaxi) Used for getting the Password for the database and other service accounts used in environment #> function Get-D365DecryptedConfigFile { param( [Parameter(Mandatory = $false, Position = 1)] [Alias('ExtractFolder')] [string]$DropPath = "C:\temp\d365fo.tools\ConfigFile_Decrypted", [Parameter(Mandatory = $false, Position = 2)] [string]$AosServiceWebRootPath = $Script:AOSPath ) $WebConfigFile = Join-Path $AosServiceWebRootPath $Script:WebConfig if (!(Test-PathExists -Path $WebConfigFile -Type Leaf)) {return} if (!(Test-PathExists -Path $DropPath -Type Container -Create)) {return} Write-PSFMessage -Level Verbose -Message "Starting the decryption logic" New-DecryptedFile $WebConfigFile $DropPath } <# .SYNOPSIS Get a .NET class from the Dynamics 365 for Finance and Operations installation .DESCRIPTION Get a .NET class from an assembly file (dll) from the package directory .PARAMETER Name Name of the .NET class that you are looking for Accepts wildcards for searching. E.g. -Name "ER*Excel*" Default value is "*" which will search for all classes .PARAMETER Assembly Name of the assembly file that you want to search for the .NET class Accepts wildcards for searching. E.g. -Name "*AX*Framework*.dll" Default value is "*.dll" which will search for assembly files .PARAMETER PackageDirectory Path to the directory containing the installed packages Normally it is located under the AOSService directory in "PackagesLocalDirectory" Default value is fetched from the current configuration on the machine .EXAMPLE PS C:\> Get-D365DotNetClass -Name "ERText*" Will search across all assembly files (*.dll) that are located in the default package directory after any class that fits the search "ERText*" .EXAMPLE PS C:\> Get-D365DotNetClass -Name "ERText*" -Assembly "*LocalizationFrameworkForAx.dll*" Will search across all assembly files (*.dll) that are fits the search "*LocalizationFrameworkForAx.dll*", that are located in the default package directory, after any class that fits the search "ERText*" .EXAMPLE PS C:\> Get-D365DotNetClass -Name "ERText*" | Export-Csv -Path c:\temp\results.txt -Delimiter ";" Will search across all assembly files (*.dll) that are located in the default package directory after any class that fits the search "ERText*" The output is saved to a file to make it easier to search inside the result set .NOTES Tags: .Net, DotNet, Class, Development Author: M�tz Jensen (@Splaxi) The cmdlet supports piping and can be used in advanced scenarios. See more on github and the wiki pages. #> function Get-D365DotNetClass { [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 1 )] [string] $Name = "*", [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 2 )] [string] $Assembly = "*.dll", [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 3 )] [string] $PackageDirectory = $Script:PackageDirectory ) begin { } process { Invoke-TimeSignal -Start $files = (Get-ChildItem -Path $PackageDirectory -Filter $Assembly -Recurse -Exclude "*Resources*" | Where-Object Fullname -Notlike "*Resources*" ) $files | ForEach-Object { $path = $_.Fullname try { Write-PSFMessage -Level Verbose -Message "Loading the dll file: $path" -Target $path [Reflection.Assembly]$ass = [Reflection.Assembly]::LoadFile($path) $res = $ass.GetTypes() Write-PSFMessage -Level Verbose -Message "Looping through all types from the assembly" foreach ($obj in $res) { if ($obj.Name -NotLike $Name) { continue } [PSCustomObject]@{ IsPublic = $obj.IsPublic IsSerial = $obj.IsSerial Name = $obj.Name BaseType = $obj.BaseType File = $path } } } catch { Write-PSFMessage -Level Host -Message "Something went wrong while trying to load the path: $path" -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" return } } Invoke-TimeSignal -End } end { } } <# .SYNOPSIS Get a .NET method from the Dynamics 365 for Finance and Operations installation .DESCRIPTION Get a .NET method from an assembly file (dll) from the package directory .PARAMETER Assembly Name of the assembly file that you want to search for the .NET method Provide the full path for the assembly file you want to work against .PARAMETER Name Name of the .NET method that you are looking for Accepts wildcards for searching. E.g. -Name "parmER*Excel*" Default value is "*" which will search for all methods .PARAMETER TypeName Name of the .NET class that you want to work against Accepts wildcards for searching. E.g. -Name "*ER*Excel*" Default value is "*" which will work against all classes .EXAMPLE PS C:\> Get-D365DotNetMethod -Assembly "C:\AOSService\PackagesLocalDirectory\ElectronicReporting\bin\Microsoft.Dynamics365.LocalizationFrameworkForAx.dll" Will get all methods, across all classes, from the assembly file .EXAMPLE PS C:\> Get-D365DotNetMethod -Assembly "C:\AOSService\PackagesLocalDirectory\ElectronicReporting\bin\Microsoft.Dynamics365.LocalizationFrameworkForAx.dll" -TypeName "ERTextFormatExcelFileComponent" Will get all methods, from the "ERTextFormatExcelFileComponent" class, from the assembly file .EXAMPLE PS C:\> Get-D365DotNetMethod -Assembly "C:\AOSService\PackagesLocalDirectory\ElectronicReporting\bin\Microsoft.Dynamics365.LocalizationFrameworkForAx.dll" -TypeName "ERTextFormatExcelFileComponent" -Name "*parm*" Will get all methods that fits the search "*parm*", from the "ERTextFormatExcelFileComponent" class, from the assembly file .EXAMPLE PS C:\> Get-D365DotNetClass -Name "ERTextFormatExcelFileComponent" -Assembly "*LocalizationFrameworkForAx.dll*" | Get-D365DotNetMethod Will get all methods, from the "ERTextFormatExcelFileComponent" class, from any assembly file that fits the search "*LocalizationFrameworkForAx.dll*" .NOTES Tags: .Net, DotNet, Class, Method, Methods, Development Author: M�tz Jensen (@Splaxi) The cmdlet supports piping and can be used in advanced scenarios. See more on github and the wiki pages. #> function Get-D365DotNetMethod { [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory = $true, ParameterSetName = 'Default', ValueFromPipelineByPropertyName = $true, Position = 1 )] [Alias('File')] [string] $Assembly, [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 2 )] [Alias('MethodName')] [string] $Name = "*", [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 3 )] [Alias('ClassName')] [string] $TypeName = "*" ) begin { } process { Invoke-TimeSignal -Start try { Write-PSFMessage -Level Verbose -Message "Loading the file" -Target $Assembly [Reflection.Assembly]$ass = [Reflection.Assembly]::LoadFile($Assembly) $types = $ass.GetTypes() foreach ($obj in $types) { Write-PSFMessage -Level Verbose -Message "Type name loaded" -Target $obj.Name if ($obj.Name -NotLike $TypeName) {continue} $members = $obj.GetMethods() foreach ($objI in $members) { if ($objI.Name -NotLike $Name) { continue } [PSCustomObject]@{ TypeName = $obj.Name TypeIsPublic = $obj.IsPublic MethodName = $objI.Name } } } } catch { Write-PSFMessage -Level Warning -Message "Something went wrong while working on: $Assembly" -ErrorRecord $_ } Invoke-TimeSignal -End } end { } } <# .SYNOPSIS Cmdlet to get the current status for the different services in a Dynamics 365 Finance & Operations environment .DESCRIPTION List status for all relevant services that is running in a D365FO environment .PARAMETER ComputerName An array of computers that you want to query for the services status on. .PARAMETER All Set when you want to query all relevant services Includes: Aos Batch Financial Reporter DMF .PARAMETER Aos Switch to instruct the cmdlet to query the AOS (IIS) service .PARAMETER Batch Switch to instruct the cmdlet query the batch service .PARAMETER FinancialReporter Switch to instruct the cmdlet query the financial reporter (Management Reporter 2012) .PARAMETER DMF Switch to instruct the cmdlet query the DMF service .EXAMPLE PS C:\> Get-D365Environment -All Will query all D365FO service on the machine .EXAMPLE PS C:\> Get-D365Environment -ComputerName "TEST-SB-AOS1","TEST-SB-AOS2","TEST-SB-BI1" -All Will query all D365FO service on the different machines .EXAMPLE PS C:\> Get-D365Environment -Aos -Batch Will query the Aos & Batch services on the machine .NOTES Tags: Environment, Service, Services, Aos, Batch, Servicing Author: M�tz Jensen (@Splaxi) #> function Get-D365Environment { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidDefaultValueSwitchParameter", "")] [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 1 )] [Parameter(Mandatory = $false, ParameterSetName = 'Specific', Position = 1 )] [string[]] $ComputerName = @($env:computername), [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 } if ( (-not ($All)) -and (-not ($Aos)) -and (-not ($Batch)) -and (-not ($FinancialReporter)) -and (-not ($DMF))) { Write-PSFMessage -Level Host -Message "You have to use at least one switch when running this cmdlet. Please run the cmdlet again." Stop-PSFFunction -Message "Stopping because of missing parameters" return } $Params = Get-DeepClone $PSBoundParameters if($Params.ContainsKey("ComputerName")){$null = $Params.Remove("ComputerName")} $Services = Get-ServiceList @Params $Results = foreach ($server in $ComputerName) { Get-Service -ComputerName $server -Name $Services -ErrorAction SilentlyContinue| Select-Object @{Name = "Server"; Expression = {$Server}}, Name, Status, DisplayName } $Results | Select-PSFObject -TypeName "D365FO.TOOLS.Environment.Service" Server, DisplayName, Status, Name } <# .SYNOPSIS Get environment configs .DESCRIPTION Get all environment configuration objects from the configuration store .PARAMETER Name The name of the environment you are looking for Default value is "*" to display all environment configs .EXAMPLE PS C:\> Get-D365EnvironmentConfig This will show all environment configs .NOTES Tags: Environment, Url, Config, Configuration, Tfs, Vsts, Sql, SqlUser, SqlPwd Author: M�tz Jensen (@Splaxi) #> function Get-D365EnvironmentConfig { [CmdletBinding()] param ( [string] $Name = "*" ) $Environments = [hashtable](Get-PSFConfigValue -FullName "d365fo.tools.environments") foreach ($item in $Environments.Keys) { if ($item -NotLike $Name) { continue } $temp = [ordered]@{Name = $item} $temp += $Environments[$item] [PSCustomObject]$temp } } <# .SYNOPSIS Get the D365FO environment settings .DESCRIPTION Gets all settings the Dynamics 365 for Finance & Operations environment uses. .EXAMPLE PS C:\> Get-D365EnvironmentSettings This will get all details available for the environment .EXAMPLE PS C:\> Get-D365EnvironmentSettings | Format-Custom -Property * This will get all details available for the environment and format it to show all details in a long custom object. .NOTES Tags: Environment, Configuration, WebConfig, Web.Config, Decryption Author: Rasmus Andersen (@ITRasmus) Author: M�tz Jensen (@Splaxi) The cmdlet wraps the call against a dll file that is shipped with Dynamics 365 for Finance & Operations. The call to the dll file gets all relevant details for the installation. #> function Get-D365EnvironmentSettings { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [CmdletBinding()] param () Get-ApplicationEnvironment } <# .SYNOPSIS Returns Exposed services .DESCRIPTION Function for getting which services there are exposed from D365 .PARAMETER ClientId Client Id from the AppRegistration .PARAMETER ClientSecret Client Secret from the AppRegistration .PARAMETER D365FO Url fro the D365 including Https:// .PARAMETER Authority The Authority to issue the token .EXAMPLE PS C:\> Get-D365ExposedService -ClientId "MyClientId" -ClientSecret "MyClientSecret" This will show a list of all the services that the D365FO instance is exposing. .NOTES Tags: DMF, OData, RestApi, Data Management Framework Author: Rasmus Andersen (@ITRasmus) Idea taken from http://www.ksaelen.be/wordpresses/dynamicsaxblog/2016/01/dynamics-ax-7-tip-what-services-are-exposed/ #> function Get-D365ExposedService { [CmdletBinding()] [OutputType([System.String])] param ( [Parameter(Mandatory = $true, Position = 1 )] [string] $ClientId, [Parameter(Mandatory = $true, Position = 2 )] [string] $ClientSecret, [Parameter(Mandatory = $false, Position = 3 )] [string] $D365FO, [Parameter(Mandatory = $false, Position = 4 )] [string] $Authority ) if($D365FO -eq "") { $D365FO = $(Get-D365Url).Url } if($Authority -eq "") { $Authority = Get-InstanceIdentityProvider } Write-PSFMessage -Level Verbose -Message "Importing type 'Microsoft.IdentityModel.Clients.ActiveDirectory.dll'" $null = add-type -path "$script:ModuleRoot\internal\dll\Microsoft.IdentityModel.Clients.ActiveDirectory.dll" -ErrorAction Stop $url = $D365FO + "/api/services" Write-PSFMessage -Level Verbose -Message "D365FO : $D365FO" Write-PSFMessage -Level Verbose -Message "Url : $url" Write-PSFMessage -Level Verbose -MEssage "Authority : $Authority" $authHeader = New-AuthorizationHeader $Authority $ClientId $ClientSecret $D365FO [System.Net.WebRequest] $webRequest = New-WebRequest $url $authHeader "GET" $response = $webRequest.GetResponse() if ($response.StatusCode -eq [System.Net.HttpStatusCode]::Ok) { $stream = $response.GetResponseStream() $streamReader = New-Object System.IO.StreamReader($stream); $exposedServices = $streamReader.ReadToEnd() $streamReader.Close(); } else { $statusDescription = $response.StatusDescription throw "Https status code : $statusDescription" } $exposedServices } <# .SYNOPSIS Get installed hotfix .DESCRIPTION Get all relevant details for installed hotfix .PARAMETER BinDir The path to the bin directory for the environment Default path is the same as the AOS Service PackagesLocalDirectory\bin .PARAMETER PackageDirectory Path to the PackagesLocalDirectory Default path is the same as the AOS Service PackagesLocalDirectory .PARAMETER Model Name of the model that you want to work against Accepts wildcards for searching. E.g. -Model "*Retail*" Default value is "*" which will search for all models .PARAMETER Name Name of the hotfix that you are looking for Accepts wildcards for searching. E.g. -Name "7045*" Default value is "*" which will search for all hotfixes .PARAMETER KB KB number of the hotfix that you are looking for Accepts wildcards for searching. E.g. -KB "4045*" Default value is "*" which will search for all KB's .EXAMPLE PS C:\> Get-D365InstalledHotfix This will display all installed hotfixes found on this machine .EXAMPLE PS C:\> Get-D365InstalledHotfix -Model "*retail*" This will display all installed hotfixes found for all models that matches the search for "*retail*" found on this machine .EXAMPLE PS C:\> Get-D365InstalledHotfix -Model "*retail*" -KB "*43*" This will display all installed hotfixes found for all models that matches the search for "*retail*" and only with KB's that matches the search for "*43*" found on this machine .NOTES Tags: Hotfix, Servicing, Model, Models, KB, Patch, Patching, PackagesLocalDirectory Author: M�tz Jensen (@Splaxi) This cmdlet is inspired by the work of "Ievgen Miroshnikov" (twitter: @IevgenMir) All credits goes to him for showing how to extract these information His blog can be found here: https://ievgensaxblog.wordpress.com The specific blog post that we based this cmdlet on can be found here: https://ievgensaxblog.wordpress.com/2017/11/17/d365foe-get-list-of-installed-metadata-hotfixes-using-metadata-api/ #> function Get-D365InstalledHotfix { [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 1 )] [string] $BinDir = "$Script:BinDir\bin", [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 2 )] [string] $PackageDirectory = $Script:PackageDirectory, [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 3 )] [string] $Model = "*", [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 4 )] [string] $Name = "*", [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 5 )] [string] $KB = "*" ) begin { } process { $files = @((Join-Path -Path $BinDir -ChildPath "Microsoft.Dynamics.AX.Metadata.Storage.dll"), (Join-Path -Path $BinDir -ChildPath "Microsoft.Dynamics.ApplicationPlatform.XppServices.Instrumentation.dll")) if(-not (Test-PathExists -Path $files -Type Leaf)) { return } Add-Type -Path $files Write-PSFMessage -Level Verbose -Message "Testing if the cmdlet is running on a OneBox or not." -Target $Script:IsOnebox if ($Script:IsOnebox) { Write-PSFMessage -Level Verbose -Message "Machine is onebox. Will continue with DiskProvider." $diskProviderConfiguration = New-Object Microsoft.Dynamics.AX.Metadata.Storage.DiskProvider.DiskProviderConfiguration $diskProviderConfiguration.AddMetadataPath($PackageDirectory) $metadataProviderFactory = New-Object Microsoft.Dynamics.AX.Metadata.Storage.MetadataProviderFactory $metadataProvider = $metadataProviderFactory.CreateDiskProvider($diskProviderConfiguration) Write-PSFMessage -Level Verbose -Message "MetadataProvider initialized." -Target $metadataProvider } else { Write-PSFMessage -Level Verbose -Message "Machine is NOT onebox. Will continue with RuntimeProvider." $runtimeProviderConfiguration = New-Object Microsoft.Dynamics.AX.Metadata.Storage.Runtime.RuntimeProviderConfiguration -ArgumentList $Script:PackageDirectory $metadataProviderFactory = New-Object Microsoft.Dynamics.AX.Metadata.Storage.MetadataProviderFactory $metadataProvider = $metadataProviderFactory.CreateRuntimeProvider($runtimeProviderConfiguration) Write-PSFMessage -Level Verbose -Message "MetadataProvider initialized." -Target $metadataProvider } Write-PSFMessage -Level Verbose -Message "Initializing the UpdateProvider from the MetadataProvider." $updateProvider = $metadataProvider.Updates Write-PSFMessage -Level Verbose -Message "Looping through all modules from the MetadataProvider." foreach ($obj in $metadataProvider.ModelManifest.ListModules()) { Write-PSFMessage -Level Verbose -Message "Filtering out all modules that doesn't match the model search." -Target $obj if ($obj.Name -NotLike $Model) {continue} Write-PSFMessage -Level Verbose -Message "Looping through all hotfixes for the module from the UpdateProvider." -Target $obj foreach ($objUpdate in $updateProvider.ListObjects($obj.Name)) { Write-PSFMessage -Level Verbose -Message "Reading all details for the hotfix through UpdateProvider." -Target $objUpdate $axUpdateObject = $updateProvider.Read($objUpdate) Write-PSFMessage -Level Verbose -Message "Filtering out all hotfixes that doesn't match the name search." -Target $axUpdateObject if ($axUpdateObject.Name -NotLike $Name) {continue} Write-PSFMessage -Level Verbose -Message "Filtering out all hotfixes that doesn't match the KB search." -Target $axUpdateObject if ($axUpdateObject.KBNumbers -NotLike $KB) {continue} [PSCustomObject]@{ Model = $obj.Name Hotfix = $axUpdateObject.Name Applied = $axUpdateObject.AppliedDateTime KBs = $axUpdateObject.KBNumbers } } } } end { } } <# .SYNOPSIS Get installed package from Dynamics 365 Finance & Operations environment .DESCRIPTION Get installed package from the machine running the AOS service for Dynamics 365 Finance & Operations .PARAMETER Name Name of the package that you are looking for Accepts wildcards for searching. E.g. -Name "Application*Adaptor" Default value is "*" which will search for all packages .PARAMETER PackageDirectory Path to the directory containing the installed packages Normally it is located under the AOSService directory in "PackagesLocalDirectory" Default value is fetched from the current configuration on the machine .EXAMPLE PS C:\> Get-D365InstalledPackage Shows the entire list of installed packages located in the default location on the machine .EXAMPLE PS C:\> Get-D365InstalledPackage -Name "Application*Adaptor" Shows the list of installed packages where the name fits the search "Application*Adaptor" A result set example: ApplicationFoundationFormAdaptor ApplicationPlatformFormAdaptor ApplicationSuiteFormAdaptor ApplicationWorkspacesFormAdaptor .EXAMPLE PS C:\> Get-D365InstalledPackage -PackageDirectory "J:\AOSService\PackagesLocalDirectory" Shows the entire list of installed packages located in "J:\AOSService\PackagesLocalDirectory" on the machine .NOTES Tags: PackagesLocalDirectory, Servicing, Model, Models, Package, Packages Author: M�tz Jensen (@Splaxi) The cmdlet supports piping and can be used in advanced scenarios. See more on github and the wiki pages. #> function Get-D365InstalledPackageOld { [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 1 )] [string] $Name = "*", [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 2 )] [string] $PackageDirectory = $Script:PackageDirectory ) Write-PSFMessage -Level Verbose -Message "Package directory is: $PackageDirectory" -Target $PackageDirectory Write-PSFMessage -Level Verbose -Message "Name is: $Name" -Target $Name $Packages = Get-ChildItem -Path $PackageDirectory -Directory -Exclude bin foreach ($obj in $Packages) { if ($obj.Name -NotLike $Name) { continue } [PSCustomObject]@{ PackageName = $obj.Name PackageDirectory = $obj.FullName } } } <# .SYNOPSIS Get installed D365 services .DESCRIPTION Get installed Dynamics 365 for Finance & Operations services that are installed on the machine .PARAMETER Path Path to the folder that contains the "InstallationRecords" folder .EXAMPLE PS C:\> Get-D365InstalledService This will get all installed services on the machine. .NOTES Tags: Services, Servicing, Topology Author: M�tz Jensen (@Splaxi) #> function Get-D365InstalledService { [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 1 )] [string] $Path = $Script:InstallationRecordsDir ) begin { } process { $servicePath = Join-Path $Path "ServiceModelInstallationRecords" Write-PSFMessage -Level Verbose -Message "Service installation log path is: $servicePath" -Target $servicePath $ServiceFiles = Get-ChildItem -Path $servicePath -Filter "*_current.xml" -Recurse foreach ($obj in $ServiceFiles) { [PSCustomObject]@{ ServiceName = ($obj.Name.Split("_")[0]) Version = (Select-Xml -XPath "/ServiceModelInstallationInfo/Version" -Path $obj.fullname).Node."#Text" } } } end { } } <# .SYNOPSIS Gets the instance name .DESCRIPTION Get the instance name that is registered in the environment .EXAMPLE PS C:\> Get-D365InstanceName This will get the service name that the environment has configured .NOTES Tags: Instance, Servicing Author: Rasmus Andersen (@ITRasmus) The cmdlet wraps the call against a dll file that is shipped with Dynamics 365 for Finance & Operations. The call to the dll file gets HostedServiceName that is registered in the environment. #> function Get-D365InstanceName { [CmdletBinding()] param () [PSCustomObject]@{ InstanceName = "$($(Get-D365EnvironmentSettings).Infrastructure.HostedServiceName)" } } <# .SYNOPSIS Get label from the label file from Dynamics 365 Finance & Operations environment .DESCRIPTION Get label from the label file from the running the Dynamics 365 Finance & Operations instance .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 LabelFileId Name / Id of the label "file" that you want to work against .PARAMETER Language Name / string representation of the language / culture you want to work against Default value is "en-US" .PARAMETER Name Name of the label that you are looking for Accepts wildcards for searching. E.g. -Name "@PRO59*" Default value is "*" which will search for all labels .EXAMPLE PS C:\> Get-D365Label -LabelFileId PRO Shows the entire list of labels that are available from the PRO label file. The language is defaulted to "en-US". .EXAMPLE PS C:\> Get-D365Label -LabelFileId PRO -Language da Shows the entire list of labels that are available from the PRO label file. Shows only all "da" (Danish) labels. .EXAMPLE PS C:\> Get-D365Label -LabelFileId PRO -Name "@PRO59*" Shows the labels available from the PRO label file where the name fits the search "@PRO59*" A result set example: Name Value Language ---- ----- -------- @PRO59 Indicates if the type of the rebate value. en-US @PRO594 Pack consumption en-US @PRO595 Pack qty now being released to production in the BOM unit. en-US @PRO596 Pack unit. en-US @PRO597 Pack proposal for release in the packing unit. en-US @PRO590 Constant pack qty en-US @PRO593 Pack proposal release in BOM unit. en-US @PRO598 Pack quantity now being released for the production in the packing unit. en-US .EXAMPLE PS C:\> Get-D365Label -LabelFileId PRO -Name "@PRO59*" -Language da,en-us Shows the labels available from the PRO label file where the name fits the search "@PRO59*". Shows for both "da" (Danish) and en-US (English) .NOTES Tags: PackagesLocalDirectory, Servicing, Language, Labels, Label Author: M�tz Jensen (@Splaxi) This cmdlet is inspired by the work of "Pedro Tornich" (twitter: @ptornich) All credits goes to him for showing how to extract these information His github repository can be found here: https://github.com/ptornich/LabelFileGenerator #> function Get-D365Label { [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 1 )] [string] $BinDir = "$Script:BinDir\bin", [Parameter(Mandatory = $true, ParameterSetName = 'Default', ValueFromPipelineByPropertyName = $true, Position = 2 )] [string] $LabelFileId, [Parameter(Mandatory = $false, ParameterSetName = 'Default', ValueFromPipelineByPropertyName = $true, Position = 3 )] [string[]] $Language = "en-US", [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 4 )] [string] $Name = "*" ) begin { } process { $files = @((Join-Path -Path $BinDir -ChildPath "Microsoft.Dynamics.AX.Xpp.AxShared.dll")) if (-not (Test-PathExists -Path $files -Type Leaf)) { return } Add-Type -Path $files foreach ($item in $Language) { $culture = New-Object System.Globalization.CultureInfo -ArgumentList $item Write-PSFMessage -Level Verbose -Message "Searching for label" -Target $culture $labels = [Microsoft.Dynamics.Ax.Xpp.LabelHelper]::GetAllLabels($LabelFileId, $culture) foreach ($itemLabel in $labels) { foreach ($key in $itemLabel.Keys) { if ($key -notlike $Name) { continue } [PSCustomObject]@{ Name = $Key Value = $itemLabel[$key] Language = $item PSTypeName = 'D365FO.TOOLS.Label' } } } } } end { } } <# .SYNOPSIS Get label file (ids) for packages / modules from Dynamics 365 Finance & Operations environment .DESCRIPTION Get label file (ids) for packages / modules from the machine running the AOS service for Dynamics 365 Finance & Operations .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 PackageDirectory Path to the directory containing the installed package / module Normally it is located under the AOSService directory in "PackagesLocalDirectory" Default value is fetched from the current configuration on the machine .PARAMETER Module Name of the module that you want to work against Default value is "*" which will search for all modules .PARAMETER Name Name of the label file (id) that you are looking for Accepts wildcards for searching. E.g. -Name "Acc*Receivable*" Default value is "*" which will search for all label file (ids) .EXAMPLE PS C:\> Get-D365LabelFile Shows the entire list of label file (ids) for all installed packages / modules located in the default location on the machine .EXAMPLE PS C:\> Get-D365LabelFile -Name "Acc*Receivable*" Shows the list of label file (ids) for all installed packages / modules where the label file (ids) name fits the search "Acc*Receivable*" A result set example: LabelFileId Languages Module ----------- --------- ------ AccountsReceivable {ar-AE, ar, cs, da...} ApplicationSuite AccountsReceivable_SalesTaxCodesSA {en-US} ApplicationSuite .EXAMPLE PS C:\> Get-D365LabelFile -PackageDirectory "J:\AOSService\PackagesLocalDirectory" Shows the list of label file (ids) for all installed packages / modules located in "J:\AOSService\PackagesLocalDirectory" on the machine .NOTES Tags: PackagesLocalDirectory, Servicing, Language, Labels, Label Author: M�tz Jensen (@Splaxi) This cmdlet is inspired by the work of "Pedro Tornich" (twitter: @ptornich) All credits goes to him for showing how to extract these information His github repository can be found here: https://github.com/ptornich/LabelFileGenerator #> function Get-D365LabelFile { [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 1 )] [string] $BinDir = "$Script:BinDir\bin", [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 2 )] [string] $PackageDirectory = $Script:PackageDirectory, [Parameter(Mandatory = $false, ParameterSetName = 'Default', ValueFromPipelineByPropertyName = $true, Position = 3 )] [Alias("ModuleName")] [string] $Module = "*", [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 4 )] [string] $Name = "*" ) begin { } process { $files = @((Join-Path -Path $BinDir -ChildPath "Microsoft.Dynamics.AX.Metadata.Storage.dll"), (Join-Path -Path $BinDir -ChildPath "Microsoft.Dynamics.ApplicationPlatform.XppServices.Instrumentation.dll")) if(-not (Test-PathExists -Path $files -Type Leaf)) { return } Add-Type -Path $files Write-PSFMessage -Level Verbose -Message "Testing if the cmdlet is running on a OneBox or not." -Target $Script:IsOnebox if ($Script:IsOnebox) { Write-PSFMessage -Level Verbose -Message "Machine is onebox. Will continue with DiskProvider." $diskProviderConfiguration = New-Object Microsoft.Dynamics.AX.Metadata.Storage.DiskProvider.DiskProviderConfiguration $diskProviderConfiguration.AddMetadataPath($PackageDirectory) $metadataProviderFactory = New-Object Microsoft.Dynamics.AX.Metadata.Storage.MetadataProviderFactory $metadataProvider = $metadataProviderFactory.CreateDiskProvider($diskProviderConfiguration) Write-PSFMessage -Level Verbose -Message "MetadataProvider initialized." -Target $metadataProvider } else { Write-PSFMessage -Level Verbose -Message "Machine is NOT onebox. Will continue with RuntimeProvider." $runtimeProviderConfiguration = New-Object Microsoft.Dynamics.AX.Metadata.Storage.Runtime.RuntimeProviderConfiguration -ArgumentList $Script:PackageDirectory $metadataProviderFactory = New-Object Microsoft.Dynamics.AX.Metadata.Storage.MetadataProviderFactory $metadataProvider = $metadataProviderFactory.CreateRuntimeProvider($runtimeProviderConfiguration) Write-PSFMessage -Level Verbose -Message "MetadataProvider initialized." -Target $metadataProvider } Write-PSFMessage -Level Verbose -Message "Initializing the LabelProvider from the MetadataProvider." $labelProvider = $metadataProvider.LabelFiles $res = New-Object 'System.Collections.Generic.Dictionary[string, System.Collections.ArrayList]' Write-PSFMessage -Level Verbose -Message "Looping through all modules from the MetadataProvider." foreach ($obj in $metadataProvider.ModelManifest.ListModules()) { Write-PSFMessage -Level Verbose -Message "Filtering out all modules that doesn't match the model search." -Target $obj if ($obj.Name -NotLike $Module) {continue} Write-PSFMessage -Level Verbose -Message "$($obj.Name)" $labelFiles = $labelProvider.ListObjects($obj.Name) foreach ($objLabelFile in $labelFiles) { Write-PSFMessage -Level Verbose -Message "$($objLabelFile)" if($objLabelFile -like "*.*") { $chars = $objLabelFile.ToCharArray() $chars[$objLabelFile.LastIndexOf(".")] = "_" $objLabelFile = $chars -join "" } $labelId = $objLabelFile.Substring(0, $objLabelFile.LastIndexOf("_")) $langString = $objLabelFile.Substring($objLabelFile.LastIndexOf("_") + 1) if ($labelId -NotLike $Name) {continue} if(-not ($res.ContainsKey($labelId))) { $null = $res.Add($labelId, (New-object -TypeName "System.Collections.ArrayList")) } $null = $res[$labelId].Add($langString) } foreach ($item in $res.Keys) { [PSCustomObject]@{ LabelFileId = $item Languages = $res[$item] Module = $obj.Name } } } } end { } } <# .SYNOPSIS Get label from the resource file .DESCRIPTION Get label details from the resource file .PARAMETER FilePath The path to resource file that you want to get label details from .PARAMETER Name Name of the label you are looking for Accepts wildcards for searching. E.g. -Name "@PRO*" Default value is "*" which will search for all labels in the resource file .PARAMETER Value Value of the label you are looking for Accepts wildcards for searching. E.g. -Name "*Qty*" Default value is "*" which will search for all values in the resource file .PARAMETER IncludePath Switch to indicate whether you want the result set to include the path to the resource file or not Default is OFF - path details will not be part of the output .EXAMPLE PS C:\> Get-D365Label -Path "C:\AOSService\PackagesLocalDirectory\ApplicationSuite\Resources\en-US\PRO.resources.dll" Will get all labels from the "PRO.resouce.dll" file The language is determined by the path to the resource file and nothing else .EXAMPLE PS C:\> Get-D365Label -Path "C:\AOSService\PackagesLocalDirectory\ApplicationSuite\Resources\en-US\PRO.resources.dll" -Name "@PRO505" Will get the label with the name "@PRO505" from the "PRO.resouce.dll" file The language is determined by the path to the resource file and nothing else .EXAMPLE PS C:\> Get-D365Label -Path "C:\AOSService\PackagesLocalDirectory\ApplicationSuite\Resources\en-US\PRO.resources.dll" -Value "*qty*" Will get all the labels where the value fits the search "*qty*" from the "PRO.resouce.dll" file The language is determined by the path to the resource file and nothing else .EXAMPLE PS C:\> Get-D365InstalledPackage -Name "ApplicationSuite" | Get-D365PackageLabelFile -Language "da" | Get-D365Label -value "*batch*" -IncludePath Will get all the labels, across all label files, for the "ApplicationSuite", where the language is "da" and where the label value fits the search "*batch*". The path to the label file is included in the output. .NOTES Tags: PackagesLocalDirectory, Label, Labels, Language, Development, Servicing Author: M�tz Jensen (@Splaxi) There are several advanced scenarios for this cmdlet. See more on github and the wiki pages. #> function Get-D365LabelOld { [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory = $true, ParameterSetName = 'Default', ValueFromPipelineByPropertyName = $true, Position = 1 )] [Parameter(Mandatory = $true, ParameterSetName = 'Specific', Position = 1 )] [Alias('Path')] [string] $FilePath, [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 2 )] [Parameter(Mandatory = $false, ParameterSetName = 'Specific', Position = 2 )] [string] $Name = "*", [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 2 )] [Parameter(Mandatory = $false, ParameterSetName = 'Specific', Position = 2 )] [string] $Value = "*", [switch] $IncludePath ) BEGIN {} PROCESS { $assembly = [Reflection.Assembly]::LoadFile($FilePath) $resNames = $assembly.GetManifestResourceNames() $resname = $resNames[0].Replace(".resources", "") $resLanguage = $resname.Split(".")[1] $resMan = New-Object -TypeName System.Resources.ResourceManager -ArgumentList $resname, $assembly $language = New-Object System.Globalization.CultureInfo -ArgumentList "en-US" $resources = $resMan.GetResourceSet($language, $true, $true) foreach ($obj in $resources) { if ($obj.Name -NotLike $Name) { continue } if ($obj.Value -NotLike $Value) { continue } $res = [PSCustomObject]@{ Name = $obj.Name Language = $resLanguage Value = $obj.Value } if ($IncludePath.IsPresent) { $res | Add-Member -MemberType NoteProperty -Name 'Path' -Value $FilePath } $res } } END {} } <# .SYNOPSIS Get installed languages from Dynamics 365 Finance & Operations environment .DESCRIPTION Get installed languages from the running the Dynamics 365 Finance & Operations instance .PARAMETER BinDir Path to the directory containing the BinDir and its assemblies Normally it is located under the AOSService directory in "PackagesLocalDirectory" Default value is fetched from the current configuration on the machine .PARAMETER Name Name of the language that you are looking for Accepts wildcards for searching. E.g. -Name "fr*" Default value is "*" which will search for all languages .EXAMPLE PS C:\> Get-D365Language Shows the entire list of installed languages that are available from the running instance .EXAMPLE PS C:\> Get-D365Language -Name "fr*" Shows the list of installed languages where the name fits the search "fr*" A result set example: fr French fr-BE French (Belgium) fr-CA French (Canada) fr-CH French (Switzerland) .NOTES Tags: PackagesLocalDirectory, Servicing, Language, Labels, Label Author: M�tz Jensen (@Splaxi) This cmdlet is inspired by the work of "Pedro Tornich" (twitter: @ptornich) All credits goes to him for showing how to extract these information His github repository can be found here: https://github.com/ptornich/LabelFileGenerator #> function Get-D365Language { [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 1 )] [string] $BinDir = "$Script:BinDir\bin", [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 2 )] [string] $Name = "*" ) begin { } process { $files = @((Join-Path -Path $BinDir -ChildPath "Microsoft.Dynamics.AX.Xpp.AxShared.dll")) if(-not (Test-PathExists -Path $files -Type Leaf)) { return } Add-Type -Path $files $languages = [Microsoft.Dynamics.Ax.Xpp.LabelHelper]::GetInstalledLanguages() foreach ($obj in $languages) { Write-PSFMessage -Level Verbose -Message "Filtering out all modules that doesn't match the model search." -Target $obj if ($obj -NotLike $Name) {continue} $lang = New-Object System.Globalization.CultureInfo -ArgumentList $obj [PSCustomObject]@{ Name = $obj LanguageName = $lang.DisplayName } } } end { } } <# .SYNOPSIS Get the LCS configuration details .DESCRIPTION Get the LCS configuration details from the configuration store .PARAMETER OutputType The output type you want the cmdlet to return to you Default value is "HashTable" Valid options: HashTable PSCustomObject .EXAMPLE PS C:\> Get-D365LcsUploadConfig This will return the saved configuration for accessing the LCS API. The object return will be a HashTable. .EXAMPLE PS C:\> Get-D365LcsUploadConfig -OutputType "PSCustomObject" This will return the saved configuration for accessing the LCS API. The object return will be a PSCustomObject. .NOTES Tags: Environment, Url, Config, Configuration, LCS, Upload, ClientId Author: M�tz Jensen (@Splaxi) #> function Get-D365LcsUploadConfig { [CmdletBinding()] [OutputType()] param ( [Parameter(Mandatory = $false, Position = 1)] [ValidateSet("HashTable", "PSCustomObject")] [string] $OutputType = "HashTable" ) Invoke-TimeSignal -Start $res = [Ordered]@{} Write-PSFMessage -Level Verbose -Message "Extracting all the LCS configuration and building the result object." foreach ($item in (Get-PSFConfig -FullName d365fo.tools.lcs*)) { $nameTemp = $item.FullName -replace "^d365fo.tools.lcs.upload.", "" $name = ($nameTemp -Split "\." | ForEach-Object { (Get-Culture).TextInfo.ToTitleCase($_) } ) -Join "" $res.$name = $item.Value } if ($OutputType -eq "HashTable") { $res } else { $res | ConvertTo-PsCustomObject } Invoke-TimeSignal -End } <# .SYNOPSIS Get the registered details for Azure Logic App .DESCRIPTION Get the details that are stored for the module when it has to invoke the Azure Logic App .EXAMPLE PS C:\> Get-D365LogicAppConfig This will fetch the current registered Azure Logic App details on the machine. .NOTES Tags: LogicApp, Logic App, Configuration, Url, Email Author: M�tz Jensen (@Splaxi) #> function Get-D365LogicAppConfig { [CmdletBinding()] param () $Details = [hashtable](Get-PSFConfigValue -FullName "d365fo.tools.active.logic.app") $temp = [ordered]@{Email = $Details.Email; Subject = $Details.Subject; URL = $Details.URL } [PSCustomObject]$temp } <# .SYNOPSIS Get the maintenance mode status of the environment .DESCRIPTION Get the maintenance mode status of the Dynamics 365 environment to make sure that things are in the correct state .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-D365MaintenanceMode This will get the current state of the maintenance mode of the environment .NOTES Tags: MaintenanceMode, Maintenance, License, Configuration, Servicing Author: M�tz Jensen (@splaxi) .LINK Enable-D365MaintenanceMode .LINK Disable-D365MaintenanceMode #> function Get-D365MaintenanceMode { [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [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 ) Write-PSFMessage -Level Verbose -Message "Getting Maintenance Mode using SQL scripts." $UseTrustedConnection = Test-TrustedConnection $PSBoundParameters $SqlParams = @{ DatabaseServer = $DatabaseServer; DatabaseName = $DatabaseName; SqlUser = $SqlUser; SqlPwd = $SqlPwd } $sqlCommand = Get-SqlCommand @SqlParams -TrustedConnection $UseTrustedConnection $sqlCommand.CommandText = (Get-Content "$script:ModuleRoot\internal\sql\get-maintenancemode.sql") -join [Environment]::NewLine try { $sqlCommand.Connection.Open() $reader = $sqlCommand.ExecuteReader() while ($reader.Read() -eq $true) { [PSCustomObject]@{ MaintenanceModeEnabled = [bool][int]"$($reader.GetString($($reader.GetOrdinal("VALUE"))))" } } } 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() if ($sqlCommand.Connection.State -ne [System.Data.ConnectionState]::Closed) { $sqlCommand.Connection.Close() } $sqlCommand.Dispose() } } <# .SYNOPSIS Get installed package / module from Dynamics 365 Finance & Operations environment .DESCRIPTION Get installed package / module from the machine running the AOS service for Dynamics 365 Finance & Operations .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 PackageDirectory Path to the directory containing the installed package / module Normally it is located under the AOSService directory in "PackagesLocalDirectory" Default value is fetched from the current configuration on the machine .PARAMETER Name Name of the package / module that you are looking for Accepts wildcards for searching. E.g. -Name "Application*Adaptor" Default value is "*" which will search for all packages / modules .PARAMETER Expand Adds the version of the package / module to the output .EXAMPLE PS C:\> Get-D365Module Shows the entire list of installed packages / modules located in the default location on the machine. .EXAMPLE PS C:\> Get-D365Module -Expand Shows the entire list of installed packages / modules located in the default location on the machine. Will include the file version for each package / module. .EXAMPLE PS C:\> Get-D365Module -Name "Application*Adaptor" Shows the list of installed packages / modules where the name fits the search "Application*Adaptor". A result set example: ApplicationFoundationFormAdaptor ApplicationPlatformFormAdaptor ApplicationSuiteFormAdaptor ApplicationWorkspacesFormAdaptor .EXAMPLE PS C:\> Get-D365Module -Name "Application*Adaptor" -Expand Shows the list of installed packages / modules where the name fits the search "Application*Adaptor". Will include the file version for each package / module. .EXAMPLE PS C:\> Get-D365Module -PackageDirectory "J:\AOSService\PackagesLocalDirectory" Shows the entire list of installed packages / modules located in "J:\AOSService\PackagesLocalDirectory" on the machine .NOTES Tags: PackagesLocalDirectory, Servicing, Model, Models, Package, Packages Author: M�tz Jensen (@Splaxi) The cmdlet supports piping and can be used in advanced scenarios. See more on github and the wiki pages. #> function Get-D365Module { [Alias("Get-D365Package")] [Alias("Get-D365Model")] [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 1 )] [string] $BinDir = "$Script:BinDir\bin", [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 2 )] [string] $PackageDirectory = $Script:PackageDirectory, [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 3 )] [string] $Name = "*", [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 4 )] [switch] $Expand ) [System.Collections.ArrayList] $Files2Process = New-Object -TypeName "System.Collections.ArrayList" $null = $Files2Process.Add((Join-Path $BinDir "Microsoft.Dynamics.AX.Metadata.Management.Delta.dll")) $null = $Files2Process.Add((Join-Path $BinDir "Microsoft.Dynamics.AX.Metadata.Management.Diff.dll")) $null = $Files2Process.Add((Join-Path $BinDir "Microsoft.Dynamics.AX.Metadata.Management.Merge.dll")) $null = $Files2Process.Add((Join-Path $BinDir "Microsoft.Dynamics.AX.Metadata.Management.Core.dll")) $null = $Files2Process.Add((Join-Path $BinDir "Microsoft.Dynamics.AX.Metadata.dll")) $null = $Files2Process.Add((Join-Path $BinDir "Microsoft.Dynamics.AX.Metadata.Core.dll")) $null = $Files2Process.Add((Join-Path $BinDir "Microsoft.Dynamics.AX.Metadata.Storage.dll")) $null = $Files2Process.Add((Join-Path $BinDir "Microsoft.Dynamics.ApplicationPlatform.XppServices.Instrumentation.dll")) Import-AssemblyFileIntoMemory -Path $($Files2Process.ToArray()) if (Test-PSFFunctionInterrupt) { return } Write-PSFMessage -Level Verbose -Message "Intializing RuntimeProvider." $runtimeProviderConfiguration = New-Object Microsoft.Dynamics.AX.Metadata.Storage.Runtime.RuntimeProviderConfiguration -ArgumentList $Script:PackageDirectory $metadataProviderFactory = New-Object Microsoft.Dynamics.AX.Metadata.Storage.MetadataProviderFactory $metadataProvider = $metadataProviderFactory.CreateRuntimeProvider($runtimeProviderConfiguration) Write-PSFMessage -Level Verbose -Message "MetadataProvider initialized." -Target $metadataProvider $modules = $metadataProvider.ModelManifest.ListModules() Write-PSFMessage -Level Verbose -Message "Testing if the cmdlet is running on a OneBox or not." -Target $Script:IsOnebox if ($Script:IsOnebox) { Write-PSFMessage -Level Verbose -Message "Machine is onebox. Initializing DiskProvider too." $diskProviderConfiguration = New-Object Microsoft.Dynamics.AX.Metadata.Storage.DiskProvider.DiskProviderConfiguration $diskProviderConfiguration.AddMetadataPath($PackageDirectory) $metadataProviderFactory = New-Object Microsoft.Dynamics.AX.Metadata.Storage.MetadataProviderFactory $metadataProvider = $metadataProviderFactory.CreateDiskProvider($diskProviderConfiguration) Write-PSFMessage -Level Verbose -Message "MetadataProvider initialized." -Target $metadataProvider $diskModules = $metadataProvider.ModelManifest.ListModules() foreach ($module in $diskModules){ if ($modules.Name -NotContains $module.Name) { $modules += $module } } } Write-PSFMessage -Level Verbose -Message "Looping through all modules." foreach ($obj in $($modules | Sort-Object Name)) { Write-PSFMessage -Level Verbose -Message "Filtering out all modules that doesn't match the model search." -Target $obj if ($obj.Name -NotLike $Name) {continue} if ($Expand -eq $true) { $modulepath = Join-Path (Join-Path $PackageDirectory $obj.Name) "bin" if (Test-Path -Path $modulepath -PathType Container) { $fileversion = Get-FileVersion -Path (Get-ChildItem $modulepath -Filter "Dynamics.AX.$($obj.Name).dll").FullName $version = $fileversion.FileVersion $versionUpdated = $fileversion.FileVersionUpdated } else { $version = "" $versionUpdated = "" } [PSCustomObject]@{ Module = $obj.Name References = $obj.References Version = $version VersionUpdated = $versionUpdated } } else { [PSCustomObject]@{ Module = $obj.Name References = $obj.References } } } } <# .SYNOPSIS Gets the registered offline administrator e-mail configured .DESCRIPTION Get the registered offline administrator from the "DynamicsDevConfig.xml" file located in the default Package Directory .EXAMPLE PS C:\> Get-D365OfflineAuthenticationAdminEmail Will read the DynamicsDevConfig.xml and display the registered Offline Administrator E-mail address. .NOTES Tags: Development, Email, DynamicsDevConfig, Offline, Authentication This cmdlet is inspired by the work of "Sheikh Sohail Hussain" (twitter: @SSohailHussain) His blog can be found here: http://d365technext.blogspot.com The specific blog post that we based this cmdlet on can be found here: http://d365technext.blogspot.com/2018/07/offline-authentication-admin-email.html #> function Get-D365OfflineAuthenticationAdminEmail { [CmdletBinding()] param () $filePath = Join-Path (Join-Path $Script:PackageDirectory "bin") "DynamicsDevConfig.xml" if(-not (Test-PathExists -Path $filePath -Type Leaf)) {return} $namespace = @{ns="http://schemas.microsoft.com/dynamics/2012/03/development/configuration"} $OfflineAuthAdminEmail = Select-Xml -XPath "/ns:DynamicsDevConfig/ns:OfflineAuthenticationAdminEmail" -Path $filePath -Namespace $namespace $AdminEmail = $OfflineAuthAdminEmail.Node.InnerText [PSCustomObject] @{Email = $AdminEmail} } <# .SYNOPSIS Get the details from an axscdppkg file .DESCRIPTION Get the details from an axscdppkg file by extracting it like a zip file. Capable of extracting the manifest details from the inner packages as well .PARAMETER Path Path to the axscdppkg file you want to analyze .PARAMETER ExtractionPath Path where you want the cmdlet to work with extraction of all the files Default value is: C:\Users\Username\AppData\Local\Temp .PARAMETER KB KB number of the hotfix that you are looking for Accepts wildcards for searching. E.g. -KB "4045*" Default value is "*" which will search for all KB's .PARAMETER Hotfix Package Id / Hotfix number the hotfix that you are looking for Accepts wildcards for searching. E.g. -Hotfix "7045*" Default value is "*" which will search for all hotfixes .PARAMETER Traverse Switch to instruct the cmdlet to traverse the inner packages and extract their details .PARAMETER KeepFiles Switch to instruct the cmdlet to keep the files for further manual analyze .PARAMETER IncludeRawManifest Switch to instruct the cmdlet to include the raw content of the manifest file Only works with the -Traverse option .EXAMPLE PS C:\> Get-D365PackageBundleDetail -Path "c:\temp\HotfixPackageBundle.axscdppkg" -Traverse This will extract all the content from the "HotfixPackageBundle.axscdppkg" file and extract all inner packages. For each inner package it will find the manifest file and fetch the KB numbers. The raw manifest file content is included to be analyzed. .EXAMPLE PS C:\> Get-D365PackageBundleDetail -Path "c:\temp\HotfixPackageBundle.axscdppkg" -ExtractionPath C:\Temp\20180905 -Traverse -KeepFiles This will extract all the content from the "HotfixPackageBundle.axscdppkg" file and extract all inner packages. It will extract the content into C:\Temp\20180905 and keep the files after completion. .EXAMPLE Advanced scenario PS C:\> Get-D365PackageBundleDetail -Path C:\temp\HotfixPackageBundle.axscdppkg -Traverse -IncludeRawManifest | ForEach-Object {$_.RawManifest | Out-File "C:\temp\$($_.PackageId).txt"} This will traverse the "HotfixPackageBundle.axscdppkg" file and save the manifest files into c:\temp. Everything else is omitted and cleaned up. .NOTES Tags: Hotfix, KB, Manifest, HotfixPackageBundle, axscdppkg, Package, Bundle, Deployable Author: M�tz Jensen (@Splaxi) #> function Get-D365PackageBundleDetail { [CmdletBinding()] param ( [Parameter(Mandatory = $True, Position = 1 )] [Alias('File')] [string] $Path, [Parameter(Mandatory = $false, Position = 2 )] [string] $ExtractionPath = ([System.IO.Path]::GetTempPath()), [string] $KB = "*", [string] $Hotfix = "*", [switch] $Traverse, [switch] $KeepFiles, [switch] $IncludeRawManifest ) begin { Invoke-TimeSignal -Start if (!(Test-Path -Path $Path -PathType Leaf)) { Write-PSFMessage -Level Host -Message "The <c='em'>$Path</c> file wasn't found. Please ensure the file <c='em'>exists </c> and you have enough <c='em'>permission/c> to access the file." Stop-PSFFunction -Message "Stopping because a file is missing." return } Unblock-File -Path $Path if(!(Test-Path -Path $ExtractionPath)) { Write-PSFMessage -Level Verbose -Message "The extract path didn't exists. Creating it." -Target $ExtractionPath $null = New-Item -Path $ExtractionPath -Force -ItemType Directory } if ($Path -notlike "*.zip") { $tempPathZip = Join-Path $ExtractionPath "$($(New-Guid).ToString()).zip" Write-PSFMessage -Level Verbose -Message "The file isn't a zip file. Copying the file to $tempPathZip" -Target $tempPathZip Copy-Item -Path $Path -Destination $tempPathZip -Force $Path = $tempPathZip } $packageTemp = Join-Path $ExtractionPath ((Get-Random -Maximum 99999).ToString()) $oldprogressPreference = $global:progressPreference $global:progressPreference = 'silentlyContinue' } process { if (Test-PSFFunctionInterrupt) {return} Write-PSFMessage -Level Verbose -Message "Extracting the zip file to $packageTemp" -Target $packageTemp Expand-Archive -Path $Path -DestinationPath $packageTemp if ($Traverse) { $files = Get-ChildItem -Path $packageTemp -Filter "*.axscdp" foreach ($item in $files) { $filename = [System.IO.Path]::GetFileNameWithoutExtension($item.Name) $tempFile = Join-Path $packageTemp "$filename.zip" Write-PSFMessage -Level Verbose -Message "Coping $($item.FullName) to $tempFile" -Target $tempFile Copy-Item -Path $item.FullName -Destination $tempFile $tempDir = (Join-Path $packageTemp ($filename.Replace("DynamicsAX_", ""))) $null = New-Item -Path $tempDir -ItemType Directory -Force Write-PSFMessage -Level Verbose -Message "Extracting the zip file $tempFile to $tempDir" -Target $tempDir Expand-Archive -Path $tempFile -DestinationPath $tempDir } $manifestFiles = Get-ChildItem -Path $packageTemp -Recurse -Filter "PackageManifest.xml" $namespace = @{ns = "http://schemas.datacontract.org/2004/07/Microsoft.Dynamics.AX.Servicing.SCDP.Packaging"; nsKB = "http://schemas.microsoft.com/2003/10/Serialization/Arrays"} Write-PSFMessage -Level Verbose -Message "Getting all the information from the manifest file" foreach ($item in $manifestFiles) { $raw = (Get-Content -Path $item.FullName) -join [Environment]::NewLine $xmlDoc = [xml]$raw $kbs = Select-Xml -Xml $xmlDoc -XPath "//ns:UpdatePackageManifest/ns:KBNumbers/nsKB:string" -Namespace $namespace $packageId = Select-Xml -Xml $xmlDoc -XPath "//ns:UpdatePackageManifest/ns:PackageId/ns:PackageId" -Namespace $namespace $strPackage = $packageId.Node.InnerText $arrKbs = $kbs.node.InnerText if($packageId.Node.InnerText -notlike $Hotfix) {continue} if(@($arrKbs) -notlike $KB) {continue} #* Search across an array with like $Obj = [PSCustomObject]@{Hotfix = $strPackage KBs = ($arrKbs -Join ";")} if($IncludeRawManifest) {$Obj.RawManifest = $raw} $Obj | Select-PSFObject -TypeName "D365FO.TOOLS.PackageBundleManifestDetail" } } else { Get-ChildItem -Path $packageTemp -Filter "*.*" | Select-PSFObject -TypeName "D365FO.TOOLS.PackageBundleDetail" "BaseName as Name" } } end { if(!$Keepfiles) { Remove-Item -Path $packageTemp -Recurse -Force -ErrorAction SilentlyContinue if(![system.string]::IsNullOrEmpty($tempPathZip)) { Remove-Item -Path $tempPathZip -Recurse -Force -ErrorAction SilentlyContinue } } $global:progressPreference = $oldprogressPreference Invoke-TimeSignal -End } } <# .SYNOPSIS Get label file from a package .DESCRIPTION Get label file (resource file) from the package directory .PARAMETER PackageDirectory Path to the package that you want to get a label file from .PARAMETER Name Name of the label file you are looking for Accepts wildcards for searching. E.g. -Name "Fixed*Accounting" Default value is "*" which will search for all label files .PARAMETER Language The language of the label file you are looking for Accepts wildcards for searching. E.g. -Language "en*" Default value is "en-US" which will search for en-US language files .EXAMPLE PS C:\> Get-D365PackageLabelFile -PackageDirectory "C:\AOSService\PackagesLocalDirectory\ApplicationSuite" Shows all the label files for ApplicationSuite package .EXAMPLE PS C:\> Get-D365PackageLabelFile -PackageDirectory "C:\AOSService\PackagesLocalDirectory\ApplicationSuite" -Name "Fixed*Accounting" Shows the label files for ApplicationSuite package where the name fits the search "Fixed*Accounting" .EXAMPLE PS C:\> Get-D365InstalledPackage -Name "ApplicationSuite" | Get-D365PackageLabelFile Shows all label files (en-US) for the ApplicationSuite package .NOTES Tags: PackagesLocalDirectory, Label, Labels, Language, Development, Servicing, Module, Package, Packages Author: M�tz Jensen (@Splaxi) The cmdlet supports piping and can be used in advanced scenarios. See more on github and the wiki pages. #> function Get-D365PackageLabelFileOld { [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory = $true, ParameterSetName = 'Default', ValueFromPipelineByPropertyName = $true, Position = 1 )] [Parameter(Mandatory = $true, ParameterSetName = 'Specific', Position = 1 )] [Alias('Path')] [string] $PackageDirectory, [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 2 )] [Parameter(Mandatory = $false, ParameterSetName = 'Specific', Position = 2 )] [string] $Name = "*", [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 3 )] [Parameter(Mandatory = $false, ParameterSetName = 'Specific', Position = 3 )] [string] $Language = "en-US" ) BEGIN {} PROCESS { $Path = $PackageDirectory if (Test-Path "$Path\Resources\$Language") { $files = Get-ChildItem -Path ("$Path\Resources\$Language\*.resources.dll") foreach ($obj in $files) { if ($obj.Name.Replace(".resources.dll", "") -NotLike $Name) { continue } [PSCustomObject]@{ LabelName = ($obj.Name).Replace(".resources.dll", "") LanguageName = (Get-Command $obj.FullName).FileVersionInfo.Language Language = $obj.directory.basename FilePath = $obj.FullName } } } else { Write-PSFMessage -Level Verbose -Message "Skipping `"$("$Path\Resources\$Language")`" because it doesn't exist." } } END {} } <# .SYNOPSIS Returns information about D365FO .DESCRIPTION Gets detailed information about application and platform .EXAMPLE PS C:\> Get-D365ProductInformation This will get product, platform and application version details for the environment .NOTES Tags: Build, Version, Reference, ProductVersion, ProductDetails, Product Author: Rasmus Andersen (@ITRasmus) The cmdlet wraps the call against a dll file that is shipped with Dynamics 365 for Finance & Operations. The call to the dll file gets all relevant product details for the environment. #> function Get-D365ProductInformation { [CmdletBinding()] param () return Get-ProductInfoProvider } <# .SYNOPSIS Get a Dynamics 365 Runbook .DESCRIPTION Get the full path and filename of a Dynamics 365 Runbook .PARAMETER Path Path to the folder containing the runbook files The default path is "InstallationRecord" which is normally located on the "C:\DynamicsAX\InstallationRecords" .PARAMETER Name Name of the runbook file that you are looking for The parameter accepts wildcards. E.g. -Name *hotfix-20181024* .PARAMETER Latest Instruct the cmdlet to only get the latest runbook file, based on the last written attribute .EXAMPLE PS C:\> Get-D365Runbook This will list all runbooks that are available in the default location. .EXAMPLE PS C:\> Get-D365Runbook -Latest This will get the latest runbook file from the default InstallationRecords directory on the machine. .EXAMPLE PS C:\> Get-D365Runbook -Latest | Invoke-D365RunbookAnalyzer This will find the latest runbook file and have it analyzed by the Invoke-D365RunbookAnalyzer cmdlet to output any error details. .EXAMPLE PS C:\> Get-D365Runbook -Latest | Invoke-D365RunbookAnalyzer | Out-File "C:\Temp\d365fo.tools\runbook-analyze-results.xml" This will find the latest runbook file and have it analyzed by the Invoke-D365RunbookAnalyzer cmdlet to output any error details. The output will be saved into the "C:\Temp\d365fo.tools\runbook-analyze-results.xml" file. .EXAMPLE PS C:\> Get-D365Runbook | Backup-D365Runbook This will save a copy of all runbooks from the default location and save them to "c:\temp\d365fo.tools\runbookbackups" .EXAMPLE PS C:\> notepad.exe (Get-D365Runbook -Latest).File This will find the latest runbook file and open it with notepad. .NOTES Tags: Runbook, Servicing, Hotfix, DeployablePackage, Deployable Package, InstallationRecordsDirectory, Installation Records Directory Author: M�tz Jensen (@Splaxi) #> function Get-D365Runbook { [CmdletBinding()] param ( [Parameter(Position = 1, ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true)] [string] $Path = (Join-Path $Script:InstallationRecordsDir "Runbooks"), [string] $Name = "*", [switch] $Latest ) begin { if (-not (Test-PathExists -Path $Path -Type Container )) { return } } process { if (Test-PSFFunctionInterrupt) { return } $files = Get-ChildItem -Path "$Path\*.xml" | Sort-Object -Descending { $_.LastWriteTime } if ($Latest) { $obj = $files | Select-Object -First 1 $obj | Select-PSFObject "Name as Filename", "LastWriteTime as LastModified", "Fullname as File" } else { foreach ($obj in $files) { if ($obj.Name -NotLike $Name) { continue } $obj | Select-PSFObject "Name as Filename", "LastWriteTime as LastModified", "Fullname as File" } } } } <# .SYNOPSIS Get runbook id .DESCRIPTION Get the runbook id from inside a runbook file .PARAMETER Path Path to the runbook file that you want to analyse Accepts value from pipeline, also by property .EXAMPLE PS C:\> Get-D365RunbookId -Path "C:\DynamicsAX\InstallationRecords\Runbooks\Runbook.xml" This will inspect the Runbook.xml file and output the runbookid from inside the XML document. .EXAMPLE PS C:\> Get-D365Runbook | Get-D365RunbookId This will find all runbook file(s) and have them analyzed by the Get-D365RunbookId cmdlet to output the runbookid(s). .EXAMPLE PS C:\> Get-D365Runbook -Latest | Get-D365RunbookId This will find the latest runbook file and have it analyzed by the Get-D365RunbookId cmdlet to output the runbookid. .NOTES Tags: Runbook, Analyze, RunbookId, Runbooks Author: M�tz Jensen (@Splaxi) #> function Get-D365RunbookId { [CmdletBinding()] [OutputType()] param ( [Parameter(Mandatory = $true, Position = 1, ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true)] [Alias('File')] [string] $Path ) process { if (-not (Test-PathExists -Path $Path -Type Leaf)) { return } [xml]$xmlRunbook = Get-Content $Path [PSCustomObject]@{ RunbookId = $xmlRunbook.SelectSingleNode("/RunbookData/RunbookID")."#text" } } } <# .SYNOPSIS Get the cleanup retention period .DESCRIPTION Gets the configured retention period before updates are deleted .EXAMPLE PS C:\> Get-D365SDPCleanUp This will get the configured retention period from the registry .NOTES Tags: CleanUp, Retention, Servicing, Cut Off, DeployablePackage, Deployable Package Author: M�tz Jensen (@Splaxi) This cmdlet is based on the findings from Alex Kwitny (@AlexOnDAX) See his blog for more info: http://www.alexondax.com/2018/04/msdyn365fo-how-to-adjust-your.html #> function Get-D365SDPCleanUp { [CmdletBinding()] param ( ) $RegSplat = @{ Path = "HKLM:\SOFTWARE\Microsoft\Dynamics\Deployment\" Name = "CutoffDaysForCleanup" } [PSCustomObject] @{ CutoffDaysForCleanup = $( if (Test-RegistryValue @RegSplat) {Get-ItemPropertyValue @RegSplat} else {""} ) } } <# .SYNOPSIS Get a table .DESCRIPTION Get a table either by TableName (wildcard search allowed) or by TableId .PARAMETER Name Name of the table that you are looking for Accepts wildcards for searching. E.g. -Name "Cust*" Default value is "*" which will search for all tables .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 Id The specific id for the table you are looking for .EXAMPLE PS C:\> Get-D365Table -Name CustTable Will get the details for the CustTable .EXAMPLE PS C:\> Get-D365Table -Id 10347 Will get the details for the table with the id 10347. .NOTES Tags: Table, Tables, AOT, TableId, Development Author: M�tz Jensen (@splaxi) The cmdlet supports piping and can be used in advanced scenarios. See more on github and the wiki pages. #> function Get-D365Table { [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 1 )] [string[]] $Name = "*", [Parameter(Mandatory = $true, ParameterSetName = 'TableId', Position = 1 )] [int] $Id, [Parameter(Mandatory = $false, Position = 2 )] [string] $DatabaseServer = $Script:DatabaseServer, [Parameter(Mandatory = $false, Position = 3 )] [string] $DatabaseName = $Script:DatabaseName, [Parameter(Mandatory = $false, Position = 4 )] [string] $SqlUser = $Script:DatabaseUserName, [Parameter(Mandatory = $false, Position = 5 )] [string] $SqlPwd = $Script:DatabaseUserPassword ) BEGIN {} PROCESS { $UseTrustedConnection = Test-TrustedConnection $PSBoundParameters $SqlParams = @{ DatabaseServer = $DatabaseServer; DatabaseName = $DatabaseName; SqlUser = $SqlUser; SqlPwd = $SqlPwd } $sqlCommand = Get-SqlCommand @SqlParams -TrustedConnection $UseTrustedConnection $sqlCommand.CommandText = (Get-Content "$script:ModuleRoot\internal\sql\get-tables.sql") -join [Environment]::NewLine $dataTable = New-Object system.Data.DataSet $dataAdapter = New-Object system.Data.SqlClient.SqlDataAdapter($sqlCommand) $dataAdapter.fill($dataTable) | Out-Null foreach ($localName in $Name) { if ($PSCmdlet.ParameterSetName -eq "Default") { foreach ($obj in $dataTable.Tables.Rows) { if ($obj.AotName -NotLike $localName) { continue } [PSCustomObject]@{ TableId = $obj.TableId TableName = $obj.AotName SqlName = $obj.SqlName } } } else { $obj = $dataTable.Tables.Rows | Where-Object TableId -eq $Id | Select-Object -First 1 [PSCustomObject]@{ TableId = $obj.TableId TableName = $obj.AotName SqlName = $obj.SqlName } } } } END {} } <# .SYNOPSIS Get a field from table .DESCRIPTION Get a field either by FieldName (wildcard search allowed) or by FieldId .PARAMETER TableId The id of the table that the field belongs to .PARAMETER Name Name of the field that you are looking for Accepts wildcards for searching. E.g. -Name "Account*" Default value is "*" which will search for all fields .PARAMETER FieldId Id of the field that you are looking for Type is integer .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 TableName Name of the table that the field belongs to Search will only return the first hit (unordered) and work against that hit .PARAMETER IncludeTableDetails Switch options to enable the result set to include extended details .PARAMETER SearchAcrossTables Switch options to force the cmdlet to search across all tables when looking for the field .EXAMPLE PS C:\> Get-D365TableField -TableId 10347 Will get all field details for the table with id 10347. .EXAMPLE PS C:\> Get-D365TableField -TableName CustTable Will get all field details for the CustTable table. .EXAMPLE PS C:\> Get-D365TableField -TableId 10347 -FieldId 175 Will get the details for the field with id 175 that belongs to the table with id 10347. .EXAMPLE PS C:\> Get-D365TableField -TableId 10347 -Name "VATNUM" Will get the details for the "VATNUM" that belongs to the table with id 10347. .EXAMPLE PS C:\> Get-D365TableField -TableId 10347 -Name "VAT*" Will get the details for all fields that fits the search "VAT*" that belongs to the table with id 10347. .EXAMPLE PS C:\> Get-D365TableField -Name AccountNum -SearchAcrossTables Will search for the AccountNum field across all tables. .NOTES Tags: Table, Tables, Fields, TableField, Table Field, TableName, TableId Author: M�tz Jensen (@splaxi) The cmdlet supports piping and can be used in advanced scenarios. See more on github and the wiki pages. #> function Get-D365TableField { [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory = $true, ParameterSetName = 'Default', ValueFromPipelineByPropertyName = $true, Position = 1 )] [int] $TableId, [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 2 )] [Parameter(Mandatory = $false, ParameterSetName = 'TableName', Position = 2 )] [Parameter(Mandatory = $false, ParameterSetName = 'SearchByNameForce', Position = 1 )] [string] $Name = "*", [Parameter(Mandatory = $false, ParameterSetName = 'Default', ValueFromPipelineByPropertyName = $true, Position = 3 )] [Parameter(Mandatory = $false, ParameterSetName = 'TableName', ValueFromPipelineByPropertyName = $true, Position = 3 )] [int] $FieldId, [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 4 )] [Parameter(Mandatory = $false, ParameterSetName = 'TableName', Position = 4 )] [Parameter(Mandatory = $false, ParameterSetName = 'SearchByNameForce', Position = 3 )] [string] $DatabaseServer = $Script:DatabaseServer, [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 5 )] [Parameter(Mandatory = $false, ParameterSetName = 'TableName', Position = 5 )] [Parameter(Mandatory = $false, ParameterSetName = 'SearchByNameForce', Position = 4 )] [string] $DatabaseName = $Script:DatabaseName, [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 6 )] [Parameter(Mandatory = $false, ParameterSetName = 'TableName', Position = 6 )] [Parameter(Mandatory = $false, ParameterSetName = 'SearchByNameForce', Position = 5 )] [string] $SqlUser = $Script:DatabaseUserName, [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 7 )] [Parameter(Mandatory = $false, ParameterSetName = 'TableName', Position = 7 )] [Parameter(Mandatory = $false, ParameterSetName = 'SearchByNameForce', Position = 6 )] [string] $SqlPwd = $Script:DatabaseUserPassword, [Parameter(Mandatory = $true, ParameterSetName = 'TableName', Position = 1 )] [string] $TableName, [Parameter(Mandatory = $false, ParameterSetName = 'Default')] [Parameter(Mandatory = $false, ParameterSetName = 'TableName')] [switch] $IncludeTableDetails, [Parameter(Mandatory = $true, ParameterSetName = 'SearchByNameForce', Position = 2 )] [switch] $SearchAcrossTables ) BEGIN { $UseTrustedConnection = Test-TrustedConnection $PSBoundParameters $SqlParams = @{ DatabaseServer = $DatabaseServer; DatabaseName = $DatabaseName; SqlUser = $SqlUser; SqlPwd = $SqlPwd } $sqlCommand = Get-SqlCommand @SqlParams -TrustedConnection $UseTrustedConnection } PROCESS { if ($PSCmdlet.ParameterSetName -eq "TableName") { $TableId = (Get-D365Table -Name $TableName | Select-Object -First 1).TableId } if ($SearchAcrossTables) { $sqlCommand.CommandText = (Get-Content "$script:ModuleRoot\internal\sql\get-alltablefields.sql") -join [Environment]::NewLine } else { $sqlCommand.CommandText = (Get-Content "$script:ModuleRoot\internal\sql\get-tablefields.sql") -join [Environment]::NewLine $null = $sqlCommand.Parameters.Add("@TableId", $TableId) } $dataTable = New-Object system.Data.DataSet $dataAdapter = New-Object system.Data.SqlClient.SqlDataAdapter($sqlCommand) $dataAdapter.fill($dataTable) | Out-Null foreach ($obj in $dataTable.Tables.Rows) { if ($obj.FieldId -eq 0) { $TableName = $obj.AotName continue } if ($PSBoundParameters.ContainsKey("FieldId")) { if ($obj.FieldId -NotLike $FieldId) { continue } } else { if ($obj.AotName -NotLike $Name) { continue } } $res = [PSCustomObject]@{ FieldId = $obj.FieldId FieldName = $obj.AotName SqlName = $obj.SqlName } if ($IncludeTableDetails) { $res | Add-Member -MemberType NoteProperty -Name 'TableId' -Value $obj.TableId $res | Add-Member -MemberType NoteProperty -Name 'TableName' -Value $TableName } if ($SearchAcrossTables) { $res | Add-Member -MemberType NoteProperty -Name 'TableId' -Value $obj.TableId } $res } } END {} } <# .SYNOPSIS Get the sequence object for table .DESCRIPTION Get the sequence details for tables .PARAMETER TableName Name of the table that you want to work against Accepts wildcards for searching. E.g. -TableName "Cust*" Default value is "*" which will search for all tables .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-D365TableSequence | Format-Table This will get all the sequence details for all tables inside the database. It will format the output as a table for better overview. .EXAMPLE PS C:\> Get-D365TableSequence -TableName "Custtable" | Format-Table This will get the sequence details for the CustTable in the database. It will format the output as a table for better overview. .EXAMPLE PS C:\> Get-D365TableSequence -TableName "Cust*" | Format-Table This will get the sequence details for all tables that matches the search "Cust*" in the database. It will format the output as a table for better overview. .EXAMPLE PS C:\> Get-D365Table -Name CustTable | Get-D365TableSequence | Format-Table This will get the table details from the Get-D365Table cmdlet and pipe that into Get-D365TableSequence. This will get the sequence details for the CustTable in the database. It will format the output as a table for better overview. .NOTES Tags: Table, RecId, Sequence, Record Id Author: M�tz Jensen (@Splaxi) #> function Get-D365TableSequence { [CmdletBinding()] param ( [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true, Position = 1 )] [Alias('Name')] [string] $TableName = "*", [Parameter(Mandatory = $false, Position = 2 )] [string] $DatabaseServer = $Script:DatabaseServer, [Parameter(Mandatory = $false, Position = 3 )] [string] $DatabaseName = $Script:DatabaseName, [Parameter(Mandatory = $false, Position = 4 )] [string] $SqlUser = $Script:DatabaseUserName, [Parameter(Mandatory = $false, Position = 5 )] [string] $SqlPwd = $Script:DatabaseUserPassword ) BEGIN {} PROCESS { $UseTrustedConnection = Test-TrustedConnection $PSBoundParameters $SqlParams = @{ DatabaseServer = $DatabaseServer; DatabaseName = $DatabaseName; SqlUser = $SqlUser; SqlPwd = $SqlPwd } $SqlCommand = Get-SqlCommand @SqlParams -TrustedConnection $UseTrustedConnection $sqlCommand.CommandText = (Get-Content "$script:ModuleRoot\internal\sql\get-tablesequence.sql") -join [Environment]::NewLine $null = $sqlCommand.Parameters.AddWithValue('@TableName', $TableName.Replace("*", "%")) $datatable = New-Object system.Data.DataSet $dataadapter = New-Object system.Data.SqlClient.SqlDataAdapter($sqlcommand) $dataadapter.fill($datatable) | Out-Null foreach ($obj in $datatable.Tables.Rows) { $res = [PSCustomObject]@{ SequenceName = $obj.sequence_name TableName = $obj.table_name StartValue = $obj.start_value Increment = $obj.increment MinimumValue = $obj.minimum_value MaximumValue = $obj.maximum_value IsCached = $obj.is_cached CacheSize = $obj.cache_size CurrentValue = $obj.current_value } $res } } END {} } <# .SYNOPSIS Get the TFS / VSTS registered URL / URI .DESCRIPTION Gets the URI from the configuration of the local tfs connection in visual studio .PARAMETER Path Path to the tf.exe file that the cmdlet will invoke .EXAMPLE PS C:\> Get-D365TfsUri This will invoke the default tf.exe client located in the Visual Studio 2015 directory and fetch the configured URI. .NOTES Tags: TFS, VSTS, URL, URI, Servicing, Development Author: M�tz Jensen (@Splaxi) #> function Get-D365TfsUri { [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 1 )] [string]$Path = $Script:TfDir ) $executable = Join-Path $Path "tf.exe" if (!(Test-PathExists -Path $executable -Type Leaf)) {return} Write-PSFMessage -Level Verbose -Message "Invoking tf.exe" #* Small hack to get the output from the execution into a variable. $res = & $executable "settings" "connections" 2>$null Write-PSFMessage -Level Verbose -Message "Result from tf.exe: $res" -Target $res if (![string]::IsNullOrEmpty($res)) { [PSCustomObject]@{ TfsUri = $res[2].Split(" ")[0] } } else { Write-PSFMessage -Level Host -Message "No TFS / VSTS connections found. It looks like you haven't configured the server connection and workspace yet." } } <# .SYNOPSIS Get the TFS / VSTS registered workspace path .DESCRIPTION Gets the workspace path from the configuration of the local tfs in visual studio .PARAMETER Path Path to the directory where the Team Foundation Client executable is located .PARAMETER TfsUri Uri to the TFS / VSTS that the workspace is connected to .EXAMPLE PS C:\> Get-D365TfsWorkspace -TfsUri https://PROJECT.visualstudio.com This will invoke the default tf.exe client located in the Visual Studio 2015 directory and fetch the configured URI. .NOTES Tags: TFS, VSTS, URL, URI, Servicing, Development Author: M�tz Jensen (@Splaxi) #> function Get-D365TfsWorkspace { [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 1 )] [string]$Path = $Script:TfDir, [Parameter(Mandatory = $false, ParameterSetName = 'Default', ValueFromPipelineByPropertyName = $true, Position = 2 )] [string]$TfsUri = $Script:TfsUri ) $executable = Join-Path $Path "tf.exe" if (!(Test-PathExists -Path $executable -Type Leaf)) {return} if([system.string]::IsNullOrEmpty($TfsUri)){ Write-PSFMessage -Level Host -Message "The supplied uri <c='em'>was empty</c>. Please update the active d365 environment configuration or simply supply the -TfsUri to the cmdlet." Stop-PSFFunction -Message "Stopping because TFS URI is missing." return } Write-PSFMessage -Level Verbose -Message "Invoking tf.exe" #* Small hack to get the output from the execution into a variable. $res = & $executable "vc" "workspaces" "/collection:$TfsUri" "/format:detailed" 2>$null if (![string]::IsNullOrEmpty($res)) { [PSCustomObject]@{ TfsWorkspacePath = ($res | select-string "meta").ToString().Trim().Split(" ")[1] } } else { Write-PSFMessage -Level Host -Message "No matching workspace configuration found for the specified URI. Either the URI is wrong or you haven't configured the server connection / workspace details correctly." } } <# .SYNOPSIS Get a hashtable with all the stored parameters .DESCRIPTION Gets a hashtable with all the stored parameters to be used with Import-D365Bacpac or New-D365Bacpac for Tier 2 environments .PARAMETER OutputType Used to specify the desired object type of the output The default value is: HashTable Valid options are: HashTable PSCustomObject .EXAMPLE PS C:\> $params = Get-D365Tier2Params This will extract the stored parameters and create a hashtable object. The hashtable is assigned to the $params variable. .NOTES Author: M�tz Jensen (@Splaxi) #> function Get-D365Tier2Params { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [CmdletBinding()] [OutputType()] param ( [Parameter(Mandatory = $false, Position = 1)] [ValidateSet("HashTable", "PSCustomObject")] [string] $OutputType = "HashTable" ) $jsonString = Get-PSFConfigValue -FullName "d365fo.tools.tier2.bacpac.params" Write-PSFMessage -Level Verbose -Message "Retrieved json string" -Target $jsonString if($OutputType -eq "HashTable") { $jsonString | ConvertFrom-Json | ConvertTo-Hashtable } else { $jsonString | ConvertFrom-Json | ConvertTo-Hashtable | ConvertTo-PsCustomObject } } <# .SYNOPSIS Get the url for accessing the instance .DESCRIPTION Get the complete URL for accessing the Dynamics 365 Finance & Operations instance running on this machine .PARAMETER Force Switch to instruct the cmdlet to retrieve the name from the system files instead of the name stored in memory after loading this module. .EXAMPLE PS C:\> Get-D365Url This will get the correct URL to access the environment .NOTES Tags: URL, URI, Servicing Author: Rasmus Andersen (@ITRasmus) The cmdlet wraps the call against a dll file that is shipped with Dynamics 365 for Finance & Operations. The call to the dll file gets all registered URL for the environment. #> function Get-D365Url { [CmdletBinding()] param ( [switch] $Force ) if ($Force) { $Url = "https://$($(Get-D365EnvironmentSettings).Infrastructure.FullyQualifiedDomainName)" } else { $Url = $Script:Url } [PSCustomObject]@{ Url = $Url } } <# .SYNOPSIS Get users from the environment .DESCRIPTION Get all relevant user details from the Dynamics 365 for Finance & Operations .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 updated The parameter supports wildcards. E.g. -Email "*@contoso.com*" Default value is "*" to get all users .PARAMETER ExcludeSystemUsers Instructs the cmdlet to filter out all known system users .EXAMPLE PS C:\> Get-D365User This will get all users from the environment. .EXAMPLE PS C:\> Get-D365User -ExcludeSystemUsers This will get all users from the environment, but filter out all known system user accounts. .EXAMPLE PS C:\> Get-D365User -Email "*contoso.com" This will search for all users with an e-mail address containing 'contoso.com' from the environment. .NOTES Tags: User, Users Author: M�tz Jensen (@Splaxi) Author: Rasmus Andersen (@ITRasmus) #> function Get-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, Position = 5)] [string]$Email = "*", [switch]$ExcludeSystemUsers ) $exclude = @("DAXMDSRunner.com", "dynamics.com") $UseTrustedConnection = Test-TrustedConnection $PSBoundParameters $SqlParams = @{ DatabaseServer = $DatabaseServer; DatabaseName = $DatabaseName; SqlUser = $SqlUser; SqlPwd = $SqlPwd } $SqlCommand = Get-SqlCommand @SqlParams -TrustedConnection $UseTrustedConnection $sqlCommand.CommandText = (Get-Content "$script:ModuleRoot\internal\sql\get-user.sql") -join [Environment]::NewLine $null = $sqlCommand.Parameters.Add("@Email", $Email.Replace("*", "%")) try { Write-PSFMessage -Level InternalComment -Message "Executing a script against the database." -Target (Get-SqlString $SqlCommand) $sqlCommand.Connection.Open() $reader = $sqlCommand.ExecuteReader() while ($reader.Read() -eq $true) { $res = [PSCustomObject]@{ UserId = "$($reader.GetString($($reader.GetOrdinal("ID"))))" Name = "$($reader.GetString($($reader.GetOrdinal("NAME"))))" NetworkAlias = "$($reader.GetString($($reader.GetOrdinal("NETWORKALIAS"))))" NetworkDomain = "$($reader.GetString($($reader.GetOrdinal("NETWORKDOMAIN"))))" Sid = "$($reader.GetString($($reader.GetOrdinal("SID"))))" IdentityProvider = "$($reader.GetString($($reader.GetOrdinal("IDENTITYPROVIDER"))))" Enabled = [bool][int]"$($reader.GetInt32($($reader.GetOrdinal("ENABLE"))))" Email = "$($reader.GetString($($reader.GetOrdinal("NETWORKALIAS"))))" Company = "$($reader.GetString($($reader.GetOrdinal("COMPANY"))))" } if ($ExcludeSystemUsers) { $temp = $res.Email.Split("@")[1] if ($exclude -contains $temp) { continue } elseif ($res.UserId -eq 'Guest') { continue } } $res } } 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() if ($sqlCommand.Connection.State -ne [System.Data.ConnectionState]::Closed) { $sqlCommand.Connection.Close() } $sqlCommand.Dispose() } } <# .SYNOPSIS Cmdlet used to get authentication details about a user .DESCRIPTION The cmdlet will take the e-mail parameter and use it to lookup all the needed details for configuring authentication against Dynamics 365 Finance & Operations .PARAMETER Email The e-mail address / login name of the user that the cmdlet must gather details about .EXAMPLE PS C:\> Get-D365UserAuthenticationDetail -Email "Claire@contoso.com" This will get all the authentication details for the user account with the email address "Claire@contoso.com" .NOTES Tags: User, Users, Security, Configuration, Authentication Author : Rasmus Andersen (@ITRasmus) Author : M�tz Jensen (@splaxi) #> function Get-D365UserAuthenticationDetail { param( [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 1)] [string]$Email ) $instanceProvider = Get-InstanceIdentityProvider [string]$identityProvider = Get-CanonicalIdentityProvider $networkDomain = get-NetworkDomain $Email $instanceProviderName = $instanceProvider.TrimEnd('/') $instanceProviderName = $instanceProviderName.Substring($instanceProviderName.LastIndexOf('/')+1) $instanceProviderIdentityProvider = Get-IdentityProvider "sample@$instanceProviderName" $emailIdentityProvider = Get-IdentityProvider $Email if ($instanceProviderIdentityProvider -ne $emailIdentityProvider) { $identityProvider = $emailIdentityProvider } $SID = Get-UserSIDFromAad $Email $identityProvider @{"SID" = $SID "NetworkDomain" = $networkDomain "IdentityProvider" = $identityProvider "InstanceProvider" = $instanceProvider } } <# .SYNOPSIS Get activation status .DESCRIPTION Get all the important license and activation information from the machine .EXAMPLE PS C:\> Get-D365WindowsActivationStatus This will get the remaining grace and rearm activation information for the machine .NOTES Tags: Windows, License, Activation, Arm, Rearm Author: M�tz Jensen (@Splaxi) The cmdlet uses CIM objects to access the activation details #> function Get-D365WindowsActivationStatus { [CmdletBinding()] param () begin {} process { $a = Get-CimInstance -Class SoftwareLicensingProduct -Namespace root/cimv2 -ComputerName . -Filter "Name LIKE '%Windows%'" $b = Get-CimInstance -Class SoftwareLicensingService -Namespace root/cimv2 -ComputerName . $res = [PSCustomObject]@{ Name = $a.Name Description = $a.Description "Grace Periode (days)" = [math]::Round(($a.graceperiodremaining / 1440)) } $res | Add-Member -MemberType NoteProperty -Name 'ReArms left' -Value $b.RemainingWindowsReArmCount $res } end {} } <# .SYNOPSIS Used to import Aad users into D365FO .DESCRIPTION Provides a method for importing a AAD UserGroup or a comma separated list of AadUsers into D365FO. .PARAMETER AadGroupName Azure Active directory user group containing users to be imported .PARAMETER Users Array of users that you want to import into the D365FO environment .PARAMETER StartupCompany Startup company of users imported. Default is DAT .PARAMETER DatabaseServer Alternative SQL Database server, Default is the one provided by the DataAccess object .PARAMETER DatabaseName Alternative SQL Database, Default is the one provided by the DataAccess object .PARAMETER SqlUser Alternative SQL user, Default is the one provided by the DataAccess object .PARAMETER SqlPwd Alternative SQL user password, Default is the one provided by the DataAccess object .PARAMETER IdPrefix A text that will be prefixed into the ID field. E.g. -IdPrefix "EXT-" will import users and set ID starting with "EXT-..." .PARAMETER NameSuffix A text that will be suffixed into the NAME field. E.g. -NameSuffix "(Contoso)" will import users and append "(Contoso)"" to the NAME .PARAMETER IdValue Specify which field to use as ID value when importing the users. Available options 'Login' / 'FirstName' Default is 'Login' .PARAMETER NameValue Specify which field to use as NAME value when importing the users. Available options 'FirstName' / 'DisplayName' Default is 'DisplayName' .PARAMETER AzureAdCredential Use a PSCredential object for connecting with AzureAd .PARAMETER SkipAzureAd Switch to instruct the cmdlet to skip validating against the Azure Active Directory .PARAMETER ForceExactAadGroupName Force to find the exact name of the Azure Active Directory Group .PARAMETER AadGroupId Azure Active directory user group ID containing users to be imported .EXAMPLE PS C:\> Import-D365AadUser -Users "Claire@contoso.com","Allen@contoso.com" Imports Claire and Allen as users .EXAMPLE PS C:\> $myPassword = ConvertTo-SecureString "MyPasswordIsSecret" -AsPlainText -Force PS C:\> $myCredentials = New-Object System.Management.Automation.PSCredential ("MyEmailIsAlso", $myPassword) PS C:\> Import-D365AadUser -Users "Claire@contoso.com","Allen@contoso.com" -AzureAdCredential $myCredentials This will import Claire and Allen as users. .EXAMPLE PS C:\> Import-D365AadUser -AadGroupName "CustomerTeam1" if more than one group match the AadGroupName, you can use the ExactAadGroupName parameter Import-D365AadUser -AadGroupName "CustomerTeam1" -ForceExactAadGroupName .EXAMPLE PS C:\> Import-D365AadUser -AadGroupId "99999999-aaaa-bbbb-cccc-9999999999" Imports all the users that is present in the AAD Group called CustomerTeam1 .NOTES Tags: User, Users, Security, Configuration, Permission, AAD, Azure Active Directory, Group, Groups Author: Rasmus Andersen (@ITRasmus) Author: Charles Colombel (@dropshind) Author: M�tz Jensen (@Splaxi) At no circumstances can this cmdlet be used to import users into a PROD environment. Only users from an Azure Active Directory that you have access to, can be imported. Use AAD B2B implementation if you want to support external people. Every imported users will get the System Administration / Administrator role assigned on import #> function Import-D365AadUser { [CmdletBinding(DefaultParameterSetName = 'UserListImport')] param ( [Parameter(Mandatory = $true, Position = 1, ParameterSetName = "GroupNameImport")] [String]$AadGroupName, [Parameter(Mandatory = $true, Position = 1, ParameterSetName = "UserListImport")] [string[]]$Users, [Parameter(Mandatory = $false, Position = 2)] [string]$StartupCompany = 'DAT', [Parameter(Mandatory = $false, Position = 3)] [string]$DatabaseServer = $Script:DatabaseServer, [Parameter(Mandatory = $false, Position = 4)] [string]$DatabaseName = $Script:DatabaseName, [Parameter(Mandatory = $false, Position = 5)] [string]$SqlUser = $Script:DatabaseUserName, [Parameter(Mandatory = $false, Position = 6)] [string]$SqlPwd = $Script:DatabaseUserPassword, [Parameter(Mandatory = $false, Position = 7)] [string]$IdPrefix = "", [Parameter(Mandatory = $false, Position = 8)] [string]$NameSuffix = "", [Parameter(Mandatory = $false, Position = 9)] [ValidateSet('Login', 'FirstName')] [string]$IdValue = "Login", [Parameter(Mandatory = $false, Position = 10)] [ValidateSet('FirstName', 'DisplayName')] [string]$NameValue = "DisplayName", [Parameter(Mandatory = $false, Position = 11)] [PSCredential]$AzureAdCredential, [Parameter(Mandatory = $false, Position = 12, ParameterSetName = "UserListImport")] [switch]$SkipAzureAd, [Parameter(Mandatory = $false, Position = 13, ParameterSetName = "GroupNameImport")] [switch]$ForceExactAadGroupName, [Parameter(Mandatory = $true, Position = 14, ParameterSetName = "GroupIdImport")] [string]$AadGroupId ) $UseTrustedConnection = Test-TrustedConnection $PSBoundParameters $SqlParams = @{ DatabaseServer = $DatabaseServer; DatabaseName = $DatabaseName; SqlUser = $SqlUser; SqlPwd = $SqlPwd } $SqlCommand = Get-SqlCommand @SqlParams -TrustedConnection $UseTrustedConnection $instanceProvider = Get-InstanceIdentityProvider $canonicalProvider = Get-CanonicalIdentityProvider try { Write-PSFMessage -Level Verbose -Message "Trying to connect to the Azure Active Directory" if ($PSBoundParameters.ContainsKey("AzureAdCredential") -eq $true) { $null = Connect-AzureAD -ErrorAction Stop -Credential $AzureAdCredential } else { if ($SkipAzureAd -eq $false) { $null = Connect-AzureAD -ErrorAction Stop } } } catch { Write-PSFMessage -Level Host -Message "Something went wrong while connecting to Azure Active Directory" -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" return } $azureAdUsers = New-Object -TypeName "System.Collections.ArrayList" if (( $PSCmdlet.ParameterSetName -eq "GroupNameImport") -or ($PSCmdlet.ParameterSetName -eq "GroupIdImport")) { if ($PSCmdlet.ParameterSetName -eq 'GroupIdImport') { Write-PSFMessage -Level Verbose -Message "Search AadGroup by its ID : $AadGroupId" $group = Get-AzureADGroup -ObjectId $AadGroupId } else { if ($ForceExactAadGroupName -eq $true) { Write-PSFMessage -Level Verbose -Message "Search AadGroup by its exactly name : $AadGroupName" $group = Get-AzureADGroup -Filter "DisplayName eq '$AadGroupName'" } else { Write-PSFMessage -Level Verbose -Message "Search AadGroup by searching with its name : $AadGroupName" $group = Get-AzureADGroup -SearchString $AadGroupName } } if ($null -eq $group) { Write-PSFMessage -Level Host -Message "Unable to find the specified group in the AAD. Please ensure the group exists and that you have enough permissions to access it." Stop-PSFFunction -Message "Stopping because of errors" return } else { Write-PSFMessage -Level Host -Message "Processing Azure AD user Group `"$($group[0].DisplayName)`"" } if ($group.Length -gt 1) { Write-PSFMessage -Level Host -Message "More than one group found" foreach ($foundGroup in $group) { Write-PSFMessage -Level Host -Message "Group found $($foundGroup.DisplayName)" } Stop-PSFFunction -Message "Stopping because of errors" return } $userlist = Get-AzureADGroupMember -ObjectId $group[0].ObjectId foreach ($user in $userlist) { if ($user.ObjectType -eq "User") { $azureAdUser = Get-AzureADUser -ObjectId $user.ObjectId if($null -eq $azureAdUser.Mail) { Write-PSFMessage -Level Critical "User $($user.ObjectId) did not have an Mail" } else { $null = $azureAdUsers.Add((Get-AzureADUser -ObjectId $user.ObjectId)) } } } } else { foreach ($user in $Users) { if ($SkipAzureAd -eq $true) { $name = Get-LoginFromEmail $user $null = $azureAdUsers.Add([PSCustomObject]@{ Mail = $user GivenName = $name DisplayName = $name ObjectId = '' }) } else { $aadUser = Get-AzureADUser -SearchString $user if ($null -eq $aadUser) { Write-PSFMessage -Level Critical "Could not find user $user in AzureAAd" } else { $null = $azureAdUsers.Add($aadUser) } } } } try { $sqlCommand.Connection.Open() foreach ($user in $azureAdUsers) { $identityProvider = $canonicalProvider Write-PSFMessage -Level Verbose -Message "Getting tenant from $($user.Mail)." $tenant = Get-TenantFromEmail $user.Mail Write-PSFMessage -Level Verbose -Message "Getting domain from $($user.Mail)." $networkDomain = get-NetworkDomain $user.Mail Write-PSFMessage -Level Verbose -Message "InstanceProvider : $InstanceProvider" Write-PSFMessage -Level Verbose -Message "Tenant : $Tenant" if ($user.Mail.ToLower().Contains("outlook.com") -eq $true) { $identityProvider = "live.com" } else { if ($instanceProvider.ToLower().Contains($tenant.ToLower()) -ne $True) { Write-PSFMessage -Level Verbose -Message "Getting identity provider from $($user.Mail)." $identityProvider = Get-IdentityProvider $user.Mail } } Write-PSFMessage -Level Verbose -Message "Getting sid from $($user.Mail) and identity provider : $identityProvider." $sid = Get-UserSIDFromAad $user.Mail $identityProvider Write-PSFMessage -Level Verbose -Message "Generated SID : $sid" $id = "" if ($IdValue -eq 'Login') { $id = $IdPrefix + $(Get-LoginFromEmail $user.Mail) } else { $id = $IdPrefix + $user.GivenName } Write-PSFMessage -Level Verbose -Message "Id for user $($user.Mail) : $id" $name = "" if ($NameValue -eq 'DisplayName') { $name = $user.DisplayName + $NameSuffix } else { $name = $user.GivenName + $NameSuffix } Write-PSFMessage -Level Verbose -Message "Name for user $($user.Mail) : $name" Write-PSFMessage -Level Verbose -Message "Importing $($user.Mail) - SID $sid - Provider $identityProvider" Import-AadUserIntoD365FO $SqlCommand $user.Mail $name $id $sid $StartupCompany $identityProvider $networkDomain $user.ObjectId if (Test-PSFFunctionInterrupt) { 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 { if ($sqlCommand.Connection.State -ne [System.Data.ConnectionState]::Closed) { $sqlCommand.Connection.Close() } $sqlCommand.Dispose() } } <# .SYNOPSIS Import a bacpac file .DESCRIPTION Import a bacpac file to either a Tier1 or Tier2 environment .PARAMETER ImportModeTier1 Switch to instruct the cmdlet that it will import into a Tier1 environment The cmdlet will expect to work against a SQL Server instance .PARAMETER ImportModeTier2 Switch to instruct the cmdlet that it will import into a Tier2 environment The cmdlet will expect to work against an Azure DB 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 BacpacFile Path to the bacpac file you want to import into the database server .PARAMETER NewDatabaseName Name of the new database that will be created while importing the bacpac file This will create a new database on the database server and import the content of the bacpac into .PARAMETER AxDeployExtUserPwd Password that is obtained from LCS .PARAMETER AxDbAdminPwd Password that is obtained from LCS .PARAMETER AxRuntimeUserPwd Password that is obtained from LCS .PARAMETER AxMrRuntimeUserPwd Password that is obtained from LCS .PARAMETER AxRetailRuntimeUserPwd Password that is obtained from LCS .PARAMETER AxRetailDataSyncUserPwd Password that is obtained from LCS .PARAMETER AxDbReadonlyUserPwd Password that is obtained from LCS .PARAMETER CustomSqlFile Parameter description .PARAMETER ImportOnly Switch to instruct the cmdlet to only import the bacpac into the new database The cmdlet will create a new database and import the content of the bacpac file into this Nothing else will be executed .EXAMPLE PS C:\> Import-D365Bacpac -ImportModeTier1 -BacpacFile "C:\temp\uat.bacpac" -NewDatabaseName "ImportedDatabase" PS C:\> Switch-D365ActiveDatabase -NewDatabaseName "ImportedDatabase" This will instruct the cmdlet that the import will be working against a SQL Server instance. It will import the "C:\temp\uat.bacpac" file into a new database named "ImportedDatabase". The next thing to do is to switch the active database out with the new one you just imported. "ImportedDatabase" will be switched in as the active database, while the old one will be named "AXDB_original". .EXAMPLE PS C:\> Import-D365Bacpac -ImportModeTier2 -SqlUser "sqladmin" -SqlPwd "XyzXyz" -BacpacFile "C:\temp\uat.bacpac" -AxDeployExtUserPwd "XxXx" -AxDbAdminPwd "XxXx" -AxRuntimeUserPwd "XxXx" -AxMrRuntimeUserPwd "XxXx" -AxRetailRuntimeUserPwd "XxXx" -AxRetailDataSyncUserPwd "XxXx" -AxDbReadonlyUserPwd "XxXx" -NewDatabaseName "ImportedDatabase" PS C:\> Switch-D365ActiveDatabase -NewDatabaseName "ImportedDatabase" -SqlUser "sqladmin" -SqlPwd "XyzXyz" This will instruct the cmdlet that the import will be working against an Azure DB instance. It requires all relevant passwords from LCS for all the builtin user accounts used in a Tier 2 environment. It will import the "C:\temp\uat.bacpac" file into a new database named "ImportedDatabase". The next thing to do is to switch the active database out with the new one you just imported. "ImportedDatabase" will be switched in as the active database, while the old one will be named "AXDB_original". .NOTES Tags: Database, Bacpac, Tier1, Tier2, Golden Config, Config, Configuration Author: Rasmus Andersen (@ITRasmus) Author: M�tz Jensen (@Splaxi) #> function Import-D365Bacpac { [CmdletBinding(DefaultParameterSetName = 'ImportTier1')] param ( [Parameter(Mandatory = $true, ParameterSetName = 'ImportTier1', Position = 0)] [switch]$ImportModeTier1, [Parameter(Mandatory = $true, ParameterSetName = 'ImportTier2', Position = 0)] [Parameter(Mandatory = $true, ParameterSetName = 'ImportOnlyTier2', Position = 0)] [switch]$ImportModeTier2, [Parameter(Mandatory = $false, Position = 1 )] [string]$DatabaseServer = $Script:DatabaseServer, [Parameter(Mandatory = $false, Position = 2 )] [string]$DatabaseName = $Script:DatabaseName, [Parameter(Mandatory = $false, Position = 3 )] [Parameter(Mandatory = $true, ParameterSetName = 'ImportTier2', ValueFromPipelineByPropertyName = $true, Position = 3)] [Parameter(Mandatory = $false, ParameterSetName = 'ImportTier1', Position = 3)] [Parameter(Mandatory = $true, ParameterSetName = 'ImportOnlyTier2', ValueFromPipelineByPropertyName = $true, Position = 3)] [string]$SqlUser = $Script:DatabaseUserName, [Parameter(Mandatory = $false, Position = 4 )] [Parameter(Mandatory = $true, ParameterSetName = 'ImportTier2', ValueFromPipelineByPropertyName = $true, Position = 4)] [Parameter(Mandatory = $false, ParameterSetName = 'ImportTier1', Position = 4)] [Parameter(Mandatory = $true, ParameterSetName = 'ImportOnlyTier2', ValueFromPipelineByPropertyName = $true, Position = 4)] [string]$SqlPwd = $Script:DatabaseUserPassword, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, Position = 5 )] [Alias('File')] [string]$BacpacFile, [Parameter(Mandatory = $true, Position = 6 )] [string]$NewDatabaseName, [Parameter(Mandatory = $true, ParameterSetName = 'ImportTier2', ValueFromPipelineByPropertyName = $true, Position = 7)] [Parameter(Mandatory = $false, ParameterSetName = 'ImportOnlyTier2', Position = 7)] [string]$AxDeployExtUserPwd, [Parameter(Mandatory = $true, ParameterSetName = 'ImportTier2', ValueFromPipelineByPropertyName = $true, Position = 8)] [Parameter(Mandatory = $false, ParameterSetName = 'ImportOnlyTier2', Position = 8)] [string]$AxDbAdminPwd, [Parameter(Mandatory = $true, ParameterSetName = 'ImportTier2', ValueFromPipelineByPropertyName = $true, Position = 9)] [Parameter(Mandatory = $false, ParameterSetName = 'ImportOnlyTier2', Position = 9)] [string]$AxRuntimeUserPwd, [Parameter(Mandatory = $true, ParameterSetName = 'ImportTier2', ValueFromPipelineByPropertyName = $true, Position = 10)] [Parameter(Mandatory = $false, ParameterSetName = 'ImportOnlyTier2', Position = 10)] [string]$AxMrRuntimeUserPwd, [Parameter(Mandatory = $true, ParameterSetName = 'ImportTier2', ValueFromPipelineByPropertyName = $true, Position = 11)] [Parameter(Mandatory = $false, ParameterSetName = 'ImportOnlyTier2', Position = 11)] [string]$AxRetailRuntimeUserPwd, [Parameter(Mandatory = $true, ParameterSetName = 'ImportTier2', ValueFromPipelineByPropertyName = $true, Position = 12)] [Parameter(Mandatory = $false, ParameterSetName = 'ImportOnlyTier2', Position = 12)] [string]$AxRetailDataSyncUserPwd, [Parameter(Mandatory = $true, ParameterSetName = 'ImportTier2', ValueFromPipelineByPropertyName = $true, Position = 13)] [Parameter(Mandatory = $false, ParameterSetName = 'ImportOnlyTier2', Position = 13)] [string]$AxDbReadonlyUserPwd, [Parameter(Mandatory = $false, Position = 14 )] [string]$CustomSqlFile, [Parameter(Mandatory = $false, ParameterSetName = 'ImportTier1')] [Parameter(Mandatory = $true, ParameterSetName = 'ImportOnlyTier2')] [switch]$ImportOnly ) if (-not (Test-PathExists -Path $BacpacFile -Type Leaf)) { return } if ($PSBoundParameters.ContainsKey("CustomSqlFile")) { if (-not (Test-PathExists -Path $CustomSqlFile -Type Leaf)) { return } else { $ExecuteCustomSQL = $true } } Invoke-TimeSignal -Start $UseTrustedConnection = Test-TrustedConnection $PSBoundParameters $BaseParams = @{ DatabaseServer = $DatabaseServer DatabaseName = $DatabaseName SqlUser = $SqlUser SqlPwd = $SqlPwd } $ImportParams = @{ Action = "import" FilePath = $BacpacFile } Write-PSFMessage -Level Verbose "Testing if we are working against a Tier2 / Azure DB" if ($ImportModeTier2) { Write-PSFMessage -Level Verbose "Start collecting the current Azure DB instance settings" $Objectives = Get-AzureServiceObjective @BaseParams if ($null -eq $Objectives) { return } $Properties = @("DatabaseEdition=$($Objectives.DatabaseEdition)", "DatabaseServiceObjective=$($Objectives.DatabaseServiceObjective)" ) $ImportParams.Properties = $Properties } $Params = Get-DeepClone $BaseParams $Params.DatabaseName = $NewDatabaseName Write-PSFMessage -Level Verbose "Start importing the bacpac with a new database name and current settings" $res = Invoke-SqlPackage @Params @ImportParams -TrustedConnection $UseTrustedConnection if (-not ($res)) {return} Write-PSFMessage -Level Verbose "Importing completed" if (-not ($ImportOnly)) { Write-PSFMessage -Level Verbose -Message "Start working on the configuring the new database" if ($ImportModeTier2) { Write-PSFMessage -Level Verbose "Building sql statement to update the imported Azure database" $InstanceValues = Get-InstanceValues @BaseParams -TrustedConnection $UseTrustedConnection if ($null -eq $InstanceValues) { return } $AzureParams = @{ AxDeployExtUserPwd = $AxDeployExtUserPwd; AxDbAdminPwd = $AxDbAdminPwd; AxRuntimeUserPwd = $AxRuntimeUserPwd; AxMrRuntimeUserPwd = $AxMrRuntimeUserPwd; AxRetailRuntimeUserPwd = $AxRetailRuntimeUserPwd; AxRetailDataSyncUserPwd = $AxRetailDataSyncUserPwd; AxDbReadonlyUserPwd = $AxDbReadonlyUserPwd; } $res = Set-AzureBacpacValues @Params @AzureParams @InstanceValues if (-not ($res)) {return} } else { Write-PSFMessage -Level Verbose "Building sql statement to update the imported SQL database" $res = Set-SqlBacpacValues @Params -TrustedConnection $UseTrustedConnection if (-not ($res)) {return} } if ($ExecuteCustomSQL) { Write-PSFMessage -Level Verbose -Message "Invoking the Execution of custom SQL script" $res = Invoke-D365SqlScript @Params -FilePath $CustomSqlFile -TrustedConnection $UseTrustedConnection if (-not ($res)) {return} } } Invoke-TimeSignal -End } <# .SYNOPSIS Import a model into Dynamics 365 for Finance & Operations .DESCRIPTION Import a model into a Dynamics 365 for Finance & Operations environment .PARAMETER Path Path to the axmodel file that you want to import .PARAMETER Model Name of the model that you want to work against .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 .PARAMETER Replace Instruct the cmdlet to replace an already existing model .EXAMPLE PS C:\> Import-D365Model -Path c:\temp\d365fo.tools\CustomModel.axmodel This will import the "c:\temp\d365fo.tools\CustomModel.axmodel" model into the PackagesLocalDirectory location. .EXAMPLE PS C:\> Import-D365Model -Path c:\temp\d365fo.tools\CustomModel.axmodel -Replace This will import the "c:\temp\d365fo.tools\CustomModel.axmodel" model into the PackagesLocalDirectory location. If the model already exists it will replace it. .NOTES Tags: ModelUtil, Axmodel, Model, Import, Replace, Source Control, Vsts, Azure DevOps Author: M�tz Jensen (@Splaxi) #> function Import-D365Model { # [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidDefaultValueSwitchParameter", "")] [CmdletBinding()] param ( [Parameter(Mandatory = $True, Position = 1 )] [Alias('File')] [string] $Path, [Parameter(Mandatory = $false, Position = 2 )] [string] $BinDir = "$Script:PackageDirectory\bin", [Parameter(Mandatory = $false, Position = 3 )] [string] $MetaDataDir = "$Script:MetaDataDir", [switch] $Replace ) Invoke-TimeSignal -Start if($Replace) { Invoke-ModelUtil -Command "Replace" -Path $Path -BinDir $BinDir -MetaDataDir $MetaDataDir } else { Invoke-ModelUtil -Command "Import" -Path $Path -BinDir $BinDir -MetaDataDir $MetaDataDir } Invoke-TimeSignal -End } <# .SYNOPSIS Create and configure test automation certificate .DESCRIPTION Creates a new self signed certificate for automated testing and reconfigures the AOS Windows Identity Foundation configuration to trust the certificate .PARAMETER CertificateFileName Filename to be used when exporting the cer file .PARAMETER PrivateKeyFileName Filename to be used when exporting the pfx file .PARAMETER Password The password that you want to use to protect your certificate with .PARAMETER CertificateOnly Switch specifying if only the certificate needs to be created. If specified, then only the certificate is created and the thumbprint is not added to the wif.config on the AOS side. If not specified (default) then the certificate is created and installed and the corresponding thumbprint is added to the wif.config on the local machine. .EXAMPLE PS C:\> Initialize-D365TestAutomationCertificate This will generate a certificate for issuer 127.0.0.1 and install it in the trusted root certificates and modify the wif.config of the AOS to include the thumbprint and trust the certificate. .EXAMPLE PS C:\> Initialize-D365TestAutomationCertificate -CertificateOnly This will generate a certificate for issuer 127.0.0.1 and install it in the trusted root certificates. No actions will be taken regarding modifying the AOS wif.config file. Use this when installing RSAT on a machine different from the AOS where RSAT is pointing to. .NOTES Tags: Automated Test, Test, Regression, Certificate, Thumbprint Author: Kenny Saelen (@kennysaelen) Author: M�tz Jensen (@Splaxi) #> function Initialize-D365TestAutomationCertificate { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "")] [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), [Parameter(Mandatory = $false, Position = 4)] [switch]$CertificateOnly ) if (-not $Script:IsAdminRuntime) { Write-PSFMessage -Level Critical -Message "The cmdlet needs administrator permission (Run As Administrator) to be able to update the configuration. 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." return } try { # Create the certificate and place it in the right stores $X509Certificate = New-D365SelfSignedCertificate -CertificateFileName $CertificateFileName -PrivateKeyFileName $PrivateKeyFileName -Password $Password if (Test-PSFFunctionInterrupt) { Write-PSFMessage -Level Critical -Message "The self signed certificate creation was interrupted." Stop-PSFFunction -Message "Stopping because of errors." return } if($false -eq $CertificateOnly) { # Modify the wif.config of the AOS to have this thumbprint added to the https://fakeacs.accesscontrol.windows.net/ authority Add-D365WIFConfigAuthorityThumbprint -CertificateThumbprint $X509Certificate.Thumbprint } Write-PSFMessage -Level Host -Message "Generated certificate: $X509Certificate" } 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" return } } <# .SYNOPSIS Download a file to Azure .DESCRIPTION Download any file to an Azure Storage Account .PARAMETER AccountId Storage Account Name / Storage Account Id where you want to fetch the file from .PARAMETER AccessToken The token that has the needed permissions for the download action .PARAMETER SAS The SAS key that you have created for the storage account or blob container .PARAMETER Container Name of the blob container inside the storage account you where the file is .PARAMETER FileName Name of the file that you want to download .PARAMETER Path Path to the folder / location you want to save the file The default path is "c:\temp\d365fo.tools" .PARAMETER Latest Instruct the cmdlet to download the latest file from Azure regardless of name .EXAMPLE PS C:\> Invoke-D365AzureStorageDownload -AccountId "miscfiles" -AccessToken "xx508xx63817x752xx74004x30705xx92x58349x5x78f5xx34xxxxx51" -Container "backupfiles" -FileName "OriginalUAT.bacpac" -Path "c:\temp" Will download the "OriginalUAT.bacpac" file from the storage account and save it to "c:\temp\OriginalUAT.bacpac" .EXAMPLE PS C:\> Invoke-D365AzureStorageDownload -AccountId "miscfiles" -AccessToken "xx508xx63817x752xx74004x30705xx92x58349x5x78f5xx34xxxxx51" -Container "backupfiles" -Path "c:\temp" -Latest Will download the file with the latest modified datetime from the storage account and save it to "c:\temp\". The complete path to the file will returned as output from the cmdlet. .EXAMPLE PS C:\> $AzureParams = Get-D365ActiveAzureStorageConfig PS C:\> Invoke-D365AzureStorageDownload @AzureParams -Path "c:\temp" -Latest This will get the current Azure Storage Account configuration details and use them as parameters to download the latest file from an Azure Storage Account Will download the file with the latest modified datetime from the storage account and save it to "c:\temp\". The complete path to the file will returned as output from the cmdlet. .EXAMPLE PS C:\> Invoke-D365AzureStorageDownload -Latest This will use the default parameter values that are based on the configuration stored inside "Get-D365ActiveAzureStorageConfig". Will download the file with the latest modified datetime from the storage account and save it to "c:\temp\d365fo.tools". .EXAMPLE PS C:\> Invoke-D365AzureStorageDownload -AccountId "miscfiles" -SAS "sv2018-03-28&siunlisted&src&sigAUOpdsfpoWE976ASDhfjkasdf(5678sdfhk" -Container "backupfiles" -Path "c:\temp" -Latest Will download the file with the latest modified datetime from the storage account and save it to "c:\temp\". A SAS key is used to gain access to the container and downloading the file from it. The complete path to the file will returned as output from the cmdlet. .NOTES Tags: Azure, Azure Storage, Config, Configuration, Token, Blob, File, Files, Latest, Bacpac, Container Author: M�tz Jensen (@Splaxi) The cmdlet supports piping and can be used in advanced scenarios. See more on github and the wiki pages. #> function Invoke-D365AzureStorageDownload { [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory = $false)] [string] $AccountId = $Script:AccountId, [Parameter(Mandatory = $false)] [string] $AccessToken = $Script:AccessToken, [Parameter(Mandatory = $false)] [string] $SAS = $Script:SAS, [Parameter(Mandatory = $false)] [Alias('Blob')] [Alias('Blobname')] [string] $Container = $Script:Container, [Parameter(Mandatory = $true, ParameterSetName = 'Default', ValueFromPipelineByPropertyName = $true)] [Alias('Name')] [string] $FileName, [Parameter(Mandatory = $false)] [string] $Path = $Script:DefaultTempPath, [Parameter(Mandatory = $true, ParameterSetName = 'Latest', Position = 4 )] [Alias('GetLatest')] [switch] $Latest ) BEGIN { if (-not (Test-PathExists -Path $Path -Type Container -Create)) { return } if (([string]::IsNullOrEmpty($AccountId) -eq $true) -or ([string]::IsNullOrEmpty($Container)) -or (([string]::IsNullOrEmpty($AccessToken)) -and ([string]::IsNullOrEmpty($SAS)))) { Write-PSFMessage -Level Host -Message "It seems that you are missing some of the parameters. Please make sure that you either supplied them or have the right configuration saved." Stop-PSFFunction -Message "Stopping because of missing parameters" return } } PROCESS { if (Test-PSFFunctionInterrupt) {return} Invoke-TimeSignal -Start try { if ([string]::IsNullOrEmpty($SAS)) { Write-PSFMessage -Level Verbose -Message "Working against Azure Storage Account with AccessToken" $storageContext = new-AzureStorageContext -StorageAccountName $AccountId.ToLower() -StorageAccountKey $AccessToken } else { Write-PSFMessage -Level Verbose -Message "Working against Azure Storage Account with SAS" $conString = $("BlobEndpoint=https://{0}.blob.core.windows.net/;QueueEndpoint=https://{0}.queue.core.windows.net/;FileEndpoint=https://{0}.file.core.windows.net/;TableEndpoint=https://{0}.table.core.windows.net/;SharedAccessSignature={1}" -f $AccountId.ToLower(), $SAS) $storageContext = new-AzureStorageContext -ConnectionString $conString } $cloudStorageAccount = [Microsoft.WindowsAzure.Storage.CloudStorageAccount]::Parse($storageContext.ConnectionString) $blobClient = $cloudStorageAccount.CreateCloudBlobClient() $blobContainer = $blobClient.GetContainerReference($Container.ToLower()); Write-PSFMessage -Level Verbose -Message "Start download from Azure Storage Account" if ($Latest) { $files = $blobContainer.ListBlobs() $File = ($files | Sort-Object -Descending { $_.Properties.LastModified } | Select-Object -First 1) $NewFile = Join-Path $Path $($File.Name) $File.DownloadToFile($NewFile, [System.IO.FileMode]::Create) $FileName = $File.Name } else { $NewFile = Join-Path $Path $FileName $blockBlob = $blobContainer.GetBlockBlobReference($FileName); $blockBlob.DownloadToFile($NewFile, [System.IO.FileMode]::Create) } Get-Item -Path $NewFile | Select-PSFObject "Name as Filename", @{Name = "Size"; Expression = {[PSFSize]$_.Length}}, "LastWriteTime as LastModified", "Fullname as File" } catch { Write-PSFMessage -Level Host -Message "Something went wrong while downloading the file from Azure" -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" return } finally { Invoke-TimeSignal -End } } END {} } <# .SYNOPSIS Upload a file to Azure .DESCRIPTION Upload any file to an Azure Storage Account .PARAMETER AccountId Storage Account Name / Storage Account Id where you want to store the file .PARAMETER AccessToken The token that has the needed permissions for the upload action .PARAMETER SAS The SAS key that you have created for the storage account or blob container .PARAMETER Container Name of the blob container inside the storage account you want to store the file .PARAMETER Filepath Path to the file you want to upload .PARAMETER DeleteOnUpload Switch to tell the cmdlet if you want the local file to be deleted after the upload completes .EXAMPLE PS C:\> Invoke-D365AzureStorageUpload -AccountId "miscfiles" -AccessToken "xx508xx63817x752xx74004x30705xx92x58349x5x78f5xx34xxxxx51" -Container "backupfiles" -Filepath "c:\temp\bacpac\UAT_20180701.bacpac" -DeleteOnUpload This will upload the "c:\temp\bacpac\UAT_20180701.bacpac" up to the "backupfiles" container, inside the "miscfiles" Azure Storage Account that is access with the "xx508xx63817x752xx74004x30705xx92x58349x5x78f5xx34xxxxx51" token. After upload the local file will be deleted. .EXAMPLE PS C:\> $AzureParams = Get-D365ActiveAzureStorageConfig PS C:\> New-D365Bacpac | Invoke-D365AzureStorageUpload @AzureParams This will get the current Azure Storage Account configuration details and use them as parameters to upload the file to an Azure Storage Account. .EXAMPLE PS C:\> New-D365Bacpac | Invoke-D365AzureStorageUpload This will generate a new bacpac file using the "New-D365Bacpac" cmdlet. The file will be uploaded to an Azure Storage Account using the "Invoke-D365AzureStorageUpload" cmdlet. This will use the default parameter values that are based on the configuration stored inside "Get-D365ActiveAzureStorageConfig" for the "Invoke-D365AzureStorageUpload" cmdlet. .EXAMPLE PS C:\> Invoke-D365AzureStorageUpload -AccountId "miscfiles" -SAS "sv2018-03-28&siunlisted&src&sigAUOpdsfpoWE976ASDhfjkasdf(5678sdfhk" -Container "backupfiles" -Filepath "c:\temp\bacpac\UAT_20180701.bacpac" -DeleteOnUpload This will upload the "c:\temp\bacpac\UAT_20180701.bacpac" up to the "backupfiles" container, inside the "miscfiles" Azure Storage Account. A SAS key is used to gain access to the container and uploading the file to it. .NOTES Tags: Azure, Azure Storage, Config, Configuration, Token, Blob, File, Files, Bacpac, Container Author: M�tz Jensen (@Splaxi) The cmdlet supports piping and can be used in advanced scenarios. See more on github and the wiki pages. #> function Invoke-D365AzureStorageUpload { [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory = $false)] [string] $AccountId = $Script:AccountId, [Parameter(Mandatory = $false)] [string] $AccessToken = $Script:AccessToken, [Parameter(Mandatory = $false)] [string] $SAS = $Script:SAS, [Parameter(Mandatory = $false)] [Alias('Blob')] [Alias('Blobname')] [string] $Container = $Script:Container, [Parameter(Mandatory = $true, ParameterSetName = 'Default', ValueFromPipeline = $true)] [Parameter(Mandatory = $true, ParameterSetName = 'Pipeline', ValueFromPipelineByPropertyName = $true)] [Alias('File')] [Alias('Path')] [string] $Filepath, [switch] $DeleteOnUpload ) BEGIN { if (([string]::IsNullOrEmpty($AccountId) -eq $true) -or ([string]::IsNullOrEmpty($Container)) -or (([string]::IsNullOrEmpty($AccessToken)) -and ([string]::IsNullOrEmpty($SAS)))) { Write-PSFMessage -Level Host -Message "It seems that you are missing some of the parameters. Please make sure that you either supplied them or have the right configuration saved." Stop-PSFFunction -Message "Stopping because of missing parameters" return } } PROCESS { if (Test-PSFFunctionInterrupt) { return } Invoke-TimeSignal -Start try { if ([string]::IsNullOrEmpty($SAS)) { Write-PSFMessage -Level Verbose -Message "Working against Azure Storage Account with AccessToken" $storageContext = new-AzureStorageContext -StorageAccountName $AccountId.ToLower() -StorageAccountKey $AccessToken } else { $conString = $("BlobEndpoint=https://{0}.blob.core.windows.net/;QueueEndpoint=https://{0}.queue.core.windows.net/;FileEndpoint=https://{0}.file.core.windows.net/;TableEndpoint=https://{0}.table.core.windows.net/;SharedAccessSignature={1}" -f $AccountId.ToLower(), $SAS) Write-PSFMessage -Level Verbose -Message "Working against Azure Storage Account with SAS" -Target $conString $storageContext = new-AzureStorageContext -ConnectionString $conString } $cloudStorageAccount = [Microsoft.WindowsAzure.Storage.CloudStorageAccount]::Parse($storageContext.ConnectionString) $blobClient = $cloudStorageAccount.CreateCloudBlobClient() $blobContainer = $blobClient.GetContainerReference($Container.ToLower()); Write-PSFMessage -Level Verbose -Message "Start uploading the file to Azure" $FileName = Split-Path $Filepath -Leaf $blockBlob = $blobContainer.GetBlockBlobReference($FileName) $blockBlob.UploadFromFile($Filepath) if ($DeleteOnUpload) { Remove-Item $Filepath -Force } [PSCustomObject]@{ File = $Filepath Filename = $FileName } } catch { Write-PSFMessage -Level Host -Message "Something went wrong while working against the Azure Storage Account" -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" return } finally { Invoke-TimeSignal -End } } END {} } <# .SYNOPSIS Run the Best Practice .DESCRIPTION Run the Best Practice checks against modules and models .PARAMETER BinDir The path to the bin directory for the environment Default path is the same as the AOS service PackagesLocalDirectory\bin .PARAMETER MetaDataDir The path to the meta data directory for the environment Default path is the same as the aos service PackagesLocalDirectory .PARAMETER Module Name of the Module to analyse .PARAMETER Model Name of the Model to analyse .PARAMETER LogDir Path where you want to store the log outputs generated from the best practice analyser .PARAMETER PackagesRoot Instructs the cmdlet to use binary metadata .PARAMETER ShowOriginalProgress Instruct the cmdlet to show the standard output in the console Default is $false which will silence the standard output .PARAMETER RunFixers Instructs the cmdlet to invoke the fixers for the identified warnings .EXAMPLE PS C:\> Invoke-D365BestPractice -module "ApplicationSuite" -model "MyOverLayerModel" This will execute the best practice checks against MyOverLayerModel in the ApplicationSuite Module. The default output will be silenced. The XML log file will be written to "c:\temp\d365fo.tools\ApplicationSuite\Dynamics.AX.MyOverLayerModel.xppbp.xml". The log file will be written to "c:\temp\d365fo.tools\ApplicationSuite\Dynamics.AX.MyOverLayerModel.xppbp.log". .EXAMPLE PS C:\> Invoke-D365BestPractice -module "ApplicationSuite" -model "MyOverLayerModel" -ShowOriginalProgress This will execute the best practice checks against MyOverLayerModel in the ApplicationSuite Module. The output from the best practice check process will be written to the console / host. The XML log file will be written to "c:\temp\d365fo.tools\ApplicationSuite\Dynamics.AX.MyOverLayerModel.xppbp.xml". The log file will be written to "c:\temp\d365fo.tools\ApplicationSuite\Dynamics.AX.MyOverLayerModel.xppbp.log". .NOTES Tags: Best Practice, BP, BPs, Module, Model, Quality Author: Gert Van Der Heyden (@gertvdheyden) Author: M�tz Jensen (@Splaxi) #> function Invoke-D365BestPractice { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding()] [OutputType('[PsCustomObject]')] param ( [Parameter(Mandatory = $false, Position = 1 )] [string] $BinDir = "$Script:PackageDirectory\bin", [Parameter(Mandatory = $false, Position = 2 )] [string] $MetaDataDir = "$Script:MetaDataDir", [Parameter(Mandatory = $true, Position = 3 )] [Alias('Package')] [string] $Module, [Parameter(Mandatory = $true, Position = 4 )] [string] $Model, [Parameter(Mandatory = $false, Position = 5 )] [string] $LogDir = (Join-Path $Script:DefaultTempPath $Module), [Parameter(Mandatory = $false, Position = 6 )] [switch] $PackagesRoot, [Parameter(Mandatory = $false, Position = 7 )] [switch] $ShowOriginalProgress, [Parameter(Mandatory = $false, Position = 8 )] [switch] $RunFixers ) Invoke-TimeSignal -Start $tool = "xppbp.exe" $executable = Join-Path $BinDir $tool if (-not (Test-PathExists -Path $MetaDataDir, $BinDir -Type Container)) {return} if (-not (Test-PathExists -Path $LogDir -Type Container -Create)) {return} if (-not (Test-PathExists -Path $executable -Type Leaf)) {return} $logFile = Join-Path $LogDir "Dynamics.AX.$Model.xppbp.log" $logXmlFile = Join-Path $LogDir "Dynamics.AX.$Model.xppbp.xml" $params = @( "-metadata=`"$MetaDataDir`"", "-all", "-module=`"$Module`"", "-model=`"$Model`"", "-xmlLog=`"$logXmlFile`"", "-log=`"$logFile`"" ) if ($PackagesRoot -eq $true) { $params +="-packagesroot=`"$MetaDataDir`"" } if ($RunFixers -eq $true) { $params +="-runfixers" } Invoke-Process -Executable $executable -Params $params -ShowOriginalProgress:$ShowOriginalProgress Invoke-TimeSignal -End [PSCustomObject]@{ LogFile = $logFile XmlLogFile = $logXmlFile } } <# .SYNOPSIS Invoke the one of the data flush classes .DESCRIPTION Invoke one of the runnable classes that is clearing cache, data or something else .PARAMETER URL URL to the Dynamics 365 instance you want to clear the AOD cache on .PARAMETER Class The class that you want to execute. Default value is "SysFlushAod" .EXAMPLE PS C:\> Invoke-D365DataFlush This will make a call against the default URL for the machine and have it execute the SysFlushAOD class. .EXAMPLE PS C:\> Invoke-D365DataFlush -Class SysFlushData,SysFlushAod This will make a call against the default URL for the machine and have it execute the SysFlushData and SysFlushAod classes. .NOTES Tags: Flush, Url, Servicing Author: M�tz Jensen (@Splaxi) #> function Invoke-D365DataFlush { [CmdletBinding()] param ( [Parameter(Mandatory = $false, Position = 1 )] [string] $Url, [ValidateSet('SysFlushData', 'SysFlushAod', 'SysDataCacheParameters')] [string[]] $Class = "SysFlushAod" ) if ($PSBoundParameters.ContainsKey("URL")) { foreach ($item in $Class) { Write-PSFMessage -Level Verbose -Message "Executing Invoke-D365SysRunnerClass with $item" -Target $item Invoke-D365SysRunnerClass -ClassName $item -Url $URL } } else { foreach ($item in $Class) { Write-PSFMessage -Level Verbose -Message "Executing Invoke-D365SysRunnerClass with $item" -Target $item Invoke-D365SysRunnerClass -ClassName $item } } } <# .SYNOPSIS Invoke the synchronization process used in Visual Studio .DESCRIPTION Uses the sync.exe (engine) to synchronize the database for the environment .PARAMETER BinDirTools Path to where the tools on the machine can be found Default value is normally the AOS Service PackagesLocalDirectory\bin .PARAMETER MetadataDir Path to where the tools on the machine can be found Default value is normally the AOS Service PackagesLocalDirectory .PARAMETER LogPath The path where the log file will be saved .PARAMETER SyncMode The sync mode the sync engine will use Default value is: "FullAll" .PARAMETER Verbosity Parameter used to instruct the level of verbosity the sync engine has to report back Default value is: "Normal" .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-D365DBSync This will invoke the sync engine and have it work against the database. .EXAMPLE PS C:\> Invoke-D365DBSync -Verbose This will invoke the sync engine and have it work against the database. It will output the same level of details that Visual Studio would normally do. .NOTES Tags: Database, Sync, SyncDB, Synchronization, Servicing Author: Rasmus Andersen (@ITRasmus) Author: M�tz Jensen (@Splaxi) When running the 'FullAll' (default) the command requires an elevated console / Run As Administrator. #> function Invoke-D365DBSync { [CmdletBinding()] param ( [Parameter(Mandatory = $false, Position = 0)] [string]$BinDirTools = $Script:BinDirTools, [Parameter(Mandatory = $false, Position = 1)] [string]$MetadataDir = $Script:MetaDataDir, [Parameter(Mandatory = $false, Position = 2)] [string]$LogPath = "C:\temp\D365FO.Tools\Sync", [Parameter(Mandatory = $false, Position = 3)] #[ValidateSet('None', 'PartialList','InitialSchema','FullIds','PreTableViewSyncActions','FullTablesAndViews','PostTableViewSyncActions','KPIs','AnalysisEnums','DropTables','FullSecurity','PartialSecurity','CleanSecurity','ADEs','FullAll','Bootstrap','LegacyIds','Diag')] [string]$SyncMode = 'FullAll', [Parameter(Mandatory = $false, Position = 4)] [ValidateSet('Normal', 'Quiet', 'Minimal', 'Normal', 'Detailed', 'Diagnostic')] [string]$Verbosity = 'Normal', [Parameter(Mandatory = $false, Position = 5)] [string]$DatabaseServer = $Script:DatabaseServer, [Parameter(Mandatory = $false, Position = 6)] [string]$DatabaseName = $Script:DatabaseName, [Parameter(Mandatory = $false, Position = 7)] [string]$SqlUser = $Script:DatabaseUserName, [Parameter(Mandatory = $false, Position = 8)] [string]$SqlPwd = $Script:DatabaseUserPassword ) #! The way the sync engine works is that it uses the connection string for some operations, #! but for FullSync / FullAll it depends on the database details from the same assemblies that #! we rely on. So the testing of how to run this cmdlet is a bit different than others Write-PSFMessage -Level Debug -Message "Testing if run on LocalHostedTier1 and console isn't elevated" if ($Script:EnvironmentType -eq [EnvironmentType]::LocalHostedTier1 -and !$script:IsAdminRuntime){ Write-PSFMessage -Level Host -Message "It seems that you ran this cmdlet <c='em'>non-elevated</c> and on a <c='em'>local VM / local vhd</c>. Being on a local VM / local VHD requires you to run this cmdlet from an elevated console. Please exit the current console and start a new with `"Run As Administrator`"" Stop-PSFFunction -Message "Stopping because of missing parameters" return } elseif (!$script:IsAdminRuntime -and $Script:UserIsAdmin -and $Script:EnvironmentType -ne [EnvironmentType]::LocalHostedTier1) { Write-PSFMessage -Level Host -Message "It seems that you ran this cmdlet <c='em'>non-elevated</c> and as an <c='em'>administrator</c>. You should either logon as a non-admin user account on this machine or run this cmdlet from an elevated console. Please exit the current console and start a new with `"Run As Administrator`" or simply logon as another user" Stop-PSFFunction -Message "Stopping because of missing parameters" return } $executable = Join-Path $BinDirTools "SyncEngine.exe" if (-not (Test-PathExists -Path $executable -Type Leaf)) {return} if (-not (Test-PathExists -Path $MetadataDir -Type Container)) {return} if (-not (Test-PathExists -Path $LogPath -Type Container -Create)) {return} Write-PSFMessage -Level Debug -Message "Testing if the SyncEngine is already running." $syncEngine = Get-Process -Name "SyncEngine" -ErrorAction SilentlyContinue if ($null -ne $syncEngine) { Write-PSFMessage -Level Host -Message "A instance of SyncEngine is <c='em'>already running</c>. Please <c='em'>wait</c> for it to finish or <c='em'>kill it</c>." Stop-PSFFunction -Message "Stopping because SyncEngine.exe already running" return } Write-PSFMessage -Level Debug -Message "Build the parameters for the command to execute." $param = " -syncmode=$($SyncMode.ToLower())" $param += " -verbosity=$($Verbosity.ToLower())" $param += " -metadatabinaries=`"$MetadataDir`"" $param += " -connect=`"server=$DatabaseServer;Database=$DatabaseName; User Id=$SqlUser;Password=$SqlPwd;`"" Write-PSFMessage -Level Debug -Message "Starting the SyncEngine with the parameters." -Target $param $process = Start-Process -FilePath $executable -ArgumentList $param -PassThru -RedirectStandardOutput "$LogPath\output.log" -RedirectStandardError "$LogPath\error.log" -WindowStyle "Hidden" $lineTotalCount = 0 $lineCount = 0 Invoke-TimeSignal -Start while ($process.HasExited -eq $false) { foreach ($line in Get-Content "$LogPath\output.log") { $lineCount++ if ($lineCount -gt $lineTotalCount) { Write-Verbose $line $lineTotalCount++ } } $lineCount = 0 Start-Sleep -Seconds 2 } foreach ($line in Get-Content "$LogPath\output.log") { $lineCount++ if ($lineCount -gt $lineTotalCount) { Write-Verbose $line $lineTotalCount++ } } foreach ($line in Get-Content "$LogPath\error.log") { Write-PSFMessage -Level Critical -Message "$line" } Invoke-TimeSignal -End } <# .SYNOPSIS Install a license for a 3. party solution .DESCRIPTION Install a license for a 3. party solution using the builtin "Microsoft.Dynamics.AX.Deployment.Setup.exe" executable .PARAMETER Path Path to the license 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 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 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-D365InstallLicense -Path c:\temp\d365fo.tools\license.txt This will use the default paths and start the Microsoft.Dynamics.AX.Deployment.Setup.exe with the needed parameters to import / install the license file. .EXAMPLE PS C:\> Invoke-D365InstallLicense -Path c:\temp\d365fo.tools\license.txt -ShowOriginalProgress This will use the default paths and start the Microsoft.Dynamics.AX.Deployment.Setup.exe with the needed parameters to import / install the license file. The output from the installation process will be written to the console / host. .NOTES Tags: License, Install, ISV, 3. Party, Servicing Author: M�tz Jensen (@splaxi) #> function Invoke-D365InstallLicense { [CmdletBinding()] param ( [Parameter(Mandatory = $True, Position = 1 )] [Alias('File')] [string] $Path, [Parameter(Mandatory = $false, Position = 2)] [string] $DatabaseServer = $Script:DatabaseServer, [Parameter(Mandatory = $false, Position = 3)] [string] $DatabaseName = $Script:DatabaseName, [Parameter(Mandatory = $false, Position = 4)] [string] $SqlUser = $Script:DatabaseUserName, [Parameter(Mandatory = $false, Position = 5)] [string] $SqlPwd = $Script:DatabaseUserPassword, [Parameter(Mandatory = $false, Position = 6 )] [string] $MetaDataDir = "$Script:MetaDataDir", [Parameter(Mandatory = $false, Position = 7 )] [string] $BinDir = "$Script:BinDir", [Parameter(Mandatory = $False)] [switch] $ShowOriginalProgress ) $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 $Path,$executable -Type Leaf)) {return} Invoke-TimeSignal -Start $params = @("-isemulated", "true", "-sqluser", "$SqlUser", "-sqlpwd", "$SqlPwd", "-sqlserver", "$DatabaseServer", "-sqldatabase", "$DatabaseName", "-metadatadir", "$MetaDataDir", "-bindir", "$BinDir", "-setupmode", "importlicensefile", "-licensefilename", "`"$Path`"") Invoke-Process -Executable $executable -Params $params -ShowOriginalProgress:$ShowOriginalProgress Invoke-TimeSignal -End } <# .SYNOPSIS Upload a file to a LCS project .DESCRIPTION Upload a file to a LCS project using the API provided by Microsoft .PARAMETER ProjectId The project id for the Dynamics 365 for Finance & Operations project inside LCS .PARAMETER ClientId The Azure Registered Application Id / Client Id obtained while creating a Registered App inside the Azure Portal .PARAMETER Username The username of the account that you want to impersonate It can either be your personal account or a service account .PARAMETER Password The password of the account that you want to impersonate .PARAMETER FilePath Path to the file that you want to upload to the Asset Library on LCS .PARAMETER FileType Type of file you want to upload Valid options: "DeployablePackage" "DatabaseBackup" .PARAMETER FileName Name to be assigned / shown on LCS .PARAMETER FileDescription 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:\> Invoke-D365LcsUpload -ProjectId 123456789 -ClientId "9b4f4503-b970-4ade-abc6-2c086e4c4929" -Username claire@contoso.com -Password "pass@word1" -FilePath "C:\temp\d365fo.tools\GOLDEN.bacpac" -FileType "DatabaseBackup" -FileName "ReadyForTesting" -FileDescription "Contains all customers & vendors" -LcsApiUri "https://lcsapi.lcs.dynamics.com" This will upload the "C:\temp\d365fo.tools\GOLDEN.bacpac" file to the LCS project 123456789. It will authenticate against the AAD with the ClientId "9b4f4503-b970-4ade-abc6-2c086e4c4929", the Username Claire@contoso.com and the Password "pass@word1". The file will be placed in the sub folder "Database Backup". The file will be named "ReadyForTesting" inside the Asset Library in LCS. The file is uploaded against the NON-EUROPE LCS API. .EXAMPLE PS C:\> Invoke-D365LcsUpload -FilePath "C:\temp\d365fo.tools\GOLDEN.bacpac" -FileType "DatabaseBackup" -FileName "ReadyForTesting" -FileDescription "Contains all customers & vendors" This will upload the "C:\temp\d365fo.tools\GOLDEN.bacpac" file. The file will be placed in the sub folder "Database Backup". The file will be named "ReadyForTesting" inside the Asset Library in LCS. The ProjectId, ClientId, Username, Password and LcsApiUri parameters are read from the configuration storage, that is configured by the Set-D365LcsUploadConfig cmdlet. .NOTES Tags: Environment, Url, Config, Configuration, LCS, Upload, Api, AAD, Token Author: M�tz Jensen (@Splaxi) #> function Invoke-D365LcsUpload { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingUserNameAndPassWordParams", "")] [CmdletBinding()] [OutputType()] param( [Parameter(Mandatory = $false, Position = 1)] [int]$ProjectId = $Script:LcsUploadProjectid, [Parameter(Mandatory = $false, Position = 2)] [string] $ClientId = $Script:LcsUploadClientid, [Parameter(Mandatory = $false, Position = 3)] [string] $Username = $Script:LcsUploadUsername, [Parameter(Mandatory = $false, Position = 4)] [string] $Password = $Script:LcsUploadPassword, [Parameter(Mandatory = $true, Position = 5)] [string]$FilePath, [Parameter(Mandatory = $false, Position = 6)] [ValidateSet("DeployablePackage", "DatabaseBackup")] [string]$FileType = "DatabaseBackup", [Parameter(Mandatory = $false, Position = 7)] [string]$FileName, [Parameter(Mandatory = $false, Position = 8)] [string]$FileDescription, [Parameter(Mandatory = $false, Position = 9)] [ValidateSet("https://lcsapi.lcs.dynamics.com", "https://lcsapi.eu.lcs.dynamics.com")] [string]$LcsApiUri = $Script:LcsUploadApiUri ) Invoke-TimeSignal -Start $fileNameExtracted = Split-Path $FilePath -Leaf if ($FileName -eq "") { $FileName = $fileNameExtracted } $scope = "openid" $grantType = "password" $authToken = Invoke-AadAuthentication -Resource $LcsApiUri -GrantType $grantType -ClientId $ClientId -Username $Username -Password $Password -Scope $scope if (Test-PSFFunctionInterrupt) { return } Write-PSFMessage -Level Verbose -Message "Auth token" -Target $authToken $bearerToken = "Bearer {0}" -f $authToken $blobDetails = Start-LcsUpload -Token $bearerToken -ProjectId $ProjectId -FileType $FileType -LcsApiUri $LcsApiUri -Name $FileName -Description $FileDescription if (Test-PSFFunctionInterrupt) { return } Write-PSFMessage -Level Verbose -Message "Start response" -Target $blobDetails $uploadResponse = Copy-FileToLcsBlob -FilePath $FilePath -FullUri $blobDetails.FileLocation if (Test-PSFFunctionInterrupt) { return } Write-PSFMessage -Level Verbose -Message "Upload response" -Target $uploadResponse $ackResponse = Complete-LcsUpload -Token $bearerToken -ProjectId $ProjectId -AssetId $blobDetails.Id -LcsApiUri $LcsApiUri if (Test-PSFFunctionInterrupt) { return } Write-PSFMessage -Level Verbose -Message "Commit response" -Target $ackResponse Invoke-TimeSignal -End [PSCustomObject]@{ AssetId = $blobDetails.Id Name = $FileName } } <# .SYNOPSIS Invoke a http request for a Logic App .DESCRIPTION Invoke a Logic App using a http request and pass a json object with details about the calling function .PARAMETER Url The URL for the http endpoint that you want to invoke .PARAMETER Payload The data content you want to send to the LogicApp .EXAMPLE PS C:\> Invoke-D365SyncDB | Invoke-D365LogicApp This will execute the sync process and when it is done it will invoke a Azure Logic App with the default parameters that have been configured for the system. .NOTES Tags: LogicApp, Logic App, Configuration, Url, Notification Author: M�tz Jensen (@Splaxi) #> function Invoke-D365LogicApp { param ( [string] $Url = (Get-D365LogicAppConfig).Url, [Parameter(Mandatory = $false)] [string] $Payload = "{}" ) Invoke-PSNHttpEndpoint -Url $URL -Payload $Payload } <# .SYNOPSIS Invoke a http request for a Logic App .DESCRIPTION Invoke a Logic App using a http request and pass a json object with details about the calling function .PARAMETER Url The URL for the http endpoint that you want to invoke .PARAMETER Email The email address of the receiver of the message that the cmdlet will send .PARAMETER Subject Subject string to apply to the email and to the IM message .PARAMETER Message The message you want to pass onto the Logic App .PARAMETER IncludeAll Switch to instruct the cmdlet to include all cmdlets (names only) from the pipeline .PARAMETER AsJob Switch to instruct the cmdlet to run the invocation as a job (async) .EXAMPLE PS C:\> Invoke-D365SyncDB | Invoke-D365LogicAppMessage This will execute the sync process and when it is done it will invoke a Azure Logic App with the default parameters that have been configured for the system. .EXAMPLE PS C:\> Invoke-D365SyncDB | Invoke-D365LogicAppMessage -Email administrator@contoso.com -Subject "Work is done" -Url https://prod-35.westeurope.logic.azure.com:443/ This will execute the sync process and when it is done it will invoke a Azure Logic App with the email, subject and URL parameters that are needed to invoke an Azure Logic App. .NOTES Tags: LogicApp, Logic App, Configuration, Url, Email, Notification, Message, Email Author: M�tz Jensen (@Splaxi) #> function Invoke-D365LogicAppMessage { param ( [string] $Url = (Get-D365LogicAppConfig).Url, [string] $Email = (Get-D365LogicAppConfig).Email, [string] $Subject = (Get-D365LogicAppConfig).Subject, [string] $Message, [switch] $IncludeAll, [switch] $AsJob ) begin { } process { $pipes = $MyInvocation.Line.Split("|") $arrList = New-Object -TypeName "System.Collections.ArrayList" foreach ($item in $pipes.Trim()) { $null = $arrList.Add( $item.Split(" ")[0]) } $strMessage = ""; if ($IncludeAll) { $strMessage = $arrList -Join ", " $strMessage = "The following list of cmdlets has executed: $strMessage" } elseif (-not ($null -eq $Message) -and (-not("" -eq $Message))) { $strMessage = $Message } else { $strMessage = $arrList[$MyInvocation.PipelinePosition - 2] $strMessage = "The following list of cmdlets has executed: $strMessage" } Invoke-PSNMessage -Url $URL -ReceiverEmail $Email -Subject $Subject -Message $strMessage -AsJob:$AsJob } end { } } <# .SYNOPSIS Compile a package / module / model .DESCRIPTION Compile a package / module / model using the builtin "xppc.exe" executable to compile source code .PARAMETER Module The package to compile .PARAMETER OutputDir The path to the folder to save generated artifacts .PARAMETER LogDir The path to the folder to save logs .PARAMETER MetaDataDir The path to the meta data directory for the environment Default path is the same as the aos service PackagesLocalDirectory .PARAMETER ReferenceDir The full path of a folder containing all assemblies referenced from X++ code 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 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-D365ModuleCompile -Module MyModel This will use the default paths and start the xppc.exe with the needed parameters to compile MyModel package. The default output from the compile will be silenced. If an error should occur, both the standard output and error output will be written to the console / host. .EXAMPLE PS C:\> Invoke-D365ModuleCompile -Module MyModel -ShowOriginalProgress This will use the default paths and start the xppc.exe with the needed parameters to compile MyModel package. The output from the compile will be written to the console / host. .NOTES Tags: Compile, Model, Servicing, X++ Author: Ievgen Miroshnikov (@IevgenMir) Author: M�tz Jensen (@Splaxi) #> function Invoke-D365ModuleCompile { [CmdletBinding()] [OutputType('[PsCustomObject]')] param ( [Parameter(Mandatory = $True, Position = 1 )] [string] $Module, [Parameter(Mandatory = $False, Position = 2 )] [Alias('Output')] [string] $OutputDir = (Join-Path $Script:MetaDataDir $Module), [Parameter(Mandatory = $False, Position = 3 )] [string] $LogDir = (Join-Path $Script:DefaultTempPath $Module), [Parameter(Mandatory = $False, Position = 4 )] [string] $MetaDataDir = $Script:MetaDataDir, [Parameter(Mandatory = $False, Position = 5)] [string] $ReferenceDir = $Script:MetaDataDir, [Parameter(Mandatory = $False, Position = 6 )] [string] $BinDir = $Script:BinDirTools, [Parameter(Mandatory = $False, Position = 7 )] [switch] $ShowOriginalProgress ) Invoke-TimeSignal -Start $tool = "xppc.exe" $executable = Join-Path $BinDir $tool if (-not (Test-PathExists -Path $MetaDataDir, $BinDir -Type Container)) {return} if (-not (Test-PathExists -Path $executable -Type Leaf)) {return} if (-not (Test-PathExists -Path $LogDir -Type Container -Create)) {return} if (Test-PSFFunctionInterrupt) { return } $logFile = Join-Path $LogDir "Dynamics.AX.$Module.xppc.log" $logXmlFile = Join-Path $LogDir "Dynamics.AX.$Module.xppc.xml" $params = @("-metadata=`"$MetaDataDir`"", "-modelmodule=`"$Module`"", "-output=`"$OutputDir\bin`"", "-referencefolder=`"$ReferenceDir`"", "-log=`"$logFile`"", "-xmlLog=`"$logXmlFile`"", "-verbose" ) Invoke-Process -Executable $executable -Params $params -ShowOriginalProgress:$ShowOriginalProgress Invoke-TimeSignal -End [PSCustomObject]@{ LogFile = $logFile XmlLogFile = $logXmlFile PSTypeName = 'D365FO.TOOLS.ModuleCompileOutput' } } <# .SYNOPSIS Compile a package .DESCRIPTION Compile a package using the builtin "xppc.exe" executable to compile source code, "labelc.exe" to compile label files and "reportsc.exe" to compile reports .PARAMETER Module The package to compile .PARAMETER OutputDir The path to the folder to save assemblies .PARAMETER LogDir The path to the folder to save logs .PARAMETER MetaDataDir The path to the meta data directory for the environment .PARAMETER ReferenceDir The full path of a folder containing all assemblies referenced from X++ code .PARAMETER BinDir The path to the bin directory for the environment Default path is the same as the aos service PackagesLocalDirectory\bin .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-D365ModuleFullCompile -Module MyModel This will use the default paths and start the xppc.exe with the needed parameters to compile MyModel package. The default output from all the different steps will be silenced. .EXAMPLE PS C:\> Invoke-D365ModuleFullCompile -Module MyModel -ShowOriginalProgress This will use the default paths and start the xppc.exe with the needed parameters to copmile MyModel package. The default output from the different steps will be written to the console / host. .NOTES Tags: Compile, Model, Servicing Author: Ievgen Miroshnikov (@IevgenMir) Author: M�tz Jensen (@Splaxi) #> function Invoke-D365ModuleFullCompile { [CmdletBinding()] [OutputType('[PsCustomObject]')] param ( [Parameter(Mandatory = $True, Position = 1 )] [string] $Module, [Parameter(Mandatory = $False, Position = 2 )] [Alias('Output')] [string] $OutputDir = (Join-Path $Script:MetaDataDir $Module), [Parameter(Mandatory = $False, Position = 3 )] [string] $LogDir = (Join-Path $Script:DefaultTempPath $Module), [Parameter(Mandatory = $False, Position = 4 )] [string] $MetaDataDir = $Script:MetaDataDir, [Parameter(Mandatory = $False, Position = 5)] [string] $ReferenceDir = $Script:MetaDataDir, [Parameter(Mandatory = $False, Position = 6 )] [string] $BinDir = $Script:BinDirTools, [Parameter(Mandatory = $False, Position = 7 )] [switch] $ShowOriginalProgress ) Invoke-TimeSignal -Start if (-not (Test-PathExists -Path $MetaDataDir, $BinDir -Type Container)) {return} if (-not (Test-PathExists -Path $LogDir -Type Container -Create)) {return} $resModuleCompile = Invoke-D365ModuleCompile @PSBoundParameters $resLabelGeneration = Invoke-D365ModuleLabelGeneration @PSBoundParameters $resReportsCompile = Invoke-D365ModuleReportsCompile @PSBoundParameters Invoke-TimeSignal -End $resModuleCompile #| Select-PSFObject -TypeName "D365FO.TOOLS.ModuleCompileOutput" @{Name = "OutputOrigin"; Expression = {"ModuleCompile"}}, "LogFile as LogFile", "XmlLogFile as XmlLogFile", @{Name = "ErrorLogFile"; Expression = {""}} $resLabelGeneration #| Select-PSFObject @{Name = "OutputOrigin"; Expression = {"LabelGeneration"}}, "OutLogFile as LogFile", @{Name = "XmlLogFile"; Expression = {""}}, "ErrorLogFile as ErrorLogFile" $resReportsCompile #| Select-PSFObject @{Name = "OutputOrigin"; Expression = {"ReportsCompile"}}, "LogFile as LogFile", "XmlLogFile as XmlLogFile", @{Name = "ErrorLogFile"; Expression = {""}} } <# .SYNOPSIS Generate labels for a package / module / model .DESCRIPTION Generate labels for a package / module / model using the builtin "labelc.exe" .PARAMETER Module Name of the package that you want to work against .PARAMETER OutputDir The path to the folder to save generated artifacts .PARAMETER LogDir The path to the folder to save logs .PARAMETER MetaDataDir The path to the meta data directory for the environment Default path is the same as the aos service PackagesLocalDirectory .PARAMETER ReferenceDir The full path of a folder containing all assemblies referenced from X++ code 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 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-D365ModuleLabelGeneration -Module MyModel This will use the default paths and start the labelc.exe with the needed parameters to labels from the MyModel package. The default output from the generation process will be silenced. If an error should occur, both the standard output and error output will be written to the console / host. .EXAMPLE PS C:\> Invoke-D365ModuleLabelGeneration -Module MyModel -ShowOriginalProgress This will use the default paths and start the labelc.exe with the needed parameters to labels from the MyModel package. The output from the compile will be written to the console / host. .NOTES Tags: Compile, Model, Servicing, Label, Labels Author: Ievgen Miroshnikov (@IevgenMir) Author: M�tz Jensen (@Splaxi) #> function Invoke-D365ModuleLabelGeneration { [CmdletBinding()] [OutputType('[PsCustomObject]')] param ( [Parameter(Mandatory = $True, Position = 1 )] [string] $Module, [Parameter(Mandatory = $False, Position = 2 )] [Alias('Output')] [string] $OutputDir = (Join-Path $Script:MetaDataDir $Module), [Parameter(Mandatory = $False, Position = 3 )] [string] $LogDir = (Join-Path $Script:DefaultTempPath $Module), [Parameter(Mandatory = $False, Position = 4 )] [string] $MetaDataDir = $Script:MetaDataDir, [Parameter(Mandatory = $False, Position = 5)] [string] $ReferenceDir = $Script:MetaDataDir, [Parameter(Mandatory = $False, Position = 6 )] [string] $BinDir = $Script:BinDirTools, [Parameter(Mandatory = $False, Position = 7 )] [switch] $ShowOriginalProgress ) Invoke-TimeSignal -Start $tool = "labelc.exe" $executable = Join-Path $BinDir $tool if (-not (Test-PathExists -Path $MetaDataDir, $BinDir -Type Container)) {return} if (-not (Test-PathExists -Path $executable -Type Leaf)) {return} if (-not (Test-PathExists -Path $LogDir -Type Container -Create)) {return} $logFile = Join-Path $LogDir "Dynamics.AX.$Module.labelc.log" $logErrorFile = Join-Path $LogDir "Dynamics.AX.$Module.labelc.err" $params = @("-metadata=`"$MetaDataDir`"", "-modelmodule=`"$Module`"", "-output=`"$OutputDir\Resources`"", "-outlog=`"$logFile`"", "-errlog=`"$logErrorFile`"" ) Invoke-Process -Executable $executable -Params $params -ShowOriginalProgress:$ShowOriginalProgress Invoke-TimeSignal -End [PSCustomObject]@{ OutLogFile = $logFile ErrorLogFile = $logErrorFile PSTypeName = 'D365FO.TOOLS.ModuleLabelGenerationOutput' } } <# .SYNOPSIS Generate reports for a package / module / model .DESCRIPTION Generate reports for a package / module / model using the builtin "ReportsC.exe" .PARAMETER Module Name of the package that you want to work against .PARAMETER OutputDir The path to the folder to save generated artifacts .PARAMETER LogDir The path to the folder to save logs .PARAMETER MetaDataDir The path to the meta data directory for the environment Default path is the same as the aos service PackagesLocalDirectory .PARAMETER ReferenceDir The full path of a folder containing all assemblies referenced from X++ code 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 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-D365ModuleReportsCompile -Module MyModel This will use the default paths and start the ReportsC.exe with the needed parameters to compile the reports from the MyModel package. The default output from the reports compile will be silenced. If an error should occur, both the standard output and error output will be written to the console / host. .EXAMPLE PS C:\> Invoke-D365ModuleReportsCompile -Module MyModel -ShowOriginalProgress This will use the default paths and start the ReportsC.exe with the needed parameters to compile the reports from the MyModel package. The output from the compile will be written to the console / host. .NOTES Tags: Compile, Model, Servicing, Report, Reports Author: Ievgen Miroshnikov (@IevgenMir) Author: M�tz Jensen (@Splaxi) #> function Invoke-D365ModuleReportsCompile { [CmdletBinding()] [OutputType('[PsCustomObject]')] param ( [Parameter(Mandatory = $True, Position = 1 )] [string] $Module, [Parameter(Mandatory = $False, Position = 2 )] [Alias('Output')] [string] $OutputDir = (Join-Path $Script:MetaDataDir $Module), [Parameter(Mandatory = $False, Position = 3 )] [string] $LogDir = (Join-Path $Script:DefaultTempPath $Module), [Parameter(Mandatory = $False, Position = 4 )] [string] $MetaDataDir = $Script:MetaDataDir, [Parameter(Mandatory = $False, Position = 5)] [string] $ReferenceDir = $Script:MetaDataDir, [Parameter(Mandatory = $False, Position = 6 )] [string] $BinDir = $Script:BinDirTools, [Parameter(Mandatory = $False, Position = 7 )] [switch] $ShowOriginalProgress ) Invoke-TimeSignal -Start $tool = "ReportsC.exe" $executable = Join-Path $BinDir $tool if (-not (Test-PathExists -Path $MetaDataDir, $BinDir -Type Container)) {return} if (-not (Test-PathExists -Path $executable -Type Leaf)) {return} if (-not (Test-PathExists -Path $LogDir -Type Container -Create)) {return} $logFile = Join-Path $LogDir "Dynamics.AX.$Module.ReportsC.log" $logXmlFile = Join-Path $LogDir "Dynamics.AX.$Module.ReportsC.xml" $params = @("-metadata=`"$MetaDataDir`"", "-modelmodule=`"$Module`"", "-LabelsPath=`"$MetaDataDir`"", "-output=`"$OutputDir\Reports`"", "-log=`"$logFile`"", "-xmlLog=`"$logXmlFile`"" ) Invoke-Process -Executable $executable -Params $params -ShowOriginalProgress:$ShowOriginalProgress Invoke-TimeSignal -End [PSCustomObject]@{ LogFile = $logFile XmlLogFile = $logXmlFile PSTypeName = 'D365FO.TOOLS.ModuleReportsCompileOutput' } } <# .SYNOPSIS Invokes the Rearm of Windows license .DESCRIPTION Function used for invoking the rearm functionality inside Windows .PARAMETER Restart Instruct the cmdlet to restart the machine .EXAMPLE PS C:\> Invoke-D365ReArmWindows This will re arm the Windows installation if there is any activation retries left .EXAMPLE PS C:\> Invoke-D365ReArmWindows -Restart This will re arm the Windows installation if there is any activation retries left and restart the computer. .NOTES Author: M�tz Jensen (@Splaxi) #> function Invoke-D365ReArmWindows { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [CmdletBinding()] param ( [Parameter(Mandatory = $false, Position = 1)] [switch]$Restart ) Write-PSFMessage -Level Verbose -Message "Invoking the rearm process." $instance = Get-CimInstance -Class SoftwareLicensingService -Namespace root/cimv2 -ComputerName . Invoke-CimMethod -InputObject $instance -MethodName ReArmWindows if ($Restart) { Restart-Computer -Force } } <# .SYNOPSIS Analyze the runbook .DESCRIPTION Get all the important details from a failed runbook .PARAMETER Path Path to the runbook file that you work against .EXAMPLE PS C:\> Invoke-D365RunbookAnalyzer -Path "C:\DynamicsAX\InstallationRecords\Runbooks\Runbook.xml" This will analyze the Runbook.xml and output all the details about failed steps, the connected error logs and all the unprocessed steps. .EXAMPLE PS C:\> Get-D365Runbook -Latest | Invoke-D365RunbookAnalyzer This will find the latest runbook file and have it analyzed by the Invoke-D365RunbookAnalyzer cmdlet to output any error details. .EXAMPLE PS C:\> Get-D365Runbook -Latest | Invoke-D365RunbookAnalyzer | Out-File "C:\Temp\d365fo.tools\runbook-analyze-results.xml" This will find the latest runbook file and have it analyzed by the Invoke-D365RunbookAnalyzer cmdlet to output any error details. The output will be saved into the "C:\Temp\d365fo.tools\runbook-analyze-results.xml" file. .EXAMPLE PS C:\> Get-D365Runbook -Latest | Backup-D365Runbook -Force | Invoke-D365RunbookAnalyzer This will get the latest runbook from the default location. This will backup the file onto the default "c:\temp\d365fo.tools\runbookbackups\". This will start the Runbook Analyzer on the backup file. .NOTES Tags: Runbook, Servicing, Hotfix, DeployablePackage, Deployable Package, InstallationRecordsDirectory, Installation Records Directory Author: M�tz Jensen (@Splaxi) #> function Invoke-D365RunbookAnalyzer { [CmdletBinding()] [OutputType('System.String')] param ( [Parameter(Mandatory = $true, Position = 1, ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true)] [Alias('File')] [string] $Path ) process { if (-not (Test-PathExists -Path $Path -Type Leaf)) { return } $null = $sb = New-Object System.Text.StringBuilder $null = $sb.AppendLine("<D365FO.Tools.Runbook.Analyzer.Output>") [xml]$xmlRunbook = Get-Content $Path $failedSteps = $xmlRunbook.SelectNodes("//RunbookStepList/Step/StepState[text()='Failed']") $failedSteps | ForEach-Object { $null = $sb.AppendLine("<FailedStepInfo>") $stepId = $_.ParentNode | Select-Object -ExpandProperty childnodes | Where-Object {$_.name -like 'ID'} | Select-Object -ExpandProperty InnerText $failedLogs = $xmlRunbook.SelectNodes("//RunbookLogs/Log/StepID[text()='$stepId']") $null = $sb.AppendLine($_.ParentNode.OuterXml) $failedLogs | ForEach-Object { $null = $sb.AppendLine( $_.ParentNode.OuterXml)} $null = $sb.AppendLine("</FailedStepInfo>") } $inProgressSteps = $xmlRunbook.SelectNodes("//RunbookStepList/Step/StepState[text()='InProgress']") $null = $sb.AppendLine("<InProgressStepInfo>") $inProgressSteps | ForEach-Object { $null = $sb.AppendLine( $_.ParentNode.OuterXml)} $null = $sb.AppendLine("</InProgressStepInfo>") $unprocessedSteps = $xmlRunbook.SelectNodes("//RunbookStepList/Step/StepState[text()='NotStarted']") $null = $sb.AppendLine("<UnprocessedStepInfo>") $unprocessedSteps | ForEach-Object { $null = $sb.AppendLine( $_.ParentNode.OuterXml)} $null = $sb.AppendLine("</UnprocessedStepInfo>") $null = $sb.AppendLine("</D365FO.Tools.Runbook.Analyzer.Output>") [xml]$xmlRaw = $sb.ToString() $stringWriter = New-Object System.IO.StringWriter; $xmlWriter = New-Object System.Xml.XmlTextWriter $stringWriter; $xmlWriter.Formatting = "indented"; $xmlRaw.WriteTo($xmlWriter); $xmlWriter.Flush(); $stringWriter.Flush(); $stringWriter.ToString(); } } <# .SYNOPSIS Invoke the SCDPBundleInstall.exe file .DESCRIPTION A cmdlet that wraps some of the cumbersome work of installing updates / hotfixes into a streamlined process .PARAMETER InstallOnly Instructs the cmdlet to only run the Install option and ignore any TFS / VSTS folders and source control in general Use it when testing an update on a local development machine (VM) / onebox .PARAMETER Command The command / job you want the cmdlet to execute Valid options are: Prepare Install Default value is "Prepare" .PARAMETER Path Path to the update package that you want to install into the environment The cmdlet only supports an already extracted ".axscdppkg" file .PARAMETER MetaDataDir The path to the meta data directory for the environment Default path is the same as the aos service PackagesLocalDirectory .PARAMETER TfsWorkspaceDir The path to the TFS Workspace directory that you want to work against Default path is the same as the aos service PackagesLocalDirectory .PARAMETER TfsUri The URI for the TFS Team Site / VSTS Portal that you want to work against Default URI is the one that is configured from inside Visual Studio .PARAMETER ShowModifiedFiles Switch to instruct the cmdlet to show all the modified files afterwards .PARAMETER ShowProgress Switch to instruct the cmdlet to output progress details while servicing the installation .EXAMPLE PS C:\> Invoke-D365SCDPBundleInstall -Path "c:\temp\HotfixPackageBundle.axscdppkg" -InstallOnly This will install the "HotfixPackageBundle.axscdppkg" into the default PackagesLocalDirectory location on the machine. .NOTES Tags: Hotfix, Hotfixes, Updates, Prepare, VSTS, axscdppkg Author: M�tz Jensen (@splaxi) Author: Tommy Skaue (@skaue) #> function Invoke-D365SCDPBundleInstall { [CmdletBinding(DefaultParameterSetName = 'InstallOnly')] param ( [Parameter(Mandatory = $True, ParameterSetName = 'InstallOnly', Position = 0 )] [switch] $InstallOnly, [Parameter(Mandatory = $false, ParameterSetName = 'Tfs', Position = 0 )] [ValidateSet('Prepare', 'Install')] [string] $Command = 'Prepare', [Parameter(Mandatory = $True, Position = 1 )] [Alias('Hotfix')] [Alias('File')] [string] $Path, [Parameter(Mandatory = $False, Position = 2 )] [string] $MetaDataDir = "$Script:MetaDataDir", [Parameter(Mandatory = $False, ParameterSetName = 'Tfs', Position = 3 )] [string] $TfsWorkspaceDir = "$Script:MetaDataDir", [Parameter(Mandatory = $False, ParameterSetName = 'Tfs', Position = 4 )] [string] $TfsUri = "$Script:TfsUri", [Parameter(Mandatory = $False, Position = 4 )] [switch] $ShowModifiedFiles, [Parameter(Mandatory = $False, Position = 5 )] [switch] $ShowProgress ) if (!$script:IsAdminRuntime) { Write-PSFMessage -Level Host -Message "The cmdlet needs <c='em'>administrator permission</c> (Run As Administrator) to be able to update the configuration. Please start an <c='em'>elevated</c> session and run the cmdlet again." Stop-PSFFunction -Message "Stopping because the function is not run elevated" return } Invoke-TimeSignal -Start $StartTime = Get-Date $executable = Join-Path $Script:BinDir "\bin\SCDPBundleInstall.exe" if (!(Test-PathExists -Path $Path,$executable -Type Leaf)) {return} if (!(Test-PathExists -Path $MetaDataDir -Type Container)) {return} Unblock-File -Path $Path #File is typically downloaded and extracted if ($InstallOnly) { $param = @("-install", "-packagepath=$Path", "-metadatastorepath=$MetaDataDir") } else{ if ($TfsUri -eq ""){ Write-PSFMessage -Level Host -Message "No TFS URI provided. Unable to complete the command." Stop-PSFFunction -Message "Stopping because missing TFS URI parameter." return } switch($Command){ "Prepare" { $param = @("-prepare") } "Install"{ $param = @("-install") } } $param = $param + @("-packagepath=`"$Path`"", "-metadatastorepath=`"$MetaDataDir`"", "-tfsworkspacepath=`"$TfsWorkspaceDir`"", "-tfsprojecturi=`"$TfsUri`"") } Write-PSFMessage -Level Verbose -Message "Invoking SCDPBundleInstall.exe with $Command" -Target $param if ($ShowProgress) { $process = Start-Process -FilePath $executable -ArgumentList $param -PassThru while (-not ($process.HasExited)) { $timeout = New-TimeSpan -Days 1 $stopwatch = [Diagnostics.StopWatch]::StartNew(); $bundleRoot = "$env:localappdata\temp\SCDPBundleInstall" [xml]$manifest = Get-Content $(join-path $bundleRoot "PackageDependencies.dgml") -ErrorAction SilentlyContinue $bundleCounter = 0 if ($manifest) { $bundleTotalCount = $manifest.DirectedGraph.Nodes.ChildNodes.Count } while ($manifest -and (-not ($process.HasExited)) -and $stopwatch.elapsed -lt $timeout) { $currentBundleFolder = Get-ChildItem $bundleRoot -Directory -ErrorAction SilentlyContinue if ($currentBundleFolder) { $currentBundle = $currentBundleFolder.Name if ($announcedBundle -ne $currentBundle) { $announcedBundle = $currentBundle $bundleCounter = $bundleCounter + 1 Write-PSFMessage -Level Verbose -Message "$bundleCounter/$bundleTotalCount : Processing hotfix package $announcedBundle" } } } Start-Sleep -Milliseconds 100 } } else { Start-Process -FilePath $executable -ArgumentList $param -NoNewWindow -Wait } if ($ShowModifiedFiles) { $res = Get-ChildItem -Path $MetaDataDir -Recurse | Where-Object {$_.LastWriteTime -gt $StartTime} $res | ForEach-Object { Write-PSFMessage -Level Verbose -Message "Object modified by the install: $($_.FullName)" } $res } Invoke-TimeSignal -End } <# .SYNOPSIS Invoke the AxUpdateInstaller.exe file from Software Deployable Package (SDP) .DESCRIPTION A cmdlet that wraps some of the cumbersome work into a streamlined process. The process are detailed in the Microsoft documentation here: https://docs.microsoft.com/en-us/dynamics365/unified-operations/dev-itpro/deployment/install-deployable-package .PARAMETER Path Path to the update package that you want to install into the environment The cmdlet only supports a path to an already extracted and unblocked zip-file .PARAMETER MetaDataDir The path to the meta data directory for the environment Default path is the same as the aos service PackagesLocalDirectory .PARAMETER QuickInstallAll Use this switch to let the runbook reside in memory. You will not get a runbook on disc which you can examine for steps .PARAMETER DevInstall Use this when running on developer box without administrator privileges (Run As Administrator) .PARAMETER Command The command you want the cmdlet to execute when it runs the AXUpdateInstaller.exe Valid options are: SetTopology Generate Import Execute RunAll ReRunStep SetStepComplete Export VersionCheck The default value is "SetTopology" .PARAMETER Step The step number that you want to work against .PARAMETER RunbookId The runbook id of the runbook that you want to work against Default value is "Runbook" .EXAMPLE PS C:\> Invoke-D365SDPInstall -Path "c:\temp\" -QuickInstallAll This will install the extracted package in c:\temp\ using a runbook in memory while executing. .EXAMPLE PS C:\> Invoke-D365SDPInstall -Path "c:\temp\" -Command SetTopology PS C:\> Invoke-D365SDPInstall -Path "c:\temp\" -Command Generate -RunbookId 'MyRunbook' PS C:\> Invoke-D365SDPInstall -Path "c:\temp\" -Command Import -RunbookId 'MyRunbook' PS C:\> Invoke-D365SDPInstall -Path "c:\temp\" -Command Execute -RunbookId 'MyRunbook' Manual operations that first create Topology XML from current environment, then generate runbook with id 'MyRunbook', then import it and finally execute it. .EXAMPLE PS C:\> Invoke-D365SDPInstall -Path "c:\temp\" -Command RunAll Create Topology XML from current environment. Using default runbook id 'Runbook' and run all the operations from generate, to import to execute. .EXAMPLE PS C:\> Invoke-D365SDPInstall -Path "c:\temp\" -Command RerunStep -Step 18 -RunbookId 'MyRunbook' Rerun runbook with id 'MyRunbook' from step 18. .EXAMPLE PS C:\> Invoke-D365SDPInstall -Path "c:\temp\" -Command SetStepComplete -Step 24 -RunbookId 'MyRunbook' Mark step 24 complete in runbook with id 'MyRunbook' and continue the runbook from the next step. .NOTES Author: Tommy Skaue (@skaue) Author: M�tz Jensen (@Splaxi) Inspired by blogpost http://dev.goshoom.net/en/2016/11/installing-deployable-packages-with-powershell/ #> function Invoke-D365SDPInstall { [CmdletBinding(DefaultParameterSetName = 'QuickInstall')] param ( [Parameter(Mandatory = $True, Position = 1 )] [Alias('Hotfix')] [Alias('File')] [string] $Path, [Parameter(Mandatory = $false, Position = 2 )] [string] $MetaDataDir = "$Script:MetaDataDir", [Parameter(Mandatory = $false, ParameterSetName = 'QuickInstall', Position = 3 )] [switch] $QuickInstallAll, [Parameter(Mandatory = $false, ParameterSetName = 'DevInstall', Position = 3 )] [switch] $DevInstall, [Parameter(Mandatory = $true, ParameterSetName = 'Manual', Position = 3 )] [ValidateSet('SetTopology', 'Generate', 'Import', 'Execute', 'RunAll', 'ReRunStep', 'SetStepComplete', 'Export', 'VersionCheck')] [string] $Command = 'SetTopology', [Parameter(Mandatory = $false, Position = 4 )] [int] $Step, [Parameter(Mandatory = $false, Position = 5 )] [string] $RunbookId = "Runbook" ) 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 ensure <c='em'>exit</c> Visual Studio and run the cmdlet again." Stop-PSFFunction -Message "Stopping because of running Visual Studio." return } Test-AssembliesLoaded if (Test-PSFFunctionInterrupt) { Write-PSFMessage -Level Host -Message "It seems that you have executed some cmdlets that required to <c='em'>load</c> some Dynamics 356 Finance & Operations <c='em'>assemblies</c> into memory. Please <c='em'>close and restart</c> you PowerShell session / console, and <c='em'>start a fresh</c>. Please note that you should execute the failed command <c='em'>immediately</c> after importing the module." Stop-PSFFunction -Message "Stopping because of loaded assemblies." return } $arrRunbookIds = Get-D365Runbook | Get-D365RunbookId if(($Command -eq "RunAll") -and ($arrRunbookIds.Runbookid -contains $RunbookId)) { Write-PSFMessage -Level Host -Message "It seems that you have entered an <c='em'>already used RunbookId</c>. Please consider if you are <c='em'>trying to re-run some steps</c> or simply pass <c='em'>another RunbookId</c>." Stop-PSFFunction -Message "Stopping because of RunbookId already used on this machine." return } Invoke-TimeSignal -Start $Util = Join-Path $Path "AXUpdateInstaller.exe" $topologyFile = Join-Path $Path 'DefaultTopologyData.xml' if (-not (Test-PathExists -Path $topologyFile, $Util -Type Leaf)) { return } Get-ChildItem -Path $Path -Recurse | Unblock-File if ($QuickInstallAll) { Write-PSFMessage -Level Verbose "Using QuickInstallAll mode" $param = "quickinstallall" Start-Process -FilePath $Util -ArgumentList $param -NoNewWindow -Wait } elseif ($DevInstall) { Write-PSFMessage -Level Verbose "Using DevInstall mode" $param = "devinstall" Start-Process -FilePath $Util -ArgumentList $param -NoNewWindow -Wait } else { $Command = $Command.ToLowerInvariant() $runbookFile = Join-Path $Path "$runbookId.xml" $serviceModelFile = Join-Path $Path 'DefaultServiceModelData.xml' $topologyFile = Join-Path $Path 'DefaultTopologyData.xml' if ($Command -eq 'runall') { Write-PSFMessage -Level Verbose "Running all manual steps in one single operation" $ok = Update-TopologyFile -Path $Path if ($ok) { $param = @( "-runbookId=$runbookId" "-topologyFile=$topologyFile" "-serviceModelFile=`"$serviceModelFile`"" "-runbookFile=`"$runbookFile`"" ) & $Util generate $param & $Util import "-runbookfile=`"$runbookFile`"" & $Util execute "-runbookId=`"$runbookId`"" } Write-PSFMessage -Level Verbose "All manual steps complete." } else { $RunCommand = $true switch ($Command) { 'settopology' { Write-PSFMessage -Level Verbose "Updating topology file xml." $ok = Update-TopologyFile -Path $Path $RunCommand = $false } 'generate' { Write-PSFMessage -Level Verbose "Generating runbook file." $param = @( "generate" "-runbookId=`"$runbookId`"" "-topologyFile=`"$topologyFile`"" "-serviceModelFile=`"$serviceModelFile`"" "-runbookFile=`"$runbookFile`"" ) } 'import' { Write-PSFMessage -Level Verbose "Importing runbook file." $param = @( "import" "-runbookfile=`"$runbookFile`"" ) } 'execute' { Write-PSFMessage -Level Verbose "Executing runbook file." $param = @( "execute" "-runbookId=`"$runbookId`"" ) } 'rerunstep' { Write-PSFMessage -Level Verbose "Rerunning runbook step number $step." $param = @( "execute" "-runbookId=`"$runbookId`"" "-rerunstep=$step" ) } 'setstepcomplete' { Write-PSFMessage -Level Verbose "Marking step $step complete and continuing from next step." $param = @( "execute" "-runbookId=`"$runbookId`"" "-setstepcomplete=$step" ) } 'export' { Write-PSFMessage -Level Verbose "Exporting runbook for reuse." & $Util export $param = @( "export" "-runbookId=`"$runbookId`"" "-runbookfile=`"$runbookFile`"" ) } 'versioncheck' { Write-PSFMessage -Level Verbose "Running version check on runbook." $param = @( "execute" "-runbookId=`"$runbookId`"" "-versioncheck=true" ) } } if ($RunCommand) { & $Util $param } } } Invoke-TimeSignal -End } <# .SYNOPSIS Downloads the Selenium web driver files and deploys them to the specified destinations. .DESCRIPTION Downloads the Selenium web driver files and deploys them to the specified destinations. .PARAMETER RegressionSuiteAutomationTool Switch to specify if the Selenium files need to be installed in the Regression Suite Automation Tool folder. .PARAMETER PerfSDK Switch to specify if the Selenium files need to be installed in the PerfSDK folder. .EXAMPLE PS C:\> Invoke-D365SeleniumDownload -RegressionSuiteAutomationTool -PerfSDK This will download the Selenium zip archives and extract the files into both the Regression Suite Automation Tool folder and the PerfSDK folder. .NOTES Author: Kenny Saelen (@kennysaelen) #> function Invoke-D365SeleniumDownload { [CmdletBinding()] param ( [Parameter(Mandatory = $false, Position = 0)] [switch]$RegressionSuiteAutomationTool, [Parameter(Mandatory = $false, Position = 1)] [switch]$PerfSDK ) if(!$RegressionSuiteAutomationTool -and !$PerfSDK) { Write-PSFMessage -Level Critical -Message "Either the -RegressionSuiteAutomationTool or the -PerfSDK switch needs to be specified." Stop-PSFFunction -Message "Stopping because of no switch parameters speficied." return } $seleniumDllZipLocalPath = (Join-Path $env:TEMP "selenium-dotnet-strongnamed-2.42.0.zip") $ieDriverZipLocalPath = (Join-Path $env:TEMP "IEDriverServer_Win32_2.42.0.zip") $zipExtractionPath = (Join-Path $env:TEMP "D365Seleniumextraction") try { Write-PSFMessage -Level Host -Message "Downloading Selenium files" $WebClient = New-Object System.Net.WebClient $WebClient.DownloadFile("http://selenium-release.storage.googleapis.com/2.42/selenium-dotnet-strongnamed-2.42.0.zip", $seleniumDllZipLocalPath) $WebClient.DownloadFile("http://selenium-release.storage.googleapis.com/2.42/IEDriverServer_Win32_2.42.0.zip", $ieDriverZipLocalPath) Write-PSFMessage -Level Host -Message "Extracting zip files" Add-Type -AssemblyName System.IO.Compression.FileSystem [System.IO.Compression.ZipFile]::ExtractToDirectory($seleniumDllZipLocalPath, $zipExtractionPath) [System.IO.Compression.ZipFile]::ExtractToDirectory($ieDriverZipLocalPath, $zipExtractionPath) $targetPath = [String]::Empty $seleniumPath = [String]::Empty if($RegressionSuiteAutomationTool) { Write-PSFMessage -Level Host -Message "Making Selenium folder structure in the Regression Suite Automation Tool folder" $targetPath = Join-Path ([Environment]::GetEnvironmentVariable("ProgramFiles(x86)")) "Regression Suite Automation Tool" $seleniumPath = Join-Path $targetPath "Common\External\Selenium" # Check if the Regression Suite Automation Tool is installed on the machine and Selenium not already installed if (Test-PathExists -Path $targetPath -Type Container) { if(-not(Test-PathExists -Path $seleniumPath -Type Container -Create)) { Write-PSFMessage -Level Critical -Message [String]::Format("The folder for the Selenium files could not be created: {0}", $seleniumPath) } Write-PSFMessage -Level Host -Message "Copying Selenium files to destination folder" Copy-Item (Join-Path $zipExtractionPath "IEDriverServer.exe") $seleniumPath Copy-Item (Join-Path $zipExtractionPath "net40\*") $seleniumPath Write-PSFMessage -Level Host -Message ([String]::Format("Selenium files have been downloaded and installed in the following folder: {0}", $seleniumPath)) } else { Write-PSFMessage -Level Warning -Message [String]::Format("The RegressionSuiteAutomationTool switch parameter is specified but the tool could not be located in the following folder: {0}", $targetPath) } } if($PerfSDK) { Write-PSFMessage -Level Host -Message "Making Selenium folder structure in the PerfSDK folder" $targetPath = [Environment]::GetEnvironmentVariable("PerfSDK") $seleniumPath = Join-Path $targetPath "Common\External\Selenium" # Check if the PerfSDK is installed on the machine and Selenium not already installed if (Test-PathExists -Path $targetPath -Type Container) { if(-not(Test-PathExists -Path $seleniumPath -Type Container -Create)) { Write-PSFMessage -Level Critical -Message [String]::Format("The folder for the Selenium files could not be created: {0}", $seleniumPath) } Write-PSFMessage -Level Host -Message "Copying Selenium files to destination folder" Copy-Item (Join-Path $zipExtractionPath "IEDriverServer.exe") $seleniumPath Copy-Item (Join-Path $zipExtractionPath "net40\*") $seleniumPath Write-PSFMessage -Level Host -Message ([String]::Format("Selenium files have been downloaded and installed in the following folder: {0}", $seleniumPath)) } else { Write-PSFMessage -Level Warning -Message [String]::Format("The PerfSDK switch parameter is specified but the tool could not be located in the following folder: {0}", $targetPath) } } } catch { Write-PSFMessage -Level Host -Message "Something went wrong while downloading and installing the Selenium files." -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" return } finally { Write-PSFMessage -Level Host -Message "Cleaning up temporary files" Remove-Item -Path $seleniumDllZipLocalPath -Recurse Remove-Item -Path $ieDriverZipLocalPath -Recurse Remove-Item -Path $zipExtractionPath -Recurse } } <# .SYNOPSIS Execute a SQL Script .DESCRIPTION Execute a SQL Script against the D365FO SQL Server database .PARAMETER FilePath Path to the file containing the SQL Script that you want executed .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 Switch to instruct the cmdlet whether the connection should be using Windows Authentication or not .EXAMPLE PS C:\> Invoke-D365SqlScript -FilePath "C:\temp\d365fo.tools\DeleteUser.sql" This will execute the "C:\temp\d365fo.tools\DeleteUser.sql" against the registered SQL Server on the machine. .NOTES Author: M�tz Jensen (@splaxi) #> Function Invoke-D365SqlScript { [CmdletBinding()] param ( [Parameter(Mandatory = $true, Position = 1 )] [string] $FilePath, [Parameter(Mandatory = $false, Position = 2 )] [string] $DatabaseServer = $Script:DatabaseServer, [Parameter(Mandatory = $false, Position = 3 )] [string] $DatabaseName = $Script:DatabaseName, [Parameter(Mandatory = $false, Position = 4 )] [string] $SqlUser = $Script:DatabaseUserName, [Parameter(Mandatory = $false, Position = 5 )] [string] $SqlPwd = $Script:DatabaseUserPassword, [Parameter(Mandatory = $false, Position = 6)] [bool] $TrustedConnection = $false ) if (-not (Test-PathExists -Path $FilePath -Type Leaf)) { return } Invoke-TimeSignal -Start $UseTrustedConnection = Test-TrustedConnection $PSBoundParameters $Params = @{} #Hack to get all variables for the function, regardless of they were assigned from the caller or with default values. #The TrustedConnection is the real deal breaker. If $true user and password are ignored in Get-SqlCommand. $MyInvocation.MyCommand.Parameters.Keys | Get-Variable -ErrorAction Ignore | ForEach-Object { $Params.Add($_.Name, $_.Value) }; $null = $Params.Remove('FilePath') $Params.TrustedConnection = $UseTrustedConnection $sqlCommand = Get-SqlCommand @Params $sqlCommand.CommandText = (Get-Content "$FilePath") -join [Environment]::NewLine 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 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() } Invoke-TimeSignal -End } <# .SYNOPSIS Invoke the SysFlushAos class .DESCRIPTION Invoke the runnable class SysFlushAos to clear the AOD cache .PARAMETER URL URL to the Dynamics 365 instance you want to clear the AOD cache on .EXAMPLE PS C:\> Invoke-D365SysFlushAodCache This will a call against the default URL for the machine and have it execute the SysFlushAOD class .NOTES Author: M�tz Jensen (@Splaxi) #> function Invoke-D365SysFlushAodCache { [CmdletBinding()] param ( [Parameter(Mandatory = $false, Position = 1 )] [string] $Url ) if ($PSBoundParameters.ContainsKey("URL")) { Invoke-D365SysRunnerClass -ClassName "SysFlushAOD" -Url $URL } else { Invoke-D365SysRunnerClass -ClassName "SysFlushAOD" } } <# .SYNOPSIS Start a browser session that executes SysRunnerClass .DESCRIPTION Makes it possible to call any runnable class directly from the browser, without worrying about the details .PARAMETER ClassName The name of the class you want to execute .PARAMETER Company The company for which you want to execute the class against Default value is: "DAT" .PARAMETER Url The URL you want to execute against Default value is the Fully Qualified Domain Name registered on the machine .EXAMPLE PS C:\> Invoke-D365SysRunnerClass -ClassName SysFlushAOD Will execute the SysRunnerClass and have it execute the SysFlushAOD class and will run it against the "DAT" (default value) company .EXAMPLE PS C:\> Invoke-D365SysRunnerClass -ClassName SysFlushAOD -Company "USMF" Will execute the SysRunnerClass and have it execute the SysFlushAOD class and will run it against the "USMF" company .EXAMPLE PS C:\> Invoke-D365SysRunnerClass -ClassName SysFlushAOD -Url https://Test.cloud.onebox.dynamics.com Will execute the SysRunnerClass and have it execute the SysFlushAOD class and will run it against the "DAT" company, on the https://Test.cloud.onebox.dynamics.com URL .NOTES Author: M�tz Jensen (@Splaxi) #> function Invoke-D365SysRunnerClass { [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory = $true, ParameterSetName = 'Default', Position = 1 )] [string] $ClassName, [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 2 )] [string] $Company = $Script:Company, [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 3 )] [string] $Url = $Script:Url ) $executingUrl = "$Url`?cmp=$Company&mi=SysClassRunner&cls=$ClassName" Start-Process $executingUrl } <# .SYNOPSIS Start a browser session that will show the table browser .DESCRIPTION Makes it possible to call the table browser for a given table directly from the web browser, without worrying about the details .PARAMETER TableName The name of the table you want to see the rows for .PARAMETER Company The company for which you want to see the data from in the given table Default value is: "DAT" .PARAMETER Url The URL you want to execute against Default value is the Fully Qualified Domain Name registered on the machine .EXAMPLE PS C:\> Invoke-D365TableBrowser -TableName SalesTable Will open the table browser and show all the records in Sales Table from the "DAT" company (default value). .EXAMPLE PS C:\> Invoke-D365TableBrowser -TableName SalesTable -Company "USMF" Will open the table browser and show all the records in Sales Table from the "USMF" company. .NOTES Author: M�tz Jensen (@Splaxi) The cmdlet supports piping and can be used in advanced scenarios. See more on github and the wiki pages. #> function Invoke-D365TableBrowser { [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory = $true, ParameterSetName = 'Default', ValueFromPipelineByPropertyName = $true, Position = 1 )] [string] $TableName, [Parameter(Mandatory = $false, ParameterSetName = 'Default', ValueFromPipelineByPropertyName = $true, Position = 2 )] [string] $Company = $Script:Company, [Parameter(Mandatory = $false, ParameterSetName = 'Default', ValueFromPipelineByPropertyName = $true, Position = 3 )] [string] $Url = $Script:Url ) BEGIN {} PROCESS { Write-PSFMessage -Level Verbose -Message "Table name: $TableName" -Target $TableName $executingUrl = "$Url`?cmp=$Company&mi=SysTableBrowser&tablename=$TableName" Start-Process $executingUrl #* Allow the browser to start and process first request if it isn't running already Start-Sleep -Seconds 1 } END {} } <# .SYNOPSIS Generate a bacpac file from a database .DESCRIPTION Takes care of all the details and steps that is needed to create a valid bacpac file to move between Tier 1 (onebox or Azure hosted) and Tier 2 (MS hosted), or vice versa Supports to create a raw bacpac file without prepping. Can be used to automate backup from Tier 2 (MS hosted) environment .PARAMETER ExportModeTier1 Switch to instruct the cmdlet that the export will be done against a classic SQL Server installation .PARAMETER ExportModeTier2 Switch to instruct the cmdlet that the export will be done against an Azure SQL DB 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 BackupDirectory The path where to store the temporary backup file when the script needs to handle that .PARAMETER NewDatabaseName The name for the database the script is going to create when doing the restore process .PARAMETER BacpacFile The path where you want the cmdlet to store the bacpac file that will be generated .PARAMETER CustomSqlFile The path to a custom sql server script file that you want executed against the database .PARAMETER ExportOnly Switch to instruct the cmdlet to either just create a dump bacpac file or run the prepping process first .EXAMPLE PS C:\> New-D365Bacpac -ExportModeTier1 -BackupDirectory c:\Temp\backup\ -NewDatabaseName Testing1 -BacpacFile "C:\Temp\Bacpac\Testing1.bacpac" Will backup the "AXDB" database and restore is as "Testing1" again the localhost SQL Server. Will run the prepping process against the restored database. Will export a bacpac file to "C:\Temp\Bacpac\Testing1.bacpac". Will delete the restored database. It will use trusted connection (Windows authentication) while working against the SQL Server. .EXAMPLE PS C:\> New-D365Bacpac -ExportModeTier2 -DatabaseServer localhost -DatabaseName AxDB -SqlUser User123 -SqlPwd "Password123" -NewDatabaseName Testing1 -BacpacFile C:\Temp\Bacpac\Testing1.bacpac Will create a copy the db database on the dbserver1 in Azure. Will run the prepping process against the copy database. Will export a bacpac file. Will delete the copy database. .EXAMPLE PS C:\> New-D365Bacpac -ExportModeTier2 -SqlUser User123 -SqlPwd "Password123" -NewDatabaseName Testing1 -BacpacFile C:\Temp\Bacpac\Testing1.bacpac Normally used for a Tier-2 export and preparation for Tier-1 import Will create a copy of the registered D365 database on the registered D365 Azure SQL DB instance. Will run the prepping process against the copy database. Will export a bacpac file. Will delete the copy database. .EXAMPLE PS C:\> New-D365Bacpac -ExportModeTier2 -SqlUser User123 -SqlPwd "Password123" -NewDatabaseName Testing1 -BacpacFile C:\Temp\Bacpac\Testing1.bacpac -ExportOnly Will export a bacpac file. The bacpac should be able to restore back into the database without any preparing because it is coming from the environment from the beginning .NOTES The cmdlet supports piping and can be used in advanced scenarios. See more on github and the wiki pages. Author: Rasmus Andersen (@ITRasmus) Author: M�tz Jensen (@Splaxi) #> function New-D365Bacpac { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding(DefaultParameterSetName = 'ExportTier2')] param ( [Parameter(Mandatory = $true, ParameterSetName = 'ExportTier1', Position = 0)] [switch]$ExportModeTier1, [Parameter(Mandatory = $true, ParameterSetName = 'ExportTier2', Position = 0)] [switch]$ExportModeTier2, [Parameter(Mandatory = $false, Position = 1 )] [string]$DatabaseServer = $Script:DatabaseServer, [Parameter(Mandatory = $false, Position = 2 )] [string]$DatabaseName = $Script:DatabaseName, [Parameter(Mandatory = $false, Position = 3 )] [Parameter(Mandatory = $true, ParameterSetName = 'ExportTier2', ValueFromPipelineByPropertyName = $true, Position = 3)] [string]$SqlUser = $Script:DatabaseUserName, [Parameter(Mandatory = $false, Position = 4 )] [Parameter(Mandatory = $true, ParameterSetName = 'ExportTier2', ValueFromPipelineByPropertyName = $true, Position = 4)] [string]$SqlPwd = $Script:DatabaseUserPassword, [Parameter(Mandatory = $false, ParameterSetName = 'ExportTier1', Position = 5 )] [string]$BackupDirectory = "C:\Temp\d365fo.tools\SqlBackups", [Parameter(Mandatory = $false, Position = 6 )] [string]$NewDatabaseName = "$Script:DatabaseName`_export", [Parameter(Mandatory = $false, Position = 7 )] [Alias('File')] [string]$BacpacFile = "C:\Temp\d365fo.tools\$DatabaseName.bacpac", [Parameter(Mandatory = $false, Position = 8 )] [string]$CustomSqlFile, [switch]$ExportOnly ) Invoke-TimeSignal -Start $UseTrustedConnection = Test-TrustedConnection $PSBoundParameters if ($PSBoundParameters.ContainsKey("CustomSqlFile")) { if (-not (Test-PathExists -Path $CustomSqlFile -Type Leaf)) {return} $ExecuteCustomSQL = $true } if ($BacpacFile -notlike "*.bacpac") { Write-PSFMessage -Level Host -Message "The path for the bacpac file must contain the <c='em'>.bacpac</c> extension. Please update the <c='em'>BacpacFile</c> parameter and try again." Stop-PSFFunction -Message "The BacpacFile path was not correct." return } if ($PSBoundParameters.ContainsKey("BackupDirectory") -or $ExportModeTier1) { if (-not (Test-PathExists -Path $BackupDirectory -Type Container -Create)) { return } } if (-not (Test-PathExists -Path (Split-Path $BacpacFile -Parent) -Type Container -Create)) { return } $Properties = @("VerifyFullTextDocumentTypesSupported=false", "Storage=File" ) $BaseParams = @{ DatabaseServer = $DatabaseServer DatabaseName = $DatabaseName SqlUser = $SqlUser SqlPwd = $SqlPwd } $ExportParams = @{ Action = "export" FilePath = $BacpacFile Properties = $Properties } if ($ExportOnly) { Write-PSFMessage -Level Verbose -Message "Invoking the export of the bacpac file only." Write-PSFMessage -Level Verbose -Message "Invoking the sqlpackage with parameters" -Target $BaseParams $res = Invoke-SqlPackage @BaseParams @ExportParams if (!$res) {return} [PSCustomObject]@{ File = $BacpacFile Filename = (Split-Path $BacpacFile -Leaf) } } else { if ($ExportModeTier1) { $Params = @{ BackupDirectory = $BackupDirectory NewDatabaseName = $NewDatabaseName TrustedConnection = $UseTrustedConnection } Write-PSFMessage -Level Verbose -Message "Invoking the Tier 1 - SQL backup & restore process" $res = Invoke-SqlBackupRestore @BaseParams @Params if ((Test-PSFFunctionInterrupt) -or (-not $res)) { return } $Params = Get-DeepClone $BaseParams $Params.DatabaseName = $NewDatabaseName Write-PSFMessage -Level Verbose -Message "Invoking the Tier 1 - Clear SQL objects" $res = Invoke-ClearSqlSpecificObjects @Params -TrustedConnection $UseTrustedConnection if ((Test-PSFFunctionInterrupt) -or (-not $res)) { return } if ($ExecuteCustomSQL) { Write-PSFMessage -Level Verbose -Message "Invoking the Tier 1 - Execution of custom SQL script" $res = Invoke-D365SqlScript @Params -FilePath $CustomSqlFile -TrustedConnection $UseTrustedConnection if (!$res) {return} } Write-PSFMessage -Level Verbose -Message "Invoking the Tier 1 - Export of the bacpac file from SQL" $res = Invoke-SqlPackage @Params @ExportParams -TrustedConnection $UseTrustedConnection if (!$res) {return} Write-PSFMessage -Level Verbose -Message "Invoking the Tier 1 - Remove database from SQL" Remove-D365Database @Params [PSCustomObject]@{ File = $BacpacFile Filename = (Split-Path $BacpacFile -Leaf) } } else { $Params = @{ NewDatabaseName = $NewDatabaseName } Write-PSFMessage -Level Verbose -Message "Invoking the Tier 2 - Creation of Azure DB copy" $res = Invoke-AzureBackupRestore @BaseParams @Params if ((Test-PSFFunctionInterrupt) -or (-not $res)) { return } $Params = Get-DeepClone $BaseParams $Params.DatabaseName = $NewDatabaseName Write-PSFMessage -Level Verbose -Message "Invoking the Tier 2 - Clear Azure DB objects" $res = Invoke-ClearAzureSpecificObjects @Params if ((Test-PSFFunctionInterrupt) -or (-not $res)) { return } if ($ExecuteCustomSQL) { Write-PSFMessage -Level Verbose -Message "Invoking the Tier 2 - Execution of custom SQL script" $res = Invoke-D365SqlScript @Params -FilePath $CustomSqlFile -TrustedConnection $false if (!$res) {return} } Write-PSFMessage -Level Verbose -Message "Invoking the Tier 2 - Export of the bacpac file from Azure DB" $res = Invoke-SqlPackage @Params @ExportParams -TrustedConnection $false if (!$res) {return} Write-PSFMessage -Level Verbose -Message "Invoking the Tier 2 - Remove database from Azure DB" Remove-D365Database @Params [PSCustomObject]@{ File = $BacpacFile Filename = (Split-Path $BacpacFile -Leaf) } } } Invoke-TimeSignal -End } <# .SYNOPSIS Generate the Customization's Analysis Report (CAR) .DESCRIPTION A cmdlet that wraps some of the cumbersome work into a streamlined process .PARAMETER Path Full path to CAR file (xlsx-file) .PARAMETER BinDir The path to the bin directory for the environment Default path is the same as the AOS service PackagesLocalDirectory\bin .PARAMETER MetaDataDir The path to the meta data directory for the environment Default path is the same as the aos service PackagesLocalDirectory .PARAMETER Module Name of the Module to analyse .PARAMETER Model Name of the Model to analyse .PARAMETER XmlLog Path where you want to store the Xml log output generated from the best practice analyser .EXAMPLE PS C:\> New-D365CAReport -Path "c:\temp\CAReport.xlsx" -module "ApplicationSuite" -model "MyOverLayerModel" This will generate a CAR report against MyOverLayerModel in the ApplicationSuite Module, and save the report to "c:\temp\CAReport.xlsx" .NOTES Author: Tommy Skaue (@Skaue) #> function New-D365CAReport { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding()] param ( [Parameter(Mandatory = $false, Position = 1 )] [Alias('File')] [string] $Path = (Join-Path $Script:DefaultTempPath "CAReport.xlsx"), [Parameter(Mandatory = $false, Position = 2 )] [string] $BinDir = "$Script:PackageDirectory\bin", [Parameter(Mandatory = $false, Position = 3 )] [string] $MetaDataDir = "$Script:MetaDataDir", [Parameter(Mandatory = $true, Position = 4 )] [Alias('Package')] [string] $Module, [Parameter(Mandatory = $true, Position = 5 )] [string] $Model, [Parameter(Mandatory = $false, Position = 6 )] [string] $XmlLog = (Join-Path $Script:DefaultTempPath "BPCheckLogcd.xml") ) if (-not (Test-PathExists -Path $MetaDataDir, $BinDir -Type Container)) {return} $executable = Join-Path $BinDir "xppbp.exe" if (-not (Test-PathExists -Path $executable -Type Leaf)) {return} $param = @( "-metadata=`"$MetaDataDir`"", "-all", "-module=`"$Module`"", "-model=`"$Model`"", "-xmlLog=`"$XmlLog`"", "-car=`"$Path`"" ) Write-PSFMessage -Level Verbose -Message "Starting the $executable with the parameter options." -Target $param Start-Process -FilePath $executable -ArgumentList ($param -join " ") -NoNewWindow -Wait } <# .SYNOPSIS Create a license deployable package .DESCRIPTION Create a deployable package with a license file inside .PARAMETER LicenseFile Path to the license file that you want to have inside a deployable package .PARAMETER Path Path to the template zip file for creating a deployable package with a license file Default path is the same as the aos service "PackagesLocalDirectory\bin\CustomDeployablePackage\ImportISVLicense.zip" .PARAMETER OutputPath Path where you want the generated deployable package stored Default value is: "C:\temp\d365fo.tools\ISVLicense.zip" .EXAMPLE PS C:\> New-D365ISVLicense -LicenseFile "C:\temp\ISVLicenseFile.txt" This will take the "C:\temp\ISVLicenseFile.txt" file and locate the "ImportISVLicense.zip" template file under the "PackagesLocalDirectory\bin\CustomDeployablePackage\". It will extract the "ImportISVLicense.zip", load the ISVLicenseFile.txt and compress (zip) the files into a deployable package. The package will be exported to "C:\temp\d365fo.tools\ISVLicense.zip" .NOTES Author: M�tz Jensen (@splaxi) #> function New-D365ISVLicense { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [Parameter(Mandatory = $true, Position = 1)] [string] $LicenseFile, [Alias('Template')] [string] $Path = "$Script:BinDirTools\CustomDeployablePackage\ImportISVLicense.zip", [string] $OutputPath = "C:\temp\d365fo.tools\ISVLicense.zip" ) begin { $oldprogressPreference = $global:progressPreference $global:progressPreference = 'silentlyContinue' } process { if (-not (Test-PathExists -Path $Path, $LicenseFile -Type "Leaf")) {return} $null = New-Item -Path (Split-Path $OutputPath -Parent) -ItemType Directory -ErrorAction SilentlyContinue Unblock-File $Path Unblock-File $LicenseFile $ExtractionPath = [System.IO.Path]::GetTempPath() $packageTemp = Join-Path $ExtractionPath ((Get-Random -Maximum 99999).ToString()) Write-PSFMessage -Level Verbose -Message "Extracting the template zip file to $packageTemp." -Target $packageTemp Expand-Archive -Path $Path -DestinationPath $packageTemp $licenseMergePath = Join-Path $packageTemp "AosService\Scripts\License" Get-ChildItem -Path $licenseMergePath | Remove-Item -Force -ErrorAction SilentlyContinue Write-PSFMessage -Level Verbose -Message "Copying the license file into place." Copy-Item -Path $LicenseFile -Destination $licenseMergePath Write-PSFMessage -Level Verbose -Message "Compressing the folder into a zip file and storing it at $OutputPath" -Target $OutputPath Compress-Archive -Path "$packageTemp\*" -DestinationPath $OutputPath -Force [PSCustomObject]@{ File = $OutputPath } } end { $global:progressPreference = $oldprogressPreference } } <# .SYNOPSIS Create a new topology file .DESCRIPTION Build a new topology file based on a template and update the ServiceModelList .PARAMETER Path Path to the template topology file .PARAMETER Services The array with all the service names that you want to fill into the topology file .PARAMETER NewPath Path to where you want to save the new file after it has been created .EXAMPLE PS C:\> New-D365TopologyFile -Path C:\Temp\DefaultTopologyData.xml -Services "ALMService","AOSService","BIService" -NewPath C:\temp\CurrentTopology.xml This will read the "DefaultTopologyData.xml" file and fill in "ALMService","AOSService" and "BIService" as the services in the ServiceModelList tag. The new file is stored at "C:\temp\CurrentTopology.xml" .EXAMPLE PS C:\> $Services = @(Get-D365InstalledService | ForEach-Object {$_.Servicename}) PS C:\> New-D365TopologyFile -Path C:\Temp\DefaultTopologyData.xml -Services $Services -NewPath C:\temp\CurrentTopology.xml This will get all the services already installed on the machine. Afterwards the list is piped to New-D365TopologyFile where all services are import into the new topology file that is stored at "C:\temp\CurrentTopology.xml" .NOTES Author: M�tz Jensen (@Splaxi) #> function New-D365TopologyFile { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [Parameter(Mandatory = $true, ParameterSetName = 'Default', Position = 1 )] [alias('File')] [string] $Path, [Parameter(Mandatory = $true, ParameterSetName = 'Default', Position = 2 )] [string[]] $Services, [Parameter(Mandatory = $true, ParameterSetName = 'Default', Position = 3 )] [alias('NewFile')] [string] $NewPath ) begin { } process { if (Test-PathExists -Path $Path -Type Leaf) { Remove-Item -Path $NewPath -Force -ErrorAction SilentlyContinue [xml]$topology = [xml](Get-Content -Path $Path) [System.Collections.ArrayList] $ServicesList = New-Object -TypeName "System.Collections.ArrayList" foreach ($obj in $Services) { $null = $ServicesList.Add("<string>$obj</string>") } $topology.TopologyData.MachineList.Machine.ServiceModelList.InnerXml = (($ServicesList.ToArray()) -join [Environment]::NewLine ) $sw = New-Object System.Io.Stringwriter $writer = New-Object System.Xml.XmlTextWriter($sw) $writer.Formatting = [System.Xml.Formatting]::Indented $writer.Indentation = 4; $topology.WriteContentTo($writer) $topology.LoadXml($sw.ToString()) $topology.Save("$NewPath") } else { Write-PSFMessage -Level Critical -Message "The base topology file wasn't found at the specified location. Please check the path and run the cmdlet again." Stop-PSFFunction -Message "Stopping because of errors" return } } end { } } <# .SYNOPSIS Deploy Report .DESCRIPTION Deploy SSRS Report to SQL Server Reporting Services .PARAMETER Module Name of the module that you want to works against Accepts an array of strings Default value is "*" and will work against all modules loaded on the machine .PARAMETER ReportName Name of the report that you want to deploy Default value is "*" and will deploy all reports from the module(s) that you speficied .PARAMETER LogFile Path to the file that should contain the logging information Default value is "c:\temp\d365fo.tools\AxReportDeployment.log" .PARAMETER PackageDirectory Path to the PackagesLocalDirectory Default path is the same as the AOS Service PackagesLocalDirectory .PARAMETER ToolsBasePath Base path to the folder containing the needed PowerShell manifests that the cmdlet utilizes Default path is the same as the AOS Service PackagesLocalDirectory .PARAMETER ReportServerIp IP Address of the server that has SQL Reporting Services installed Default value is "127.0.01" .EXAMPLE PS C:\> Publish-D365SsrsReport -Module ApplicationSuite -ReportName TaxVatRegister.Report This will deploy the report which is named "TaxVatRegister.Report". The cmdlet will look for the report inside the ApplicationSuite module. The cmdlet will be using the default 127.0.0.1 while deploying the report. .EXAMPLE PS C:\> Publish-D365SsrsReport -Module ApplicationSuite -ReportName * This will deploy the all reports from the ApplicationSuite module. The cmdlet will be using the default 127.0.0.1 while deploying the report. .NOTES Tags: SSRS, Report, Reports, Deploy, Publish Author: M�tz Jensen (@Splaxi) #> function Publish-D365SsrsReport { [CmdletBinding()] [OutputType('[PsCustomObject]')] param ( [Parameter(Mandatory = $false)] [string[]] $Module = "*", [Parameter(Mandatory = $false)] [string[]] $ReportName = "*", [Parameter(Mandatory = $false)] [string] $LogFile = (Join-Path $Script:DefaultTempPath "AxReportDeployment.log"), [Parameter(Mandatory = $false)] [string] $PackageDirectory = $Script:PackageDirectory, [Parameter(Mandatory = $false)] [string] $ToolsBasePath = $Script:PackageDirectory, [Parameter(Mandatory = $false)] [string[]]$ReportServerIp = "127.0.0.1" ) Invoke-TimeSignal -Start $LogDirectory = Split-Path $LogFile -Parent $toolsPath = Join-Path $ToolsBasePath "Plugins\AxReportVmRoleStartupTask" if (-not (Test-PathExists -Path $toolsPath, $PackageDirectory -Type Container)) { return } if (-not (Test-PathExists -Path $LogDirectory -Type Container -Create)) { return } $aosCommonManifest = Join-Path $toolsPath "AosCommon.psm1" $reportingManifest = Join-Path $toolsPath "Reporting.psm1" if (-not (Test-PathExists -Path $aosCommonManifest, $reportingManifest -Type Leaf)) { return } Write-PSFMessage -Level Verbose -Message "Importing the Microsoft AosCommon PowerShell manifest file." -Target $aosCommonManifest Import-Module "$aosCommonManifest" -Force -DisableNameChecking Write-PSFMessage -Level Verbose -Message "Importing the Microsoft Reporting PowerShell manifest file." -Target $reportingManifest Import-Module "$reportingManifest" -Force -DisableNameChecking # create JSON config string for Deploy-AxReports $settings = New-Object -TypeName PSCustomObject -Property @{ "BiReporting.ReportingServers" = $($ReportServerIp -join ",") "Microsoft.Dynamics.AX.AosConfig.AzureConfig.bindir" = $PackageDirectory "Module" = $Module "ReportName" = $ReportName } Write-PSFMessage -Level Verbose -Message "Done building the settings object that will be parsed." -Target $settings $jsonConfig = ConvertTo-Json $settings Write-PSFMessage -Level Verbose -Message "Settings object converted to json." -Target $jsonConfig $jsonConfig = [System.Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($jsonConfig)) try { Write-PSFMessage -Level Verbose -Message "Invoking the 'Deploy-AxReport' cmdlet from Microsoft." Deploy-AxReport -Config $jsonConfig -Log $LogFile } catch { Write-PSFMessage -Level Host -Message "Something went wrong while deploying the SSRS Report(s)" -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" return } Invoke-TimeSignal -End [PSCustomObject]@{ LogFile = $LogFile } } <# .SYNOPSIS Register Azure Storage Configurations .DESCRIPTION Register all Azure Storage Configurations .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 as default for all users, so they can access the configuration objects .EXAMPLE PS C:\> Register-D365AzureStorageConfig -ConfigStorageLocation "System" This will store all Azure Storage Configurations as defaults for all users on the machine. .NOTES Tags: Configuration, Azure, Storage Author: M�tz Jensen (@Splaxi) #> function Register-D365AzureStorageConfig { [CmdletBinding()] [OutputType()] param ( [ValidateSet('User', 'System')] [string] $ConfigStorageLocation = "User" ) $configScope = Test-ConfigStorageLocation -ConfigStorageLocation $ConfigStorageLocation Register-PSFConfig -FullName "d365fo.tools.azure.storage.accounts" -Scope $configScope } <# .SYNOPSIS Remove broadcast message configuration .DESCRIPTION Remove a broadcast message configuration from the configuration store .PARAMETER Name Name of the broadcast message configuration you want to remove from the configuration store .PARAMETER Temporary Instruct the cmdlet to only temporarily remove the broadcast message configuration from the configuration store .EXAMPLE PS C:\> Remove-D365BroadcastMessageConfig -Name "UAT" This will remove the broadcast message configuration name "UAT" from the machine. .NOTES Tags: Servicing, Broadcast, Message, Users, Environment, Config, Configuration, ClientId, ClientSecret Author: M�tz Jensen (@Splaxi) .LINK Add-D365BroadcastMessageConfig .LINK Clear-D365ActiveBroadcastMessageConfig .LINK Get-D365ActiveBroadcastMessageConfig .LINK Get-D365BroadcastMessageConfig .LINK Send-D365BroadcastMessage .LINK Set-D365ActiveBroadcastMessageConfig #> function Remove-D365BroadcastMessageConfig { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] [OutputType()] param ( [Parameter(Mandatory = $true, Position = 1)] [string] $Name, [switch] $Temporary ) $Name = $Name.ToLower() if ($Name -match '\*') { Write-PSFMessage -Level Host -Message "The name cannot contain <c='em'>wildcard character</c>." Stop-PSFFunction -Message "Stopping because the name contains wildcard character." return } if (-not ((Get-PSFConfig -FullName "d365fo.tools.broadcast.*.name").Value -contains $Name)) { Write-PSFMessage -Level Host -Message "A broadcast message configuration with that name <c='em'>doesn't exists</c>." Stop-PSFFunction -Message "Stopping because a broadcast message configuration with that name doesn't exists." return } $res = (Get-PSFConfig -FullName "d365fo.tools.active.broadcast.message.config.name").Value if ($res -eq $Name) { Write-PSFMessage -Level Host -Message "The active broadcast message configuration is the <c='em'>same as the one you're trying to remove</c>. Please set another configuration as active, before removing this one. You could also call Clear-D365ActiveBroadcastMessageConfig." Stop-PSFFunction -Message "Stopping because the active broadcast message configuration is the same as the one trying to be removed." return } foreach ($config in Get-PSFConfig -FullName "d365fo.tools.broadcast.$Name.*") { Set-PSFConfig -FullName $config.FullName -Value "" if (-not $Temporary) { Unregister-PSFConfig -FullName $config.FullName -Scope UserDefault } } } <# .SYNOPSIS Removes a Database .DESCRIPTION Removes a Database .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:\> Remove-D365Database -DatabaseName "ExportClone" This will remove the "ExportClone" from the default SQL Server instance that is registered on the machine. .NOTES Author: M�tz Jensen (@Splaxi) #> function Remove-D365Database { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [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 ) $UseTrustedConnection = Test-TrustedConnection $PSBoundParameters $null = [System.Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.SMO') $srv = new-object Microsoft.SqlServer.Management.Smo.Server("$DatabaseServer") if (-not $UseTrustedConnection) { $srv.ConnectionContext.set_LoginSecure($false) $srv.ConnectionContext.set_Login("$SqlUser") $srv.ConnectionContext.set_Password("$SqlPwd") } try { $db = $srv.Databases["$DatabaseName"] if (!$db) { Write-PSFMessage -Level Verbose -Message "Database $DatabaseName not found. Nothing to remove." return } if ($srv.ServerType -ne "SqlAzureDatabase") { $srv.KillAllProcesses("$DatabaseName") } Write-PSFMessage -Level Verbose -Message "Dropping $DatabaseName" -Target $DatabaseName $db.Drop() } catch { Write-PSFMessage -Level Host -Message "Something went wrong while removing the DB" -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" return } } <# .SYNOPSIS Remove a model from Dynamics 365 for Finance & Operations .DESCRIPTION Remove a model from a Dynamics 365 for Finance & Operations environment .PARAMETER Model Name of the model that you want to work against .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 .PARAMETER DeleteFolders Instruct the cmdlet to delete the model folder This is useful when you are trying to clean up the folders in your source control / branch .EXAMPLE PS C:\> Remove-D365Model -Model CustomModelName This will remove the "CustomModelName" model from the D365FO environment. It will NOT remove the folders inside the PackagesLocalDirectory location. .EXAMPLE PS C:\> Remove-D365Model -Model CustomModelName -DeleteFolders This will remove the "CustomModelName" model from the D365FO environment. It will remove the folders inside the PackagesLocalDirectory location. This is helpful when dealing with source control and you want to remove the model entirely. .NOTES Tags: ModelUtil, Axmodel, Model, Remove, Delete, Source Control, Vsts, Azure DevOps Author: M�tz Jensen (@Splaxi) #> function Remove-D365Model { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [Parameter(Mandatory = $True, Position = 1 )] [string] $Model, [Parameter(Mandatory = $false, Position = 2 )] [string] $BinDir = "$Script:PackageDirectory\bin", [Parameter(Mandatory = $false, Position = 3 )] [string] $MetaDataDir = "$Script:MetaDataDir", [switch] $DeleteFolders ) Invoke-TimeSignal -Start Invoke-ModelUtil -Command "Delete" -Path $Path -BinDir $BinDir -MetaDataDir $MetaDataDir -Model $Model if (Test-PSFFunctionInterrupt) { return } $modelPath = Join-Path $MetaDataDir $Model if ($DeleteFolders) { if (-not (Test-PathExists -Path $modelPath -Type Container)) { return } Remove-Item $modelPath -Force -Recurse -ErrorAction SilentlyContinue } Invoke-TimeSignal -End } <# .SYNOPSIS Delete an user from the environment .DESCRIPTION Deletes the user from the database, including security configuration .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 updated. You have to specific the explicit email address of the user you want to remove The cmdlet will not be able to delete the ADMIN user, this is to prevent you from being locked out of the system. .EXAMPLE PS C:\> Remove-D365User -Email "Claire@contoso.com" This will move all security and user details from the user with the email address "Claire@contoso.com" .EXAMPLE PS C:\> Get-D365User -Email *contoso.com | Remove-D365User This will first get all users from the database that matches the *contoso.com search and pipe their emails to Remove-D365User for it to delete them. .NOTES Author: Rasmus Andersen (@ITRasmus) Author: M�tz Jensen (@Splaxi) #> function Remove-D365User { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [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 = $true, ValueFromPipelineByPropertyName = $true, Position = 5)] [string] $Email ) BEGIN { $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\remove-user.sql") -join [Environment]::NewLine $null = $SqlCommand.Parameters.AddWithValue("@Email", $Email) try { Write-PSFMessage -Level InternalComment -Message "Executing a script against the database." -Target (Get-SqlString $SqlCommand) $null = $SqlCommand.ExecuteNonQuery() } 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 } $SqlCommand.Parameters.Clear() } END { try { if ($sqlCommand.Connection.State -ne [System.Data.ConnectionState]::Closed) { $sqlCommand.Connection.Close() } $sqlCommand.Dispose() } 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 Function for renaming computer. Renames Computer and changes the SSRS Configration .DESCRIPTION When doing development on-prem, there is as need for changing the Computername. Function both changes Computername and SSRS Configuration .PARAMETER NewName The new name for the computer .PARAMETER SSRSReportDatabase Name of the SSRS reporting database .EXAMPLE PS C:\> Rename-D365ComputerName -NewName "Demo-8.1" -SSRSReportDatabase "ReportServer" This will rename the local machine to the "Demo-8.1" as the new Windows machine name. It will update the registration inside the SQL Server Reporting Services configuration to handle the new name of the machine. .NOTES Author: Rasmus Andersen (@ITRasmus) Author: M�tz Jensen (@Splaxi) #> function Rename-D365ComputerName { [CmdletBinding()] param ( [Parameter(Mandatory = $true, Position = 1)] [string] $NewName, [Parameter(Mandatory = $false,Position = 2)] [string] $SSRSReportDatabase = "DynamicsAxReportServer" ) Write-PSFMessage -Level Verbose -Message "Testing for elevated runtime" if (!$script:IsAdminRuntime) { Write-PSFMessage -Level Host -Message "The cmdlet needs <c='em'>administrator permission</c> (Run As Administrator) to be able to update the configuration. Please start an <c='em'>elevated</c> session and run the cmdlet again." Stop-PSFFunction -Message "Stopping because the function is not run elevated" return } Write-PSFMessage -Level Verbose -Message "Renaming computer to $NewName" Rename-Computer -NewName $NewName -Force Write-PSFMessage -Level Verbose -Message "Setting SSRS Reporting server database server to localhost" $rsconfig = "$Script:SQLTools\rsconfig.exe" $arguments = "-s localhost -a Windows -c -d `"$SSRSReportDatabase`"" Start-Process -Wait -NoNewWindow -FilePath $rsconfig -ArgumentList $arguments -Verbose } <# .SYNOPSIS Rename as D365FO Demo/Dev box .DESCRIPTION The Rename function, changes the config values used by a D365FO dev box for identifying its name. Standard it is called 'usnconeboxax1aos' .PARAMETER NewName The new name wanted for the D365FO instance .PARAMETER AosServiceWebRootPath Path to the webroot folder for the AOS service 'Default value : C:\AOSService\Webroot .PARAMETER IISServerApplicationHostConfigFile Path to the IISService Application host file, [Where the binding configurations is stored] 'Default value : C:\Windows\System32\inetsrv\Config\applicationHost.config' .PARAMETER HostsFile Place of the host file on the current system [Local DNS record] ' Default value C:\Windows\System32\drivers\etc\hosts' .PARAMETER BackupExtension Backup name for all the files that are changed .PARAMETER MRConfigFile Path to the Financial Reporter (Management Reporter) configuration file .EXAMPLE PS C:\> Rename-D365Instance -NewName "Demo1" This will rename the D365 for Finance & Operations instance to "Demo1". This IIS will be restarted while doing it. .NOTES Author: Rasmus Andersen (@ITRasmus) Author: M�tz Jensen (@Splaxi) The function restarts the IIS Service. Elevated privileges are required. #> function Rename-D365Instance { [CmdletBinding()] param ( [Parameter(Mandatory = $true, Position = 1)] [string]$NewName, [Parameter(Mandatory = $false, Position = 2)] [string]$AosServiceWebRootPath = $Script:AOSPath, [Parameter(Mandatory = $false, Position = 3)] [string]$IISServerApplicationHostConfigFile = $Script:IISHostFile, [Parameter(Mandatory = $false, Position = 4)] [string]$HostsFile = $Script:Hosts, [Parameter(Mandatory = $false, Position = 5)] [string]$BackupExtension = "bak", [Parameter(Mandatory = $false, Position = 6)] [string]$MRConfigFile = $Script:MRConfigFile ) Write-PSFMessage -Level Verbose -Message "Testing for elevated runtime" if ($Script:EnvironmentType -ne [EnvironmentType]::LocalHostedTier1) { Write-PSFMessage -Level Host -Message "It seems that you ran this cmdlet on a machine that is not a local hosted tier 1 / one box. This cmdlet is only supporting on a <c='em'>onebox / local tier 1</c> machine." Stop-PSFFunction -Message "Stopping because machine isn't a onebox" return } elseif (!$script:IsAdminRuntime) { Write-PSFMessage -Level Host -Message "The cmdlet needs <c='em'>administrator permission</c> (Run As Administrator) to be able to update the configuration. Please start an <c='em'>elevated</c> session and run the cmdlet again." Stop-PSFFunction -Message "Stopping because the function is not run elevated" return } $OldName = (Get-D365InstanceName).Instancename Write-PSFMessage -Level Verbose -Message "Old name collected and will be used to rename." -Target $OldName # Variables $replaceValue = $OldName $NewNameDot = "$NewName." $replaceValueDot = "$replaceValue." $WebConfigFile = join-Path -path $AosServiceWebRootPath $Script:WebConfig $WifServicesFile = Join-Path -Path $AosServiceWebRootPath $Script:WifServicesConfig $Files = @($WebConfigFile, $WifServicesFile, $IISServerApplicationHostConfigFile, $HostsFile, $MRConfigFile) if(-not (Test-PathExists -Path $Files -Type Leaf)) { return } Write-PSFMessage -Level Verbose -Message "Stopping the IIS." iisreset /stop # Backup files if ($null -ne $BackupExtension -and $BackupExtension -ne '') { foreach ($item in $Files) { Backup-File $item $BackupExtension } } # WebConfig - D365 web config file Rename-ConfigValue $WebConfigFile $NewName $replaceValue # Wif.Services - D365 web config file (services) Rename-ConfigValue $WifServicesFile $NewName $replaceValue #ApplicationHost - IIS Bindings Rename-ConfigValue $IISServerApplicationHostConfigFile $NewNameDot $replaceValueDot #Hosts file - local DNS cache Rename-ConfigValue $HostsFile $NewNameDot $replaceValueDot #Management Reporter Rename-ConfigValue $MRConfigFile $NewName $replaceValue #Start IIS again Write-PSFMessage -Level Verbose -Message "Starting the IIS." iisreset /start Get-D365Url -Force } <# .SYNOPSIS Restart the different services .DESCRIPTION Restart the different services in a Dynamics 365 Finance & Operations environment .PARAMETER ComputerName An array of computers that you want to work against .PARAMETER All Instructs the cmdlet work against all relevant services Includes: Aos Batch Financial Reporter DMF .PARAMETER Aos Instructs the cmdlet to work against the AOS (IIS) service .PARAMETER Batch Instructs the cmdlet to work against the Batch service .PARAMETER FinancialReporter Instructs the cmdlet to work against the Financial Reporter (Management Reporter 2012) .PARAMETER DMF Instructs the cmdlet to work against the DMF service .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:\> Restart-D365Environment -All This will stop all services and then start all services again. .EXAMPLE PS C:\> Restart-D365Environment -All -ShowOriginalProgress This will stop all services and then start all services again. The progress of Stopping the different services will be written to the console / host. The progress of Starting the different services will be written to the console / host. .EXAMPLE PS C:\> Restart-D365Environment -ComputerName "TEST-SB-AOS1","TEST-SB-AOS2","TEST-SB-BI1" -All This will work against the machines: "TEST-SB-AOS1","TEST-SB-AOS2","TEST-SB-BI1". This will stop all services and then start all services again. .EXAMPLE PS C:\> Restart-D365Environment -Aos -Batch This will stop the AOS and Batch services and then start the AOS and Batch services again. .NOTES Tags: Environment, Service, Services, Aos, Batch, Servicing Author: M�tz Jensen (@Splaxi) #> function Restart-D365Environment { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidDefaultValueSwitchParameter", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 1 )] [Parameter(Mandatory = $false, ParameterSetName = 'Specific', Position = 1 )] [string[]] $ComputerName = @($env:computername), [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, [Parameter(Mandatory = $False)] [switch] $ShowOriginalProgress ) Stop-D365Environment @PSBoundParameters | Format-Table Start-D365Environment @PSBoundParameters | Format-Table } <# .SYNOPSIS Send broadcast message to online users in D365FO .DESCRIPTION Utilize the same messaging framework available from LCS and send a broadcast message to all online users in the environment .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 StartTime The time and date you want the message to be displayed for the users Default value is NOW 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 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. .EXAMPLE PS C:\> Send-D365BroadcastMessage This will send a message to all active users that are working on default D365FO environment. See the RELATED LINKS section for the supporting cmdlets needed to store a default configuration. .EXAMPLE PS C:\> Send-D365BroadcastMessage -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 send a message to all active users that are working on the D365FO environment located at "https://usnconeboxax1aos.cloud.onebox.dynamics.com". It will authenticate against the Azure Active Directory with the "e674da86-7ee5-40a7-b777-1111111111111" guid. It will use the ClientId "dea8d7a9-1602-4429-b138-111111111111" and ClientSecret "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522" go get access to the environment. It will use the default value "UTC" Time Zone for converting the different time and dates. It will use the default start time which is NOW. It will use the default end time which is 60 minutes. .NOTES 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. Tags: Servicing, Message, Users, Environment Author: M�tz Jensen (@Splaxi) .LINK Add-D365BroadcastMessageConfig .LINK Clear-D365ActiveBroadcastMessageConfig .LINK Get-D365ActiveBroadcastMessageConfig .LINK Get-D365BroadcastMessageConfig .LINK Remove-D365BroadcastMessageConfig .LINK Set-D365ActiveBroadcastMessageConfig #> function Send-D365BroadcastMessage { [CmdletBinding()] [OutputType()] param ( [Parameter(Mandatory = $false, Position = 1)] [Alias('$AADGuid')] [string] $Tenant = $Script:BroadcastTenant, [Parameter(Mandatory = $false, Position = 2)] [Alias('URI')] [string] $URL = $Script:BroadcastUrl, [Parameter(Mandatory = $false, Position = 3)] [string] $ClientId = $Script:BroadcastClientId, [Parameter(Mandatory = $false, Position = 4)] [string] $ClientSecret = $Script:BroadcastClientSecret, [Parameter(Mandatory = $false, Position = 5)] [string] $TimeZone = $Script:BroadcastTimeZone, [Parameter(Mandatory = $false, Position = 6)] [datetime] $StartTime = (Get-Date), [Parameter(Mandatory = $false, Position = 7)] [int] $EndingInMinutes = $Script:BroadcastEndingInMinutes ) $bearerParms = @{ AuthProviderUri = "https://login.microsoftonline.com/$Tenant/oauth2/token" Resource = $URL ClientId = $ClientId ClientSecret = $ClientSecret } $bearer = Get-ClientCredentialsBearerToken @bearerParms $headerParms = @{ URL = $URL BearerToken = $bearer } $headers = New-AuthorizationHeaderBearerToken @headerParms [System.UriBuilder] $messageEndpoint = $URL $messageEndpoint.Path = "api/services/SysBroadcastMessageServices/SysBroadcastMessageService/AddMessage" $endTime = $StartTime.AddMinutes($EndingInMinutes) $timeZoneFound = Get-TimeZone -InputObject $TimeZone if (Test-PSFFunctionInterrupt) { return } $startTimeConverted = [System.TimeZoneInfo]::ConvertTime($startTime, [System.TimeZoneInfo]::Local, $timeZoneFound) $endTimeConverted = [System.TimeZoneInfo]::ConvertTime($endTime, [System.TimeZoneInfo]::Local, $timeZoneFound) $body = @" { "request": { "FromDateTime": "$($startTimeConverted.ToString("s"))", "ToDateTime": "$($endTimeConverted.ToString("s"))" } } "@ try { [PSCustomObject]@{ MessageId = Invoke-RestMethod -Method Post -Uri $messageEndpoint.Uri.AbsoluteUri -Headers $headers -ContentType 'application/json' -Body $body } } catch { Write-PSFMessage -Level Host -Message "Something went wrong while trying to send a message to the users." -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors." return } } <# .SYNOPSIS Set the active Azure Storage Account configuration .DESCRIPTION Updates the current active Azure Storage Account configuration with a new one .PARAMETER Name The name the Azure Storage Account configuration you want to load into the active Azure Storage Account configuration .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 Temporary Instruct the cmdlet to only temporarily override the persisted settings in the configuration storage .EXAMPLE PS C:\> Set-D365ActiveAzureStorageConfig -Name "UAT-Exports" This will import the "UAT-Exports" set from the Azure Storage Account configurations. It will update the active Azure Storage Account configuration. .EXAMPLE PS C:\> Set-D365ActiveAzureStorageConfig -Name "UAT-Exports" -ConfigStorageLocation "System" This will import the "UAT-Exports" set from the Azure Storage Account configurations. It will update the active Azure Storage Account configuration. The data will be stored in the system wide configuration storage, which makes it accessible from all users. .EXAMPLE PS C:\> Set-D365ActiveAzureStorageConfig -Name "UAT-Exports" -Temporary This will import the "UAT-Exports" set from the Azure Storage Account configurations. It will update the active Azure Storage Account configuration. The update will only last for the rest of this PowerShell console session. .NOTES Author: M�tz Jensen (@Splaxi) You will have to run the Initialize-D365Config cmdlet first, before this will be capable of working. You will have to run the Add-D365AzureStorageConfig cmdlet at least once, before this will be capable of working. #> function Set-D365ActiveAzureStorageConfig { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [string] $Name, [ValidateSet('User', 'System')] [string] $ConfigStorageLocation = "User", [switch] $Temporary ) $configScope = Test-ConfigStorageLocation -ConfigStorageLocation $ConfigStorageLocation if (Test-PSFFunctionInterrupt) { return } $azureStorageConfigs = [hashtable] (Get-PSFConfigValue -FullName "d365fo.tools.azure.storage.accounts") if (-not ($azureStorageConfigs.ContainsKey($Name))) { Write-PSFMessage -Level Host -Message "An Azure Storage Account with that name <c='em'>doesn't exists</c>." Stop-PSFFunction -Message "Stopping because an Azure Storage Account with that name doesn't exists." return } else { $azureDetails = $azureStorageConfigs[$Name] Set-PSFConfig -FullName "d365fo.tools.active.azure.storage.account" -Value $azureDetails if (-not $Temporary) { Register-PSFConfig -FullName "d365fo.tools.active.azure.storage.account" -Scope $configScope } $Script:AccountId = $azureDetails.AccountId $Script:AccessToken = $azureDetails.AccessToken $Script:Container = $azureDetails.Container $Script:SAS = $azureDetails.SAS } } <# .SYNOPSIS Set the active broadcast message configuration .DESCRIPTION Updates the current active broadcast message configuration with a new one .PARAMETER Name Name of the broadcast message configuration you want to load into the active broadcast message configuration .PARAMETER Temporary Instruct the cmdlet to only temporarily override the persisted settings in the configuration store .EXAMPLE PS C:\> Set-D365ActiveBroadcastMessageConfig -Name "UAT" This will set the broadcast message configuration named "UAT" as the active configuration. .NOTES Tags: Servicing, Message, Users, Environment, Config, Configuration, ClientId, ClientSecret Author: M�tz Jensen (@Splaxi) .LINK Add-D365BroadcastMessageConfig .LINK Clear-D365ActiveBroadcastMessageConfig .LINK Get-D365ActiveBroadcastMessageConfig .LINK Get-D365BroadcastMessageConfig .LINK Remove-D365BroadcastMessageConfig .LINK Send-D365BroadcastMessage #> function Set-D365ActiveBroadcastMessageConfig { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] [OutputType()] param ( [Parameter(Mandatory = $true, Position = 1)] [string] $Name, [switch] $Temporary ) if($Name -match '\*') { Write-PSFMessage -Level Host -Message "The name cannot contain <c='em'>wildcard character</c>." Stop-PSFFunction -Message "Stopping because the name contains wildcard character." return } if (-not ((Get-PSFConfig -FullName "d365fo.tools.broadcast.*.name").Value -contains $Name)) { Write-PSFMessage -Level Host -Message "A broadcast message configuration with that name <c='em'>doesn't exists</c>." Stop-PSFFunction -Message "Stopping because a broadcast message configuration with that name doesn't exists." return } Set-PSFConfig -FullName "d365fo.tools.active.broadcast.message.config.name" -Value $Name if (-not $Temporary) { Register-PSFConfig -FullName "d365fo.tools.active.broadcast.message.config.name" -Scope UserDefault } Update-BroadcastVariables } <# .SYNOPSIS Set the active environment configuration .DESCRIPTION Updates the current active environment configuration with a new one .PARAMETER Name The name the environment configuration you want to load into the active environment configuration .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 Temporary Switch to instruct the cmdlet to only temporarily override the persisted settings in the configuration storage .EXAMPLE PS C:\> Set-D365ActiveEnvironmentConfig -Name "UAT" This will import the "UAT-Exports" set from the Environment configurations. It will update the active Environment Configuration. .EXAMPLE PS C:\> Set-D365ActiveEnvironmentConfig -Name "UAT" -ConfigStorageLocation "System" This will import the "UAT-Exports" set from the Environment configurations. It will update the active Environment Configuration. The data will be stored in the system wide configuration storage, which makes it accessible from all users. .EXAMPLE PS C:\> Set-D365ActiveEnvironmentConfig -Name "UAT" -Temporary This will import the "UAT-Exports" set from the Environment configurations. It will update the active Environment Configuration. The update will only last for the rest of this PowerShell console session. .NOTES Author: M�tz Jensen (@Splaxi) You will have to run the Initialize-D365Config cmdlet first, before this will be capable of working. You will have to run the Add-D365EnvironmentConfig cmdlet at least once, before this will be capable of working. #> function Set-D365ActiveEnvironmentConfig { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [string] $Name, [ValidateSet('User', 'System')] [string] $ConfigStorageLocation = "User", [switch] $Temporary ) $configScope = Test-ConfigStorageLocation -ConfigStorageLocation $ConfigStorageLocation if (Test-PSFFunctionInterrupt) { return } $environmentConfigs = [hashtable](Get-PSFConfigValue -FullName "d365fo.tools.environments") if (-not ($environmentConfigs.ContainsKey($Name))) { Write-PSFMessage -Level Host -Message "An environment with that name <c='em'>doesn't exists</c>." Stop-PSFFunction -Message "Stopping because an environment with that name doesn't exists." return } else { $environmentDetails = $environmentConfigs[$Name] Set-PSFConfig -FullName "d365fo.tools.active.environment" -Value $environmentDetails if (-not $Temporary) { Register-PSFConfig -FullName "d365fo.tools.active.environment" -Scope $configScope } $Script:Url = $environmentDetails.URL $Script:DatabaseUserName = $environmentDetails.SqlUser $Script:DatabaseUserPassword = $environmentDetails.SqlPwd $Script:Company = $environmentDetails.Company $Script:TfsUri = $environmentDetails.TfsUri } } <# .SYNOPSIS Powershell implementation of the AdminProvisioning tool .DESCRIPTION Cmdlet using the AdminProvisioning tool from D365FO .PARAMETER AdminSignInName Email for the Admin .PARAMETER DatabaseServer Alternative SQL Database server, Default is the one provided by the DataAccess object .PARAMETER DatabaseName Alternative SQL Database, Default is the one provided by the DataAccess object .PARAMETER SqlUser Alternative SQL user, Default is the one provided by the DataAccess object .PARAMETER SqlPwd Alternative SQL user password, Default is the one provided by the DataAccess object .EXAMPLE PS C:\> Set-D365Admin "claire@contoso.com" This will provision claire@contoso.com as administrator for the environment .NOTES Author: Rasmus Andersen (@ITRasmus) Author: M�tz Jensen (@Splaxi) #> function Set-D365Admin { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [Parameter(Mandatory = $true, Position = 1)] [Alias('Email')] [String]$AdminSignInName, [Parameter(Mandatory = $false, Position = 2)] [string]$DatabaseServer = $Script:DatabaseServer, [Parameter(Mandatory = $false, Position = 3)] [string]$DatabaseName = $Script:DatabaseName, [Parameter(Mandatory = $false, Position = 4)] [string]$SqlUser = $Script:DatabaseUserName, [Parameter(Mandatory = $false, Position = 5)] [string]$SqlPwd = $Script:DatabaseUserPassword ) if (-not ($script:IsAdminRuntime)) { Write-PSFMessage -Level Host -Message "The cmdlet needs <c='em'>administrator permission</c> (Run As Administrator) to be able to update the configuration. Please start an <c='em'>elevated</c> session and run the cmdlet again." Stop-PSFFunction -Message "Stopping because the function is not run elevated" return } Set-AdminUser $AdminSignInName $DatabaseServer $DatabaseName $SqlUser $SqlPwd } <# .SYNOPSIS Set the ClickOnce needed configuration .DESCRIPTION Creates the needed registry keys and values for ClickOnce to work on the machine .EXAMPLE PS C:\> Set-D365ClickOnceTrustPrompt This will create / or update the current ClickOnce configuration. .NOTES Author: M�tz Jensen (@Splaxi) #> function Set-D365ClickOnceTrustPrompt { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( ) begin { } process { Write-PSFMessage -Level Verbose -Message "Testing if the registry key exists or not" if (-not (Test-Path -Path "HKLM:\SOFTWARE\MICROSOFT\.NETFramework\Security\TrustManager\PromptingLevel")) { Write-PSFMessage -Level Verbose -Message "Registry key was not found. Will create it now." $null = New-Item -Path "HKLM:\SOFTWARE\MICROSOFT\.NETFramework\Security\TrustManager" -Name "PromptingLevel" -Force } Write-PSFMessage -Level Verbose -Message "Setting all necessary registry keys." Set-ItemProperty -Path "HKLM:\SOFTWARE\MICROSOFT\.NETFramework\Security\TrustManager\PromptingLevel" -Name "UntrustedSites" -Type STRING -Value "Disabled" -Force Set-ItemProperty -Path "HKLM:\SOFTWARE\MICROSOFT\.NETFramework\Security\TrustManager\PromptingLevel" -Name "Internet" -Type STRING -Value "Enabled" -Force Set-ItemProperty -Path "HKLM:\SOFTWARE\MICROSOFT\.NETFramework\Security\TrustManager\PromptingLevel" -Name "MyComputer" -Type STRING -Value "Enabled" -Force Set-ItemProperty -Path "HKLM:\SOFTWARE\MICROSOFT\.NETFramework\Security\TrustManager\PromptingLevel" -Name "LocalIntranet" -Type STRING -Value "Enabled" -Force Set-ItemProperty -Path "HKLM:\SOFTWARE\MICROSOFT\.NETFramework\Security\TrustManager\PromptingLevel" -Name "TrustedSites" -Type STRING -Value "Enabled" -Force } end { } } <# .SYNOPSIS Enable the favorite bar and add an URL .DESCRIPTION Enable the favorite bar in internet explorer and put in the URL as a favorite .PARAMETER URL The URL of the shortcut you want to add to the favorite bar .EXAMPLE PS C:\> Set-D365FavoriteBookmark -Url "https://usnconeboxax1aos.cloud.onebox.dynamics.com" This will add the "https://usnconeboxax1aos.cloud.onebox.dynamics.com" to the favorite bar, enable the favorite bar and lock it. .EXAMPLE PS C:\> Get-D365Url | Set-D365FavoriteBookmark This will get the URL from the environment and add that to the favorite bar, enable the favorite bar and lock it. .NOTES Author: M�tz Jensen (@Splaxi) #> function Set-D365FavoriteBookmark { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [Parameter(ValueFromPipelineByPropertyName = $true)] [string] $URL ) begin { } process { $fileName = "D365FO.url" $filePath = Join-Path (Join-Path $Home "Favorites\Links") $fileName $pathShowBar = 'HKCU:\Software\Microsoft\Internet Explorer\MINIE\' $propShowBar = 'LinksBandEnabled' $pathLockBar = 'HKCU:\Software\Microsoft\Internet Explorer\Toolbar\' $propLockBar = 'Locked' $value = "00000001" Write-PSFMessage -Level Verbose -Message "Setting the show bar and lock bar registry values." Set-ItemProperty -Path $pathShowBar -Name $propShowBar -Value $value -Type "DWord" Set-ItemProperty -Path $pathLockBar -Name $propLockBar -Value $value -Type "DWord" $null = New-Item -Path $filePath -Force -ErrorAction SilentlyContinue $LinkContent = (Get-Content "$script:ModuleRoot\internal\misc\$fileName") -Join [Environment]::NewLine $LinkContent.Replace("##URL##", $URL) | Out-File $filePath } end { } } <# .SYNOPSIS Set the LCS configuration details .DESCRIPTION Set the LCS configuration details and save them into the configuration store .PARAMETER ProjectId The project id for the Dynamics 365 for Finance & Operations project inside LCS .PARAMETER ClientId The Azure Registered Application Id / Client Id obtained while creating a Registered App inside the Azure Portal .PARAMETER Username The username of the account that you want to impersonate It can either be your personal account or a service account .PARAMETER Password The password of the account that you want to impersonate .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" .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 Temporary Instruct the cmdlet to only temporarily override the persisted settings in the configuration storage .PARAMETER Clear Instruct the cmdlet to clear out all the stored configuration values .EXAMPLE PS C:\> Set-D365LcsUploadConfig -ProjectId 123456789 -ClientId "9b4f4503-b970-4ade-abc6-2c086e4c4929" -Username claire@contoso.com -Password "pass@word1" -LcsApiUri "https://lcsapi.lcs.dynamics.com" This will save the ProjectId 123456789 and ClientId "9b4f4503-b970-4ade-abc6-2c086e4c4929" to be default values when using the Invoke-D365LcsUpload cmdlet. The Username Claire@contoso.com and the Password "pass@word1" will also be stored as default values when using the Invoke-D365LcsUpload cmdlet. The NON-EUROPE LCS API address will be configured as the endpoint when using the Invoke-D365LcsUpload cmdlet. .NOTES Tags: Environment, Url, Config, Configuration, LCS, Upload, ClientId Author: M�tz Jensen (@Splaxi) #> function Set-D365LcsUploadConfig { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingUserNameAndPassWordParams", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] [OutputType()] param( [Parameter(Mandatory = $false, Position = 1)] [int]$ProjectId, [Parameter(Mandatory = $false, Position = 2)] [string] $ClientId, [Parameter(Mandatory = $false, Position = 3)] [string] $Username, [Parameter(Mandatory = $false, Position = 4)] [string] $Password, [Parameter(Mandatory = $false, Position = 9)] [ValidateSet("https://lcsapi.lcs.dynamics.com", "https://lcsapi.eu.lcs.dynamics.com")] [string]$LcsApiUri = "https://lcsapi.lcs.dynamics.com", [ValidateSet('User', 'System')] [string] $ConfigStorageLocation = "User", [switch] $Temporary, [switch] $Clear ) $configScope = Test-ConfigStorageLocation -ConfigStorageLocation $ConfigStorageLocation if (Test-PSFFunctionInterrupt) { return } if ($Clear) { Write-PSFMessage -Level Verbose -Message "Clearing all the d365fo.tools.lcs configurations." foreach ($item in (Get-PSFConfig -FullName d365fo.tools.lcs*)) { Set-PSFConfig -Fullname $item.FullName -Value "" if (-not $Temporary) { Register-PSFConfig -FullName $item.FullName -Scope $configScope } } } else { foreach ($key in $PSBoundParameters.Keys) { $value = $PSBoundParameters.Item($key) Write-PSFMessage -Level Verbose -Message "Working on $key with $value" -Target $value switch ($key) { "ProjectId" { Set-PSFConfig -FullName "d365fo.tools.lcs.upload.projectid" -Value $value if (-not $Temporary) { Register-PSFConfig -FullName "d365fo.tools.lcs.upload.projectid" -Scope $configScope } } "ClientId" { Set-PSFConfig -FullName "d365fo.tools.lcs.upload.clientid" -Value $value if (-not $Temporary) { Register-PSFConfig -FullName "d365fo.tools.lcs.upload.clientid" -Scope $configScope } } "Username" { Set-PSFConfig -FullName "d365fo.tools.lcs.upload.username" -Value $value if (-not $Temporary) { Register-PSFConfig -FullName "d365fo.tools.lcs.upload.username" -Scope $configScope } } "Password" { Set-PSFConfig -FullName "d365fo.tools.lcs.upload.password" -Value $value if (-not $Temporary) { Register-PSFConfig -FullName "d365fo.tools.lcs.upload.password" -Scope $configScope } } "LcsApiUri" { Set-PSFConfig -FullName "d365fo.tools.lcs.upload.api.uri" -Value $value if (-not $Temporary) { Register-PSFConfig -FullName "d365fo.tools.lcs.upload.api.uri" -Scope $configScope } } Default {} } } } Write-PSFMessage -Level Verbose -Message "Rebuilding the LCS variables." foreach ($item in (Get-PSFConfig -FullName d365fo.tools.lcs*)) { $nameTemp = $item.FullName -replace "^d365fo.tools.", "" $name = ($nameTemp -Split "\." | ForEach-Object { (Get-Culture).TextInfo.ToTitleCase($_) } ) -Join "" Set-Variable -Name $name -Value $item.Value } } <# .SYNOPSIS Set the details for the logic app invoke cmdlet .DESCRIPTION Store the needed details for the module to execute an Azure Logic App using a HTTP request .PARAMETER Url The URL for the http request endpoint of the desired logic app .PARAMETER Email The receiving email address that should be notified .PARAMETER Subject The subject of the email that you want to send .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 Temporary Switch to instruct the cmdlet to only temporarily override the persisted settings in the configuration storage .EXAMPLE PS C:\> Set-D365LogicAppConfig -Email administrator@contoso.com -Subject "Work is done" -Url https://prod-35.westeurope.logic.azure.com:443/ This will set all the details about invoking the Logic App. .EXAMPLE PS C:\> Set-D365LogicAppConfig -Email administrator@contoso.com -Subject "Work is done" -Url https://prod-35.westeurope.logic.azure.com:443/ -ConfigStorageLocation "System" This will set all the details about invoking the Logic App. The data will be stored in the system wide configuration storage, which makes it accessible from all users. .EXAMPLE PS C:\> Set-D365LogicAppConfig -Email administrator@contoso.com -Subject "Work is done" -Url https://prod-35.westeurope.logic.azure.com:443/ -Temporary This will set all the details about invoking the Logic App. The update will only last for the rest of this PowerShell console session. .NOTES Author: M�tz Jensen (@Splaxi) #> function Set-D365LogicAppConfig { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [Parameter(Mandatory = $true )] [string] $Url, [Parameter(Mandatory = $false )] [string] $Email, [Parameter(Mandatory = $false )] [string] $Subject, [ValidateSet('User', 'System')] [string] $ConfigStorageLocation = "User", [switch] $Temporary ) $configScope = Test-ConfigStorageLocation -ConfigStorageLocation $ConfigStorageLocation if (Test-PSFFunctionInterrupt) { return } $logicDetails = @{URL = $URL; Email = $Email; Subject = $Subject; } Set-PSFConfig -FullName "d365fo.tools.active.logic.app" -Value $logicDetails if (-not $Temporary) { Register-PSFConfig -FullName "d365fo.tools.active.logic.app" -Scope $configScope } $Script:LogicAppEmail = $logicDetails.Email $Script:LogicAppSubject = $logicDetails.Subject $Script:LogicAppUrl = $logicDetails.Url } <# .SYNOPSIS Sets the offline administrator e-mail .DESCRIPTION Sets the registered offline administrator in the "DynamicsDevConfig.xml" file located in the default Package Directory .PARAMETER Email The desired email address of the to be offline administrator .EXAMPLE PS C:\> Set-D365OfflineAuthenticationAdminEmail -Email "admin@contoso.com" Will update the Offline Administrator E-mail address in the DynamicsDevConfig.xml file with "admin@contoso.com" .NOTES This cmdlet is inspired by the work of "Sheikh Sohail Hussain" (twitter: @SSohailHussain) His blog can be found here: http://d365technext.blogspot.com The specific blog post that we based this cmdlet on can be found here: http://d365technext.blogspot.com/2018/07/offline-authentication-admin-email.html Author: M�tz Jensen (@Splaxi) #> function Set-D365OfflineAuthenticationAdminEmail { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory = $true, ParameterSetName = 'Default', Position = 1 )] [string] $Email ) if (-not ($script:IsAdminRuntime)) { Write-PSFMessage -Level Host -Message "The cmdlet needs <c='em'>administrator permission</c> (Run As Administrator) to be able to update the configuration. Please start an <c='em'>elevated</c> session and run the cmdlet again." Stop-PSFFunction -Message "Stopping because the function is not run elevated" return } $filePath = Join-Path (Join-Path $Script:PackageDirectory "bin") "DynamicsDevConfig.xml" if (-not (Test-PathExists -Path $filePath -Type Leaf)) {return} $namespace = @{ns="http://schemas.microsoft.com/dynamics/2012/03/development/configuration"} $xmlDoc = [xml] (Get-Content -Path $filePath) $OfflineAuthAdminEmail = Select-Xml -Xml $xmlDoc -XPath "/ns:DynamicsDevConfig/ns:OfflineAuthenticationAdminEmail" -Namespace $namespace $oldValue = $OfflineAuthAdminEmail.Node.InnerText Write-PSFMessage -Level Verbose -Message "Old value found in the file was: $oldValue" -Target $oldValue $OfflineAuthAdminEmail.Node.InnerText = $Email $xmlDoc.Save($filePath) } <# .SYNOPSIS Set the cleanup retention period .DESCRIPTION Sets the configured retention period before updates are deleted .PARAMETER NumberOfDays Number of days that deployable software packages should remain on the server .EXAMPLE PS C:\> Set-D365SDPCleanUp -NumberOfDays 10 This will set the retention period to 10 days inside the the registry The cmdlet REQUIRES elevated permissions to run, otherwise it will fail .NOTES This cmdlet is based on the findings from Alex Kwitny (@AlexOnDAX) See his blog for more info: http://www.alexondax.com/2018/04/msdyn365fo-how-to-adjust-your.html Author: M�tz Jensen (@Splaxi) #> function Set-D365SDPCleanUp { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [int] $NumberOfDays = 30 ) if (-not ($Script:IsAdminRuntime)) { Write-PSFMessage -Level Host -Message "It seems that you ran this cmdlet <c='em'>non-elevated</c>. Making changes to the registry requires you to run this cmdlet from an elevated console. Please exit the current console and start a new with `"Run As Administrator`"" Stop-PSFFunction -Message "Stopping because of missing parameters" return } Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Dynamics\Deployment" -Name "CutoffDaysForCleanup" -Type STRING -Value "$NumberOfDays" -Force } <# .SYNOPSIS Sets the start page in internet explorer .DESCRIPTION Function for setting the start page in internet explorer .PARAMETER Name Name of the D365 Instance .PARAMETER Url URL of the D365 for Finance & Operations instance that you want to have as your start page .EXAMPLE PS C:\> Set-D365StartPage -Name 'Demo1' This will update the start page for the current user to "https://Demo1.cloud.onebox.dynamics.com" .EXAMPLE PS C:\> Set-D365StartPage -URL "https://uat.sandbox.operations.dynamics.com" This will update the start page for the current user to "https://uat.sandbox.operations.dynamics.com" .NOTES Author: Rasmus Andersen (@ITRasmus) Author: M�tz Jensen (@Splaxi) #> function Set-D365StartPage() { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding(DefaultParameterSetName = 'Default')] param( [Parameter(Mandatory = $true, Position = 1, ParameterSetName = 'Default')] [String] $Name, [Parameter(Mandatory = $true, Position = 1, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Url')] [String] $Url ) $path = 'HKCU:\Software\Microsoft\Internet Explorer\Main\' $propName = 'start page' if ($PSBoundParameters.ContainsKey("URL")) { $value = $Url } else { $value = "https://$Name.cloud.onebox.dynamics.com" } Set-Itemproperty -Path $path -Name $propName -Value $value } <# .SYNOPSIS Set a user to sysadmin .DESCRIPTION Set a user to sysadmin inside the SQL Server .PARAMETER User The user that you want to make sysadmin Most be well formatted server\user or domain\user. Default value is: machinename\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-D365SysAdmin This will configure the local administrator on the machine as a SYSADMIN inside SQL Server For this to run you need to be running it from a elevated console .EXAMPLE PS C:\> Set-D365SysAdmin -SqlPwd Test123 This will configure the local administrator on the machine as a SYSADMIN inside SQL Server. It will logon as the default SqlUser but use the provided SqlPwd. This can be run from a non-elevated console .NOTES Author: M�tz Jensen (@splaxi) #> function Set-D365SysAdmin { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [Parameter(Mandatory = $false, Position = 1)] [string] $User = "$env:computername\administrator", [Parameter(Mandatory = $false, Position = 2)] [string] $DatabaseServer = $Script:DatabaseServer, [Parameter(Mandatory = $false, Position = 3)] [string] $DatabaseName = $Script:DatabaseName, [Parameter(Mandatory = $false, Position = 4)] [string] $SqlUser = $Script:DatabaseUserName, [Parameter(Mandatory = $false, Position = 5)] [string] $SqlPwd = $Script:DatabaseUserPassword ) $SqlParams = @{ DatabaseServer = $DatabaseServer; DatabaseName = $DatabaseName; SqlUser = $SqlUser; SqlPwd = $SqlPwd } Write-PSFMessage -Level Debug -Message "Testing if running either elevated or with -SqlPwd set." if ((-not ($script:IsAdminRuntime)) -and (-not ($PSBoundParameters.ContainsKey("SqlPwd")))) { Write-PSFMessage -Level Host -Message "It seems that you ran this cmdlet <c='em'>non-elevated</c> and without the <c='em'>-SqlPwd parameter</c>. If you don't want to supply the -SqlPwd you must run the cmdlet elevated (Run As Administrator) otherwise simply use the -SqlPwd parameter" Stop-PSFFunction -Message "Stopping because of missing parameters" return } $commandText = (Get-Content "$script:ModuleRoot\internal\sql\set-sysadmin.sql") -join [Environment]::NewLine $commandText = $commandText.Replace('@USER', $User) $sqlCommand = Get-SqlCommand @SqlParams $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 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 Save hashtable with parameters .DESCRIPTION Saves the hashtable as a json string into the configuration store This cmdlet is only intended to be used for New-D365Bacpac and Import-D365Bacpac for Tier2 environments .PARAMETER InputObject The hashtable containing all the parameters you want to store .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 Temporary Switch to instruct the cmdlet to only temporarily override the persisted settings in the configuration storage .EXAMPLE PS C:\> $params = @{ SqlUser = "sqladmin" PS C:\> SqlPwd = "pass@word1" PS C:\> } PS C:\> Set-D365Tier2Params -InputObject $params .NOTES Author: M�tz Jensen (@Splaxi) #> function Set-D365Tier2Params { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding()] param ( [Parameter(Mandatory = $true, Position = 1)] [HashTable] $InputObject, [ValidateSet('User', 'System')] [string] $ConfigStorageLocation = "User", [switch] $Temporary ) if ($null -eq $($InputObject.Keys)) { Write-PSFMessage -Level Host -Message "The input object seems to be empty. Please ensure that the input object is a hashtable and it actually contains data." Stop-PSFFunction -Message "Stopping because the input object didn't contain data." return } $configScope = Test-ConfigStorageLocation -ConfigStorageLocation $ConfigStorageLocation $jsonString = ConvertTo-Json -InputObject $InputObject Write-PSFMessage -Level Verbose -Message "Converted hashtable to json string" -Target $jsonString Set-PSFConfig -FullName "d365fo.tools.tier2.bacpac.params" -Value $jsonString if (-not $Temporary) { Register-PSFConfig -FullName "d365fo.tools.tier2.bacpac.params" -Scope $configScope } } <# .SYNOPSIS Set the Workstation mode .DESCRIPTION Set the Workstation mode to enabled or not It is used to enable the tool to run on a personal machine and still be able to call Invoke-D365TableBrowser and Invoke-D365SysRunnerClass .PARAMETER Enabled $True enables the workstation mode while $false deactivated the workstation mode .EXAMPLE PS C:\> Set-D365WorkstationMode -Enabled $true This will enable the Workstation mode. You will have to restart the powershell session when you switch around. .NOTES Author: M�tz Jensen (@Splaxi) You will have to run the Initialize-D365Config cmdlet first, before this will be capable of working. #> function Set-D365WorkstationMode { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [boolean] $Enabled ) Set-PSFConfig -FullName "d365fo.tools.workstation.mode" -Value $Enabled Get-PSFConfig -FullName "d365fo.tools.workstation.mode" | Register-PSFConfig Write-PSFMessage -Level Host -Message "Please <c='em'>restart</c> the powershell session / console. This change affects core functionality that <c='em'>requires</c> the module to be <c='em'>reloaded</c>." } <# .SYNOPSIS Cmdlet to start the different services in a Dynamics 365 Finance & Operations environment .DESCRIPTION Can start all relevant services that is running in a D365FO environment .PARAMETER ComputerName An array of computers that you want to start services on. .PARAMETER All Set when you want to start all relevant services Includes: Aos Batch Financial Reporter .PARAMETER Aos Start the Aos (iis) service .PARAMETER Batch Start the batch service .PARAMETER FinancialReporter Start the financial reporter (Management Reporter 2012) service .PARAMETER DMF Start the Data Management Framework service .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:\> Start-D365Environment This will run the cmdlet with the default parameters. Default is "-All". This will start all D365FO services on the machine. .EXAMPLE PS C:\> Start-D365Environment -ShowOriginalProgress This will run the cmdlet with the default parameters. Default is "-All". This will start all D365FO services on the machine. The progress of starting the different services will be written to the console / host. .EXAMPLE PS C:\> Start-D365Environment -All This will start all D365FO services on the machine. .EXAMPLE PS C:\> Start-D365Environment -Aos -Batch This will start the Aos & Batch D365FO services on the machine. .NOTES Author: M�tz Jensen (@Splaxi) #> function Start-D365Environment { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidDefaultValueSwitchParameter", "")] [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 1 )] [Parameter(Mandatory = $false, ParameterSetName = 'Specific', Position = 1 )] [string[]] $ComputerName = @($env:computername), [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, [Parameter(Mandatory = $false, ParameterSetName = 'Specific', Position = 6 )] [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 6 )] [switch] $ShowOriginalProgress ) if ($PSCmdlet.ParameterSetName -eq "Specific") { $All = $false } if ( (-not ($All)) -and (-not ($Aos)) -and (-not ($Batch)) -and (-not ($FinancialReporter)) -and (-not ($DMF))) { Write-PSFMessage -Level Host -Message "You have to use at least <c='em'>one switch</c> when running this cmdlet. Please run the cmdlet again." Stop-PSFFunction -Message "Stopping because of missing parameters" return } $warningActionValue = "SilentlyContinue" if ($ShowOriginalProgress) {$warningActionValue = "Continue"} $Params = Get-DeepClone $PSBoundParameters if ($Params.ContainsKey("ComputerName")) {$null = $Params.Remove("ComputerName")} if ($Params.ContainsKey("ShowOriginalProgress")) {$null = $Params.Remove("ShowOriginalProgress")} $Services = Get-ServiceList @Params $Results = foreach ($server in $ComputerName) { Write-PSFMessage -Level Verbose -Message "Working against: $server - starting services" Get-Service -ComputerName $server -Name $Services -ErrorAction SilentlyContinue | Start-Service -ErrorAction SilentlyContinue -WarningAction $warningActionValue } $Results = foreach ($server in $ComputerName) { Write-PSFMessage -Level Verbose -Message "Working against: $server - listing services" Get-Service -ComputerName $server -Name $Services -ErrorAction SilentlyContinue| Select-Object @{Name = "Server"; Expression = {$Server}}, Name, Status, DisplayName } Write-PSFMessage -Level Verbose "Results are: $Results" -Target ($Results.Name -join ",") $Results | Select-PSFObject -TypeName "D365FO.TOOLS.Environment.Service" Server, DisplayName, Status, Name } <# .SYNOPSIS Cmdlet to stop the different services in a Dynamics 365 Finance & Operations environment .DESCRIPTION Can stop all relevant services that is running in a D365FO environment .PARAMETER ComputerName An array of computers that you want to stop services on. .PARAMETER All Set when you want to stop all relevant services Includes: Aos Batch Financial Reporter .PARAMETER Aos Stop the Aos (iis) service .PARAMETER Batch Stop the batch service .PARAMETER FinancialReporter Start the financial reporter (Management Reporter 2012) service .PARAMETER DMF Start the Data Management Framework service .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:\> Stop-D365Environment This will run the cmdlet with the default parameters. Default is "-All". This will stop all D365FO services on the machine. .EXAMPLE PS C:\> Stop-D365Environment -ShowOriginalProgress This will run the cmdlet with the default parameters. Default is "-All". This will Stop all D365FO services on the machine. The progress of Stopping the different services will be written to the console / host. .EXAMPLE PS C:\> Stop-D365Environment -All This will stop all D365FO services on the machine. .EXAMPLE PS C:\> Stop-D365Environment -Aos -Batch This will stop the Aos & Batch D365FO services on the machine. .NOTES Author: M�tz Jensen (@Splaxi) #> function Stop-D365Environment { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidDefaultValueSwitchParameter", "")] [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 1 )] [Parameter(Mandatory = $false, ParameterSetName = 'Specific', Position = 1 )] [string[]] $ComputerName = @($env:computername), [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, [Parameter(Mandatory = $false, ParameterSetName = 'Specific', Position = 6 )] [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 6 )] [switch] $ShowOriginalProgress ) if ($PSCmdlet.ParameterSetName -eq "Specific") { $All = $false } if ((-not ($All)) -and (-not ($Aos)) -and (-not ($Batch)) -and (-not ($FinancialReporter)) -and (-not ($DMF))) { Write-PSFMessage -Level Host -Message "You have to use at least <c='em'>one switch</c> when running this cmdlet. Please run the cmdlet again." Stop-PSFFunction -Message "Stopping because of missing parameters" return } $warningActionValue = "SilentlyContinue" if ($ShowOriginalProgress) {$warningActionValue = "Continue"} $Params = Get-DeepClone $PSBoundParameters if ($Params.ContainsKey("ComputerName")) {$null = $Params.Remove("ComputerName")} if ($Params.ContainsKey("ShowOriginalProgress")) {$null = $Params.Remove("ShowOriginalProgress")} $Services = Get-ServiceList @Params $Results = foreach ($server in $ComputerName) { Write-PSFMessage -Level Verbose -Message "Working against: $server - stopping services" Get-Service -ComputerName $server -Name $Services -ErrorAction SilentlyContinue | Stop-Service -Force -ErrorAction SilentlyContinue -WarningAction $warningActionValue } $Results = foreach ($server in $ComputerName) { Write-PSFMessage -Level Verbose -Message "Working against: $server - listing services" Get-Service -ComputerName $server -Name $Services -ErrorAction SilentlyContinue| Select-Object @{Name = "Server"; Expression = {$Server}}, Name, Status, DisplayName } Write-PSFMessage -Level Verbose "Results are: $Results" -Target ($Results.Name -join ",") $Results | Select-PSFObject -TypeName "D365FO.TOOLS.Environment.Service" Server, DisplayName, Status, Name } <# .SYNOPSIS Switches the 2 databases. The Old wil be renamed _original .DESCRIPTION Switches the 2 databases. The Old wil be renamed _original .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 The database that takes the DatabaseName's place .EXAMPLE PS C:\> Switch-D365ActiveDatabase -NewDatabaseName "GoldenConfig" This will switch the default database AXDB out and put "GoldenConfig" in its place instead. .NOTES Author: Rasmus Andersen (@ITRasmus) Author: M�tz Jensen (@Splaxi) #> function Switch-D365ActiveDatabase { [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 = $true, Position = 5)] [string]$NewDatabaseName ) $Params = Get-DeepClone $PSBoundParameters if ($Params.ContainsKey("NewDatabaseName")) { $null = $Params.Remove("NewDatabaseName") } $dbName = Get-D365Database -Name "$DatabaseName`_original" @Params if (-not($null -eq $dbName)) { Write-PSFMessage -Level Host -Message "There <c='em'>already exists</c> a database named: <c='em'>`"$DatabaseName`_original`"</c> on the server. You need to run the <c='em'>Remove-D365Database</c> cmdlet to remove the already existing database. Re-run this cmdlet once the other database has been removed." Stop-PSFFunction -Message "Stopping because database already exists on the server." return } $UseTrustedConnection = Test-TrustedConnection $PSBoundParameters $SqlParams = @{ DatabaseServer = $DatabaseServer; DatabaseName = $NewDatabaseName; SqlUser = $SqlUser; SqlPwd = $SqlPwd } $SqlCommand = Get-SqlCommand @SqlParams -TrustedConnection $UseTrustedConnection $SqlCommand.CommandText = "SELECT COUNT(1) FROM dbo.USERINFO WHERE ID = 'Admin'" try { Write-PSFMessage -Level InternalComment -Message "Executing a script against the database." -Target (Get-SqlString $SqlCommand) $sqlCommand.Connection.Open() $null = $sqlCommand.ExecuteScalar() } catch { Write-PSFMessage -Level Host -Message "It seems that the new database either doesn't exists, isn't a valid AxDB database or your don't have enough permissions." -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" return } finally { if ($sqlCommand.Connection.State -ne [System.Data.ConnectionState]::Closed) { $sqlCommand.Connection.Close() } } $SqlParams = @{ DatabaseServer = $DatabaseServer; DatabaseName = "Master"; SqlUser = $SqlUser; SqlPwd = $SqlPwd } $SqlCommand = Get-SqlCommand @SqlParams -TrustedConnection $UseTrustedConnection if ($DatabaseServer -like "*database.windows.net") { $commandText = (Get-Content "$script:ModuleRoot\internal\sql\switch-database-tier2.sql") -join [Environment]::NewLine } else { $commandText = (Get-Content "$script:ModuleRoot\internal\sql\switch-database-tier1.sql") -join [Environment]::NewLine } $sqlCommand.CommandText = $commandText $null = $sqlCommand.Parameters.AddWithValue("@OrigName", $DatabaseName) $null = $sqlCommand.Parameters.AddWithValue("@NewName", $NewDatabaseName) 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 working against the 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() } [PSCustomObject]@{ OldDatabaseNewName = "$DatabaseName`_original" } } <# .SYNOPSIS Validate or show parameter set details with colored output .DESCRIPTION Analyze a function and it's parameters The cmdlet / function is capable of validating a string input with function name and parameters .PARAMETER CommandText The string that you want to analyze If there is parameter value present, you have to use the opposite quote strategy to encapsulate the string correctly E.g. for double quotes -CommandText 'Import-D365Bacpac -ImportModeTier2 -SqlUser "sqladmin" -SqlPwd "XyzXyz" -BacpacFile2 "C:\temp\uat.bacpac"' E.g. for single quotes -CommandText "Import-D365Bacpac -ExportModeTier2 -SqlUser 'sqladmin' -SqlPwd 'XyzXyz' -BacpacFile2 'C:\temp\uat.bacpac'" .PARAMETER Mode The operation mode of the cmdlet / function Valid options are: - Validate - ShowParameters .PARAMETER SplatInput Pass in your hashtable that you use for your command execution and have it validated .PARAMETER ShowSplatStyleV1 Include an hashtable splatting for all parameter sets in the output The example is built like this: PS C:\> $params = @{} PS C:\> $params.PropertyName = "SAMPLEVALUE" PS C:\> Test-FakeCommand @params .PARAMETER ShowSplatStyleV2 Include an hashtable splatting for all parameter sets in the output The example is built like this: PS C:\> $params = @{ PS C:\> PropertyName = "SAMPLEVALUE" PS C:\> } PS C:\> Test-FakeCommand @params .PARAMETER IncludeHelp Switch to instruct the cmdlet / function to output a simple guide with the colors in it .EXAMPLE PS C:\> Test-D365Command -CommandText 'Import-D365Bacpac -ImportModeTier2 -SqlUser "sqladmin" -SqlPwd "XyzXyz" -BacpacFile2 "C:\temp\uat.bacpac"' -Mode "Validate" -IncludeHelp This will validate all the parameters that have been passed to the Import-D365Bacpac cmdlet. All supplied parameters that matches a parameter will be marked with an asterisk. Will print the coloring help. .EXAMPLE PS C:\> Test-D365Command -CommandText 'Import-D365Bacpac' -Mode "ShowParameters" -IncludeHelp This will display all the parameter sets and their individual parameters. Will print the coloring help. .EXAMPLE PS C:\> $params = @{} PS C:\> $params.DatabaseName = "SAMPLEVALUE" PS C:\> Test-D365Command -CommandText 'Import-D365Bacpac -ImportModeTier2' -SplatInput $params -Mode "Validate" This builds a hashtable with a property names "DatabaseName". The hashtable is passed to the cmdlet to be part of the validation. .NOTES Author: M�tz Jensen (@Splaxi) #> function Test-D365Command { [CmdletBinding()] param ( [Parameter(Mandatory = $true, Position = 1)] [string] $CommandText, [Parameter(Mandatory = $true, Position = 2)] [ValidateSet('Validate', 'ShowParameters')] [string] $Mode, [hashtable] $SplatInput, [switch] $ShowSplatStyleV1, [switch] $ShowSplatStyleV2, [switch] $IncludeHelp ) $commonParameters = 'Verbose', 'Debug', 'ErrorAction', 'WarningAction', 'InformationAction', 'ErrorVariable', 'WarningVariable', 'InformationVariable', 'OutVariable', 'OutBuffer', 'PipelineVariable', 'Confirm', 'WhatIf' $colorParmsNotFound = "Red" $colorCommandName = "Green" $colorMandatoryParam = "Yellow" $colorNonMandatoryParam = "DarkGray" $colorFoundAsterisk = "Green" $colorNotFoundAsterisk = "Magenta" $colParmValue = "DarkCyan" $colorEqualSign = "DarkGray" $colorVariable = "Green" $colorProperty = "White" $colorCommandNameSplat = "Yellow" $colorComment = "DarkGreen" if(-not ($null -eq $SplatInput)) { $CommandText = "$CommandText "+ $(($SplatInput.Keys | ForEach-Object {"-$($_) `"$($SplatInput.Item($_))`""}) -Join " " ) } #Match to find the command name: Non-Whitespace until the first whitespace $commandMatch = ($CommandText | Select-String '\S+\s*').Matches if ($null -eq $commandMatch) { Write-PSFMessage -Level Host -Message "The function was unable to extract a valid command name from the supplied command text. Please try again." Stop-PSFFunction -Message "Stopping because of missing command name." return } $commandName = $commandMatch.Value.Trim() $res = Get-Command $commandName -ErrorAction Ignore if ($null -eq $res) { Write-PSFMessage -Level Host -Message "The function was unable to get the help of the command. Make sure that the command name is valid and try again." Stop-PSFFunction -Message "Stopping because command name didn't return any help." return } $sbHelp = New-Object System.Text.StringBuilder $sbParmsNotFound = New-Object System.Text.StringBuilder $sbSplatStyleV1 = New-Object System.Text.StringBuilder $sbSplatStyleV2 = New-Object System.Text.StringBuilder switch ($Mode) { "Validate" { #Match to find the parameters: Whitespace Dash Non-Whitespace $inputParameterMatch = ($CommandText | Select-String '\s{1}[-]\S+' -AllMatches).Matches if (-not ($null -eq $inputParameterMatch)) { $inputParameterNames = $inputParameterMatch.Value.Trim("-", " ") Write-PSFMessage -Level Verbose -Message "All input parameters - $($inputParameterNames -join ",")" -Target ($inputParameterNames -join ",") } else { Write-PSFMessage -Level Host -Message "The function was unable to extract any parameters from the supplied command text. Please try again." Stop-PSFFunction -Message "Stopping because of missing input parameters." return } $availableParameterNames = (Get-Command $commandName).Parameters.keys | Where-Object {$commonParameters -NotContains $_} Write-PSFMessage -Level Verbose -Message "Available parameters - $($availableParameterNames -join ",")" -Target ($availableParameterNames -join ",") $inputParameterNotFound = $inputParameterNames | Where-Object {$availableParameterNames -NotContains $_} if ($inputParameterNotFound.Length -gt 0) { $null = $sbParmsNotFound.AppendLine("Parameters that <c='em'>don't exists</c>") $inputParameterNotFound | ForEach-Object { $null = $sbParmsNotFound.AppendLine("<c='$colorParmsNotFound'>$($_)</c>") } } foreach ($parmSet in (Get-Command $commandName).ParameterSets) { $null = $sb = New-Object System.Text.StringBuilder $null = $sb.AppendLine("ParameterSet Name: <c='em'>$($parmSet.Name)</c> - Validated List") $null = $sb.Append("<c='$colorCommandName'>$commandName </c>") $parmSetParameters = $parmSet.Parameters | Where-Object name -NotIn $commonParameters foreach ($parameter in $parmSetParameters) { $parmFoundInCommandText = $parameter.Name -In $inputParameterNames $color = "$colorNonMandatoryParam" if ($parameter.IsMandatory -eq $true) { $color = "$colorMandatoryParam" } $null = $sb.Append("<c='$color'>-$($parameter.Name)</c>") if ($parmFoundInCommandText) { $null = $sb.Append("<c='$colorFoundAsterisk'>* </c>") } elseif ($parameter.IsMandatory -eq $true) { $null = $sb.Append("<c='$colorNotFoundAsterisk'>* </c>") } else { $null = $sb.Append(" ") } if (-not ($parameter.ParameterType -eq [System.Management.Automation.SwitchParameter])) { $null = $sb.Append("<c='$colParmValue'>PARAMVALUE </c>") } } $null = $sb.AppendLine("") Write-PSFHostColor -String "$($sb.ToString())" } $null = $sbHelp.AppendLine("") $null = $sbHelp.AppendLine("<c='$colorParmsNotFound'>$colorParmsNotFound</c> = Parameter not found") $null = $sbHelp.AppendLine("<c='$colorCommandName'>$colorCommandName</c> = Command Name") $null = $sbHelp.AppendLine("<c='$colorMandatoryParam'>$colorMandatoryParam</c> = Mandatory Parameter") $null = $sbHelp.AppendLine("<c='$colorNonMandatoryParam'>$colorNonMandatoryParam</c> = Optional Parameter") $null = $sbHelp.AppendLine("<c='$colParmValue'>$colParmValue</c> = Parameter value") $null = $sbHelp.AppendLine("<c='$colorFoundAsterisk'>*</c> = Parameter was filled") $null = $sbHelp.AppendLine("<c='$colorNotFoundAsterisk'>*</c> = Mandatory missing") } "ShowParameters" { foreach ($parmSet in (Get-Command $commandName).ParameterSets) { $sb = New-Object System.Text.StringBuilder $sbSplatStyleV1 = New-Object System.Text.StringBuilder $sbSplatStyleV2 = New-Object System.Text.StringBuilder $null = $sb.AppendLine("ParameterSet Name: <c='em'>$($parmSet.Name)</c> - Parameter List") $null = $sb.Append("<c='$colorCommandName'>$commandName </c>") $null = $sbSplatStyleV1.AppendLine("<c='$colorComment'>#Hashtable splatting style V1 - ParameterSet Name: </c><c='em'>$($parmSet.Name)</c>").AppendLine("<c='$colorVariable'>`$params</c> <c='$colorEqualSign'>=</c> <c='$colorProperty'>@{}</c>") $null = $sbSplatStyleV2.AppendLine("<c='$colorComment'>#Hashtable splatting style V2 - ParameterSet Name: </c><c='em'>$($parmSet.Name)</c>").AppendLine("<c='$colorVariable'>`$params</c> <c='$colorEqualSign'>=</c> <c='$colorProperty'>@{</c>") $parmSetParameters = $parmSet.Parameters | Where-Object name -NotIn $commonParameters foreach ($parameter in $parmSetParameters) { $color = "$colorNonMandatoryParam" $mandatoryComment = $null if ($parameter.IsMandatory -eq $true) { $color = "$colorMandatoryParam" $mandatoryComment = " <c='$color'>#MANDATORY</c>" } $null = $sbSplatStyleV1.AppendLine("<c='$colorVariable'>`$params</c><c='$colorProperty'>.$($parameter.Name)</c> <c='$colorEqualSign'>=</c> <c='$colParmValue'>`"SAMPLEVALUE`"</c>$mandatoryComment") $null = $sbSplatStyleV2.AppendLine("<c='$colorProperty'>$($parameter.Name)</c> <c='$colorEqualSign'>=</c> <c='$colParmValue'>`"SAMPLEVALUE`"</c>$mandatoryComment") $null = $sb.Append("<c='$color'>-$($parameter.Name) </c>") if (-not ($parameter.ParameterType -eq [System.Management.Automation.SwitchParameter])) { $null = $sb.Append("<c='$colParmValue'>PARAMVALUE </c>") } } $null = $sb.AppendLine("") $null = $sbSplatStyleV2.AppendLine("<c='$colorProperty'>}</c>") $null = $sbSplatStyleV1.AppendLine("<c='$colorCommandNameSplat'>$commandName</c> <c='$colorVariable'>@params</c>") $null = $sbSplatStyleV2.AppendLine("<c='$colorCommandNameSplat'>$commandName</c> <c='$colorVariable'>@params</c>") Write-PSFHostColor -String "$($sb.ToString())" if ($ShowSplatStyleV1) { Write-PSFHostColor -String "$($sbSplatStyleV1.ToString())" } if ($ShowSplatStyleV2) { Write-PSFHostColor -String "$($sbSplatStyleV2.ToString())" } } $null = $sbHelp.AppendLine("") $null = $sbHelp.AppendLine("<c='$colorCommandName'>$colorCommandName</c> = Command Name") $null = $sbHelp.AppendLine("<c='$colorMandatoryParam'>$colorMandatoryParam</c> = Mandatory Parameter") $null = $sbHelp.AppendLine("<c='$colorNonMandatoryParam'>$colorNonMandatoryParam</c> = Optional Parameter") $null = $sbHelp.AppendLine("<c='$colParmValue'>$colParmValue</c> = Parameter value") } Default {} } if ($sbParmsNotFound.ToString().Trim().Length -gt 0) { Write-PSFHostColor -String "$($sbParmsNotFound.ToString())" } if ($IncludeHelp) { Write-PSFHostColor -String "$($sbHelp.ToString())" } } <# .SYNOPSIS Updates the user details in the database .DESCRIPTION Is capable of updating all the user details inside the UserInfo table to enable a user to sign in .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 updated. The parameter supports wildcards. E.g. -Email "*@contoso.com*" .PARAMETER Company The company the user should start in. .EXAMPLE PS C:\> Update-D365User -Email "claire@contoso.com" This will search for the user with the e-mail address claire@contoso.com and update it with needed information based on the tenant owner of the environment .EXAMPLE PS C:\> Update-D365User -Email "*contoso.com" This will search for all users with an e-mail address containing 'contoso.com' and update them with needed information based on the tenant owner of the environment .NOTES Author: Rasmus Andersen (@ITRasmus) Author: M�tz Jensen (@Splaxi) #> function Update-D365User { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [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 = $true, ValueFromPipelineByPropertyName = $true, Position = 5)] [string]$Email, [Parameter(Mandatory = $false, Position = 6)] [string]$Company ) begin { Invoke-TimeSignal -Start $UseTrustedConnection = Test-TrustedConnection $PSBoundParameters $SqlParams = @{ DatabaseServer = $DatabaseServer; DatabaseName = $DatabaseName; SqlUser = $SqlUser; SqlPwd = $SqlPwd } $SqlCommand = Get-SqlCommand @SqlParams -TrustedConnection $UseTrustedConnection $sqlCommand_Update = Get-SqlCommand @SqlParams -TrustedConnection $UseTrustedConnection try { $sqlCommand.Connection.Open() $sqlCommand_Update.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 { $sqlCommand.CommandText = (Get-Content "$script:ModuleRoot\internal\sql\get-user.sql") -join [Environment]::NewLine $null = $sqlCommand.Parameters.Add("@Email", $Email.Replace("*", "%")) $sqlCommand_Update.CommandText = (Get-Content "$script:ModuleRoot\internal\sql\update-user.sql") -join [Environment]::NewLine try { Write-PSFMessage -Level InternalComment -Message "Executing a script against the database." -Target (Get-SqlString $SqlCommand) $reader = $sqlCommand.ExecuteReader() while ($reader.Read() -eq $true) { Write-PSFMessage -Level Verbose -Message "Building the update statement with the needed details." $userId = "$($reader.GetString($($reader.GetOrdinal("ID"))))" $networkAlias = "$($reader.GetString($($reader.GetOrdinal("NETWORKALIAS"))))" $userAuth = Get-D365UserAuthenticationDetail $networkAlias $null = $sqlCommand_Update.Parameters.AddWithValue("@id", $userId) $null = $sqlCommand_Update.Parameters.AddWithValue("@networkDomain", $userAuth["NetworkDomain"]) $null = $sqlCommand_Update.Parameters.AddWithValue("@sid", $userAuth["SID"]) $null = $sqlCommand_Update.Parameters.AddWithValue("@identityProvider", $userAuth["IdentityProvider"]) $null = $sqlCommand_Update.Parameters.AddWithValue("@Company", $Company) Write-PSFMessage -Level InternalComment -Message "Executing a script against the database." -Target (Get-SqlString $sqlCommand_Update) $null = $sqlCommand_Update.ExecuteNonQuery() $sqlCommand_Update.Parameters.Clear() } } 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_Update.Connection.State -ne [System.Data.ConnectionState]::Closed) { $sqlCommand_Update.Connection.Close() } $sqlCommand_Update.Dispose() if ($sqlCommand.Connection.State -ne [System.Data.ConnectionState]::Closed) { $sqlCommand.Connection.Close() } $sqlCommand.Dispose() Invoke-TimeSignal -End } } |