High-Performance PowerShell Pipeline Input for Office365

I figured we would take a small deviation from my normal posts on Skype and Teams related stuff and talk more about optimizing your PowerShell scripts with Office365 or any other service that can take some time to perform actions.

If you’re working with Office365 on any sort of professional level, it’s highly likely you have written a PowerShell script or two to automate things like creating new Common Area Phones etc.

You might have even functionalized your code and gotten it to accept pipeline input with BEGIN, PROCESS and END blocks.

If you have, GREAT. Here’s a cookie

A Pony eating your cookies, Art by Sugar Morning

The issue is, however, most actions with Office365 must be performed in order and take some time to replicate in the backend.

In the case of my Common Area Phone for Teams, I need to create an Office365 user, wait for that user to appear, then create a Teams Common Area Phone Object, wait for it to appear, voice enable it, wait until it voice provisions, and finally assign the object policies.

Here’s a cut down process block from one of my old scripts.

Process
{
#Create the Common Area Phone User
$Return =(New-UcmOffice365User -UPN $upn -Password $Password -FirstName 'CAP' -LastName $DisplayName -Country "AU" -DisplayName $DisplayName)

#Do Until Loop to wait for user creation

#Grant the user a Common Area Phone Licence
$Return =(Grant-UcmOffice365UserLicence -UPN $upn -Country 'AU' -LicenceType 'MCOCAP')

#Do Until Loop to wait for object creation

#Enable EV and set number
$Return =(Set-CsUser -Identity $Upn -OnPremLineURI $LineURI -EnterpriseVoiceEnabled $true)

#Do Until Loop to wait for Voice enablement 

#Reset Password
Set-MsolUserPassword -UserPrincipalName $upn -NewPassword $password -ForceChangePassword $False

#Grant Policies 
Grant-CsOnlineVoiceRoutingPolicy -Identity $Upn -PolicyName $VoicePolicy
Grant-CsTenantDialplan -Identity $Upn  -PolicyName $Dialplan
Grant-CsTeamsIPPhonePolicy -Identity $Upn -PolicyName 'CAP'
Grant-CsTeamsUpgradePolicy -PolicyName UpgradeToTeams -Identity $Upn
Grant-CsVoiceRoutingPolicy -Identity $upn -PolicyName "InternationalCallsAllowed"
}

Looks good, doesn’t it? It runs through all the steps we need to do, with waiting in between each step to allow for the background replication in Office365 before moving onto the next step.

The issue

Between the creation of each object, there is a 10-30 second delay before its visible in Office365 for the next step.

So that means creating Phone 1, then waiting, enabling Phone 1, then waiting, Setting Phone 1’s policies then waiting.

The creation of a single object, setting all its assigned policies etc could take up to 2 minutes. That’s fine when you’re creating 1 or 2 objects, but what about 100’s, 1000’s or even 10,000’s of objects?

An Idea?

What if we could create all of the user objects, (100’s for example) then wait a small amount of time. Then create all of the Common Area Phone objects, wait a small amount of time. Voice enable all the objects etc etc.

That would reduce our waiting considerably! only one problem, due to the way the PowerShell pipeline and PROCESS{} blocks work. Each object is fed to your script ONE. AT. A. TIME.

This means your PROCESS block has no idea how many more items are in the pipeline, so there is no way to create all the users first

No Bueno.

The Pipeline Problem

The idea of the PROCESS block is that it’s processed for each object on the pipeline, over and over again with no context of any other objects, with the workflow being similar to below.

PowerShell Pipeline Visualized

What ends up happening though with my earlier example is something like this. Which ends up Extremely inefficient and SLOW.

That’s a lot of waiting

Even if we were to create a ForEach loop inside our PROCESS block to handle each of the steps, we only get passed one object, so the result would still be the same.

A Dodgy Solution?

What we need is a way to get all the objects on the pipeline and process them how WE want to, not how PowerShell wants to.
Sure, we could write our own CSV handler that imports the CSV then processes the items within it. Allowing us to create 100s of users at once before moving onto the next step.

To do that, we would need to remove the BEGIN, PROCESS and END blocks and handle objects ourselves.
Something like this might work well…

#Get everything to import.
$Objects = (Import-CSV .\CommonAreaPhones.csv)

#Create all the users in one go
ForEach ($Phone in $Objects)
{
	#Create the Common Area Phone User
	$Return =(New-UcmOffice365User -UPN $phone.upn -Password $phone.Password - FirstName 'CAP' -LastName $phone.DisplayName -Country "AU" -DisplayName $$phone.DisplayName)
}

