Sunday, October 22, 2017

Use Azure Automation to start and stop your VMs on a schedule

This post is long overdue, and I have been meaning to post it over a year ago.  I did present an early version of this script at the AXUG event in Stuttgart, but since then the API has changed around tags, and also it has become very easy to solve authentication using Run as Accounts. The code I am sharing here works on the latest version of the modules, and I hope it will keep working for years to come.

I few notes before I continue:
  • I base this script off from Automys own code, and it is heavily inspired by the commits done by other users out there in the community. I will refer to the project on GitHub where you will find contributors and authors. 
  • I've only tested and used the script for ARM Resources
  • I removed references to credentials and certificates and it relies on using "Run As Account". Setting up "Run As Account" in Azure is very easy and quick to do.
You will find the Feature branch here:

Setup

I recommend starting by creating a new Automation Account. Yes, you can probably reuse an existing one, but creating a new account does not incur additional costs, and you can get this up and running fairly quick and easy just by following the steps in this blog post.



Make sure you select "Yes" on the option of creating "Azure Run as Account". Let it create all the artifacts and while you wait you can read the rest of this post.

When the Automation account is up and running, the next step is to create a new Runbook of type "PowerShell" - just straight up PowerShell, and no fancy stuff.

Then you grab the script from my feature branch based off the original trunk. You can either take the script from this post, or take the latest from GitHub. I probably won't maintain this blog post on any future updates of the script, but I might maintain the one on GitHub. I'll put a copy down below.

So with the script added as a PowerShell Runbook, and saved. Now you need to Schedule it. This is where a small cost may incur, because it is necessary to set the Runbook to run every hour. Yes - every hour. Using Automation for free only allow for a limited number of runs, and with the Runbook running every hour throughout the day, I believe it will stop running after 20 days - per month. There is a 500 minute limit per month for free, but the cost incurred when you exceed this is extremely low.

With the script running every hour you are ready to schedule "downtime". And this is easy.
You basically just either TAG the VM or the Resource Group holding a collection of VMs.

By TAG I mean you type on the downtime you want for your resource in the VALUE of a specific TAG. The script looks for a tag named "AutoShutdownSchedule". Example of value would be "20:00->06:00, Saturday, Sunday", and you can probably guess when the server will be shutdown with that value... That is correct, all weekdays between 8 pm at night and 6 am in the morning. You can imagine the flexibility this gives.

Added Features

In addition, the script is inspired by other nice ideas from the community, like providing a TimeZone for your schedule, just to ensure your 8 pm is consistent to when the script interprets the value.

Another feature added is the ability to use a "NeverStart" value keyword, to enforce the resource does not start. You can use this to schedule automatic shutdown that does not trigger startup again after the schedule ends. Example is the value "20:00->21:00,NeverStart". This would stop the resource at 8 pm, and when the RunBook runs again at 9 pm, the resource will not start even though the schedule has ended.

Finally, I want to comment the added feature of disabling the schedule without removing the schedule. If you provide an additional tag with the name "AutoShutdownDisabled" with a value of Yes/1/True. This means you can keep the schedule and temporarily disable the shutdown schedule altogether.

The script

