Teams Network Planner Automated!

When you are uploading building data to Teams CQD you are provided with a neath CSV file to ease the input of data(CQD Buildingdata). When done, you just upload the CSV to the CQD portal. Now what about the Teams Network Planner? It uses the same data you have in the CQD CSV, but with a few additions of information. Since there is no “Upload CSV” button in the Network Planner you have to go through the painstaking process of entering all the sites manually. Something i did for almost 200 sites a few months ago.

Then it hit me one day, i know already that the API calls that are run in the background can be found for other parts of the Teams Admin Portal when running a network trace in your web browser. What can we discover if we do a trace for the Network Planner?

In my case i use the Edge Chromium browser. Hit F12 or Ctrl+Shift+I to open developer mode. Click the ‘Network’ tab and click ‘Clear’. Recording must be activated(Red dot when activated). Go to hte Teams Network Planner.

If you click “GetAll”, you find the first thing we need, which is an access token to do API call against the Teams Admin Portal.

What if we clear the Network tab again, and now try to create a new Network Plan? What do we see here? Click NetworkPlans. Here you see a request URL and that this was a POST request.

If you scroll all the way down, you can see a Request Payload, and this looks like a JSON request body.

If we now do a API request in PowerShell with the URL, token and request payload, we then get a networkplanID back.

Next we try adding a network site through the GUI. Now we can see the request payload for creating a network site.

We basically now have what we need to automate upload of a CSV file to the Network Planner!

I extended the CQD CSV file with the needed data i found in the Request Payload, and after a lot of trial and error i managed to get it to work. I now have a script that automatically uploads the CSV file to the Network Planner. You only need to manually go into the Teams Admin Portal to get a token in developer mode.

Things to know:
– If a site is connected to WAN, have local internet breakout and have remote PSTN breakout at another site, then you cant use this site as internet breakout for other sites.
– “Calling plans” are called “MicrosoftCalling” in the API calls.

Download the CSV template here.

### Teams Network Planner CSV uploader                ###
### Version 1.0                                       ###
### Author: Alexander Holmeset                        ###
### Email: [email protected]               ###
### Twitter: twitter.com/alexholmeset                 ###
### Blog: alexholmeset.blog                           ###


#Location of the CSV file you want to upload.
$sites = Import-Csv c:\temp\locations.csv

