I have noticed that on one of my PC’s VS Code just would not remember my credentials when I started it up and I would have to re-login every time to get the sync to work.
I finally decided to try and fix it. It turns out that there are too many remembered credentials in the Windows Credential Manager
If you see a whole load of Windows credentials all starting with vscode then you may need to delete them, restart VS Code and re-enter you logins again. This time they should stick 😉
If you have a substantial number, then you can run the following command to remove all the VSCode credentials. This is what I needed to do. I had hundreds.
Sometimes you just want to enable diagnostics on everything* (* = eligiable resource types) in a Resource Group and to point to the same Log Analytics workspace.
Here is a PowerShell script that allows you to do this. See the Examples for details on what you can do.
The Log Analytics and Storage accounts do need to be in the same subscription.
<#PSScriptInfo
.VERSION 1.0
.GUID 4859bbd0-103e-4089-a6a1-35af0f9c5e63
.AUTHOR Nicholas Rogoff
.RELEASENOTES
Initial version. Could do with more error handling
#>
<#
.SYNOPSIS
Script to enable diagnostics on all* resources (* = eligible resource types)
.DESCRIPTION
Iterates through all eligible resources and enables diagnostics on all them.
Diagnostic data is sent to Log analytics workspace and storage account if set.
.NOTES
Version: 1.0
Author: Nicholas Rogoff
Creation Date: 2020-10-28
Purpose/Change: Initial script development
.PARAMETER ResourceGroupName
The resource group to scan for resources that can have diagnostics enabled
.PARAMETER LogAnalyticsWS
The Log Analytics workspace to forward logs too
.PARAMETER StorageAccName
[Optional] If this is given then diagnostics will be set to ship the logs for longer term archiving to the chosen storage account.
The storage account MUST be in the same region as the resource.
.PARAMETER ResourceTypes
[Optional] An array of resource types
(see https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/azure-services-resource-providers)
to enable diagnostcs on. If not passed a default set is used as follows:
"Microsoft.Automation/automationAccounts", "Microsoft.Logic/workflows", "Microsoft.Storage/storageAccounts",
"Microsoft.DesktopVirtualization/workspaces", "Microsoft.DesktopVirtualization/applicationgroups",
"Microsoft.DesktopVirtualization/hostpools", "Microsoft.Compute/virtualMachines","Microsoft.Network/virtualNetworks","Microsoft.Web/serverFarms"
.EXAMPLE
.\EnableDiagnostics.ps1 -ResourceGroupName $ResourceGroupName -LogAnalyticsWS $LogAnalyticsWS -StorageAccName $StorageAccName
Enables Diagnostics on eveything in a resource group it can and includes shipping logs to storage account
.EXAMPLE
.\EnableDiagnostics.ps1 -ResourceGroupName $ResourceGroupName -LogAnalyticsWS $LogAnalyticsWS
Enables Diagnostics on eveything in a resource group it can to the chosen LogAnalytics Workspace Only
.EXAMPLE
$ResourceTypes = @('Microsoft.Compute/virtualMachines','Microsoft.Network/virtualNetworks')
.\EnableDiagnostics.ps1 -ResourceGroupName $ResourceGroupName -LogAnalyticsWS $LogAnalyticsWS -ResourceTypes $ResourceTypes
Enables Diagnostics on eveything in a resource group it can to the chosen LogAnalytics Workspace and for Resource Type of VMs
and Virtual Networks only
#>
#---------------------------------------------------------[Script Parameters]------------------------------------------------------
[CmdletBinding()]
Param (
#Script parameters go here
[Parameter(mandatory = $true)]
[string] $ResourceGroupName,
[Parameter(mandatory = $true)]
[string] $LogAnalyticsWS,
[Parameter(mandatory = $false)]
[string] $StorageAccName,
[Parameter(mandatory = $false)]
[string[]] $ResourceTypes = @("Microsoft.Automation/automationAccounts", "Microsoft.Logic/workflows", "Microsoft.Storage/storageAccounts", "Microsoft.DesktopVirtualization/workspaces", "Microsoft.DesktopVirtualization/applicationgroups", "Microsoft.DesktopVirtualization/hostpools","Microsoft.Compute/virtualMachines","Microsoft.Network/virtualNetworks","Microsoft.Web/sites","Microsoft.Web/serverFarms")
)
#---------------------------------------------------------[Initialisations]--------------------------------------------------------
#Set Error Action to Silently Continue
$ErrorActionPreference = 'Continue'
#Variable to hold Passed and failed resources
$Passed = "Successfully Enabled On : "
$Failed = "Failed On : "
#----------------------------------------------------------[Declarations]----------------------------------------------------------
#Any Global Declarations go here
#-----------------------------------------------------------[Functions]------------------------------------------------------------
# Function to check if the module is imported
function EnableDiagnostics {
[CmdletBinding()]
param(
[Parameter(mandatory = $true)]
[string]$ResourceGroupName,
[Parameter(mandatory = $true)]
[string]$LogAnalyticsWS,
[Parameter(mandatory = $false)]
[string]$StorageAccName
)
Write-Debug ("Script EnableDiagnostics function execution started...")
#Variables to hold log analytics resource id's
$LogAnlyResId = Get-AzResource -Name $LogAnalyticsWS | Select-Object ResourceId
#Iterate over all configured resource types
foreach ($resType in $ResourceTypes) {
#Variable to hold Resource list for each resource type
$resources = Get-AzResource -ResourceGroupName $ResourceGroupName -ResourceType $resType | Select-Object Name, ResourceId, Location
#Enable Diagnostics for each resource in resource list
foreach ($resource in $resources) {
$Error.clear()
#Command to enable diagnostics
$DiagName = $resource.Name + "-Diagnostics"
$resName = $resource.Name
Write-Output "=== Setting diagnostics on $resName"
if($StorageAccName)
{
$StrAccResId = Get-AzResource -Name $StorageAccName | Select-Object ResourceId
Set-AzDiagnosticSetting -Name $DiagName `
-ResourceId $resource.ResourceId `
-Enabled $True `
-StorageAccountId $StrAccResId.ResourceId `
-WorkspaceId $LogAnlyResId.ResourceId
} else {
Set-AzDiagnosticSetting -Name $DiagName `
-ResourceId $resource.ResourceId `
-Enabled $True `
-WorkspaceId $LogAnlyResId.ResourceId
}
#Log Error and success
if (!$Error[0]) {
Write-Output ("--- Diagnostics Successfully enabled on :" + $resource.Name)
$Passed = $Passed + $resource.Name + " , "
}
else {
Write-Error ("!!! Error Occurred on :" + $resource.Name + "Error Message :" + $Error[0])
$Failed = $Failed + $resource.Name + " , "
}
}
}
Write-Output ("Finished for Resource Group :" + $ResourceGroupName)
If ($?) {
Write-Output "Script executed successfully."
Write-Output("Diagnostics Script Run Results ")
Write-Output("======================================== ")
Write-Output("======================================== ")
$Passed
$Failed
}
}
#-----------------------------------------------------------[Execution]------------------------------------------------------------
# Script Execution goes here
# Execute Function
if($StorageAccName)
{
EnableDiagnostics $ResourceGroupName $LogAnalyticsWS $StorageAccName
} else {
EnableDiagnostics $ResourceGroupName $LogAnalyticsWS
}
When working across multiple environments, with restricted access, it can by difficult tracking which Key Vault secrets have been configured and which still need values set.
On my projects I like to give the dev teams autonomy on the dev environments setting their own Key Vault Keys and Secrets (following an agreed convention).
The following script can be run to output all the Key Vault secrets, or only show the ones that need configuration. It can also be set to output pure markdown like…
that can be copy and pasted into the Azure or Github Wiki too and displays like the table below when rendered.
hms-sapp-kv-qa-ne (2021-07-03 14:31:04.590)
Secret Name
Needs Configuration
apim-api-subscription-primary-key
******
apim-tests-subscription-primary-key
******
b2c-client-id-extensions-app
******
b2c-client-id-postman
******
saleforce-client-id
Needs Configuration
salesforce-client-secret
Needs Configuration
sql-database-ro-password
******
sql-database-rw-password
******
The script will output ‘Needs Configuration’ for any Secret values that are either blank, contain ‘Needs Configuration’ or ‘TBC’.
Script below, aslo contains examples:
<#PSScriptInfo
.VERSION 1.1
.GUName 48b4b27a-b77e-41e6-8a37-b3767da5caee
.AUTHOR Nicholas Rogoff
.RELEASENOTES
Initial version.
#>
<#
.SYNOPSIS
Copy Key Vault Secrets from one Vault to another. If the secret name exists it will NOT overwrite the value.
.DESCRIPTION
Loops through all secrets and copies them or fills them with a 'Needs Configuration'.
Will not copy secrets of ContentType application/x-pkcs12
PRE-REQUIREMENT
---------------
Run 'Import-Module Az.Accounts'
Run 'Import-Module Az.KeyVault'
You need to be logged into Azure and have the access necessary rights to both Key Vaults.
.INPUTS
None. You cannot pipe objects to this!
.OUTPUTS
None.
.PARAMETER SrcSubscriptionName
This is the Source Subscription Name
.PARAMETER SrcKvName
The name of the Source Key Vault
.PARAMETER MarkdownOnly
Output Markdown Only
.PARAMETER NameOnly
Set to only copy across the secret name and NOT the actual secret. The secret will be populated with 'Needs Configuration'
.NOTES
Version: 1.1
Author: Nicholas Rogoff
Creation Date: 2021-08-09
Purpose/Change: Refined for publication
.EXAMPLE
PS> .\List-UnSet-KeyVault-Secrets.ps1 -SrcSubscriptionName $srcSubscriptionName -SrcKvName $srcKvName
This will list the secrets keys in the specified key vault and diplay 'Needs Configuration' for those that are blank, contain 'Needs Configuration' or 'TBC'
.EXAMPLE
PS> .\List-UnSet-KeyVault-Secrets.ps1 -SrcSubscriptionName $srcSubscriptionName -SrcKvName $srcKvName -MarkdownOnly
Use for updating the Wiki. This will list the secrets keys, as MarkDown ONLY, in the specified key vault and diplay 'Needs Configuration' for those that are blank, contain 'Needs Configuration' or 'TBC'
.EXAMPLE
PS> .\List-UnSet-KeyVault-Secrets.ps1 -SrcSubscriptionName $srcSubscriptionName -SrcKvName $srcKvName -ShowSecrets
This will list the secrets keys in the specified key vault and show the secrets
#>
#---------------------------------------------------------[Script Parameters]------------------------------------------------------
[CmdletBinding()]
Param(
[Parameter(Mandatory = $true, HelpMessage = "This is the Source Subscription Name")]
[string] $SrcSubscriptionName,
[Parameter(Mandatory = $true, HelpMessage = "The name of the Source Key Vault")]
[string] $SrcKvName,
[Parameter(Mandatory = $false, HelpMessage = "Output Markdown Only")]
[switch] $MarkdownOnly,
[Parameter(Mandatory = $false, HelpMessage = "Output the secrets set")]
[switch] $ShowSecrets
)
#---------------------------------------------------------[Initialisations]--------------------------------------------------------
# Set Error Action to Silently Continue
$ErrorActionPreference = 'Continue'
# Set Warnings Action to Silently Continue
$WarningPreference = "SilentlyContinue"
#----------------------------------------------------------[Declarations]----------------------------------------------------------
# Any Global Declarations go here
$SecretsFound = @()
#----------------------------------------------------------[Functions]----------------------------------------------------------
# Inline If Function
Function IIf($If, $Right, $Wrong) { If ($If) { $Right } Else { $Wrong } }
#-----------------------------------------------------------[Execution]------------------------------------------------------------
$checked = 0
$failed = 0
if (!$MarkdownOnly) {
Write-Host "======================================================"
Write-Host ($(Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff') + " Starting to check secrets in " + $SrcKvName + "... ") -ForegroundColor Blue
Write-Host "======================================================"
}
else {
Write-Host ("# " + $SrcKvName + " (" + $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff') + ")") -ForegroundColor Blue
}
Write-Host ""
$sourceSecrets = Get-AzKeyVaultSecret -VaultName $SrcKvName | Where-Object { $_.ContentType -notmatch "application/x-pkcs12" }
if (!$MarkdownOnly) {
# Headers
Write-Host -NoNewline "|".PadRight(60, "-")
Write-Host -NoNewline "|".PadRight(60, "-")
Write-Host "|"
}
Write-Host -NoNewline "| Secret Name".PadRight(60)
if (!$ShowSecrets) {
Write-Host -NoNewline "| Needs Configuration".PadRight(60)
}
else {
Write-Host -NoNewline "| Secret Value".PadRight(60)
}
Write-Host "|"
Write-Host -NoNewline "|".PadRight(60, "-")
Write-Host -NoNewline "|".PadRight(60, "-")
Write-Host "|"
ForEach ($sourceSecret in $sourceSecrets) {
$Error.clear()
$name = $sourceSecret.Name
$plainTxtSecret = Get-AzKeyVaultSecret -VaultName $srckvName -Name $name -AsPlainText
if ($plainTxtSecret -eq "Needs Configuration" -or $plainTxtSecret -eq "TBC" -or !$plainTxtSecret) {
$secretToShow = $plainTxtSecret
}
elseif ($ShowSecrets) {
$secretToShow = $plainTxtSecret
}
else {
$secretToShow = "******"
}
Write-Host -NoNewline "| $name".PadRight(60)
if ($secretToShow -eq "Needs Configuration" -or $plainTxtSecret -eq "TBC" -or !$plainTxtSecret) {
Write-Host -NoNewline "| $secretToShow".PadRight(60) -ForegroundColor Magenta
}
else {
Write-Host -NoNewline "| $secretToShow".PadRight(60) -ForegroundColor DarkGray
}
Write-Host "|"
if (!$Error[0]) {
$checked += 1
}
else {
$failed += 1
Write-Error "!! Failed to get secret $name"
}
}
if (!$MarkdownOnly) {
Write-Host -NoNewline "|".PadRight(60, "-")
Write-Host -NoNewline "|".PadRight(60, "-")
Write-Host "|"
Write-Host ""
Write-Host ""
Write-Host "================================="
Write-Host "Completed Key Vault Secrets Copy"
Write-Host "Checked: $checked"
Write-Host "Failed: $failed"
Write-Host "================================="
}
else {
Write-Host ""
Write-Host ("**Total Secrets Listed: " + $checked + "**")
if ($failed -gt 0) {
Write-Host "Failed: $failed" -ForegroundColor Red
}
Write-Host ""
}
<#PSScriptInfo
.VERSION 1.1
.GUName 48b4b27a-b77e-41e6-8a37-b3767da5caee
.AUTHOR Nicholas Rogoff
.RELEASENOTES
Initial version.
#>
<#
.SYNOPSIS
Copy Key Vault Secrets from one Vault to another.
.DESCRIPTION
Loops through all secrets and copies them or fills them with a 'Needs Configuration'.
PRE-REQUIREMENT
---------------
Run 'Import-Module Az.Accounts'
Run 'Import-Module Az.KeyVault'
You need to be logged into Azure and have the access necessary rights to both Key Vaults.
.INPUTS
None. You cannot pipe objects to this!
.OUTPUTS
None.
.PARAMETER SrcSubscriptionName
This is the Source Subscription Name
.PARAMETER SrcKvName
The name of the Source Key Vault
.PARAMETER DestSubscriptionName
This is the destination Subscription Name
.PARAMETER DestKvName
The name of the destination Key Vault
.PARAMETER NameOnly
Set to only copy across the secret name and NOT the actual secret. The secret will be populated with 'Needs Configuration'
.NOTES
Version: 1.1
Author: Nicholas Rogoff
Creation Date: 2021-08-09
Purpose/Change: Refined for publication
.EXAMPLE
PS> .\Copy-KeyVault-Secrets.ps1 -SrcSubscriptionName $srcSubscriptionName -SrcKvName $srcKvName -DestSubscriptionName $destSubscriptionName -DestKvName $destKvName -NameOnly
This will copy across only the secret names, filling the secret with 'Needs Configuration'
#>
#---------------------------------------------------------[Script Parameters]------------------------------------------------------
[CmdletBinding()]
Param(
[Parameter(Mandatory = $true, HelpMessage = "This is the Source Subscription Name")]
[string] $SrcSubscriptionName,
[Parameter(Mandatory = $true, HelpMessage = "The name of the Source Key Vault")]
[string] $SrcKvName,
[Parameter(Mandatory = $false, HelpMessage = "This is the destination Subscription Name. If not set or blank then same subscription is assumed")]
[string] $DestSubscriptionName,
[Parameter(Mandatory = $true, HelpMessage = "The name of the destination Key Vault")]
[string] $DestKvName,
[Parameter(Mandatory = $false, HelpMessage = "Only copy across the secret name and NOT the actual secret. The secret will be populated with 'Needs Configuration'")]
[switch] $NameOnly
)
#---------------------------------------------------------[Initialisations]--------------------------------------------------------
Write-Host ($(Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff') + " Starting copying from " + $SrcKvName + " to " + $DestKvName + "... ") -ForegroundColor Blue
# Set Error Action to Silently Continue
$ErrorActionPreference = 'Continue'
#----------------------------------------------------------[Declarations]----------------------------------------------------------
# Any Global Declarations go here
#----------------------------------------------------------[Functions]----------------------------------------------------------
#-----------------------------------------------------------[Execution]------------------------------------------------------------
$success = 0
$failed = 0
# ensure source subscription is selected
Select-AzSubscription -Subscription $SrcSubscriptionName
$Tags = @{ 'Migrated' = 'true'; 'Source Key Vault' = $SrcKvName }
$sourceSecrets = Get-AzKeyVaultSecret -VaultName $SrcKvName
if ($DestSubscriptionName) {
#Need to switch subscriptions
Select-AzSubscription -Subscription $DestSubscriptionName
}
ForEach ($sourceSecret in $sourceSecrets) {
$Error.clear()
$name = $sourceSecret.Name
$tags = $sourceSecret.Tags
$secret = Get-AzKeyVaultSecret -VaultName $srckvName -Name $name
Write-Host "Adding SecretName: $name ..."
if ($NameOnly) {
$value = ConvertTo-SecureString 'Needs Configuration' -AsPlainText -Force
}
else {
$value = $secret.SecretValue
}
$secret = Set-AzKeyVaultSecret -VaultName $destkvName -Name $sourceSecret.Name -SecretValue $value -ContentType $sourceSecret.ContentType -Tags $tags
if (!$Error[0]) {
$success += 1
}
else {
$failed += 1
Write-Error "!! Failed to copy secret $name"
}
}
Write-Output "================================="
Write-Output "Completed Key Vault Secrets Copy"
Write-Output "Succeeded: $success"
Write-Output "Failed: $failed"
Write-Output "================================="
You can add a Group (only some types of AD or Microsoft 365 Groups) to a Team, but this will only add the members in a one time action. It will not maintain the membership in sync.
It gets complicated, but you can make a Team from a Microsoft (Office) 365 Group and have it’s membership dynamic, linked to an AD group (see https://docs.microsoft.com/en-us/microsoftteams/dynamic-memberships ), but if you group is large (over 10,000) this can still also run into problems and the group can’t have additional members not part of that group (outsiders!).
I had a client that required a Team to include the membership of an Group that contained over 4,000 members. They also did NOT want members that had made the effort to join the Team, but were not members of the Microsoft 365 group to be removed.
The whole organization was much larger, so much so that they were not able to even create an ‘Org-wide Team’ which allows you to include everyone automatically in a Team up to 5,000 ( see https://docs.microsoft.com/en-us/microsoftteams/create-an-org-wide-team ). We had lots of fun and games with the limits of teams, but came to the following solution.
To get over these hurdles, in the end it was easier to use a PowerShell script and schedule the synching of the members. The following script can be used to add missing members of a Microsoft 365 AD group to a Teams membership. You can also choose whether you want to remove users that are no longer a member of the AD group or just leave them be 😉
Save the script below as Sync-Team-Members-With-AD-Group.ps1 and run the following, replacing the variables as appropriate
Sync-Team-Members-With-AD-Group.ps1
$ADGroupId = "67h3rc03e286-FAKE-ID HERE-8d1c-7b176431"
$TeamDisplayName = "My Big Team"
.\Sync-Team-Members-With-AD-Group.ps1 -ADGroupId $ADGroupId -TeamDisplayName $TeamDisplayName -Credential $Credential -RemoveInvalidTeamMembers $false -Verbose
from a PowerShell terminal. You will need to get the ObjectId of the Azure AD Group and an administrator should be able to help you if you don’t know it.
See the comments in the script for more options, such as limiting the number of users to process etc..
<#PSScriptInfo
.VERSION 1.1
.GUID 21a6ad93-df53-4a1a-82fd-4a902cb57350
.AUTHOR Nicholas Rogoff
.RELEASENOTES
Initial version.
#>
<#Â
.SYNOPSISÂ
Synchronizes Team membership with an Azure AD Group.Â
Â
.DESCRIPTIONÂ
Loops through all members of an AD group and adds any missing users to the membership.
The script then loops through all the existing Team members and removes any that are no longer in the AD Group.
NOTE: This script will NOT remove Owners.
PRE-REQUIREMENT
---------------
Install Teams Module (MS Online)
PS> Install-Module -Name MSOnline
Install Microsoft Teams cmdlets module
PS> Install-Module -Name MicrosoftTeams
.INPUTS
None. You cannot pipe objects to this!
.OUTPUTS
None.
.PARAMETER ADGroupId
This is the Object Id of the Active Directory group you wish to populate the Team with
.PARAMETER TeamDisplayName
The display name of the Team who's membership you wish to alter
.PARAMETER Credential
The credentials (PSCredential object) for an Owner of the Team. Use '$Credential = Get-Credential' to prompt and store the credentials to securely pass
.PARAMETER MaxUsers
Max number of AD Group Users to process. This can be used to test a small batch. Set to 0 to process all members of the AD group
.PARAMETER RemoveInvalidTeamMembers
Default = False. All members of the Team that have are not part of the AD Group will remain members of the Team. E.g. Add-only.
If you do want sync the membership of the Team exactly, e.g. to remove any Team Members that are not part of the AD group, then set this to True
.NOTES
Version: 1.1
Author: Nicholas Rogoff
Creation Date: 2020-10-08
Purpose/Change: Added more detailed help
Â
.EXAMPLEÂ
PS>Â .\Sync-Team-Members-With-AD-Group.ps1 -ADGroupId "4b3f7f45-e8e3-47af-aa82-ecaf41f5e78d" -TeamDisplayName "My Team" -Credential $Credential
This will add all missing members of the AD Group to the Team
.EXAMPLE
PS>Â .\Sync-Team-Members-With-AD-Group.ps1 -ADGroupId "4b3f7f45-e8e3-47af-aa82-ecaf41f5e78d" -TeamDisplayName "My Team"Â -Credential $Credential -MaxUsers 10 -Verbose
This will add all missing members of the first 10 members AD Group to the Team and will output verbose details
.EXAMPLEÂ
PS>Â .\Sync-Team-Members-With-AD-Group.ps1 -ADGroupId "4b3f7f45-e8e3-47af-aa82-ecaf41f5e78d" -TeamDisplayName "My Team" -Credential $Credential -RemoveInvalidTeamMembers
This will add all missing members of the AD Group to the Team, and REMOVE any members of the Team that are not in the AD Group (Except for Team Owners)
#>
#---------------------------------------------------------[Script Parameters]------------------------------------------------------
[CmdletBinding()]
Param(
[Parameter(Mandatory = $true, HelpMessage = "This is the Object Id of the Azure Active Directory group you wish to populate the Team with")]
[string] $ADGroupId,
[Parameter(Mandatory = $true, HelpMessage = "The display name of the Team")]
[string] $TeamDisplayName,
[Parameter(Mandatory = $true, HelpMessage = "The credentials for an Owner of the Team")]
[System.Management.Automation.PSCredential] $Credential,
[Parameter(Mandatory = $false, HelpMessage = "Max number of AD Group Users to process")]
[int] $MaxUsers = 0,
[Parameter(Mandatory = $false, HelpMessage = "Default = False. If you do want to remove any Team Members that are not part of the AD group, then set this to True")]
[bool] $RemoveInvalidTeamMembers = $false
)
#---------------------------------------------------------[Initialisations]--------------------------------------------------------
Write-Host ($(Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff') + " Starting synchonisation") -ForegroundColor Blue
# Set Error Action to Silently Continue
$ErrorActionPreference = 'Continue'
# Signin to Office 365 stuff
Connect-MicrosoftTeams -Credential $Credential
Connect-MsolService -Credential $Credential
#----------------------------------------------------------[Declarations]----------------------------------------------------------
if (Get-Module -ListAvailable -Name MSOnline) {
Write-Host "MSOnline Module exists"
}
else {
throw "MSOnline Module does not exist. You can install it by using 'Install-Module -Name MSOnline'"
}
if (Get-Module -ListAvailable -Name MicrosoftTeams) {
Write-Host "MicrosoftTeams Module exists"
}
else {
throw "MicrosoftTeams Module does not exist. You can install it by using 'Install-Module -Name MicrosoftTeams'"
}
#----------------------------------------------------------[Functions]----------------------------------------------------------
function Add-MissingTeamMembers {
[CmdletBinding()]
Param(
[Parameter(Mandatory = $true, HelpMessage = "This is the Team to add the users to")]
[Microsoft.TeamsCmdlets.PowerShell.Custom.Model.TeamSettings] $team,
[Parameter(Mandatory = $true, HelpMessage = "This is the AD Group membership from which to add missing members")]
[Microsoft.Online.Administration.GroupMember[]] $ADGroupMembers
)
$TeamMembersAdded = [System.Collections.ArrayList]@()
#Add missing members
Write-Host ($(Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff') + " Checking membership of Team: " + $team.DisplayName + " ( " + $team.GroupId + " ) ") -ForegroundColor Yellow
Write-Host ($(Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff') + " Against AD Group: " + $ADGroup.DisplayName + " ( " + $ADGroup.ObjectId + " ) ") -ForegroundColor Yellow
Write-Host ("--------------------------------------------------------")
Write-Host ($(Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff') + " Team Membership Total: " + $ExistingTeamMembers.count) -ForegroundColor Yellow
Write-Host ($(Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff') + " AD Group Membership Total: " + $ADGroupMembers.count) -ForegroundColor Yellow
foreach ($groupMember in $ADGroupMembers) {
#Check if exists in Teams already
if ((($ExistingTeamMembers | Select-Object User) -Match $groupMember.EmailAddress).Count -eq 0 ) {
#Add missing member
Add-TeamUser -GroupId $team.GroupId -User $groupMember.EmailAddress
Write-Verbose ("+ Added: " + $groupMember.EmailAddress)
$TeamMembersAdded.Add($groupMember)
}
else {
Write-Verbose ("| Existed: " + $groupMember.EmailAddress)
}
}
Write-Host ("=====================")
Write-Host ($(Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff') + " " + $TeamMembersAdded.count + " new members added") -ForegroundColor Yellow
Write-Host ("")
}
function Remove-MissingADUsers {
[CmdletBinding()]
Param(
[Parameter(Mandatory = $true, HelpMessage = "This is the list of existing Team users you want to search. The full type should be Microsoft.TeamsCmdlets.PowerShell.Custom.GetTeamUser+GetTeamUserResponse")]
[object] $ExistingTeamMembers,
[Parameter(Mandatory = $true, HelpMessage = "This is the AD Group membership from which to check against for invalid team members")]
[Microsoft.Online.Administration.GroupMember[]] $ADGroupMembers
)
$TeamMembersRemoved = [System.Collections.ArrayList]@()
# Now check for existing Team members that are no longer AD Group members
foreach ($teamMember in $ExistingTeamMembers) {
#Check if exists in Teams already
if (((($ADGroupMembers | Select-Object EmailAddress) -Match $teamMember.User).Count -eq 0) -and ($teamMember.Role -notmatch "owner") ) {
#Remove from team
Remove-TeamUser -GroupId $team.GroupId -User $teamMember.User
$TeamMembersRemoved.Add($teamMember)
Write-Verbose (" - Removed: " + $teamMember.User)
}
else {
Write-Verbose (" | Not removed: " + $teamMember.User)
}
}
Write-Host ("---------------------")
Write-Host ($(Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff') + " " + $TeamMembersRemoved.Count + " Team members removed") -ForegroundColor Yellow
}
#-----------------------------------------------------------[Execution]------------------------------------------------------------
# Get Team
Write-Host ($(Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff') + " Getting the Team..." + $TeamDisplayName) -ForegroundColor Blue
$team = Get-Team -DisplayName $TeamDisplayName
# Get AD / Outlook Group Members
$ADGroup = Get-MsolGroup -ObjectId $ADGroupId
Write-Host ($(Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff') + " Getting the AD Group..." + $ADGroup.DisplayName) -ForegroundColor Blue
if ($MaxUsers -gt 0) {
$ADGroupMembers = Get-MsolGroupMember -GroupObjectId $ADGroupId -MaxResults $MaxUsers
}
else {
$ADGroupMembers = Get-MsolGroupMember -GroupObjectId $ADGroupId -All
}
Write-Host ($(Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff') + " " + $ADGroupMembers.Count + " ...AD Group Members fetched" ) -ForegroundColor Blue
#Get existing Team members
Write-Host ($(Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff') + " Getting the latest Team members..." + $TeamDisplayName) -ForegroundColor Blue
$ExistingTeamMembers = Get-TeamUser -GroupId $team.GroupId
Write-Host ($(Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff') + " " + $ADGroupMembers.Count + " ...Team Members fetched") -ForegroundColor Blue
Add-MissingTeamMembers -team $team -ADGroupMembers $ADGroupMembers
if (($RemoveInvalidTeamMembers) -and ($MaxUsers -eq 0)) {
Remove-MissingADUsers -ExistingTeamMembers $ExistingTeamMembers -ADGroupMembers $ADGroupMembers
}
Write-Host ("=====================")
Write-Host ("****** Completed ******") -ForegroundColor Blue
Ideally, you should have this script run on a scheduled basis to keep things up to date. I may post a blog later on how to create a robust scheduler solution for your whole organisation to implement Team membership synching.
There will be times when you will want to start clearing down all active sessions on you Window Virtual Desktop hostpool hosts. This maybe because you want to perform some maintenance or migrate users to another pool.
The following script allows you to set and unset ‘Drain’ mode on all the Hosts or just a specific VM and below is an Automation Account runbook friendly version 😉
<#
.SYNOPSIS
Set the drain mode on VMs in a hostpool.
.DESCRIPTION
Used to enable or diable new sessions on a host
.PARAMETER HostPoolName
The name of the host pool that contains the VMs you want to remove th tag from
.PARAMETER HostPoolResourceGroupName
The name of the resource group that contains the Host Pool
.PARAMETER VMName
The name of a single VM to change the drain mode on. If left blank then all VMs in a hostpool will be set
.PARAMETER SetDrainModeOn
Sets the drain mode to On (not allow new sessions). If not set then the default is to set drain mode off (allow ne sessions)
.NOTES
Version: 1.0
Author: Nicholas Rogoff
Creation Date: 2020-09-17
Purpose/Change: Initial script development
.EXAMPLE
./SetDrainModeOnHostPoolVMs.ps1 -HostPoolName $HostPoolName -HostPoolResourceGroupName $HostPoolResourceGroupName -SetDrainModeOn
Sets drain mode on all hosts in the host pool that currently Allow New Sessions
.EXAMPLE
./SetDrainModeOnHostPoolVMs.ps1 -HostPoolName $HostPoolName -HostPoolResourceGroupName $HostPoolResourceGroupName
Sets 'Allow New Sessions' on all hosts in the host pool that currently Drain mode on
.EXAMPLE
./SetDrainModeOnHostPoolVMs.ps1 -HostPoolName $HostPoolName -HostPoolResourceGroupName $HostPoolResourceGroupName -VMName $VMName -SetDrainModeOn
Sets drain mode on a specific host in the host pool that currently Drain mode off
#>
#---------------------------------------------------------[Script Parameters]------------------------------------------------------
[CmdletBinding()]
Param (
[Parameter(mandatory = $true)]
[string]$HostPoolName,
[Parameter(mandatory = $true)]
[string]$HostPoolResourceGroupName,
[Parameter(mandatory = $false)]
[string]$VMName,
[Parameter(Mandatory = $False, HelpMessage = "Sets drain mode on. If not set then drain mode is turned off whereever it's found to be on")]
[switch]$SetDrainModeOn
)
#---------------------------------------------------------[Initialisations]--------------------------------------------------------
#Set Error Action to Silently Continue
$ErrorActionPreference = 'Continue'
#----------------------------------------------------------[Declarations]----------------------------------------------------------
#Any Global Declarations go here
#-----------------------------------------------------------[Functions]------------------------------------------------------------
function Get-VMNameFromSessionHost {
<#
.SYNOPSIS
Extracts the VM Name from the full SessionHost name returned in the SessionHost object
#>
[CmdletBinding()]
param (
$SessionHost
)
#Name is in the format 'uol-vwdgdev003-hp-dev-uks/vwdgdev003-0.ds.leeds.ac.uk' so need to split the last part
$VMName = $SessionHost.Name.Split("/")[1]
$VMName = $VMName.Split(".")[0]
return $VMName
}
#-----------------------------------------------------------[Execution]------------------------------------------------------------
Write-Output "Starting to set Drain Mode..."
$success = 0
$failed = 0
$skipped = 0
$sessionHosts = Get-AzWvdSessionHost -ResourceGroupName $HostPoolResourceGroupName -HostPoolName $HostPoolName
$sessionHosts | Select-Object Name, Status, Session, AllowNewSession
if ($SetDrainModeOn) {
Write-Output "Setting Drain Mode: OFF"
if ($VMName) {
#Get matching host with drain mode on
$HostPoolVMName = $HostPoolName + "/" + $VMName + ".ds.leeds.ac.uk"
$hostsToProcess = $sessionHosts | Where-Object Name -eq $HostPoolVMName | Where-Object AllowNewSession -eq $true
}
else {
#Get all hosts with drain mode on
$hostsToProcess = $sessionHosts | Where-Object AllowNewSession -eq $true
}
}
else {
if ($VMName) {
#Get matching host with drain mode on
$HostPoolVMName = $HostPoolName + "/" + $VMName + ".ds.leeds.ac.uk"
$hostsToProcess = $sessionHosts | Where-Object Name -eq $HostPoolVMName | Where-Object AllowNewSession -eq $false
}
else {
#Get all host with drain mode off
Write-Output "Setting Drain Mode: ON"
$hostsToProcess = $sessionHosts | Where-Object AllowNewSession -eq $false
}
}
foreach ($sh in $hostsToProcess) {
$Error.clear()
$VMName = Get-VMNameFromSessionHost($sh)
$thisAllowNewSessions = $sh.AllowNewSession
if ($SetDrainModeOn -and $thisAllowNewSessions) {
Write-Output "Setting " $VMName " to drain"
Update-AzWvdSessionHost -ResourceGroupName $HostPoolResourceGroupName `
-HostPoolName $HostPoolName `
-Name $sh.Name.Split("/")[1] `
-AllowNewSession:$false
}
elseif (! $SetDrainModeOn -and ! $thisAllowNewSessions) {
Write-Output "Setting " $VMName " to Allow New Sessions"
Update-AzWvdSessionHost -ResourceGroupName $HostPoolResourceGroupName `
-HostPoolName $HostPoolName `
-Name $sh.Name.Split("/")[1] `
-AllowNewSession:$true
}
else {
$skipped ++
}
if (!$Error[0]) {
$success += 1
}
else {
$failed += 1
Write-Error "!! Failed to set drain mode on $VMName"
}
}
Write-Output "================================="
Write-Output "Completed Drain Mode changes"
Write-Output "Succeeded: $success"
Write-Output "Failed: $failed"
Write-Output "================================="
And for a Azure Automation Account Runbook friendly version:
<#
.SYNOPSIS
Set the drain mode on VMs in a hostpool.
.DESCRIPTION
Used to enable or diable new sessions on a host
.PARAMETER SubscriptionName
The subscription name
.PARAMETER HostPoolName
The name of the host pool that contains the VMs you want to remove th tag from
.PARAMETER HostPoolResourceGroupName
The name of the resource group that contains the Host Pool
.PARAMETER VMName
The name of a single VM to change the drain mode on. If left blank then all VMs in a hostpool will be set
.PARAMETER SetDrainModeOn
Sets the drain mode to On (not allow new sessions). If not set then the default is to set drain mode off (allow ne sessions)
.NOTES
Version: 1.0
Author: Nicholas Rogoff
Creation Date: 2020-09-17
Purpose/Change: Initial script development
.EXAMPLE
./SetDrainModeOnHostPoolVMs.ps1 -HostPoolName $HostPoolName -HostPoolResourceGroupName $HostPoolResourceGroupName -SetDrainModeOn
Sets drain mode on all hosts in the host pool that currently Allow New Sessions
.EXAMPLE
./SetDrainModeOnHostPoolVMs.ps1 -HostPoolName $HostPoolName -HostPoolResourceGroupName $HostPoolResourceGroupName
Sets 'Allow New Sessions' on all hosts in the host pool that currently Drain mode on
.EXAMPLE
./SetDrainModeOnHostPoolVMs.ps1 -HostPoolName $HostPoolName -HostPoolResourceGroupName $HostPoolResourceGroupName -VMName $VMName -SetDrainModeOn
Sets drain mode on a specific host in the host pool that currently Drain mode off
#>
#---------------------------------------------------------[Script Parameters]------------------------------------------------------
[CmdletBinding()]
Param (
[Parameter(mandatory = $true)]
[string]$SubscriptionName,
[Parameter(mandatory = $true)]
[string]$HostPoolName,
[Parameter(mandatory = $true)]
[string]$HostPoolResourceGroupName,
[Parameter(mandatory = $false)]
[string]$VMName,
[Parameter(Mandatory = $False, HelpMessage = "Sets drain mode on. If not set then drain mode is turned off whereever it's found to be on")]
[switch]$SetDrainModeOn
)
#---------------------------------------------------------[Initialisations]--------------------------------------------------------
#Set Error Action to Silently Continue
$ErrorActionPreference = 'Continue'
Install-Module -Name Az.DesktopVirtualization
$connectionName = "AzureRunAsConnection"
try {
# Get the connection "AzureRunAsConnection "
$servicePrincipalConnection = Get-AutomationConnection -Name $connectionName
"Logging in to Azure..."
Add-AzAccount `
-ServicePrincipal `
-TenantId $servicePrincipalConnection.TenantId `
-ApplicationId $servicePrincipalConnection.ApplicationId `
-CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint
}
catch {
if (!$servicePrincipalConnection) {
$ErrorMessage = "Connection $connectionName not found."
throw $ErrorMessage
}
else {
Write-Error -Message $_.Exception
throw $_.Exception
}
}
Select-AzSubscription $SubscriptionName
#----------------------------------------------------------[Declarations]----------------------------------------------------------
#Any Global Declarations go here
#-----------------------------------------------------------[Functions]------------------------------------------------------------
function Get-VMNameFromSessionHost {
<#
.SYNOPSIS
Extracts the VM Name from the full SessionHost name returned in the SessionHost object
#>
[CmdletBinding()]
param (
$SessionHost
)
#Name is in the format 'uol-vwdgdev003-hp-dev-uks/vwdgdev003-0.ds.leeds.ac.uk' so need to split the last part
$VMName = $SessionHost.Name.Split("/")[1]
$VMName = $VMName.Split(".")[0]
return $VMName
}
#-----------------------------------------------------------[Execution]------------------------------------------------------------
Write-Output "Starting to set Drain Mode..."
$success = 0
$failed = 0
$skipped = 0
$sessionHosts = Get-AzWvdSessionHost -ResourceGroupName $HostPoolResourceGroupName -HostPoolName $HostPoolName
$sessionHosts | Select-Object Name, Status, Session, AllowNewSession
if ($SetDrainModeOn) {
Write-Output "Setting Drain Mode: OFF"
if ($VMName) {
#Get matching host with drain mode on
$HostPoolVMName = $HostPoolName + "/" + $VMName + ".ds.leeds.ac.uk"
$hostsToProcess = $sessionHosts | Where-Object Name -eq $HostPoolVMName | Where-Object AllowNewSession -eq $true
}
else {
#Get all hosts with drain mode on
$hostsToProcess = $sessionHosts | Where-Object AllowNewSession -eq $true
}
}
else {
if ($VMName) {
#Get matching host with drain mode on
$HostPoolVMName = $HostPoolName + "/" + $VMName + ".ds.leeds.ac.uk"
$hostsToProcess = $sessionHosts | Where-Object Name -eq $HostPoolVMName | Where-Object AllowNewSession -eq $false
}
else {
#Get all host with drain mode off
Write-Output "Setting Drain Mode: ON"
$hostsToProcess = $sessionHosts | Where-Object AllowNewSession -eq $false
}
}
foreach ($sh in $hostsToProcess) {
$Error.clear()
$VMName = Get-VMNameFromSessionHost($sh)
$thisAllowNewSessions = $sh.AllowNewSession
if ($SetDrainModeOn -and $thisAllowNewSessions) {
Write-Output "Setting " $VMName " to drain"
Update-AzWvdSessionHost -ResourceGroupName $HostPoolResourceGroupName `
-HostPoolName $HostPoolName `
-Name $sh.Name.Split("/")[1] `
-AllowNewSession:$false
}
elseif (! $SetDrainModeOn -and ! $thisAllowNewSessions) {
Write-Output "Setting " $VMName " to Allow New Sessions"
Update-AzWvdSessionHost -ResourceGroupName $HostPoolResourceGroupName `
-HostPoolName $HostPoolName `
-Name $sh.Name.Split("/")[1] `
-AllowNewSession:$true
}
else {
$skipped ++
}
if (!$Error[0]) {
$success += 1
}
else {
$failed += 1
Write-Error "!! Failed to set drain mode on $VMName"
}
}
Write-Output "================================="
Write-Output "Completed Drain Mode changes"
Write-Output "Succeeded: $success"
Write-Output "Failed: $failed"
Write-Output "================================="