<#
        .SYNOPSIS
        This Azure Automation runbook automates the scheduled shutdown and startup of resources in an Azure subscription. 

        .DESCRIPTION
        The runbook implements a solution for scheduled power management of Azure resources in combination with tags
        on resources or resource groups which define a shutdown schedule. Each time it runs, the runbook looks for all
        supported resources or resource groups with a tag named "AutoShutdownSchedule" having a value defining the schedule, 
        e.g. "10PM -> 6AM". It then checks the current time against each schedule entry, ensuring that resourcess with tags or in tagged groups 
        are deallocated/shut down or started to conform to the defined schedule.

        This is a PowerShell runbook, as opposed to a PowerShell Workflow runbook.

        This script requires the "AzureRM.Resources" modules which are present by default in Azure Automation accounts.
        For detailed documentation and instructions, see: 
        
        CREDITS: Initial version credits goes to automys from which this script started :
        https://automys.com/library/asset/scheduled-virtual-machine-shutdown-startup-microsoft-azure

        .PARAMETER Simulate
        If $true, the runbook will not perform any power actions and will only simulate evaluating the tagged schedules. Use this
        to test your runbook to see what it will do when run normally (Simulate = $false).

        .PARAMETER DefaultScheduleIfNotPresent
        If provided, will set the default schedule to apply on all resources that don't have any scheduled tag value defined or inherited.

        Description | Tag value
        Shut down from 10PM to 6 AM UTC every day | 10pm -> 6am
        Shut down from 10PM to 6 AM UTC every day (different format, same result as above) | 22:00 -> 06:00
        Shut down from 8PM to 12AM and from 2AM to 7AM UTC every day (bringing online from 12-2AM for maintenance in between) | 8PM -> 12AM, 2AM -> 7AM
        Shut down all day Saturday and Sunday (midnight to midnight) | Saturday, Sunday
        Shut down from 2AM to 7AM UTC every day and all day on weekends | 2:00 -> 7:00, Saturday, Sunday
        Shut down on Christmas Day and New Year?s Day | December 25, January 1
        Shut down from 2AM to 7AM UTC every day, and all day on weekends, and on Christmas Day | 2:00 -> 7:00, Saturday, Sunday, December 25
        Shut down always ? I don?t want this VM online, ever | 0:00 -> 23:59:59
        
    
        .PARAMETER TimeZone
        Defines the Timezone used when running the runbook. "GMT Standard Time" by default.
        Microsoft Time Zone Index Values:
        https://msdn.microsoft.com/en-us/library/ms912391(v=winembedded.11).aspx

        .EXAMPLE
        For testing examples, see the documentation at:

        https://automys.com/library/asset/scheduled-virtual-machine-shutdown-startup-microsoft-azure
    
        .INPUTS
        None.

        .OUTPUTS
        Human-readable informational and error messages produced during the job. Not intended to be consumed by another runbook.
#>
[CmdletBinding()]
param(
    [parameter(Mandatory=$false)]
    [bool]$Simulate = $false,
    [parameter(Mandatory=$false)]
    [string]$DefaultScheduleIfNotPresent,
    [parameter(Mandatory=$false)]
    [String] $Timezone = "W. Europe Standard Time"
)

$VERSION = '3.3.0'
$autoShutdownTagName = 'AutoShutdownSchedule'
$autoShutdownOrderTagName = 'ProcessingOrder'
$autoShutdownDisabledTagName = 'AutoShutdownDisabled'
$defaultOrder = 1000

$ResourceProcessors = @(
    @{
        ResourceType = 'Microsoft.ClassicCompute/virtualMachines'
        PowerStateAction = { param([object]$Resource, [string]$DesiredState) (Get-AzureRmResource -ResourceId $Resource.ResourceId).Properties.InstanceView.PowerState }
        StartAction = { param([string]$ResourceId) Invoke-AzureRmResourceAction -ResourceId $ResourceId -Action 'start' -Force } 
        DeallocateAction = { param([string]$ResourceId) Invoke-AzureRmResourceAction -ResourceId $ResourceId -Action 'shutdown' -Force } 
    },
    @{
        ResourceType = 'Microsoft.Compute/virtualMachines'
        PowerStateAction = { 
            param([object]$Resource, [string]$DesiredState)
      
            $vm = Get-AzureRmVM -ResourceGroupName $Resource.ResourceGroupName -Name $Resource.Name -Status
            $currentStatus = $vm.Statuses | Where-Object Code -like 'PowerState*' 
            $currentStatus.Code -replace 'PowerState/',''
        }
        StartAction = { param([string]$ResourceId) Invoke-AzureRmResourceAction -ResourceId $ResourceId -Action 'start' -Force } 
        DeallocateAction = { param([string]$ResourceId) Invoke-AzureRmResourceAction -ResourceId $ResourceId -Action 'deallocate' -Force } 
    },
    @{
        ResourceType = 'Microsoft.Compute/virtualMachineScaleSets'
        #since there is no way to get the status of a VMSS, we assume it is in the inverse state to force the action on the whole VMSS
        PowerStateAction = { param([object]$Resource, [string]$DesiredState) if($DesiredState -eq 'StoppedDeallocated') { 'Started' } else { 'StoppedDeallocated' } }
        StartAction = { param([string]$ResourceId) Invoke-AzureRmResourceAction -ResourceId $ResourceId -Action 'start' -Parameters @{ instanceIds = @('*') } -Force } 
        DeallocateAction = { param([string]$ResourceId) Invoke-AzureRmResourceAction -ResourceId $ResourceId -Action 'deallocate' -Parameters @{ instanceIds = @('*') } -Force } 
    }
)