#Grant all the users a Common Area Phone Licence
ForEach ($Phone in $Objects)
{
	$Return =(Grant-UcmOffice365UserLicence -UPN $phone.upn -Country 'AU' -LicenceType 'MCOCAP')
}

#Enable EV and set number
ForEach ($Phone in $Objects)
{
	$Return =(Set-CsUser -Identity $phone.Upn -OnPremLineURI $phone.LineURI -EnterpriseVoiceEnabled $true)
}

#Reset Password
ForEach ($Phone in $Objects)
{
	Set-MsolUserPassword -UserPrincipalName $phone.upn -NewPassword $phone.password -ForceChangePassword $False
}
#Grant Policies 
ForEach ($Phone in $Objects)
{
	Grant-CsOnlineVoiceRoutingPolicy -Identity $phone.Upn -PolicyName $phone.VoicePolicy
	Grant-CsTenantDialplan -Identity $phone.Upn  -PolicyName $phone.Dialplan
	Grant-CsTeamsIPPhonePolicy -Identity $phone.Upn -PolicyName 'CAP'
	Grant-CsTeamsUpgradePolicy -PolicyName UpgradeToTeams -Identity $phone.Upn
	Grant-CsVoiceRoutingPolicy -Identity $phone.upn -PolicyName "InternationalCallsAllowed"
}

Whilst this solution is pretty close. As it creates all the objects in one go before moving onto the next step. It doesn’t work with pipeline input, breaking any automation and making creating one or two phones a pain.

A Proper Solution

As I alluded to before, we came pretty close with our last solution. It stored all the objects in an array, then enumerated each object for the relevant step. If only there was a way to do that with pipeline support?

There is

Instead of doing the work in the PROCESS block, we can move it to the END block and process objects as we like there.

Now, all we need to do for each PROCESS block is, store the object from the pipeline and move on to the next object.

When we get to the end of the pipeline, all our objects are stored in our array and can be enumerated any way we like.

Here’s our earlier solution updated to allow pipeline input, you can even add some checking in the BEGIN block to import a CSV if the user didn’t pass us anything.

BEGIN
{
	#Create our Array
	$Objects= @()
}

PROCESS
{

	#add the current object on the pipeline to the Objects Array
	$Objects += $_

}

END
{
	#Create all the users in one go
	ForEach ($Phone in $Objects)
	{
		#Create the Common Area Phone User
		$Return =(New-UcmOffice365User -UPN $phone.upn -Password $phone.Password - FirstName 'CAP' -LastName $phone.DisplayName -Country "AU" -DisplayName $$phone.DisplayName)
		
	}

	#Grant all the users a Common Area Phone Licence
	ForEach ($Phone in $Objects)
	{
		$Return =(Grant-UcmOffice365UserLicence -UPN $phone.upn -Country 'AU' -LicenceType 'MCOCAP')
	}

	#Enable EV and set number
	ForEach ($Phone in $Objects)
	{
		$Return =(Set-CsUser -Identity $phone.Upn -OnPremLineURI $phone.LineURI -EnterpriseVoiceEnabled $true)
	}

	#Reset Password
	ForEach ($Phone in $Objects)
	{
		Set-MsolUserPassword -UserPrincipalName $phone.upn -NewPassword $phone.password -ForceChangePassword $False
	}
	#Grant Policies 
	ForEach ($Phone in $Objects)
	{
		Grant-CsOnlineVoiceRoutingPolicy -Identity $phone.Upn -PolicyName $phone.VoicePolicy
		Grant-CsTenantDialplan -Identity $phone.Upn  -PolicyName $phone.Dialplan
		Grant-CsTeamsIPPhonePolicy -Identity $phone.Upn -PolicyName 'CAP'
		Grant-CsTeamsUpgradePolicy -PolicyName UpgradeToTeams -Identity $phone.Upn
		Grant-CsVoiceRoutingPolicy -Identity $phone.upn -PolicyName "InternationalCallsAllowed"
	}
}

And there we have it, a pipeline friendly script that can create, 1, 10, 100 or even 10,000 Common Area Phones without needing to wait more time than needed.

I hope this has been eye opening in that we don’t have to do all our work PROCESS blocks and makes you think a little bit differently about writing your next script.

Until next time.

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.