#Enter API token you get from the Teams Admin portal here.
$token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Im5PbzNaRHJPRFhFSzFqS1doWHNsSFJfS1hFZyIsImtpZCI6Im5PbzNaRHJPRFhFSzFqS1doWHNsSFJfS1hFZyJ9.eyJhdWQiOiJodHRwczovL2FkbWluLnRlYW1zLm1pY3Jvc29mdC5jb20iLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC8yOTU5NGZiMC01M2YyLTQ5MGMtYTFiZS04YjJjY2E4MjQ4MDUvIiwiaWF0IjoxNjE0NTEwMTY0LCJuYmYiOjE2MTQ1MTAxNjQsImV4cCI6MTYxNDUxNDA2NCwiYWNyIjoiMSIsImFpbyI6IkFVUUF1LzhUQUFBQXFrbk5UWVh2TWkzK0puc0poeFBsZXF3akI0VGwwL0lOL0hNSnhlY1R0ajFRWWJwVDlybXE4U2lDYXEwQmhKcnE2MnNBRndYcXNnRjRlR1NYay9GZGd3PT0iLCJhbXIiOlsicHdkIiwibWZhIl0sImFwcGlkIjoiMmRkZmJlNzEtZWQxMi00MTIzLWI5OWItZDVmYzhhMDYyYTc5IiwiYXBwaWRhY3IiOiIwIiwiZmFtaWx5X25hbWUiOiJBZG1pbiIsImdpdmVuX25hbWUiOiJBZG1pbiIsImlwYWRkciI6IjgxLjE2Ny4yNS4xMzQiLCJuYW1lIjoiQWRtaW4iLCJvaWQiOiIxOGM0YTkxYi00ZmE5LTRmYmQtYWIxYy1iMzk0NjVkODFiYWYiLCJwdWlkIjoiMTAwMzIwMDA1NjQzRTk1NiIsInJoIjoiMC5BVEVBc0U5WktmSlRERW1odm9zc3lvSklCWEctM3kwUzdTTkJ1WnZWX0lvR0tua3hBQU0uIiwic2NwIjoidXNlcl9pbXBlcnNvbmF0aW9uIiwic3ViIjoiTWhsLVltMTJPS3J4VEpHczNIdE5ZZGxzRHY2QS1JMmQ0RnlhUmh6NjhRZyIsInRpZCI6IjI5NTk0ZmIwLTUzZjItNDkwYy1hMWJlLThiMmNjYTgyNDgwNSIsInVuaXF1ZV9uYW1lIjoiYnJ1bXVuZGRhbEBhbGV4aG9sbWVzZXQub25taWNyb3NvZnQuY29tIiwidXBuIjoiYnJ1bXVuZGRhbEBhbGV4aG9sbWVzZXQub25taWNyb3NvZnQuY29tIiwidXRpIjoicThEdHR0LTYzRUdwTTNJOG9YSWhBQSIsInZlciI6IjEuMCJ9.ixo-vPZ7uw8QZFUYDC9g2atLWcYD8m30MboL8ioJYDNEctTlLJK_Sy8m_qfFCpynwhGcsOTbhhG_4CLEj8QHhWbPi4yD00plIJEpJHjaABYdDYH_0cWqV_Nk8abSly7eCo979VeQT2wzhVI_Z4YPIx9Bqn9EtNzt07SJURl56eRVLfMNnoW9LN7xw1lHY9Pm3dZw8-nlX5LuBPEsEbO9o3Rw5BZKHWd62_i7j3vcMIaQYslVmpruE58ZveozsPacnIoF2dCNJVFtHgI3Qkd7hwwYUpsa657Ne0qaNCi4pfbEo8VFnFpdP5ekeCR84jY5dDrPhHGTC3JMJ-Eih7HVIQ'

#Name and description of the networkplan.
$NetworkPlanName = "DemoPlan"
$NetworkPlanDescription = "This is a description."

$url = 'https://admin.teams.microsoft.com/api/v1/NetworkPlans'


$Header = @{"Authorization" = "Bearer $token" }


$body = @"

    {name: "$NetworkPlanName", description: "NetworkPlanDescription"}


"@


$data = (Invoke-WebRequest -Uri $url -Method POST -Headers $Header -ContentType 'application/json' -Body $body).content | ConvertFrom-Json

$SitesNetworkNameLocal = $sites | Where-Object{$_.InternetEgress -like "Local" -and !$_.pstnEgressPoint} | Select-Object NetworkName -Unique
$SitesNetworkNameLocalRemotePSTN =  $sites | Where-Object{$_.InternetEgress -like "Local" -and $_.pstnEgressPoint} | Select-Object NetworkName -Unique
$SitesNetworkNameRemote = $sites | Where-Object{$_.InternetEgress -like "Remote"} | Select-Object NetworkName -Unique 

$InternetEgress = @()
$url2 = 'https://admin.teams.microsoft.com/api/v1/NetworkSites'