# Define function to get current date using the TimeZone Paremeter
function GetCurrentDate
{
    return [system.timezoneinfo]::ConvertTime($(Get-Date),$([system.timezoneinfo]::GetSystemTimeZones() | ? id -eq $Timezone))
}

# Define function to check current time against specified range
function Test-ScheduleEntry ([string]$TimeRange)
{ 
    # Initialize variables
    $rangeStart, $rangeEnd, $parsedDay = $null
    $currentTime = GetCurrentDate
    $midnight = $currentTime.AddDays(1).Date         

    try
    {
        # Parse as range if contains '->'
        if($TimeRange -like '*->*')
        {
            $timeRangeComponents = $TimeRange -split '->' | foreach {$_.Trim()}
            if($timeRangeComponents.Count -eq 2)
            {
                $rangeStart = Get-Date $timeRangeComponents[0]
                $rangeEnd = Get-Date $timeRangeComponents[1]
 
                # Check for crossing midnight
                if($rangeStart -gt $rangeEnd)
                {
                    # If current time is between the start of range and midnight tonight, interpret start time as earlier today and end time as tomorrow
                    if($currentTime -ge $rangeStart -and $currentTime -lt $midnight)
                    {
                        $rangeEnd = $rangeEnd.AddDays(1)
                    }
                    # Otherwise interpret start time as yesterday and end time as today   
                    else
                    {
                        $rangeStart = $rangeStart.AddDays(-1)
                    }
                }
            }
            else
            {
                Write-Output "`tWARNING: Invalid time range format. Expects valid .Net DateTime-formatted start time and end time separated by '->'" 
            }
        }
        # Otherwise attempt to parse as a full day entry, e.g. 'Monday' or 'December 25' 
        else
        {
            # If specified as day of week, check if today
            if([System.DayOfWeek].GetEnumValues() -contains $TimeRange)
            {
                if($TimeRange -eq (Get-Date).DayOfWeek)
                {
                    $parsedDay = Get-Date '00:00'
                }
                else
                {
                    # Skip detected day of week that isn't today
                }
            }
            # Otherwise attempt to parse as a date, e.g. 'December 25'
            else
            {
                $parsedDay = Get-Date $TimeRange
            }
     
            if($parsedDay -ne $null)
            {
                $rangeStart = $parsedDay # Defaults to midnight
                $rangeEnd = $parsedDay.AddHours(23).AddMinutes(59).AddSeconds(59) # End of the same day
            }
        }
    }
    catch
    {
        # Record any errors and return false by default
        Write-Output "`tWARNING: Exception encountered while parsing time range. Details: $($_.Exception.Message). Check the syntax of entry, e.g. ' -> ', or days/dates like 'Sunday' and 'December 25'"
        return $false
    }
 
    # Check if current time falls within range
    if($currentTime -ge $rangeStart -and $currentTime -le $rangeEnd)
    {
        return $true
    }
    else
    {
        return $false
    }
 
} # End function Test-ScheduleEntry


# Function to handle power state assertion for resources
function Assert-ResourcePowerState
{
    param(
        [Parameter(Mandatory=$true)]
        [object]$Resource,
        [Parameter(Mandatory=$true)]
        [string]$DesiredState,
        [bool]$Simulate
    )

    $processor = $ResourceProcessors | Where-Object ResourceType -eq $Resource.ResourceType
    if(-not $processor) {
        throw ('Unable to find a resource processor for type ''{0}''. Resource: {1}' -f $Resource.ResourceType, ($Resource | ConvertTo-Json -Depth 5000))
    }
    # If should be started and isn't, start resource
    $currentPowerState = & $processor.PowerStateAction -Resource $Resource -DesiredState $DesiredState
    if($DesiredState -eq 'Started' -and $currentPowerState -notmatch 'Started|Starting|running')
    {
        if($Simulate)
        {
            Write-Output "`tSIMULATION -- Would have started resource. (No action taken)"
        }
        else
        {
            Write-Output "`tStarting resource"
            & $processor.StartAction -ResourceId $Resource.ResourceId
        }
    }
  
    # If should be stopped and isn't, stop resource
    elseif($DesiredState -eq 'StoppedDeallocated' -and $currentPowerState -notmatch 'Stopped|deallocated')
    {
        if($Simulate)
        {
            Write-Output "`tSIMULATION -- Would have stopped resource. (No action taken)"
        }
        else
        {
            Write-Output "`tStopping resource"
            & $processor.DeallocateAction -ResourceId $Resource.ResourceId
        }
    }

    # Otherwise, current power state is correct
    else
    {
        Write-Output "`tCurrent power state [$($currentPowerState)] is correct."
    }
}

