Category Archives: Automation

The mysterious X-MS-Forest header

When working with the api.interfaces.records.teams.microsoft.com API, I noticed that the MS portal uses an X-MS-Forest header.

At first, ignoring this went fine as doing GET calls to this api didn’t seem to require it. But, of course the moment I wanted more, it suddenly WAS required (PUT/POST).

The question was; how does the portal determine the value for this header and how do we replicate that? Well, that wasn’t difficult: apparently a call to api.interfaces.records.teams.microsoft.com/Teams.Tenant/tenants suffices and returns the value for the X-MS-Forest header for the tenant identified in your token. Example:

    $headers = Get-GraphToken -tenantid $tenantId -scope "https://api.interfaces.records.teams.microsoft.com/user_impersonation"
    #get the correct forest
    $tenantInfo = Invoke-RestMethod -Method GET -uri "https://api.interfaces.records.teams.microsoft.com/Teams.Tenant/tenants" -UseBasicParsing -ContentType "application/json" -Headers $headers
    #add the X-MS-Forest header (required) for subsequent calls
    $headers["X-MS-Forest"] = $tenantInfo.serviceDiscovery.Headers.'X-MS-Forest'

Local client SPO migration script

For a customer case/project, we wanted to move only recently synced/modified Sharepoint Online data from Tenant A to the user’s Desktop on the device itself.

The Desktop was synced to Onedrive for Business in Tenant B.

After copying, files from Tenant A should become read-only on the local device, and the link in Explorer to Tenant A’s sharepoint should be removed, including the actual onedrive sync relationship to prevent further ul/dl’s.

Resulting in https://gitlab.com/Lieben/assortedFunctions/-/blob/master/migrate-modifiedSpOSyncedFilesToUserDesktop.ps1

Deallocate Azure AD Joined Azure Virtual Desktop VMs when a user logs off

When you shut down a VM or log off, the VM isn’t actually deallocated and still costs money.

Bernd wrote a nice guide on how to deallocate a VM when a user logs off, using GPO’s, since combined with Start On Connect the user experience is still pretty decent.

For Intune / Microsoft Endpoint Manager, no solution was known yet. So I base64 encoded Bernd’s solution and wrapped it into a SYSTEM wide scheduled task that is triggered by a security eventlog logoff entry.

Deploy this to your VM’s in Intune (either through a user or a machine group) and it’ll ensure users’ VM’s get deallocated when they log off.

This also works on shared VM’s, as it will only deallocate if it is the last user logging off.

You can download/view set-AVDDeallocateOnLogoff.ps1 here.

Using an automation account for Azure Automated Right Sizing

The recommended method to run my ADDRS on a schedule is through an Azure Runbook or an Azure DevOps pipeline.

This post describes how to configure an Automation Account to run ADDRS in detailed steps.

  1. Create a resourcegroup:
create addrs resource group

2. Create an automation account in the resource group you just created:

create addrs automation account

3. Use system assigned managed identity

use system assigned managed identity

4. Accept further defaults and when created click Go to resource:

go to resource ADDRS

5. Under the Identity option, click Azure Role Assignments:

Go to azure role assignments

6. Be sure to select the same subscription your Virtual Machines are in:

7. If you wish to scope permissions more specifically (always recommended), at minimum you’ll need to assign Virtual Machine Contributor to the resource group(s) containing your VM’s and Log Analytics Reader on your log analytics workspace.

8. Now create a runbook in your new automation account:

create runbook
select runbook type and click Create

9. After clicking Create, copy paste the following example code in the editor and uncomment either set-rsgRightSize or set-vmRightSize depending on which way you’re using the module. Update the RSG or VM name you’re targetting and update the workspaceId

Connect-AzAccount -Identity

#set-rsgRightSize -targetRSG "rg-avd-01" -workspaceId "7ccd0949-2fd4-414e-b58c-c013cc6e445d"

#set-vmRightSize -targetVMName "azvm01"  -workspaceId "7ccd0949-2fd4-414e-b58c-c013cc6e445d"

10. Save & Publish

11. Go to modules and click browse gallery, search for ADDRS:

browse PS gallery for modules
Search for ADDRS
Select ADDRS
Select runtime v 7.1 and import

12. Wait for the import to complete. If it fails, check if you have installed the 7.1 versions of these requires modules, install if needed and try again:

  • Az.Compute
  • Az.OperationalInsights
  • Az.Resources
  • Az.Accounts

13. Run the runbook to test, or link it to a schedule:

link runbook to a new schedule
Create a schedule as desired and press create.

14. if you want to use a schedule different from weekly, make sure to also add the -measurePeriodHours parameter to match, and if you use maintenance windows, include those as well as described in the module manual.