foreach($SiteNetworkNameLocal in $SitesNetworkNameLocal.NetworkName){

$SiteLocal = $sites | Where-Object{$_.NetworkName -like "$SiteNetworkNameLocal"} | Select-Object -First 1
$siteLocalCollection = $sites | Where-Object{$_.NetworkName -like "$SiteNetworkNameLocal"} | Select-Object NetworkIP,NetworkRange

#All sites with local internet breakout, but connected to WAN to be internetbreakout for other sites.
If($SiteLocal.WAN -eq 1){

$SiteCollectionObject = @()

foreach($SLCObject in $siteLocalCollection){

$myObject = @"
{subnet: "$($SLCObject.networkIP)", networkRange: "$($SLCObject.networkRange)"}, 
"@
$SiteCollectionObject += $myObject 
}
$SiteCollectionObject = $SiteCollectionObject -join ""




$bodyWANEgressSite = @"
{"id":"",
"networkPlanId":"$($data.resourceId)",
"name":"$($SiteLocal.NetworkName)",
"description":"$($SiteLocal.description)",
"users":"$($SiteLocal.users)",
"isExpressRoute":"$(If($SiteLocal.ExpressRoute -eq 1){"true"}Else{"false"})",
"isConnectedToWan":"$(If($SiteLocal.WAN -eq 1){"true"}Else{"false"})",
"wanLinkCapacity":"$($SiteLocal.WANLinkCapacity)",
"wanAudioQueue":"$(If(!$SiteLocal.WANAudio){"0"}Else{$SiteLocal.WANAudio})",
"wanVideoQueueSize":"$(If(!$SiteLocal.WANVideo){"0"}Else{$SiteLocal.WANVideo})",
"internetEgress":"Local",
"internetEgressPoint":"",
"internetLinkCapacity":"$($SiteLocal.InternetCapacity)",
"pstnEgressPoint":"",
"pstnConnectivityType":"$(if($SiteLocal.PSTNType){$SiteLocal.PSTNType}Else{"Empty"})",
"isLocalPstnSite":"$(If($SiteLocal.PSTNEgress -like "Local"){"true"}Else{"false"})",
"pstnConnectivity":"$(If($SiteLocal.PSTNEgress -like "VoIP"){"None"}Else{"$($SiteLocal.PSTNEgress)"})",
"networkSettings":[$SiteCollectionObject],
"isLocalSite":true,
"location":{"companyName":"$($SiteLocal.OwnershipType)","description":"","countryOrRegion":"$($SiteLocal.Country)","houseNumber":"","streetName":"","city":"$($SiteLocal.City)","postalCode":"$($SiteLocal.ZipCode)","state":"$($SiteLocal.State)","companyTaxId":""},"isAddressValidated":true,"status":true}
"@



$ResourceID = (Invoke-RestMethod -Uri $url2 -Method POST -Headers $Header -ContentType 'application/json' -Body $bodyWANEgressSite).resourceId

$myObject = [PSCustomObject]@{
    ID     = "$ResourceID"
    NetworkName = "$($SiteLocal.NetworkName)"
    PSTNEgressName = "$($SiteLocal.PSTNEgressPoint)"

}
$InternetEgress += $myObject 
}
Else{
#All sites with local internetbreakout but not conenctoed to WAN.
$SiteLocalNoWan = $sites | Where-Object{$_.NetworkName -like "$SiteNetworkNameLocal"} | Select-Object -First 1
$siteLocalCollectionNoWan = $sites | Where-Object{$_.NetworkName -like "$SiteNetworkNameLocal"} | Select-Object NetworkIP,NetworkRange
foreach($SLCObject in $siteLocalCollectionNoWan){
$siteLocalCollectionNoWan = @()
$myObject = @"
{subnet: "$($SLCObject.networkIP)", networkRange: "$($SLCObject.networkRange)"}, 
"@
$siteLocalCollectionNoWan += $myObject 
}
$siteLocalCollectionNoWan = $siteLocalCollectionNoWan -join ""


$bodyNoWan=  @"
{"id":"","networkPlanId":"$($data.resourceId)","name":"$($SiteLocalNoWan.NetworkName)","description":"$($SiteLocalNoWan.description)","users":"$($SiteLocalNoWan.users)","isExpressRoute":$(If($SiteLocalNoWan.ExpressRoute -eq 1){"true"}Else{"false"}),"isConnectedToWan":$(If($SiteLocalNoWan.WAN -eq 1){"true"}Else{"false"}),"wanLinkCapacity":"0","wanAudioQueue":"0","wanVideoQueueSize":"0","internetEgress":"Local","internetEgressPoint":"","internetLinkCapacity":"$($SiteLocalNoWan.InternetCapacity)","pstnEgressPoint":"","pstnConnectivityType":"$(if($SiteLocalNoWan.PSTNType){$SiteLocalNoWan.PSTNType}Else{"Empty"})","pstnConnectivity":"$(If($SiteLocalNoWan.PSTNEgress -like "VoIP"){"None"}Else{"Local"})","networkSettings":[$siteLocalCollectionNoWan],"isLocalSite":false,"isLocalPstnSite":false,"location":{"companyName":"$($SiteLocalNoWan.OwnershipType)","description":"","countryOrRegion":"$($SiteLocalNoWan.Country)","houseNumber":"","streetName":"","city":"$($SiteLocalNoWan.City)","postalCode":"$($SiteLocalNoWan.ZipCode)","state":"$($SiteLocalNoWan.State)","companyTaxId":""},"isAddressValidated":true,"status":true}
"@



$ResourceID = Invoke-RestMethod -Uri $url2 -Method POST -Headers $Header -ContentType 'application/json' -Body $bodyNoWan


}

}