# Main runbook content
try
{
    $currentTime = GetCurrentDate
    Write-Output "Runbook started. Version: $VERSION"
    if($Simulate)
    {
        Write-Output '*** Running in SIMULATE mode. No power actions will be taken. ***'
    }
    else
    {
        Write-Output '*** Running in LIVE mode. Schedules will be enforced. ***'
    }
    Write-Output "Current UTC/GMT time [$($currentTime.ToString('dddd, yyyy MMM dd HH:mm:ss'))] will be checked against schedules"
        
    
    $Conn = Get-AutomationConnection -Name AzureRunAsConnection
    $resourceManagerContext = Add-AzureRMAccount -ServicePrincipal -Tenant $Conn.TenantID -ApplicationId $Conn.ApplicationID -CertificateThumbprint $Conn.CertificateThumbprint
    
    $resourceList = @()
    # Get a list of all supported resources in subscription
    $ResourceProcessors | % {
        Write-Output ('Looking for resources of type {0}' -f $_.ResourceType)
        $resourceList += @(Find-AzureRmResource -ResourceType $_.ResourceType)
    }

    $ResourceList | % {     
        if($_.Tags -and $_.Tags.ContainsKey($autoShutdownOrderTagName) ) {
            $order = $_.Tags | % { if($_.ContainsKey($autoShutdownOrderTagName)) { $_.Item($autoShutdownOrderTagName) } }
        } else {
            $order = $defaultOrder
        }
        Add-Member -InputObject $_ -Name ProcessingOrder -MemberType NoteProperty -TypeName Integer -Value $order
    }

    $ResourceList | % {     
        if($_.Tags -and $_.Tags.ContainsKey($autoShutdownDisabledTagName) ) {
            $disabled = $_.Tags | % { if($_.ContainsKey($autoShutdownDisabledTagName)) { $_.Item($autoShutdownDisabledTagName) } }
        } else {
            $disabled = '0'
        }
        Add-Member -InputObject $_ -Name ScheduleDisabled -MemberType NoteProperty -TypeName String -Value $disabled
    }

    # Get resource groups that are tagged for automatic shutdown of resources
    $taggedResourceGroups = Find-AzureRmResourceGroup -Tag @{ "AutoShutdownSchedule" = $null }
    $taggedResourceGroupNames = @($taggedResourceGroups | select Name)
    
    Write-Output "Found [$($taggedResourceGroupNames.Count)] schedule-tagged resource groups in subscription" 
    
    if($DefaultScheduleIfNotPresent) {
        Write-Output "Default schedule was specified, all non tagged resources will inherit this schedule: $DefaultScheduleIfNotPresent"
    }

    # For each resource, determine
    #  - Is it directly tagged for shutdown or member of a tagged resource group
    #  - Is the current time within the tagged schedule 
    # Then assert its correct power state based on the assigned schedule (if present)
    Write-Output "Processing [$($resourceList.Count)] resources found in subscription"
    foreach($resource in $resourceList)
    {
        $schedule = $null

        if ($resource.ScheduleDisabled)
        {
            $disabledValue = $resource.ScheduleDisabled
            if ($disabledValue -eq "1" -or $disabledValue -eq "Yes"-or $disabledValue -eq "True")
            {
                Write-Output "[$($resource.Name)]: `r`n`tIGNORED -- Found direct resource schedule with $autoShutdownDisabledTagName value: $disabledValue."
                continue
            }
        }

        # Check for direct tag or group-inherited tag
        if($resource.Tags.Count -gt 0 -and $resource.Tags.ContainsKey($autoShutdownTagName) -eq $true)
        {
            # Resource has direct tag (possible for resource manager deployment model resources). Prefer this tag schedule.
            $schedule = $resource.Tags.Item($autoShutdownTagName)
            Write-Output "[$($resource.Name)]: `r`n`tADDING -- Found direct resource schedule tag with value: $schedule"
        }
        elseif($taggedResourceGroupNames -contains $resource.ResourceGroupName)
        {
            # resource belongs to a tagged resource group. Use the group tag
            $parentGroup = $resourceGroups | Where-Object Name -eq $resource.ResourceGroupName
            $schedule = $parentGroup.Tags.Item($AUTOSHUTDOWNSCHEDULE_KEYWORD)
            Write-Output "[$($resource.Name)]: `r`n`tADDING -- Found parent resource group schedule tag with value: $schedule"
        }
        elseif($DefaultScheduleIfNotPresent)
        {
            $schedule = $DefaultScheduleIfNotPresent
            Write-Output "[$($resource.Name)]: `r`n`tADDING -- Using default schedule: $schedule"
        }
        else
        {
            # No direct or inherited tag. Skip this resource.
            Write-Output "[$($resource.Name)]: `r`n`tIGNORED -- Not tagged for shutdown directly or via membership in a tagged resource group. Skipping this resource."
            continue
        }

        # Check that tag value was succesfully obtained
        if($schedule -eq $null)
        {
            Write-Output "[$($resource.Name) `- $($resource.ProcessingOrder)]: `r`n`tIGNORED -- Failed to get tagged schedule for resource. Skipping this resource."
            continue
        }

        # Parse the ranges in the Tag value. Expects a string of comma-separated time ranges, or a single time range
        $timeRangeList = @($schedule -split ',' | foreach {$_.Trim()})
    
        # Check each range against the current time to see if any schedule is matched
        $scheduleMatched = $false
        $matchedSchedule = $null
        $neverStart = $false #if NeverStart is specified in range, do not wake-up machine
        foreach($entry in $timeRangeList)
        {
            if((Test-ScheduleEntry -TimeRange $entry) -eq $true)
            {
                $scheduleMatched = $true
                $matchedSchedule = $entry
                break
            }
            
            if ($entry -eq "NeverStart")
            {
                $neverStart = $true
            }
        }
        Add-Member -InputObject $resource -Name ScheduleMatched -MemberType NoteProperty -TypeName Boolean -Value $scheduleMatched
        Add-Member -InputObject $resource -Name MatchedSchedule -MemberType NoteProperty -TypeName Boolean -Value $matchedSchedule
        Add-Member -InputObject $resource -Name NeverStart -MemberType NoteProperty -TypeName Boolean -Value $neverStart
    }
    
    foreach($resource in $resourceList | Group-Object ScheduleMatched) {
        if($resource.Name -eq '') {continue}
        $sortedResourceList = @()
        if($resource.Name -eq $false) {
            # meaning we start resources, lower to higher
            $sortedResourceList += @($resource.Group | Sort ProcessingOrder)
        } else { 
            $sortedResourceList += @($resource.Group | Sort ProcessingOrder -Descending)
        }

        foreach($resource in $sortedResourceList)
        {  
            # Enforce desired state for group resources based on result. 
            if($resource.ScheduleMatched)
            {
                # Schedule is matched. Shut down the resource if it is running. 
                Write-Output "[$($resource.Name) `- P$($resource.ProcessingOrder)]: `r`n`tASSERT -- Current time [$currentTime] falls within the scheduled shutdown range [$($resource.MatchedSchedule)]"
                Add-Member -InputObject $resource -Name DesiredState -MemberType NoteProperty -TypeName String -Value 'StoppedDeallocated'

            }
            else
            {
                if ($resource.NeverStart)
                {
                    Write-Output "[$($resource.Name)]: `tIGNORED -- Resource marked with NeverStart. Keeping the resources stopped."
                    Add-Member -InputObject $resource -Name DesiredState -MemberType NoteProperty -TypeName String -Value 'StoppedDeallocated'
                }
                else
                {
                    # Schedule not matched. Start resource if stopped.
                    Write-Output "[$($resource.Name) `- P$($resource.ProcessingOrder)]: `r`n`tASSERT -- Current time falls outside of all scheduled shutdown ranges. Start resource."
                    Add-Member -InputObject $resource -Name DesiredState -MemberType NoteProperty -TypeName Boolean -Value 'Started'
                }                
            }     
            Assert-ResourcePowerState -Resource $resource -DesiredState $resource.DesiredState -Simulate $Simulate
        }
    }

    Write-Output 'Finished processing resource schedules'
}
catch
{
    $errorMessage = $_.Exception.Message
    throw "Unexpected exception: $errorMessage"
}
finally
{
    Write-Output "Runbook finished (Duration: $(('{0:hh\:mm\:ss}' -f ((GetCurrentDate) - $currentTime))))"
}