Trigger logic app when Azure Virtual Desktop starts

We have several use cases where we want to “do something” when a user starts an Azure Virtual Desktop. One method could be a login/startup script, but this would run under the user’s or Managed Identity’s context.

A better way is to use an Azure Event Grid System Topic on the resource group that contains the VM’s, which can then forward any event that happens in the resource group.

A system topic is easily deployed using ARM:

    {
        "type": "Microsoft.EventGrid/systemTopics",
        "apiVersion": "2021-12-01",
        "name": "evgt-listenToAvdEvents-01",
        "location": "global",
        "properties": {
            "source": "[concat('/subscriptions/',subscription().subscriptionId,'/resourceGroups/rg-avd-weeu-01')]",
            "topicType": "microsoft.resources.resourcegroups"
        }
    }

That having been deployed, we’ll deploy a logic app that is triggered by the topic. In this case, I want to do some advanced filtering so the logic app is only triggered when a VM is started by a user (vs automation). This is indicated by the Guid (principal ID) of Microsoft’s AVD serviceprincipal, in our case 068e1c948d874baba249f9a122cd8003 because we use ‘Start On Connect

To use advanced filtering in a logic app, use “enableAdvancedFilteringOnArrays”: true

The full trigger section of the logic app (in ARM) is as follows:

                    "triggers": {
                        "When_a_resource_event_occurs": {
                            "splitOn": "@triggerBody()",
                            "type": "ApiConnectionWebhook",
                            "inputs": {
                                "body": {
                                    "properties": {
                                        "destination": {
                                            "endpointType": "webhook",
                                            "properties": {
                                                "endpointUrl": "@{listCallbackUrl()}"
                                            }
                                        },
                                        "filter": {
                                            "includedEventTypes": [
                                                "Microsoft.Resources.ResourceActionSuccess",
                                                "Microsoft.Resources.ResourceDeleteSuccess",
                                                "Microsoft.Resources.ResourceWriteSuccess"
                                            ],
                                            "subjectBeginsWith": "[concat('/subscriptions/',subscription().subscriptionId,'/resourceGroups/rg-avd-weeu-01/providers/Microsoft.Compute/virtualMachines')]",
                                            "enableAdvancedFilteringOnArrays": true,
                                            "advancedFilters": [
                                                {
                                                    "operatorType": "StringIn",
                                                    "key": "data.authorization.action",
                                                    "values": [
                                                        "Microsoft.Compute/virtualMachines/start/action"
                                                    ]
                                                },
                                                {
                                                    "operatorType": "StringIn",
                                                    "key": "data.authorization.evidence.principalId",
                                                    "values": [
                                                        "068e1c948d874baba249f9a122cd8003"
                                                    ]
                                                }
                                            ]
                                        },
                                        "topic": "[concat('/subscriptions/',subscription().subscriptionId,'/resourceGroups/rg-avd-weeu-01')]"
                                    }
                                },
                                "host": {
                                    "connection": {
                                        "name": "@parameters('$connections')['azureeventgrid']['connectionId']"
                                    }
                                },
                                "path": "[concat('/subscriptions/@{encodeURIComponent(''',subscription().subscriptionId,''')}/providers/@{encodeURIComponent(''Microsoft.Resources.ResourceGroups'')}/resource/eventSubscriptions')]",
                                "queries": {
                                    "x-ms-api-version": "2021-12-01"
                                }
                            }
                        }
                    },

You may also want to use the VM’s name in your logic app, this is easily parsed from the Subject field, e.g. as follows:

"Parse_Subject": {
    "runAfter": {},
    "type": "InitializeVariable",
    "inputs": {
        "variables": [
            {
                "name": "subject",
                "type": "string",
                "value": "@triggerBody()?['subject']"
            }
        ]
    }
},
"Parse_MachineName": {
    "runAfter": {
        "Parse_Subject": [
            "Succeeded"
        ]
    },
    "type": "InitializeVariable",
    "inputs": {
        "variables": [
            {
                "name": "machineName",
                "type": "string",
                "value": "@{last(split(variables('subject'),'/'))}"
            }
        ]
    }
},

Important considerations:

  1. the Logic App needs to have a managed identity
  2. The LA’s MI needs to have the EventGrid Contributor role on the system topic
  3. you cannot edit this logic app through the gui, doing so will break it and cause the following error: “Unable to match incoming request to an operation”

Adding eventgrid contributor:

New-AzRoleAssignment -ObjectId $la.Identity.PrincipalId -RoleDefinitionName "EventGrid Contributor" -Scope "/subscriptions/$($context.Subscription.Id)"