foreach($SiteNetworkNameLocalRemotePSTN in $SitesNetworkNameLocalRemotePSTN.NetworkName){




#All sites with local internet breakout, but connected to WAN because of remote PSTN breakout.    
$SiteLocalRemotePSTN = $sites | Where-Object{$_.NetworkName -like "$SiteNetworkNameLocalRemotePSTN"} | Select-Object -First 1
$siteLocalRemotePSTNCollection = $sites | Where-Object{$_.NetworkName -like "$SiteNetworkNameLocalRemotePSTN"} | Select-Object NetworkIP,NetworkRange
If($SiteLocalRemotePSTN.WAN -eq 1){

$SiteCollectionObject = @()

foreach($SLCObject in $siteLocalRemotePSTNCollection){

$myObject = @"
{subnet: "$($SLCObject.networkIP)", networkRange: "$($SLCObject.networkRange)"}, 
"@
$SiteCollectionObject += $myObject 
}
$SiteCollectionObject = $SiteCollectionObject -join ""




$bodyWANEgressSiteRemotePSTN = @"
{"id":"",
"networkPlanId":"$($data.resourceId)",
"name":"$($SiteLocalRemotePSTN.NetworkName)",
"description":"$($SiteLocalRemotePSTN.description)",
"users":"$($SiteLocalRemotePSTN.users)",
"isExpressRoute":"$(If($SiteLocalRemotePSTN.ExpressRoute -eq 1){"true"}Else{"false"})",
"isConnectedToWan":"$(If($SiteLocalRemotePSTN.WAN -eq 1){"true"}Else{"false"})",
"wanLinkCapacity":"$($SiteLocalRemotePSTN.WANLinkCapacity)",
"wanAudioQueue":"$(If(!$SiteLocalRemotePSTN.WANAudio){"0"}Else{$SiteLocalRemotePSTN.WANAudio})",
"wanVideoQueueSize":"$(If(!$SiteLocalRemotePSTN.WANVideo){"0"}Else{$SiteLocalRemotePSTN.WANVideo})",
"internetEgress":"Local",
"internetEgressPoint":"",
"internetLinkCapacity":"$($SiteLocalRemotePSTN.InternetCapacity)",
"pstnEgressPoint":"$(($InternetEgress | Where-Object{$_.NetworkName -contains "$($SiteLocalRemotePSTN.PSTNEgressPoint)"}).ID)",
"pstnConnectivityType":"$(if($SiteLocalRemotePSTN.PSTNType){$SiteLocalRemotePSTN.PSTNType}Else{"Empty"})",
"isLocalPstnSite":"$(If($SiteLocalRemotePSTN.PSTNEgress -like "Local"){"true"}Else{"false"})",
"pstnConnectivity":"$(If($SiteLocalRemotePSTN.PSTNEgress -like "VoIP"){"None"}Else{"$($SiteLocalRemotePSTN.PSTNEgress)"})",
"networkSettings":[$SiteCollectionObject],"isLocalSite":true,
"location":{"companyName":"$($SiteLocalRemotePSTN.OwnershipType)","description":"","countryOrRegion":"$($SiteLocalRemotePSTN.Country)","houseNumber":"","streetName":"","city":"$($SiteLocalRemotePSTN.City)","postalCode":"$($SiteLocalRemotePSTN.ZipCode)","state":"$($SiteLocal.State)","companyTaxId":""},"isAddressValidated":true,"status":true}
"@


$ResourceID = (Invoke-RestMethod -Uri $url2 -Method POST -Headers $Header -ContentType 'application/json' -Body $bodyWANEgressSiteRemotePSTN).resourceId

$myObject = [PSCustomObject]@{
    ID     = "$ResourceID"
    NetworkName = "$($SiteLocal.NetworkName)"
    PSTNEgressName = "$($SiteLocal.PSTNEgressPoint)"

}
$InternetEgress += $myObject 



}
}




