All posts by Jos

Incremental WVD deployments or redeploying persistent WVD hosts

When you add hosts to a WVD hostpool, and want to do so programmatically, this can be done by seperately deploying new hosts.

Redeploying an existing host manually is easy if it isn’t persistent, but if the disk should be retained, you’ll want to disconnect and reconnect it. This would require a lot of scripting (or third party tools).

As I wanted to keep our WVD solution contained to a single ARM template, some creativity was needed. For example, when deploying a new host that has a newer version of a gallery image, ARM would normally complain that the existing hosts don’t have the correct image version and it can’t change that for you since it is a read-only property (imageReference). Another example would be to move the VM between availability zones.

Linked WVD ARM template solves all these issues, but has a couple of important parameters to be aware of if you’re going to use it;

  1. redeploy: set to true in a scenario where you want to upgrade existing hosts, e.g. from a different VM type, or want to redeploy to another availability Zone. It will use the provided list of disk names to create new VM’s based on these existing disks
  2. existingDiskNames: if redeploy = true, supply the list of disk names of the persist VM’s you just deleted.
  3. vmNumberOfInstances: the number of VM’s you want to have (including existing VM’s)
  4. existingNumberOfInstances: the number of VM’s already present in the hostpool when running this arm template.
    #3 and #4 should be the same if redeploy = true, the template does not support redeploying and expanding the hostpool in the same deployment, this should be done in order.

After redeploying hosts, make sure to reconnect the users as they were assigned before you deleted them.

Download the ARM template here from GIT:

https://gitlab.com/Lieben/assortedFunctions/-/blob/master/ARM%20templates/wvdDynamicHostpool.json

Enable WVD diagnostics for Azure Monitor using ARM Templates

Now that Azure Monitor has support for Windows Virtual Desktop, I wanted to enable this for all WVD workspaces, host pools and application groups.

Christiaan beautifully described how to do this through the Azure Portal, but of course, in an enterprise environment we don’t click around, we define everything as Code!

This example ARM template will enable the Diagnostics setting for the hostpool, workspace and application group vectors. To get data from VM’s (hosts), use your preferred method (plenty of examples exist already and depend on your setup).

After applying / including / redeploying this ARM template in your WVD environment, you’ll be able to see metrics coming in to the Azure Monitor Insights dashboard for WVD ūüôā

https://gitlab.com/Lieben/assortedFunctions/-/blob/master/ARM%20templates/WVDDiagnosticSettings.json

Self scheduling cleanup job for MEM kiosk machines

Normally I’d recommend using the Unified Write Filter in Windows 10 to keep Kiosk machines in a semi-decent state.

For a customer that did not have this luxury, I wrote a tiny self-scheduling PowerShell script that will run as SYSTEM and clean up any of the specified folders in any of the user profiles on the machine.

This example can be used for many purposes to drop a script and maintain a scheduled task. Redeploying it will overwrite the dropped script and scheduled task as per the new config.

Git link or direct code here:

#Module name:      Invoke-wipeSpecifiedProfileFolders
#Author:           Jos Lieben
#Author Blog:      https://www.lieben.nu
#Date:             18-12-2020
#License:          Free to use and modify non-commercially, leave headers intact. For commercial use, contact me
#Purpose:          Delete all files in the specified folder names in all user profiles on the machine, self-installs as a scheduled task
#Setup:            Deploy to machines, in system context
#Requirements:     Windows 10 build 1803 or higher

$folderWipeList = "Downloads,Network Shortcuts,Temp,Documents" #comma seperated list of folders to wipe

$desiredScriptFolder = Join-Path $env:ProgramData -ChildPath "Lieben.nu"
$desiredScriptPath = Join-Path $desiredScriptFolder -ChildPath "Invoke-wipeSpecifiedProfileFolders.ps1"
if(![System.IO.Directory]::($desiredScriptFolder)){
    New-Item -Path $desiredScriptFolder -Type Directory -Force
}
Start-Transcript -Path (Join-Path $desiredScriptFolder -ChildPath "\folderWiperInstaller.log")

Write-Output "Configuring scheduled task..."

