A while ago I wrote a post regarding schedule your Office 365 automation jobs in Azure Automation. At that time, the Azure AD PowerShell module did not support Azure Automation due to the Online Services Sign in Assistant dependency.
The preview Azure AD PowerShell module released in september do however use ADAL for authentication which solves this problem.
This is great news for all currently running licensing scripts in regular servers as scheduled tasks!
Since many probably will move from their scheduled tasks to Automation for their licensing scripts, I thought it was a good idea sharing on how to make the switch. I will use the “new” Azure portal and assume that you already have an Automation account.
1. Since the MSOnline module is not included by default in Azure Automation you need to download and install the module on your computer. When that is done you need to add the module folders (MSOnline and MSOnlineExtended) to a zip file each. The easiest way to do that is of course with PowerShell 🙂
The following code assumes that you have installed the modules in the default location and the zip files will be put on your desktop.
Add-Type -AssemblyName 'system.io.compression.filesystem' Get-ChildItem -Path $PSHOME\Modules\MSonline* | ForEach-Object -Process { $Source = $_.FullName $Destination = '{0}\Desktop\{1}.zip' -f $env:USERPROFILE,$_.Name [Io.compression.zipfile]::CreateFromDirectory($Source, $Destination) }
2. Upload the zipped modules files as assets to your automation account.
3. Now add your license service account as an asset to your automation account.
If you haven’t created one you can use the below PowerShell lines to do it. Note that the account only needs to have the “User management administrator” role.
#Create the service account New-MsolUser -UserPrincipalName licenserobot@acme333.onmicrosoft.com ` -DisplayName "LicenseRobot" ` -FirstName "License" ` -LastName "Robot" ` -Password 'password' ` -ForceChangePassword $false ` -PasswordNeverExpires $true #Add the user as an "User Account Administrator" Add-MsolRoleMember -RoleName "User Account Administrator" ` -RoleMemberEmailAddress "licenserobot@acme333.onmicrosoft.com"
4. Create your runbook and import the script. In this case I am using a slight modified version of this script, which you will find if you expand the script below below. This version are using the GroupObjectGuid to identify the groups instead of the DisplayName. Since the script is using Write-Output and Write-Warning for logging, Azure Automation will automatically handle basic logging.
LicenseAzureADUsers
#Connect to Azure AD $Credentials = Get-AutomationPSCredential -Name 'LicenseRobot' Connect-MsolService -Credential $Credentials $Licenses = @{ 'E1' = @{ LicenseSKU = 'tenant:STANDARDPACK' Group = 'cb41a390-4312-4ecf-896e-086f64652690' DisabledPlans = "SHAREPOINTSTANDARD" } 'E3' = @{ LicenseSKU = 'tenant:ENTERPRISEPACK' Group = '4549bd70-232a-4b71-8afe-4d431e5464ae' DisabledPlans = "SHAREPOINTENTERPRISE","SHAREPOINTWAC" } 'EMS' = @{ LicenseSKU = 'tenant:EMS' Group = '9abc1736-049b-4c97-9fb6-8b6b02f9e7d5' } } $UsageLocation = 'SE' #Get all currently licensed users and put them in a custom object $LicensedUserDetails = Get-MsolUser -All -Synchronized | Where-Object {$_.IsLicensed -eq 'True'} | ForEach-Object { [pscustomobject]@{ UserPrincipalName = $_.UserPrincipalName License = $_.Licenses.AccountSkuId DisabledPlans = $_.Licenses.Servicestatus | Where-Object {$_.Provisioningstatus -contains "Disabled"} } } #Create array for users to change or delete $UsersToChangeOrDelete = @() foreach ($license in $Licenses.Keys) { #Get current group name and ObjectID from Hashtable $GroupID = $Licenses[$license].Group $AccountSKU = Get-MsolAccountSku | Where-Object {$_.AccountSKUID -eq $Licenses[$license].LicenseSKU} $DisabledPlans = $licenses[$license].DisabledPlans Write-Output "Checking for unlicensed $license users in group $GroupID..." #Get all members of the group in current scope $GroupMembers = '' $GroupMembers = (Get-MsolGroupMember -GroupObjectId $GroupID -All).EmailAddress #Get all already licensed users in current scope $ActiveUsers = ($LicensedUserDetails | Where-Object {$_.License -eq $licenses[$license].LicenseSKU}).UserPrincipalName $UsersToHandle = '' $UsersToAdd = '' if ($GroupMembers) { if ($ActiveUsers) { #Compare $Groupmembers and $Activeusers #Users which are in the group but not licensed, will be added #Users licensed, but not, will be evaluated for deletion or change of license $UsersToHandle = Compare-Object -ReferenceObject $GroupMembers -DifferenceObject $ActiveUsers -ErrorAction SilentlyContinue -WarningAction SilentlyContinue $UsersToAdd = ($UsersToHandle | Where-Object {$_.SideIndicator -eq '<='}).InputObject $UsersToChangeOrDelete += ($UsersToHandle | Where-Object {$_.SideIndicator -eq '=>'}).InputObject } else { #No licenses currently assigned for the license in scope, assign licenses to all group members. $UsersToAdd = $GroupMembers } } else { Write-Warning "Group $GroupID is empty - will process removal or move of all users with license $($AccountSKU.AccountSkuId)" #If no users are a member in the group, add them for deletion or change of license. $UsersToChangeOrDelete += $ActiveUsers } if ($UsersToAdd -match "[.]") { foreach ($User in $UsersToAdd){ #Process all users for license assignment, if not already licensed with the SKU in order. $MsolUser = Get-MsolUser -UserPrincipalName $User #Assign licenses for users if ($MsolUser.Licenses.AccountSkuId -notcontains $AccountSku.AccountSkuId) { try { #Assign UsageLocation and License. $LicenseAssignmentHt = @{ UserPrincipalname = $User AddLicenses = $AccountSKU.AccountSkuId } #Set custom license options to not enable SharePoint / OneDrive by Default if ($DisabledPlans) { $LicenseOptions = New-MsolLicenseOptions -AccountSkuId $AccountSKU.AccountSkuId -DisabledPlans $DisabledPlans $LicenseAssignmentHt["LicenseOptions"] = $LicenseOptions } Set-MsolUser -UserPrincipalName $user -UsageLocation $UsageLocation -ErrorAction Stop -WarningAction Stop Set-MsolUserLicense @LicenseAssignmentHt -ErrorAction Stop -WarningAction Stop Write-Output "SUCCESS: Licensed $User with $license" } catch { Write-Warning "Error when licensing $User$_" } } } } } #Process users for change or deletion if ($UsersToChangeOrDelete -ne $null) { foreach ($User in $UsersToChangeOrDelete) { if ($user -ne $null) { #Fetch users old license for later usage $OldLicense = ($LicensedUserDetails | Where-Object {$_.UserPrincipalName -eq $User}).License #Loop through to check if the user group assignment has been changed, and put the old and the new license in a custom object. #Only one license group per user is currently supported. $ChangeLicense = $Licenses.Keys | ForEach-Object { $GroupID = $Licenses[$_].Group if (Get-MsolGroupMember -All -GroupObjectId $GroupID | Where-Object {$_.EmailAddress -eq $User}) { [pscustomobject]@{ OldLicense = $OldLicense NewLicense = $Licenses[$_].LicenseSKU } } } if ($ChangeLicense) { #The user were assigned to another group, switch license to the new one. try { Set-MsolUserLicense -UserPrincipalName $User -RemoveLicenses $ChangeLicense.OldLicense -AddLicenses $ChangeLicense.NewLicense -ErrorAction Stop -WarningAction Stop Write-Output "SUCCESS: Changed license for user $User from $($ChangeLicense.OldLicense) to $($ChangeLicense.NewLicense)" } catch { Write-Warning "Error when changing license on $User`r`n$_" } } else { #The user is no longer a member of any license group, remove license Write-Warning "$User is not a member of any group, license will be removed... " try { Set-MsolUserLicense -UserPrincipalName $User -RemoveLicenses $OldLicense -ErrorAction Stop -WarningAction Stop Write-Output "SUCCESS: Removed $OldLicense for $User" } catch { Write-Warning "Error when removing license on user`r`n$_" } } } } }
5. Verify that the runbook is working as intended by starting it in the test pane.
Make sure to solve eventual errors before you publish and schedule the runbook. The most common error is login errors for the automation service account.
6. Publish and schedule the runbook. In my case I will run it one time every hour.
You are now all set and can hopefully disable a scheduled task in your on premises environment. If thinking about running this in production, keep in mind that the PowerShell module is still in preview (I haven’t experienced any problems, but…).
Let me know if you have any questions or suggestions, and happy automation!
/Johan
The script that is used for Add/Change/Remove O365 licenses has some issues. The main one is critical.
I cannot see that you handle users that changes Licensetype. They will end up in $UserstoAdd. This is because you are only verifying against users with the license type in Focus. E.g. when you check your E3 licens you do as follows:
1. Get O365 group-members into array $Groupmembers
2. Get users with the E3 licenses assigned – from $LicensedUserDetails
3. Then you compare these two arrays
4. This results in a list of $UsersToAdd (even if they have another license already, because you are only comparing with already licensed E3 users – not E1 etc). $userstoChangeOrDelete will only contain users, where E3 license is removed – not the changed.
I’ve solved it by adding an extra comparison, where you compare $Userstoadd with $LicensedUserDetails. If they already are licensed, then they are added to $userstochangeordelete
Hi Laurits,
Thanks for the feedback. The change part works as far as I know as long as you are not in both groups. If you check the original version https://365lab.net/2014/04/22/office-365-assign-licenses-based-on-groups-using-powershell-advanced-version/, it should be pretty much the same.
If you have done changes that can help others, please post them on github or somewhere so I can update my version(s) if the issues are relevant.
/Johan
Hi Johan,
I noticed you are using the older MSOnline PowerShell module in your examples. It may be useful to start using the newer Azure Active Directory PowerShell V2 module instead, as we will begin deprecating the MSOnline module when we have migrated the functionality of the MSOnline module to the newer module – currently planned for the Spring of 2017.
Thanks,
Rob de Jong
Hi Rob,
Thanks for the comment. My plan is to release new versions with the 2.0 module beginning of january 🙂
/Johan
Johan — very good walk-through! You mention near the end that this would replace an on-prem scheduled task. Did you succeed in having an on-prem scheduled task run the the O365 licensing portion of your script? I ask because I have a script which flawlessly prepares the AD users and licenses them in O365, IF I run it from an interactive PS window. However, if I run the same script as a scheduled task or PS scheduled job, only the on-prem portion works. This is true even if I am still logged into the server. Thanks for any thoughts you have on this.