MSOLLicenseManagement.psm1
############################################################################################# # DISCLAIMER: # # # # THE SAMPLE SCRIPTS ARE NOT SUPPORTED UNDER ANY MICROSOFT STANDARD SUPPORT # # PROGRAM OR SERVICE. THE SAMPLE SCRIPTS ARE PROVIDED AS IS WITHOUT WARRANTY # # OF ANY KIND. MICROSOFT FURTHER DISCLAIMS ALL IMPLIED WARRANTIES INCLUDING, WITHOUT # # LIMITATION, ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR OF FITNESS FOR A PARTICULAR # # PURPOSE. THE ENTIRE RISK ARISING OUT OF THE USE OR PERFORMANCE OF THE SAMPLE SCRIPTS # # AND DOCUMENTATION REMAINS WITH YOU. IN NO EVENT SHALL MICROSOFT, ITS AUTHORS, OR # # ANYONE ELSE INVOLVED IN THE CREATION, PRODUCTION, OR DELIVERY OF THE SCRIPTS BE LIABLE # # FOR ANY DAMAGES WHATSOEVER (INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF BUSINESS # # PROFITS, BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION, OR OTHER PECUNIARY LOSS) # # ARISING OUT OF THE USE OF OR INABILITY TO USE THE SAMPLE SCRIPTS OR DOCUMENTATION, # # EVEN IF MICROSOFT HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES # ############################################################################################# #========================== # Utility Functions #========================== # Writes output to a log file with a time date stamp Function Write-Log { Param ([string]$string) # Get the current date [string]$date = Get-Date -Format G # Write everything to our log file ( "[" + $date + "] - " + $string) | Out-File -FilePath $LogFile -Append # If NonInteractive true then suppress host output if (!($NonInteractive)){ ( "[" + $date + "] - " + $string) | Write-Host } } # Make sure that we are connected to the MSOL Server if not connect us #TODO: Need to move from $error.clear() to try-catch Function Test-MSOLServiceConnection { # Set the Error Action Prefernce to stop $ErrorActionPreference = "Stop" # Make sure that the MSOL Module is installed if ($null -eq (Get-Module -ListAvailable MSOnline)) { Write-Error "MSOL Module Not installed. Please install from https://docs.microsoft.com/en-us/office365/enterprise/powershell/connect-to-office-365-powershell" -ErrorAction Stop } # Make sure we are connected to the MSOL service $error.clear() # Call Get-msolaccountsku if we are not connected we will get an error if we are we will not $null = Get-MSOLAccountSKU -ErrorVariable error -ErrorAction SilentlyContinue # Check to see if we threw an error if ($error.count -gt 0) { # If we have the expected error for not being connected the call connect-msolservice if ($error.Exception -like "*You must call the Connect-MsolService cmdlet*") { Write-Log "Not Connected to MSOLService calling Connect-MSOLService" import-module MSOnline Connect-MsolService } # If we get any other error then throw and error and stop else { Write-log ("Unexpected Error encountered" + $error) Write-Error "Unexpected Error stopping execution" -ErrorAction Stop } } else { # Do nothing because we are connected } } # Write out the current user collection for the purpose of being able to resume Function Write-UserSet {} # Updates a progress bar Function Update-Progress { Param ( [Parameter(Mandatory=$true)] [int]$CurrentCount, [Parameter(Mandatory=$true)] [int]$MaxCount, [Parameter(Mandatory=$true)] [string]$Message ) # If currentcount = maxcount we are done so we need to kill the progress bar if ($CurrentCount -ge $MaxCount) { Write-Progress -Completed -Activity "User Modification" } # Every 25 update the progress bar elseif ($CurrentCount%25 -eq 0) { [int]$Percent = (($CurrentCount/$MaxCount)*100) [string]$Operation = ([string]$Percent + "% " + $Message) Write-Progress -Activity "User Modification" -CurrentOperation $Operation -PercentComplete $Percent } else { # Nothing to do if not divisible by 25 } } # Test the logfile path to ensure the dir exists and that a file was provided Function Test-LogPath { param ( # Log File e.g. c:\temp\out.log [Parameter(Mandatory)] [string] $LogFile ) #Get the parent of the provided path $parent = split-path $LogFile -Parent # If the parent is null then the user provided the root of a drive for the log file if ([string]::IsNullOrEmpty($parent)) { Write-Error -Message "Log File path appears to be a drive root. Please provide a folder and file name with -logfile e.g. C:\Temp\out.log" -ErrorAction Stop break } # Verify that the parent folder exists since we won't create it if it doesn't if (!(test-path $parent)) { Write-Error -Message ("Path not found " + $parent + " ;please provide a valid path.") -ErrorAction Stop break } # Check to make sure the path contains a file with an extension if ((($LogFile).split('\'))[-1] -notmatch '\.') { Write-Error -Message ("Log file path does not appear to contain a file extension. Please provide a path and file name e.g. c:\temp\out.log") break } # Check to see if the value passed in a container (folder) if so we need to abort if (test-path $LogFile -PathType Container) { Write-Error -Message "Log File Path appears to be a folder. Please provide a file name with -logfile e.g. C:\Temp\out.log" -ErrorAction Stop break } } # Validate we have provided a valid SKU or collect one if we have provided none Function Select-SKU { param ( [string]$SKUToCheck, [string]$Message ) # If we don't have a value for the SKU then we need to ask for one. if ([string]::IsNullOrEmpty($SKUToCheck)) { Write-Log ("No SKU value provided. Please select from the availible account SKUs") # Setup a counter $i = 1 # Write out the provided message Write-Host "`n$Message`n" # Get all of the SKUs and display them with a "selection number" $TenantSKU = Get-MSOLAccountSKU ForEach ($sku in $TenantSKU) { Write-Host ([string]$i + " - " + $sku.SKUPartNumber) $i++ } # Collect the selected item from the user [int]$Selected = 0 While (($Selected -ge $i) -or ($Selected -le 0) -or ($null -eq $Selected)) { [int]$Selected = Read-Host ("`nSelect the Number for the SKU to use.") } Return ($TenantSKU[$Selected-1].AccountSkuID) } # We need to verify that the submitted SKU is valid else { Write-Log ("Verifying SKU " + $SKUToCheck) # If we can't find it then throw and error and abort if ($null -eq (Get-MSOLAccountSKU | Where-Object {$_.AccountSKUID -eq $SKUToCheck})) { Write-Log ("[ERROR] - Did not find SKU with ID: " + $SKUToCheck) Write-Error ("Unable to locate SKU " + $SKUToCheck) -ErrorAction Stop } # If we found it log it else { Write-Log ("Found SKU") Return $SKUToCheck } } } # Takes in a list of plans and creates a new list based currently disabled plans + list Function Update-DisabledPlan { param ( [string]$SKU, [string[]]$PlansToDisable, [string]$UserUPN ) Write-Log "Determining Plans to Disable" # Null out our array [array]$CurrentDisabledPlans = $null # Get the MSOLUser object from the provided UPN to get the current plan options $user = Get-MsolUser -UserPrincipalName $UserUPN # Get the License object $license = $user.licenses | Where-Object {$_.accountskuid -eq $SKU} # Make sure we found the license on the user if ($null -eq $license) { Write-Log ("[ERROR] - Cannot find SKU " + $SKU + " assigned to " + $UserUPN) Write-Error -Message ("Cannot find SKU " + $SKU + " assigned to " + $UserUPN) -ErrorAction Stop } # Get currently disabled plans [array]$CurrentDisabledPlans = ($license.servicestatus | Where-Object {$_.provisioningstatus -eq "disabled"}) # Make sure we got back some disabled plans if ($CurrentDisabledPlans.Count -le 0) { Write-Log "No Currently Disabled Plans." # Pull our plans to disable into Output and return it [string[]]$Output = $PlansToDisable.ToUpper() Return $Output } # There are currently disabled plans we need to combine the current with the new else { # Show the currently Disabled Plans and then determine the new list foreach ($plan in $CurrentDisabledPlans) { [string[]]$CurrentDisabledPlansNames = $CurrentDisabledPlansNames + ($plan.serviceplan.servicename).toupper() } Write-Log ("Currently Disabled plans:" + $CurrentDisabledPlansNames) # Combine the two lists of disabled plans [string[]]$Output = $CurrentDisabledPlansNames + $PlansToDisable # Make all of them uppercase for comparison $Output = $Output.toupper() # Throw out all of the duplicates $Output = ($Output | Select-Object -Unique) Return $Output } Write-Error "Problem with Update-DisabledPlan should never end up here." -ErrorAction Stop } # Takes in a list of plans and creates a new list based on currently enabled plans + list Function Update-EnabledPlan { param ( [string]$SKU, [string[]]$PlansToEnable, [string]$UserUPN ) Write-Log "Determining Plans to Enable" # Null out our values [string[]]$CurrentDisabledPlansNames = $null [array]$CurrentDisabledPlans = $null # Get the MSOLUser object from the provided UPN to get the current plan options $user = Get-MsolUser -UserPrincipalName $UserUPN # Get the License object $license = $user.licenses | Where-Object {$_.accountskuid -eq $SKU} # Make sure we found the license on the user if ($null -eq $license) { Write-Log ("[ERROR] - Cannot find SKU " + $SKU + " assigned to " + $UserUPN) Write-Error -Message ("Cannot find SKU " + $SKU + " assigned to " + $UserUPN) -ErrorAction Stop } # Get currently disabled plans [array]$CurrentDisabledPlans = ($license.servicestatus | Where-Object {$_.provisioningstatus -eq "disabled"}) # If there are no currently disabled plans then return null since there are no plans to update if ($null -eq $CurrentDisabledPlans) { Write-Log "No Currently Disabled Plans; No Plan updates needed." $Output = "0x0" Return $Output } # Otherwise we need to show what is currently disabled and then calculate the new list to disable else { # Pull out the plan names foreach ($plan in $CurrentDisabledPlans) { [string[]]$CurrentDisabledPlansNames = $CurrentDisabledPlansNames + ($plan.serviceplan.servicename).toupper() } Write-Log ("Currently Disabled plans:" + $CurrentDisabledPlansNames) # Go thru each plan that needs to be enabled and remove it from the list of currently disabled plans foreach ($Plan in $PlansToEnable) { # Remove the plans we want to enable from the list of disabled plans $CurrentDisabledPlansNames = ($CurrentDisabledPlansNames | Where-Object {$_ -ne $Plan}) } # Make sure we havne't pulled out all disabled plans and if we have just return a null if ($null -eq $CurrentDisabledPlansNames) { $Output = $null Return $Output } # As long as we have at least one return that one else { Return $CurrentDisabledPlansNames } } Write-Error "Something went horribly Wrong with Update-EnabledPlan" -ErrorAction Stop } # Takes in a list of plans and creates a new list based on currently enabled plans + list Function Get-PlansToMaintain { param ( [string]$SKU, [string]$SKUToReplace, [string]$UserUPN ) Write-Log "Determining Plans to Maintain" # Null out our values [string[]]$CurrentDisabledPlansNames = $null [array]$CurrentDisabledPlans = $null # Get the Plan names for our new SKU [string[]]$NewPlans = ((Get-MsolAccountSku | Where-Object {$_.accountskuid -eq $SKU}).servicestatus.serviceplan.servicename).toupper() # Get the MSOLUser object from the provided UPN to get the current plan options $user = Get-MsolUser -UserPrincipalName $UserUPN # Get the License object $license = $user.licenses | Where-Object {$_.accountskuid -eq $SKUToReplace} # Make sure we found the license on the user if ($null -eq $license) { Write-Log ("[ERROR] - Cannot find SKU " + $SKUToReplace + " assigned to " + $UserUPN) Write-Error -Message ("Cannot find SKU " + $SKUToReplace + " assigned to " + $UserUPN) -ErrorAction Stop } # Get currently disabled plans [array]$CurrentDisabledPlans = ($license.servicestatus | Where-Object {$_.provisioningstatus -eq "disabled"}) # If there are no currently disabled plans then return null since there are no plans to update if ($null -eq $CurrentDisabledPlans) { Write-Log "No Currently Disabled Plans; No Plan updates needed." $Output = "0x0" Return $Output } # Otherwise we need to show what is currently disabled and then calculate the new list to disable else { # Pull out the plan names foreach ($plan in $CurrentDisabledPlans) { [string[]]$CurrentDisabledPlansNames = $CurrentDisabledPlansNames + ($plan.serviceplan.servicename).toupper() } Write-Log ("Plans disabled in current SKU: " + $CurrentDisabledPlansNames) # Go thru each plan that needs to be disabled and if it is there remove it from the list of plans in the new SKU foreach ($Plan in $CurrentDisabledPlansNames) { # Builds a list of plans to enable from the new SKU $NewPlans = ($NewPlans | Where-Object {$_ -ne $Plan}) } # If we didn't get back any plans to enable then something went wrong and we need to error out if ($null -eq $NewPlans) { Write-Error "All Plans will be disabled." -ErrorAction Stop } # Take back our list of plans that need to be enabled and get back a list of plans to disable else { [string[]]$Output = Set-EnabledPlan -SKU $SKU -PlansToEnable $NewPlans Return $Output } } Write-Error "Something went horribly Wrong with Update-EnabledPlan" -ErrorAction Stop } # Tests a list of plans agiast availible plans in the SKU # Return the first plan that fails or null Function Test-Plan { param ( [string[]]$Plan, [string]$Sku ) Write-Log "Validating Provided Plans" $PlansInSKU = (Get-MsolAccountSku | Where-Object {$_.accountskuid -eq $sku}).servicestatus.serviceplan.servicename # Take each of the provided plans and check it against the plans we found in the SKU foreach ($PlanToCheck in $Plan) { if ($PlansInSKU -contains $PlanToCheck){} # If we don't find it then throw an error and stop else { Write-Log ("[ERROR] - Failed to find plan: " + $PlanToCheck) Write-Error ("Invalid plan " + $PlanToCheck + " provided in plan list. Please correct input and try again.") -ErrorAction Stop } } # If we validate them all then log it Write-Log "All Plans Valid" } # Determine if we have an array with UPNs or just a single UPN / UPN array unlabeled Function Test-UserObject { param ([array]$ToTest) # See if we can get the UserPrincipalName property off of the input object # If we can't then we need to see if this is a UPN and convert it into an object for acceptable input if ($null -eq $ToTest[0].UserPrincipalName) { # Very basic check to see if this is a UPN if ($ToTest[0] -match '@') { [array]$Output = $ToTest | Select-Object -Property @{Name="UserPrincipalName";Expression={$_}} Return $Output } else { Write-Log "[ERROR] - Unable to determine if input is a UserPrincipalName" Write-Log "Please provide a UPN or array of objects with propertly UserPrincipalName populated" Write-Error "Unable to determine if input is a User Principal Name" -ErrorAction Stop } } # If we can pull the value of UserPrincipalName then just return the same object back else { Return $ToTest } } # sets list of disabled plans to provided list # Nothing to do here this one is provided for consistency and future use if needed Function Set-DisabledPlan {} # sets list of enabled plans to provided list Function Set-EnabledPlan { param ( [string]$SKU, [string[]]$PlansToEnable ) Write-Log "Determining plans to Disable" [string[]]$Output = (Get-MsolAccountSku | Where-Object {$_.accountskuid -eq $SKU}).servicestatus.serviceplan.servicename Foreach ($PlanToRemove in $PlansToEnable) { # Take out each of the plans that were provided to us $Output = $Output | Where-Object {$_ -ne $PlanToRemove} } Return $Output } # Creates the LicenseOptions variable for setting diabled plans Function Set-LicenseOption { param ( [string[]]$DisabledPlansArray, [Parameter(Mandatory=$true)] [string]$SKU ) # if there are no plans to disable we create the default option set if ($null -eq $DisabledPlansArray){ Write-log "Setting all SKU Plans to Enabled" $licenseOptions = New-MsolLicenseOptions -AccountSKUId $SKU } # Otherwise add the disabled plans to the option set else { Write-Log "Setting Disabled Plans License Options" Write-Log ("Disabled Plans: " + $DisabledPlansArray) $licenseOptions = New-MsolLicenseOptions -AccountSKUId $SKU -DisabledPlans $DisabledPlansArray } Return $licenseOptions } # Convert SkuID into Common Name Function Get-SKUCommonName { param ( [Parameter(Mandatory=$true)] [string]$Skuid, [Parameter(Mandatory=$true)] [array]$SkuArray ) # Find the Common name from the SkuID [string]$CN = ($Skuarray | where-object {$_.skuid -eq $Skuid}).CommonName # If we can't find the SkuID then we set it to "Not found" if ([string]::IsNullOrEmpty($CN)){[string]$CN = "Not Found"} Return $CN } #========================== # Adds a SKU to a specified collection of users Function Add-MSOLUserLicense { Param ( [Parameter(Mandatory)] [array]$Users, [Parameter(Mandatory)] [string]$LogFile, [string]$SKU, [string]$Location, [string[]]$PlansToDisable, [string[]]$PlansToEnable ) # Make sure we have a valid log file path Test-LogPath -LogFile $LogFile # Make sure we have the connection to MSOL Test-MSOLServiceConnection # Make user our Users object is valid [array]$Users = Test-UserObject -ToTest $Users # If no value of SKU passed in then call Select-Sku to allow one to be picked if ([string]::IsNullOrEmpty($SKU)) { $SKU = Select-SKU -Message "Select SKU to Add to the Users:" } # If a value has been passed in verify it else { $SKU = Select-SKU -SKUToCheck $SKU } # Make sure we didn't get both enable and disable if (!([string]::IsNullOrEmpty($PlansToDisable)) -and !([string]::IsNullOrEmpty($PlansToEnable))) { Write-Log "[ERROR] - Cannot use both -PlansToDisable and -PlansToEnable at the same time" Write-Error "Cannot use both -PlansToDisable and -PlansToEnable at the same time" -ErrorAction Stop } # Testing the plan inputs to make sure they are valid if (!([string]::IsNullOrEmpty($PlansToDisable))) { Test-Plan -Sku $SKU -Plan $PlansToDisable # Get the license options $LicenseOption = Set-LicenseOption -DisabledPlansArray $PlansToDisable -SKU $SKU } # If plans to enable has a value then we test them elseif (!([string]::IsNullOrEmpty($PlansToEnable))) { Test-Plan -Sku $SKU -Plan $PlansToEnable # Get the disabled plans and License options [string[]]$CalculatedPlansToDisable = Set-EnabledPlan -SKU $SKU -Plan $PlansToEnable $LicenseOption = Set-LicenseOption -DisabledPlansArray $CalculatedPlansToDisable -SKU $SKU } # If neither has been provided then just set default options else { $LicenseOption = Set-LicenseOption -SKU $SKU } # "Zero" out the user counter $i = 1 # Add License to the users passed in Foreach ($Account in $Users) { Write-Log ("==== Processing User " + $account.UserPrincipalName + " ====") # Set our skip variable to false so we process the user # Will change it to true if we need to skip the actual change $Skip = $false # Check to see if the sku we are trying to assign is already on the user is so break out of the foreach if (!($null -eq ((Get-MsolUser -UserPrincipalName $Account.UserPrincipalName).licenses | Where-Object {$_.accountskuid -eq $SKU}))) { Write-Log ("[WARNING] - " + $SKU + " is already assigned to the user.") Write-Warning "User already has $SKU assigned. Please use Set-MSOLUserLicensePlans or Update-MSOLUserLicensePlans to modify existing Plans." $Skip = $true } else {} if ($Skip) { Write-Log "Skipping user " + $Account.UserPrincipalName } else { # If location has a value then we need to set it if (!([string]::IsNullOrEmpty($Location))) { $command = ("Set-MsolUser -UserPrincipalName `"" + $Account.UserPrincipalName + "`" -UsageLocation " + $Location) Write-Log ("Running: " + $Command) Invoke-Expression $Command } # Build and run our license set command [string]$Command = ("Set-MsolUserLicense -UserPrincipalName `"" + $Account.UserPrincipalName + "`" -AddLicenses " + $SKU + " -LicenseOptions `$LicenseOption -ErrorAction Stop -ErrorVariable CatchError") Write-Log ("Running: " + $Command) # Try our command try { Invoke-Expression $Command } # If we have any error write out and stop # Doing this so I can customize the error later ## TODO: Update error with resume information! catch { Write-Log ("[ERROR] - " + $CatchError.ErrorRecord) Write-Error ("Failed to successfully add license to user " + $account.UserPrincipalName) -ErrorAction Stop } } # Update the progress bar and increment our counter Update-Progress -CurrentCount $i -MaxCount $Users.Count -Message "Coverting from Inherited to Explicit" # Update our user counter $i++ } <# .SYNOPSIS Adds licenses to users. .DESCRIPTION Adds a license SKU to a user or collection of users. * Can specify plans to enable or disable * Logs all activity to a log file * Generates and error and stops if there are any problems setting the license * Can set location if desired .PARAMETER Users Single UserPrincipalName, Comma seperated List, or Array of objects with UserPrincipalName property. .PARAMETER SKU SKU that should be added. .PARAMETER Location If provided will set the location of the user. .PARAMETER PlansToDisable Comma seperated list of SKU plans to Disable. .PARAMETER PlansToEnable Comma seperated list of SKU plans to Enable. .PARAMETER LogFile File to log all actions taken by the function. .OUTPUTS Log file showing all actions taken by the function. .EXAMPLE Add-MSOLUserLicense -Users $NewUsers -Logfile C:\temp\add_license.log -SKU company:ENTERPRISEPACK Adds the ENTERPRISEPACK SKU to all users in $NewUsers with all plans turned on. .EXAMPLE Add-MSOLUserLicense -Users $NewUsers -Logfile C:\temp\add_license.log -SKU company:ENTERPRISEPACK -PlanstoEnable Deskless,Sway,Teams1 Adds the ENTERPRISEPACK SKU to all users in $NewUsers with ONLY the Deskless, Sway, and Teams1 Plans turned on. #> } # Find all MSOLusers that have a specific SKU assigned ## Not sure if I am going to make this one or not? Still trying to figure out if this would be useful or if there is a fast way to do this ## Sweeping all users to try and get this would be annoying in large tenants ... wonder if there is something I can do with the REST API Function Find-MSOLUserBySku {} # Removes a SKU from a specified collection of users Function Remove-MSOLUserLicense { Param ( [Parameter(Mandatory)] [array]$Users, [Parameter(Mandatory)] [string]$LogFile, [string]$SKU ) # Make sure we have a valid log file path Test-LogPath -LogFile $LogFile # Make sure we have the connection to MSOL Test-MSOLServiceConnection # Make user our Users object is valid [array]$Users = Test-UserObject -ToTest $Users # If no value of SKU passed in then call Select-Sku to allow one to be picked if ([string]::IsNullOrEmpty($SKU)) { $SKU = Select-SKU -Message "Select SKU to remove from the Users:" } # If a value has been passed in verify it else { $SKU = Select-SKU -SKUToCheck $SKU } # "Zero" out the user counter $i = 1 # Add License to the users passed in Foreach ($Account in $Users) { Write-Log ("==== Processing User " + $account.UserPrincipalName + " ====") # Build and run our license set command [string]$Command = ("Set-MsolUserLicense -UserPrincipalName `"" + $Account.UserPrincipalName + "`" -RemoveLicense " + $SKU + " -ErrorAction Stop -ErrorVariable CatchError") Write-Log ("Running: " + $Command) # Try our command try { Invoke-Expression $Command } # If we have any error write out and stop # Doing this so I can customize the error later ## TODO: Update error with resume information! catch { Write-Log ("[ERROR] - " + $CatchError.ErrorRecord) Write-Error ("Failed to successfully remove license from user " + $account.UserPrincipalName) -ErrorAction Stop } # Update the progress bar and increment our counter Update-Progress -CurrentCount $i -MaxCount $Users.Count -Message "Removing SKU" # Update our user counter $i++ } <# .SYNOPSIS Removes a license SKU from users. .DESCRIPTION Removes a licese SKU from a user or collection of users. * Prompts for SKU if none provided * Logs all activity to a log file * Generates and error and stops if there are any problems removing the license .PARAMETER Users Single UserPrincipalName, Comma seperated List, or Array of objects with UserPrincipalName property. .PARAMETER SKU SKU that should be removed. .PARAMETER LogFile File to log all actions taken by the function. .OUTPUTS Log file showing all actions taken by the function. .EXAMPLE Remove-MSOLUserLicense -Users $LeavingEmployees -Logfile C:\temp\add_license.log -SKU company:ENTERPRISEPACK Removes the SKU ENTERPRISEPACK from all users in the $LeavingEmployees variable #> } # Changes from one SKU to another SKU for a collection of users Function Switch-MSOLUserLicense { Param ( [Parameter(Mandatory)] [array]$Users, [Parameter(Mandatory)] [string]$LogFile, [string]$SKU, [string]$SKUToReplace, [string[]]$PlansToDisable, [string[]]$PlansToEnable, [switch]$AttemptToMaintainPlans ) # Make sure we have a valid log file path Test-LogPath -LogFile $LogFile # Make sure we have the connection to MSOL Test-MSOLServiceConnection # Set Error prefernece to stop $ErrorActionPreference = "Stop" # Make user our Users object is valid [array]$Users = Test-UserObject -ToTest $Users # Make sure we didn't get an invalid switch combination if ($AttemptToMaintainPlans -and (!([string]::IsNullOrEmpty($PlansToDisable)) -or !([string]::IsNullOrEmpty($PlansToEnable)))) { Write-Log "[ERROR] - Cannot use -AttemptToMaintainPlans with -PlansToDisable or -PlansToEnable" Write-Error "Cannot use -AttemptToMaintainPlans with -PlansToDisable or -PlansToEnable. Please review input and try again." -ErrorAction Stop } # If no value of SKU was passed in then call Select-Sku to allow one to be picked if ([string]::IsNullOrEmpty($SKU)) { $SKU = Select-SKU -Message "Select New SKU For Users:" } # If a value has been passed in verify it else { $SKU = Select-SKU -SKUToCheck $SKU } # If no value of SKUToReplace was passed in then call Select-Sku to allow one to be picked if ([string]::IsNullOrEmpty($SKUToReplace)) { $SKUToReplace = Select-SKU -Message "Select SKU to be replaced" } # If a value has been passed in verify it else { $SKUToReplace = Select-SKU -SKUToCheck $SKUToReplace } ## Make sure skutoreplace and sku don't match if ($SKUToReplace.toupper() -eq $SKU.ToUpper()) { Write-Log "[ERROR] - `$SKU and `$SKUToReplace match. Unable to replace license with itself." Write-Log "[ERROR] - Please use Update-MSOLUserLicensePlan or Set-MSOLUserLicensePlan to modify license plans." Write-Error -Message "-SKU and -SKUToReplace can not have the same value." -ErrorAction Stop } # Make sure we didn't get both enable and disable if (!([string]::IsNullOrEmpty($PlansToDisable)) -and !([string]::IsNullOrEmpty($PlansToEnable))) { Write-Log "[ERROR] - Cannot use both -PlansToDisable and -PlansToEnable at the same time" Write-Error "Cannot use both -PlansToDisable and -PlansToEnable at the same time" -ErrorAction Stop } # Testing the plan inputs to make sure they are valid if (!([string]::IsNullOrEmpty($PlansToDisable))) { Test-Plan -Sku $SKU -Plan $PlansToDisable # Get the license options $LicenseOption = Set-LicenseOption -DisabledPlansArray $PlansToDisable -SKU $SKU } # If plans to enable has a value then we test them elseif (!([string]::IsNullOrEmpty($PlansToEnable))) { Test-Plan -Sku $SKU -Plan $PlansToEnable # Get the disabled plans and License options [string[]]$CalculatedPlansToDisable = Set-EnabledPlan -SKU $SKU -Plan $PlansToEnable $LicenseOption = Set-LicenseOption -DisabledPlansArray $CalculatedPlansToDisable -SKU $SKU } elseif($AttemptToMaintainPlans) { Write-Log "Will attempt to maintain existing user plans when moving to new License" $ExistingPlans = (Get-MsolAccountSku | Where-Object {$_.accountskuid -eq $SKUToReplace}).servicestatus.serviceplan.servicename $NewPlans = (Get-MsolAccountSku | Where-Object {$_.accountskuid -eq $SKU}).servicestatus.serviceplan.servicename foreach ($Plan in $NewPlans) { # If the new plan name isn't in the existing list of plans then we will end up enabling it if ($ExistingPlans -contains $Plan){} else { # Add to our list of plans we will be enabling by default [string[]]$PlansEnabledByDefault = $PlansEnabledByDefault + $Plan } } # If we generated at least one plan that didn't match between the two SKUs then inform the user and get consent to continue if ($PlansEnabledByDefault.Count -gt 0) { Write-Log "Cannot match the following plans between the two SKUs so they will be enabled by default:" Write-Log $PlansEnabledByDefault # Prompt the user to upgrade or not $title = "Agree to Enable" $message = "When the SKU switch is complete the plans listed above will be enabled by default. `nIs this OK?" $Yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes","Continues Switching SKUs enabling the listed plans." $No = New-Object System.Management.Automation.Host.ChoiceDescription "&No","Stops the function." $options = [System.Management.Automation.Host.ChoiceDescription[]]($Yes, $No) $result = $host.ui.PromptForChoice($title, $message, $options, 0) # Check to see what the user choose switch ($result) { 0 { Write-Log "Agreed to enabled listed plans by default." } 1 { Write-Log "[ERROR] - Did not accept default plan enablement." Write-Error "User terminated function. Unable to enable default plans." -ErrorAction Stop } } } } # If neither has been provided then just set default options else { $LicenseOption = Set-LicenseOption -SKU $SKU } # "Zero" out the user counter $i = 1 foreach ($Account in $Users) { Write-Log ("==== Processing User " + $account.UserPrincipalName + " ====") # Set our skip variable to false $Skip=$false # Null out our reused variables $CalculatedPlansToDisable = $null # Check to see if the sku we are trying to assign is already on the user is so break out of the foreach if (!($null -eq ((Get-MsolUser -UserPrincipalName $Account.UserPrincipalName).licenses | Where-Object {$_.accountskuid -eq $SKU}))) { Write-Log ("[WARNING] - " + $SKU + " is already assigned to the user.") Write-Warning "User already has $SKU assigned." $Skip = $true } else {} if ($Skip) { Write-Log ("Skipping user " + $Account.UserPrincipalName) } else { # Since attempt to maintain is per user we need to calculate per user license options if ($AttemptToMaintainPlans) { # Get the disabled plans and License options [string[]]$CalculatedPlansToDisable = Get-PlansToMaintain -SKU $SKU -SKUToReplace $SKUToReplace -UserUPN $account.UserPrincipalName # If we found no disabled plans then turn on everything if ($CalculatedPlansToDisable -eq "0x0") { Write-Log ("Turning on all Plans for " + $Sku + " on user " + $Account.UserPrincipalName) $LicenseOption = Set-LicenseOption -SKU $SKU } # Turn on only the plans we found that need to be disable in the new SKU else { $LicenseOption = Set-LicenseOption -DisabledPlansArray $CalculatedPlansToDisable -SKU $SKU } } # If we are not trying to maintain plans we can use the license options that were calculated before the foreach else {} [string]$Command = ("Set-MsolUserLicense -UserPrincipalName `"" + $Account.UserPrincipalName + "`" -AddLicenses " + $SKU + " -RemoveLicenses " + $SKUToReplace + " -LicenseOptions `$LicenseOption -ErrorAction Stop -ErrorVariable CatchError") Write-Log ("Running: " + $Command) # Try our command try { Invoke-Expression $Command } # If we have any error write out and stop # Doing this so I can customize the error later ## TODO: Update error with resume information! catch { Write-Log ("[ERROR] - " + $CatchError.ErrorRecord) Write-Error ("Failed to successfully switch licenses for user " + $account.UserPrincipalName) -ErrorAction Stop } } # Update the progress bar and increment our counter Update-Progress -CurrentCount $i -MaxCount $Users.Count -Message "Switching Licenses" # Update our user counter $i++ } <# .SYNOPSIS Switches a user from one SKU to another SKU .DESCRIPTION Replaces one SKU with another SKU on a collection of users. * Prompts for SKUs if none are provided * Allows for Disabling or Enabling Plans on the new SKU * -AttemptToMaintainPlans will try to keep the current plan setting for the user on the new SKU .PARAMETER Users Single UserPrincipalName, Comma seperated List, or Array of objects with UserPrincipalName property. .PARAMETER SKU SKU that should be added. .PARAMETER SKUToReplace SKU that will be removed. .PARAMETER Location If provided will set the location of the user. .PARAMETER PlansToDisable Comma seperated list of SKU plans to Disable. .PARAMETER PlansToEnable Comma seperated list of SKU plans to Enable. .PARAMETER AttemptToMaintainPlans Tries to keep the same plan states on the new SKU that is being assigned. For plans that are unique to the new SKU it will default to enabling them. .PARAMETER LogFile File to log all actions taken by the function. .OUTPUTS Log file showing all actions taken by the function. .EXAMPLE Switch-MSOLUserLicense -Users $NewUsers -Logfile C:\temp\add_license.log -SKU company:ENTERPRISEPACK -SKUToReplace company:STANDARDPACK Replaces the STANDARDPACK with the ENTERPRISEPACK and enables all plans. .EXAMPLE Add-MSOLUserLicense -Users $NewUsers -Logfile C:\temp\add_license.log -SKU company:ENTERPRISEPACK -SKUTOReplace company:STANDARDPACK -PlanstoDisable Deskless,Sway,Teams1 Replaces the STANDARDPACK with the ENTERPRISEPACK and disables Deskless, Sway, and Teams. #> } # Adds or subtracts plans from the specified SKU Function Update-MSOLUserLicensePlan { Param ( [Parameter(Mandatory)] [array]$Users, [Parameter(Mandatory)] [string]$LogFile, [string]$SKU, [string[]]$PlansToDisable, [string[]]$PlansToEnable ) # Make sure we have a valid log file path Test-LogPath -LogFile $LogFile # Make sure we have the connection to MSOL Test-MSOLServiceConnection # Make user our Users object is valid [array]$Users = Test-UserObject -ToTest $Users # If no value of SKU passed in then call Select-Sku to allow one to be picked if ([string]::IsNullOrEmpty($SKU)) { $SKU = Select-SKU -Message "Select SKU to be updated:" } # If a value has been passed in verify it else { $SKU = Select-SKU -SKUToCheck $SKU } # Make sure we didn't get both enable and disable if (!([string]::IsNullOrEmpty($PlansToDisable)) -and !([string]::IsNullOrEmpty($PlansToEnable))) { Write-Log "[ERROR] - Cannot use both -PlansToDisable and -PlansToEnable at the same time" Write-Error "Cannot use both -PlansToDisable and -PlansToEnable at the same time" -ErrorAction Stop } # Testing the plan inputs to make sure they are valid if (!([string]::IsNullOrEmpty($PlansToDisable))) { Test-Plan -Sku $SKU -Plan $PlansToDisable } # If plans to enable has a value then we test them elseif (!([string]::IsNullOrEmpty($PlansToEnable))) { Test-Plan -Sku $SKU -Plan $PlansToEnable } # If neither has been provided then we don't need to do anything else {} # "Zero" out the user counter $i = 1 # Update the Plans for the Users Passed in Foreach ($Account in $Users) { # Set our skip variable to false so we process the user # Will change it to true if we need to skip the actual change $Skip = $false Write-Log ("==== Processing User " + $account.UserPrincipalName + " ====") # Testing the plan inputs to make sure they are valid if (!([string]::IsNullOrEmpty($PlansToDisable))) { # Get the license options [string[]]$CalculatedPlansToDisable = Update-DisabledPlan -SKU $SKU -Plan $PlansToDisable -UserUPN $Account.UserPrincipalName $LicenseOption = Set-LicenseOption -DisabledPlansArray $CalculatedPlansToDisable -SKU $SKU } # If planstoenable has a value then we need to determine what plans to enable and set them elseif (!([string]::IsNullOrEmpty($PlansToEnable))) { # Get the disabled plans and License options [string[]]$CalculatedPlansToDisable = Update-EnabledPlan -SKU $SKU -Plan $PlansToEnable -UserUPN $account.UserPrincipalName # If the return is null then everything is already on and we don't need to do anything if ($null -eq $CalculatedPlansToDisable) { Write-Log ("Turning on all Plans for " + $Sku + " on user " + $Account.UserPrincipalName) $LicenseOption = Set-LicenseOption -SKU $SKU } # Check to see if we got back the return for all plans enabled elseif ($CalculatedPlansToDisable -contains "0x0") { Write-Log ("No actions needed for " + $Account.UserPrincipalName) $Skip = $true } # If we have a value then build our license options with the new list of plans to disable else { $LicenseOption = Set-LicenseOption -DisabledPlansArray $CalculatedPlansToDisable -SKU $SKU } } # If Neither disable or enable were passed then we turn on all plans else { Write-Log ("Turning on all Plans for " + $Sku + " on user " + $Account.UserPrincipalName) $LicenseOption = Set-LicenseOption -SKU $SKU } # If we determined no actions are needed then skip the user if ($Skip){Write-Log "Skipping user"} # Process the user else { # Build and run our license set command [string]$Command = ("Set-MsolUserLicense -UserPrincipalName `"" + $Account.UserPrincipalName + "`" -LicenseOptions `$LicenseOption -ErrorAction Stop -ErrorVariable CatchError") Write-Log ("Running: " + $Command) # Try our command try { Invoke-Expression $Command } # If we have any error write out and stop # Doing this so I can customize the error later ## TODO: Update error with resume information! catch { Write-Log ("[ERROR] - " + $CatchError.ErrorRecord) Write-Error ("Failed to successfully add license to user " + $account.UserPrincipalName) -ErrorAction Stop } } # Update the progress bar and increment our counter Update-Progress -CurrentCount $i -MaxCount $Users.Count -Message "Updating License Plan" # Update our user counter $i++ } <# .SYNOPSIS Updates the current plan settings for an assigned SKU while maintaining existing plan settings. .DESCRIPTION Updates the current plan settings for an assigned SKU while maintaining existing plan settings. * UPDATES existing plan settings with the new enabled or disabled plans * All current plan setting are left in place. * Can specify plans to enable or disable * Logs all activity to a log file * Generates an error and stops if there are any problems setting the license .PARAMETER Users Single UserPrincipalName, Comma seperated List, or Array of objects with UserPrincipalName property. .PARAMETER SKU SKU that should be modified. .PARAMETER PlansToDisable Comma seperated list of SKU plans to Disable. .PARAMETER PlansToEnable Comma seperated list of SKU plans to Enable. .PARAMETER LogFile File to log all actions taken by the function. .OUTPUTS Log file showing all actions taken by the function. .EXAMPLE Update-MSOLUserLicensePlan -Users $Promoted -Logfile C:\temp\add_license.log -SKU company:ENTERPRISEPACK -PlanstoEnable SWAY,TEAMS1,EXCHANGE_S_ENTERPRISE,YAMMER_ENTERPRISE,OFFICESUBSCRIPTION Adds the enabled Plans SWAY,TEAMS1,EXCHANGE_S_ENTERPRISE,YAMMER_ENTERPRISE,OFFICESUBSCRIPTION to the ENTERPRISEPACK for all users in $Promoted. Maintaining any existing plan settings. #> } # Overwrites plans on the specified SKU Function Set-MSOLUserLicensePlan { Param ( [Parameter(Mandatory)] [array]$Users, [Parameter(Mandatory)] [string]$LogFile, [string]$SKU, [string[]]$PlansToDisable, [string[]]$PlansToEnable ) # Make sure we have a valid log file path Test-LogPath -LogFile $LogFile # Make sure we have the connection to MSOL Test-MSOLServiceConnection # Make user our Users object is valid [array]$Users = Test-UserObject -ToTest $Users # If no value of SKU passed in then call Select-Sku to allow one to be picked if ([string]::IsNullOrEmpty($SKU)) { $SKU = Select-SKU -Message "Select SKU to Set:" } # If a value has been passed in verify it else { $SKU = Select-SKU -SKUToCheck $SKU } # Make sure we didn't get both enable and disable if (!([string]::IsNullOrEmpty($PlansToDisable)) -and !([string]::IsNullOrEmpty($PlansToEnable))) { Write-Log "[ERROR] - Cannot use both -PlansToDisable and -PlansToEnable at the same time" Write-Error "Cannot use both -PlansToDisable and -PlansToEnable at the same time" -ErrorAction Stop } # Testing the plan inputs to make sure they are valid if (!([string]::IsNullOrEmpty($PlansToDisable))) { Test-Plan -Sku $SKU -Plan $PlansToDisable # Get the license options $LicenseOption = Set-LicenseOption -DisabledPlansArray $PlansToDisable -SKU $SKU } # If plans to enable has a value then we test them elseif (!([string]::IsNullOrEmpty($PlansToEnable))) { Test-Plan -Sku $SKU -Plan $PlansToEnable # Get the disabled plans and License options [string[]]$CalculatedPlansToDisable = Set-EnabledPlan -SKU $SKU -Plan $PlansToEnable $LicenseOption = Set-LicenseOption -DisabledPlansArray $CalculatedPlansToDisable -SKU $SKU } # If neither has been provided then just set default options else { $LicenseOption = Set-LicenseOption -SKU $SKU } # "Zero" out the user counter $i = 1 # Add License to the users passed in Foreach ($Account in $Users) { # Build and run our license set command [string]$Command = ("Set-MsolUserLicense -UserPrincipalName `"" + $Account.UserPrincipalName + "`" -LicenseOptions `$LicenseOption -ErrorAction Stop -ErrorVariable CatchError") Write-Log ("Running: " + $Command) # Try our command try { Invoke-Expression $Command } # If we have any error write out and stop # Doing this so I can customize the error later ## TODO: Update error with resume information! catch { Write-Log ("[ERROR] - " + $CatchError.ErrorRecord) Write-Error ("Failed to successfully add license to user " + $account.UserPrincipalName) -ErrorAction Stop } # Update the progress bar and increment our counter Update-Progress -CurrentCount $i -MaxCount $Users.Count -Message "Setting License Plan" # Update our user counter $i++ } <# .SYNOPSIS Set the disabled plans on a user overwriting current settings. .DESCRIPTION Overwrites the current plan settings for an assigned SKU with the new plan settings provided. * OVERWRITES existing plan settings and makes them match the provided values * Can specify plans to enable or disable * Logs all activity to a log file * Generates and error and stops if there are any problems setting the license .PARAMETER Users Single UserPrincipalName, Comma seperated List, or Array of objects with UserPrincipalName property. .PARAMETER SKU SKU that should be added. .PARAMETER PlansToDisable Comma seperated list of SKU plans to Disable. .PARAMETER PlansToEnable Comma seperated list of SKU plans to Enable. .PARAMETER LogFile File to log all actions taken by the function. .OUTPUTS Log file showing all actions taken by the function. .EXAMPLE Set-MSOLUserLicensePlan -Users $Promoted -Logfile C:\temp\add_license.log -SKU company:ENTERPRISEPACK Enable all of the plans in the ENTERPRISEPACK SKU for all users in $Promoted. Overwriting any current settings. .EXAMPLE Set-MSOLUserLicensePlan -Users $Promoted -Logfile C:\temp\add_license.log -SKU company:ENTERPRISEPACK -PlanstoEnable SWAY,TEAMS1,EXCHANGE_S_ENTERPRISE,YAMMER_ENTERPRISE,OFFICESUBSCRIPTION Set the enabled Plans for the ENTERPRISEPACK to be SWAY,TEAMS1,EXCHANGE_S_ENTERPRISE,YAMMER_ENTERPRISE,OFFICESUBSCRIPTION for all users in $Promoted. This will overwrite any current plan settings for the user. #> } # Converts from an inherited SKU to an Explicit SKU with the same plan settings Function Convert-MSOLUserLicenseToExplicit { param ( [Parameter(Mandatory)] [array]$Users, [Parameter(Mandatory)] [string]$LogFile, [string]$SKU ) # Make sure we have a valid log file path Test-LogPath -LogFile $LogFile # Make sure we have the connection to MSOL Test-MSOLServiceConnection # Start Processing Write-Log ("Converting " + $SKU + " from inherited to explicit") # Make user our Users object is valid [array]$Users = Test-UserObject -ToTest $Users # If no value of SKU passed in then call Select-Sku to allow one to be picked if ([string]::IsNullOrEmpty($SKU)) { $SKU = Select-SKU -Message "Select SKU to Convert to Explicit:" } # If a value has been passed in verify it else { $SKU = Select-SKU -SKUToCheck $SKU } # Start Processing Write-Log ("Converting " + $SKU + " from inherited to explicit") # "Zero" out of counter $i = 1 Foreach ($Account in $Users) { Write-Log ("==== Processing User " + $account.UserPrincipalName + " ====") # Set our skip variable to false so we process the user # Will change it to true if we need to skip the actual change $Skip = $false # Null out the variables we use [array]$CurrentDisabledPlansArray = $null $CurrentLicenseDetails = $null # Verify that the SKU is inherited and isn't already explicit ... don't want to overwrite any explicit settings try { $MSOLUser = Get-MsolUser -UserPrincipalName $Account.UserPrincipalName -ErrorAction Stop } catch { Write-Log ("[Error] - Unable to find user:`n " + $_.Exception) Write-Error $_ break } $CurrentLicenseDetails = ($MSOLUser.licenses | Where-Object {$_.accountskuid -eq $SKU}) # Make sure we found the SKU on the user if ($Null -eq $CurrentLicenseDetails) { Write-Log ("[Warning] - SKU " + $SKU + " not Assigned to user " + $Account.UserPrincipalName) Write-Warning ("SKU " + $SKU + " not Assigned to user " + $Account.UserPrincipalName) # Since we can't set this we need to skip it $Skip = $true } # Make sure the SKU is inherited elseif (($CurrentLicenseDetails.GroupsAssigningLicense -contains $Users.objectid) -or ($CurrentLicenseDetails.GroupsAssigningLicense.count -le 0)) { Write-Log ("[Warning] - User " + $Account.UserPrincipalName + " already has explicit assignment for " + $SKU) Write-Warning ("User " + $Account.UserPrincipalName + " already has explicit assignment for " + $SKU) # Since it is already explict we need to skip the actual set $Skip = $true } else { # Get the currently disabled plans [array]$CurrentDisabledPlansArray = (($CurrentLicenseDetails).ServiceStatus | Where-Object {$_.ProvisioningStatus -eq "Disabled"}).serviceplan | Select-Object -ExpandProperty ServiceName Write-Log ("Disabling Plans: " + [string]$CurrentDisabledPlansArray) } # If we set SKIP to true then we are not processing this user log it and move on if ($Skip -eq $true) { Write-Log ("Skipping user " + $Account.UserPrincipalName) } # Else we need to set the license else { # Create our License Options from the disabled plan array [Microsoft.Online.Administration.LicenseOption]$CurrentSKULicenseOptions = Set-LicenseOption -DisabledPlansArray $CurrentDisabledPlansArray -SKU $SKU # Build, log, and run the command $cmd = "Set-MsolUserLicense -UserPrincipalName `"" + $Account.UserPrincipalName + "`" -AddLicenses " + $SKU + " -LicenseOptions `$CurrentSKULicenseOptions" Write-Log ("Running: " + $cmd) Invoke-Expression $cmd } # Update the progress bar and increment our counter Update-Progress -CurrentCount $i -MaxCount $Users.Count -Message "Coverting from Inherited to Explicit" $i++ } <# .SYNOPSIS Converts a specified SKU from Group assiged to Explicitly assigned .DESCRIPTION Will explicitly apply the SKU specified to a user if that user is inheriting it from Group based licensing. https://docs.microsoft.com/en-us/azure/active-directory/active-directory-licensing-whatis-azure-portal * Will skip any users that don't have the specified SKU * Will skip any users that already have the SKU explicitly assigned * Will maintain the current plan state for the SKU on that user .PARAMETER Users Single UserPrincipalName, Comma seperated List, or Array of objects with UserPrincipalName property. .PARAMETER SKU SKU that should be converted to explict from inherited. .PARAMETER LogFile File to log all actions taken by the function. .OUTPUTS Log file showing all actions taken by the function. .EXAMPLE Convert-MSOLUserLicenseToExplicit -Users $UsersToConvert -SKU company:AAD_PREMIUM -logfile C:\temp\conversion.log Converts inherited AAD_PREMIUM licenses into explicit licenses for all users in $UsersToConvert and logs all actions in C:\temp\conversion.log #> } # Generates an MSOL User License Report # TODO: Allow function to take input from the pipeline Function Get-MSOLUserLicenseReport { param ( [array]$Users, [Parameter(Mandatory)] [string]$LogFile, [switch]$OverWrite=$false ) # Make sure we have a valid log file path Test-LogPath -LogFile $LogFile # Make sure we have the connection to MSOL Test-MSOLServiceConnection Write-Log "Generating Sku and Plan Report" # Get all of the availible SKUs $AllSku = Get-MsolAccountSku Write-Log ("Found " + $AllSku.count + " SKUs in the tenant") # Make sure out plan array is null [array]$Plans = $null # Build a list of all of the plans from all of the SKUs foreach ($Sku in $AllSku){ $SKU.servicestatus.serviceplan | ForEach-Object {[array]$Plans = $Plans + $_.servicename} } # Need just the unique plans $Plans = $Plans | Select-Object -Unique | Sort-Object Write-Log ("Found " + $Plans.count + " Unique plans in the tenant") # Make sure the output array is null $Output = $null # Create a false SKU object so we populate the first entry in the array with all needed values so we get a good CSV export # Basically a cheat so we can easily use export-csv we will remove this entry from the actual output $Object = $Null $Object = New-Object -TypeName PSobject $Object | Add-Member -MemberType NoteProperty -Name DisplayName -Value "BASELINE_IGNORE" $Object | Add-Member -MemberType NoteProperty -Name UserPrincipalName -Value "BASELINE_IGNORE@contoso.com" $Object | Add-Member -MemberType NoteProperty -Name UsageLocation -Value "BASELINELOCATION_IGNORE" $Object | Add-Member -MemberType NoteProperty -Name SkuID -Value "BASELINESKU_IGNORE" $Object | Add-Member -MemberType NoteProperty -Name CommonName -Value "BASELINESKUNAME_IGNORE" $Object | Add-Member -MemberType NoteProperty -Name Assignment -Value "BASELINEINHERIT_IGNORE" # Populate a value into all of the plan names foreach ($value in $Plans) { $Object | Add-Member -MemberType NoteProperty -Name $value -Value "---" } # Create the output list array $Output = New-Object System.Collections.ArrayList # Populate in the generic object to the list array $Output.Add($Object) | Out-Null # Make sure our UserToProcess array is created and is null [array]$UserToProcess = $null # See if our user array is null and pull all users if needed if ($null -eq $Users) { Write-Log "Getting all users in the tenant." Write-Log "This can take some time." # Get all of the users in the tenant [array]$UserToProcess = Get-MsolUser -All Write-log ("Found " + $UserToProcess.count + " users in the tenant") } # Gather just the users provided else { # Make user our Users object is valid [array]$Users = Test-UserObject -ToTest $Users Write-Log "Gathering License information for provided users" $i = 0 # Get the data for each user to use in generating the report foreach ($account in $Users) { $i++ [array]$UserToProcess = [array]$UserToProcess + (Get-MsolUser -UserPrincipalName $Account.UserPrincipalName) if (!($i%100)){Write-log ("Gathered Data for " + $i + " Users")} } Write-log ("Found " + $UserToProcess.count + " users to report on") } # Now that we have all of the user objects we need to start processing them and building our output object # Setup a counter for informing the user of progress $i = 0 # Null out the group cache array # Array holds group names and GUIDs so we do not have a lookup a group more than once [array]$GroupCache = $Null # Pull in our SkuID to Common Name map $SkuArray = join-path (Split-path (((get-module MSOLLicenseManagement)[0]).path) -Parent) "SkuMap.json" # Process each user Write-Log "Generating Report" foreach ($UserObject in $UserToProcess) { # Increase the counter $i++ # Output every 100 users for progress if (!($i%100)){Write-log ("Finished " + $i + " Users")} # If no license is assigned we need to create that object if ($UserObject.isLicensed -eq $false){ $Object = $null # Create our object and populate in our values $Object = New-Object -TypeName PSobject $Object | Add-Member -MemberType NoteProperty -Name DisplayName -Value $UserObject.displayname $Object | Add-Member -MemberType NoteProperty -Name UserPrincipalName -Value $UserObject.userprincipalname $Object | Add-Member -MemberType NoteProperty -Name UsageLocation -Value $UserObject.UsageLocation $Object | Add-Member -MemberType NoteProperty -Name SkuID -Value "UNASSIGNED" $Object | Add-Member -MemberType NoteProperty -Name CommonName -Value "UNASSIGNED" $Object | Add-Member -MemberType NoteProperty -Name Assignment -Value "Explicit" # Add the object to the output array #[array]$Output = $Output + $Object $Output.Add($Object) | Out-Null } #If we have a license then add that information else { # We can have more than one license on a user so we need to do each one seperatly foreach ($license in $UserObject.licenses) { $Object = $null # Get the common name of the licnese from our skuid.json $CommonName = Get-SKUCommonName -Skuid ((($license.accountskuid).split(":"))[-1]) -SkuArray $SkuArray # Create our object and populate in our values $Object = New-Object -TypeName PSobject $Object | Add-Member -MemberType NoteProperty -Name DisplayName -Value $UserObject.displayname $Object | Add-Member -MemberType NoteProperty -Name UserPrincipalName -Value $UserObject.userprincipalname $Object | Add-Member -MemberType NoteProperty -Name UsageLocation -Value $UserObject.UsageLocation $Object | Add-Member -MemberType NoteProperty -Name SkuID -Value $license.accountskuid $Object | Add-Member -MemberType NoteProperty -Name CommonName -Value $CommonName # Add each of the plans for the license in along with its status foreach ($value in $license.servicestatus) { $Object | Add-Member -MemberType NoteProperty -Name $value.serviceplan.servicename -Value $value.ProvisioningStatus } # Get the inherited from / status of the license based on GBL # If there are no groups in GroupsAssigningLicense then it is explicitly assigned if ($license.GroupsAssigningLicense.count -le 0) { $Object | Add-Member -MemberType NoteProperty -Name Assignment -Value "Explicit" } # If it is populated then we need to process ALL values else { # Null out our assigned by string [string]$assignedby = $null # Process each group entry and work to resolve them foreach ($entry in $license.GroupsAssigningLicense) { # If the GUID = the ObjectID then it is direct assigned if ($entry.guid -eq $UserObject.objectid){[string]$assignedby = $assignedby + "Explicit;" } # If it doesn't match the objectid of the user then it is a group and we need to resolve it else { # if groupcache isn't populated yet then we know we need to look it up if ($null -eq $GroupCache) { [array]$GroupCache = $GroupCache + (Get-MsolGroup -objectid $entry.guid | Select-Object -Property objectid,displayname) } # If not null check if we have a cache hit else { # If the guid is in the groupcache then no action needed if ($GroupCache.objectid -contains $entry.guid){} # If we have a cache miss then we need to populate it into the cache else { [array]$GroupCache = $GroupCache + (Get-MsolGroup -objectid $entry.guid | Select-Object -Property objectid,displayname)} } # Add the group information from the cache to the output [string]$assignedby = $assignedby + ($GroupCache | Where-Object {$_.objectid -eq $entry.guid}).DisplayName + ";" } } # Add the value of assignment to the object $Object | Add-Member -MemberType NoteProperty -Name Assignment -Value ($assignedby.trimend(";")) } # Add the object created from the user information to the output array #[array]$Output = $Output + $Object $Output.Add($Object) | Out-Null } } } # Now that we have all of our output objects in an array we need to output them to a file # If overwrite has been set then just take the file name as the date and force overwrite the file if ($OverWrite) { # Build our file name $path = Join-path (Split-Path $LogFile -Parent) ("License_Report_" + [string](Get-Date -UFormat %Y%m%d) + ".csv") } # Default behavior will be to create a new incremental file name else { # Build our file name $RootPath = Join-path (Split-Path $LogFile -Parent) ("License_Report_" + [string](Get-Date -UFormat %Y%m%d)) $TryPath = $RootPath + "*" # Find any existing files that start with our planed file name $FilesInPath = Get-ChildItem $TryPath # Found files so we need to increment our file name with _X if ($FilesInPath.count -gt 0) { $Path = $RootPath + "_" + ($FilesInPath.count) + ".csv" } # Didn't find anything so we are good with the base file name else { $Path = $RootPath + ".csv" } } Write-Log ("Exporting report to " + $path) # Export everything to the CSV file $Output | Export-Csv $path -Force -NoTypeInformation # Pull it back in so we can remove our fake object $Temp = Import-Csv $path $Temp | Where-Object {$_.UserPrincipalName -ne "BASELINE_IGNORE@contoso.com"} | Export-Csv $path -Force -NoTypeInformation Write-log "Report generation finished" <# .SYNOPSIS Generates a comprehensive license report. .DESCRIPTION Generates a license report on a all users or a specified group of users. By Default it will generate a new report file each time it is run. Report includes the following: * All licenses assigned to each user provided. * State of all plans inside of each license assignment * Where the user is getting the license from Group name(s) and/or Explict. .PARAMETER Users Single UserPrincipalName, Comma seperated List, or Array of objects with UserPrincipalName property. .PARAMETER OverWrite If specified it will Over Write the current output CSV file instead of generating a new one. .PARAMETER LogFile File to log all actions taken by the function. .OUTPUTS Log file showing all actions taken by the function. CSV file named License_Report_YYYYMMDD_X.csv that contains the report. .EXAMPLE Get-MSOLUserLicenseReport -LogFile C:\temp\report.log Creates a new License_Report_YYYYMMDD_X.csv file with the license report in the c:\temp directory for all users. .EXAMPLE Get-MSOLUserLicenseReport -LogFile C:\temp\report.log -Users $SalesUsers -Overwrite OverWrites the existing c:\temp\License_Report_YYYYMMDD.csv file with a license report for all users in $SalesUsers. #> } |