Monday, October 2, 2017

Importing users in D365 Operations using Excel

Let me start off by admitting I was initially thinking about naming this post "Importing users in AX7 using Excel", so there, now this post suddenly became a little bit more "searcher friendly".

In this post I will show how easy you can connect to your Dynamics 365 Operations instance using Excel. Before I begin the post, let me just remind you that importing users from Azure Active Directory is perhaps easier and quicker. So this post is just to show you it is also possible to import users using Excel with the Dynamics Office Add-in.

You may have seen the Data Entity "System User" (SystemUserEntity), and you may have tried using it to add users with it, and  furthermore you may also have seen the error "A row created in data set SystemUser was not published. Error message: Write failed for table row of type 'SystemUserEntity'. Infolog: Error: Error in getting SID."


You will get the same error through Excel if you do not provide some additional columns and information while trying to create a user through that Data Entity.

You can either start off with opening Excel, install the Dynamics Office Add-in and connect it to the target instance. Or you can open the list of users directly on the instance, and open the list in Excel from there. Either way you should start with a view where you have the System User list in your spreadsheet.

The next step is to modify the Design of the view. Click the Design link first.



Then edit the System User table.



Then add the following following columns: Enabled, AccountType and Alias (Email).



Save the design changes and ensure you update the view so the added columns are populated with data.