foreach($SiteNetworkNameRemote in $SitesNetworkNameRemote.NetworkName){

#All sites with remote internet breakout and remote PSTN breakout.
$SiteRemoteWan = $sites | Where-Object{$_.NetworkName -like "$SiteNetworkNameRemote"} | Select-Object -First 1
$siteRemoteCollectionWan = $sites | Where-Object{$_.NetworkName -like "$SiteNetworkNameRemote"} | Select-Object NetworkIP,NetworkRange
$pstnEgressPoint = ($InternetEgress | Where-Object{$_.NetworkName -like $SiteRemoteWan.PSTNEgressPoint}).ID
$InternetEgressPoint = ($InternetEgress | Where-Object{$_.NetworkName -like $SiteRemoteWan.InternetEgressPoint}).ID
$RemoteCollectionWan = @()
foreach($SWCObject in $siteRemoteCollectionWan){

$myObject = @"
{subnet: "$($SWCObject.networkIP)", networkRange: "$($SWCObject.networkRange)"}, 
"@
$RemoteCollectionWan += $myObject 
}
$RemoteCollectionWan = $RemoteCollectionWan -join ""


$bodyWanSite = @"
{"id":"","networkPlanId":"$($data.resourceId)",
"name":"$($SiteRemoteWan.NetworkName)",
"description":"$($SiteRemoteWan.description)",
"users":"$($SiteRemoteWan.users)",
"isExpressRoute":$(If($SiteRemoteWan.ExpressRoute -eq 1){"true"}Else{"false"}),
"isConnectedToWan":$(If($SiteRemoteWan.WAN -eq 1){"true"}Else{"false"}),
"wanLinkCapacity":$(If($SiteRemoteWan.WANLinkCapacity){$SiteRemoteWan.WANLinkCapacity}Else{"0"}),
"wanAudioQueue":$(If($SiteRemoteWan.wanAudioQueue){$SiteRemoteWan.wanAudioQueue}Else{"0"}),
"wanVideoQueueSize":$(If($SiteRemoteWan.wanVideoQueueSize){$SiteRemoteWan.wanVideoQueueSize}Else{"0"}),
"internetEgress":"$($SiteRemoteWan.InternetEgress)",
"internetEgressPoint":"$InternetEgressPoint",
"internetLinkCapacity":$($SiteRemoteWan.InternetCapacity),
"pstnEgressPoint":"$pstnEgressPoint",
"pstnConnectivityType":"$(if($SiteRemoteWan.PSTNType){$SiteRemoteWan.PSTNType}Else{"Empty"})",
"pstnConnectivity":"$(If($SiteRemoteWan.PSTNEgress -contains "VoIP"){"None"}Else{"$($SiteRemoteWan.PSTNEgress)"})",
"networkSettings":[$RemoteCollectionWan],
"isLocalSite":false,
"isLocalPstnSite":$(If($SiteRemoteWan.PSTNEgress -contains "Local"){"true"}Else{"false"}),
"location":{"companyName":"$($SiteRemoteWan.OwnershipType)","description":"","countryOrRegion":"$($SiteRemoteWan.Country)","houseNumber":"","streetName":"","city":"$($SiteRemoteWan.City)","postalCode":"$($SiteRemoteWan.ZipCode)","state":"$($SiteRemoteWan.State)","companyTaxId":""},"isAddressValidated":true,"status":true}
"@


$ResourceID = (Invoke-RestMethod -Uri $url2 -Method POST -Headers $Header -ContentType 'application/json' -Body $bodyWanSite).resourceId



}





One thought on “Teams Network Planner Automated!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s