$taskname = "Invoke-wipeSpecifiedProfileFolders"
$taskdescription = "Delete all files in the specified folder names in all user profiles on the machine"
$action = New-ScheduledTaskAction -Execute 'Powershell.exe' -Argument "-NoProfile -WindowStyle Hidden -NonInteractive -ExecutionPolicy ByPass -File `"$desiredScriptPath`""
$triggers =  @()
$triggers += (New-ScheduledTaskTrigger -AtStartup)
$triggers += (New-ScheduledTaskTrigger -Daily -At 23:00)
$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 5) -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1)
$task = Register-ScheduledTask -Action $action -Trigger $triggers -TaskName $taskname -Description $taskdescription -Settings $settings -User "System" -Force -RunLevel Highest

Write-Output "task info: "
Write-Output $task

Write-Output "Writing script file to local disk..."

$scriptContent = "
Start-Transcript -Path (Join-Path $desiredScriptFolder -ChildPath `"\folderWiper.log`")
`$folderWipeList = `"$folderwipeList`"
`$folderWipeList = `$folderwipeList.Split(`",`")
Get-ChildItem 'HKLM:\Software\Microsoft\Windows NT\CurrentVersion\ProfileList' | ForEach-Object {
    `$rootPath =  `$_.GetValue('ProfileImagePath') 
    Write-Output `"Parsing folders in `$rootPath`"
    `$childItems = `$Null
    `$childItems = Get-ChildItem -Path `$rootPath -Directory -ErrorAction SilentlyContinue -Recurse -Force | where{`$folderWipeList -contains `$_.BaseName}
    if(`$childItems){
    	foreach(`$folder in `$childItems){
    		Write-Output `"Wiping matched folder: `$(`$folder.FullName)`"
    		Get-ChildItem -Path `$folder.FullName -Force -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue -Recurse -Confirm:`$False
    	}
    }
}

Stop-Transcript"

Set-Content -Value $scriptContent -Path $desiredScriptPath -Force -Confirm:$False

Write-Output "Starting script as task for the first time..."

Start-ScheduledTask -InputObject $task

Write-Output "Install script has finished running"

Stop-Transcript

Legal hold and attribute conflicts in Exchange Online

Consider a large organisation, where deleted mailboxes are kept for many years.

Consider a new user with the same name as an offboarded user, or a user getting rehired, but policy stating that the deleted mailbox should not be restored. The user should start with a clean mailbox.

The inactive old mailbox still has the standardized primary smtp / alias etc and will not allow you to set these on the new user, causing a conflict.

Of course this should be handled during the offboarding process, where perhaps the email address of the user could be appended with _old123 before being soft-deleted.

Since that wasn’t the case here, I had to write a quick script to retroactively add a random string to all such attributes for all inactive mailboxes, hope it helps someone else with the same legacy ūüôā

connect-exchangeonline
$inactiveMailboxes = Get-Mailbox -InactiveMailboxOnly -ResultSize unlimited

foreach($inactiveMailbox in $inactiveMailboxesNovib){
    $rand = Get-Random -Maximum 999
    $primary = $inactiveMailbox.PrimarySmtpAddress
    $newMailbox = New-Mailbox -InactiveMailbox $inactiveMailbox.DistinguishedName -name $inactiveMailbox.Name -FirstName $inactiveMailbox.DisplayName.Split(" ")[0] -LastName $inactiveMailbox.DisplayName.Split(" ")[1] -DisplayName $inactiveMailbox.DisplayName -MicrosoftOnlineServicesID $inactiveMailbox.PrimarySmtpAddress.Replace("@","_old$rand@") -Password (ConvertTo-SecureString -String 'W1pos3wsd03?!' -AsPlainText -Force) -ResetPasswordOnNextLogon $true    

    $newMailbox | Remove-Mailbox -Force -Confirm:$False
}

Controlling a dumb floor heating pump with Tado smart thermostat and a Philips Hue power plug

My home has a floor heating system on the main floor, which basically has a pump that normally runs 24/7 and has no smart controls or connectivity. If hot water comes from the Central Heating, it’ll heat the floor, otherwise it’ll cool it.

I wanted to save on;

  1. not having the pump running at all times (~35‚ā¨ / year savings)
  2. not heating the floor when ONLY heating other rooms in the house , like upstairs in the evening/morning (~110‚ā¨/year savings)

All rooms in my house have a Tado smart thermostat which measures the temperature and humidity, and which can turn on the Central Heating unit if temperatures drop below a preset value. Using a Tado in your home and if have a ‘dumb’ pump like me, you could automate and save a lot of money ūüôā Kitlist:

  1. Philips Hue Bridge
  2. Philips Hue Smart Plug
  3. Tado Thermostat
  4. Raspberry pi zero

First, set up your Pi Zero so it has connectivity (same subnet as the Hue Bridge, and outbound internet access), and install Python3 on it.

Then set up your Hue Bridge and ensure the Hue Smart Plug shows up in your Hue app.

Put the Smart Plug in between your wall outlet and the Floor Heating pump:

Yes, the color of the light makes one wonder what else happens in this room, I’ll blog about my Hydroponics hobby some other time ūüôā

Create a Hue API username and determine the plug’s ID

Create a Tado account and set up a heating profile for the room. If you don’t have an old radiator in the room, just plug it onto another one to set it up, then remove it and place it somewhere in the kitchen to make it think it is controlling a radiator, as these plugs are actually mechanical and meant to rotate:

A tado thermostat tricked into controlling the floor heating

Customize my script and run! I scheduled it to run every minute using a cronjob on the pi.

Since we already had Tado and Hue, the total cost of the project was only 35‚ā¨ (raspberry pi + smart plug), so the investment was well worth it.

Get the code at my git lab or below:

Git lab Tado Floor Heating Pump Controller with Philips Hue Smart Plug

import requests 
import json
import os
import datetime
import time

data = {'grant_type':'password', 
        'scope':'home.user',
        'client_id':'tado-web-app', 
        'client_secret':'wZaRN7rpjn3FoNyF5IFuxg9uMzYJcvOoQ8QWiIqS3hfk6gLhVlG57j5YNoZL2Rtc', #see https://shkspr.mobi/blog/2019/02/tado-api-guide-updated-for-2019/ on how to get this client secret
        'username':'Test@test.com', #your tado login/email
        'password':'MyPassword' #your tado password
        }
hueBaseURL = "http://HUEBRIDGEIPHERE/api/USERNAMEHERE" #the IP and username of your HUE, see https://github.com/tigoe/hue-control for instructions on how to get the username
hueLightID = '6' #the ID of the Hue Power Plug in which you connected your floor heating pump, see https://github.com/tigoe/hue-control for instructions on how to get all ID's
tadoHomeId = '12345' #the home ID of your tado account, see https://shkspr.mobi/blog/2019/02/tado-api-guide-updated-for-2019/ on how to get the ID
tadoHeatingZoneId = '6' #the ID of the tado zone you are heating using a floor heating system, see https://shkspr.mobi/blog/2019/02/tado-api-guide-updated-for-2019/ on how to get the ID
r = requests.post('https://auth.tado.com/oauth/token', data = data) 
j = json.loads(r.text)
TOKEN = j["access_token"]
headers={'Authorization': "Bearer " + TOKEN}
r = requests.get('https://my.tado.com/api/v2/homes/'+tadoHomeId+'/zones/'+tadoHeatingZoneId+'/state', headers=headers)
j = json.loads(r.text)
desiredTemp = j["setting"]["temperature"]["celsius"]
currentTemp = j["sensorDataPoints"]["insideTemperature"]["celsius"]
currentPower = j["activityDataPoints"]["heatingPower"]["percentage"]
print("Status zone "+tadoHeatingZoneId+" : "+str(j["setting"]["power"]))
print("Heating power for zone "+tadoHeatingZoneId+" : "+str(currentPower)+"%")
print("Current temperature in zone "+tadoHeatingZoneId+" : "+str(currentTemp))
print("Target temp in zone "+tadoHeatingZoneId+" : "+str(desiredTemp))
r = requests.get(hueBaseURL+'/lights/'+hueLightID)
j = json.loads(r.text)
pumpState = j["state"]["on"]
print("Floor heating pump state: "+str(pumpState))
if currentPower > 0 and pumpState == False :
    print("PUMP SHOULD BE TURNED ON")
    r = requests.put(hueBaseURL+'/lights/'+hueLightID+'/state', data = '{"on":true}') 
    print(r.content)
    print("PUMP TURNED ON")
elif currentPower <= 0 and pumpState == True :
    print("PUMP SHOULD BE OFF")
    r = requests.put(hueBaseURL+'/lights/'+hueLightID+'/state', data = '{"on":false}')  
    print(r.content)
    print("PUMP TURNED OFF")