You will notice the Type (AccountType) and Alias (Email) carry important information for how the user authenticates, in addition to the Provider column. With these columns properly populated, you should be able to add multiple rows and hit a single "Publish" from within Excel.

Given this, you can have two open Excel instances, and connect to two different instances. And then copy over users from a source to a target using Excel. As long as all the columns are available and in the same order, of course.

This post should also give you some clue to how you can use Data Management to populate a system with users through a Data Package, if that is your preference.

Monday, September 25, 2017

Initial steps to troubleshoot failed environment servicing

On the topic of patching and updating an existing D365 Operations environment I will refer to the online documentation.
There are also some great community posts that aims to help you, and you may want to check out.
I expect more posts to show up. As it is of writing this, installing updates can be a bit tedious and cumbersome. 

I will use this post to share a recent update that failed on a Platform Update. A Platform Update is expected to be a fairly straight forward and safe operation. You simply import the update to your assets in LCS and apply it to your environment (assuming you're running your environment in the cloud). I will not discuss On-Premise in this post. 

I had an environment running application 1611 with Platform Update 7. I was trying to install Platform Update 10. After it failed on several attempts, I started to try investigate why it failed. 



Here are the steps I took.

1) Identify which step failed. In my case it was step 13. (Not exactly my lucky number)



2) Find the runbook output (normally under C:\RunbookOutput) and find the PowerShell Script that fails. I simply searched the log for "13"

3) Open PowerShell ISE in Admin mode and open the PowerShell Script. You will find the script in the J:\DeployablePackages folder, and you can match the GUID from the log with the Runbook folder. The Scripts will be located in a standardized folder path.


4) Run the script and let it fail. From there you can add breakpoints and try run again and step through to try see why it failed. Use whatever you find as information when you contact Microsoft Support. Some updates fails, but should not fail, and it is important that anyone with support agreements make sure to report findings back to Microsoft. 

Now, in my particular case, the script did not fail when I ran it manually. It succeeded. I can only guess to why that is the case, but after going back to LCS and letting the update "Resume" it eventually finished with all the upgrade steps successfully. 

In any case, the initial steps above can help you push through a failing update and potentially lead you to the answer why an update unexpectedly failed. 

Wednesday, August 23, 2017

Consider changing your password Pop Up

Currently the machines deployed through LCS runs with the Account Policy that passwords has a 42 days age. Interestingly you should not change the password for servers deployed according to this statement of guidelines.

So if you get annoyed by the reminder to change the password and do not plan to republish the box any time soon, why not go ahead and get rid of the pop up.


Click the start button and type in "local". You should find the Local Security Policy Console.



From there it is just a matter of changing the expiration of the password to something other than 42, or simply set it to 0 for "never expire".



Quick and easy.

Alternatively you can use a Command Prompt (Run as Admin) with the statement:

net accounts /maxpwage:unlimited

Wednesday, August 9, 2017

Excel Applet not loading when working with D365 Office Add-in

This post is somewhat related to the post by Ievgen (AX MVP) on the Office Word integration not working with Dynamics 365 for Finance and Operations (Enterprise Edition).

If you try to connect Excel to an existing environment using the Microsoft Dynamics Office Add-in and all you see is the text "Load applets" after signing in, then it might very well be because the applets needs to be initiated from within the environment.

If you click the little flag at the bottom right, you will be able to open the messages and see the error "No applet registrations found".



Solution is simple. Open the D365 environment in your favorite browser (assuming your favorite browser is on the compatible list - hehe) and either search for the form (ie type in "Office") or navigate directly through System Administration, Setup and Office app parameters


If you see an emply grid, then the settings have not been initialized and that is the problem. Most likely you are missing settings for Office apps in general, so go ahead and initialize all parameters for all the grids accordingly.


Head back to Excel and try make it reload the applets (simply try add a trailing slash to the url). Hopefully you should get the expected result.

Sunday, July 9, 2017

Error when installing Reporting Extensions for AX2012 R3 on SQL Server 2016

On my previous post I wrote on installing Reporting Extensions for AX2012 R3 on SQL Server 2014. In this post I want to emphasize that the same hotfix for SQL 2014 is needed for SQL 2016.
The error behaves slightly different on SQL 2016 if you do not have the patch. The setup experience simply crashes during install and while the components is ticked as "installed" next time you run setup, it is only "half-baked". You need to start over, this time with the hotfix ready.

Here is a screenshot of the installer crash with "AxSetup.exe has stopped working". Ignore that it is on the SSAS step, I simply chose to install both extensions at the same time. The error actually relates to Reporting Extensions.



And if you open the setup logs for further inspection, you will see it ends while trying to setup the SSRS bits. Here is an excerpt from the install log:

2017-07-05 11:30:56Z Setting the SQL Server Reporting Services service account to the Microsoft Dynamics AX .Net Business Connector account.
2017-07-05 11:30:56Z Generating database rights script.
2017-07-05 11:30:56Z Opening connection to the database using the connection string server=SERVER\INSTANCE;Integrated Security=SSPI.
2017-07-05 11:30:56Z Writing the database rights script to C:\Users\USERID\AppData\Local\Temp\3\tmpADC0.tmp.
2017-07-05 11:30:56Z Executing database rights script.


I got this error even though the installation was slipstreamed with CU12, which is a later version, compared to the hotfix.

So if you're planning on installing these bits for SQL 2016 (or SQL 2014), do yourself the favor of downloading the KB3216898 and slipstream your install by extracting it into your installation Update folder.

Here is the link, again: https://fix.lcs.dynamics.com/Issue/Resolved?kb=3216898&bugId=3800976

Thursday, April 20, 2017

Error when installing Reporting Extensions for AX2012 R3 on SQL 2014 SSRS

Hi

I could not really find any posts on this out there, so I decided to just share this here.

You may experience installation errors when you try to install Reporting Extensions for AX2012 R3 on SQL 2014. The setup crashes internally, rolls back the installation and fails.
In the installation log you will see the error "Version string portion was too short or too long".

The solution is available on LCS as a downloadable hotfix KB3216898 (released 10th of January 2017) here:
https://fix.lcs.dynamics.com/Issue/Resolved?kb=3216898&bugId=3800976

Unpack the content of the hotfix and slipstream it as part of your AX2012 R3 installation and run the installation again. Now it will work.

Just to make this sure people find this post if they search for the errors, I'll add the full call stack below:

An error occurred during setup of Reporting Services extensions.
Reason: Version string portion was too short or too long.
System.ArgumentException: Version string portion was too short or too long.
  at System.Version..ctor(String version)
  at Microsoft.Dynamics.AX.Framework.Reporting.Shared.SrsWmi.get_ReportManagerUrl()
  at Microsoft.Dynamics.Setup.ReportsServerInstaller.GetOrCreateServerConfiguration(String instanceName, String sharePointServiceApplicationSite, Boolean& createdConfiguration)
  at Microsoft.Dynamics.Setup.Components.ReportingServicesExtensions.InstallConfigurationFiles(String instanceName, String sharePointServiceApplicationSite, Boolean& createdConfiguration)
  at Microsoft.Dynamics.Setup.Components.ReportingServicesExtensions.RunReportingSetupManagerDeploy()