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\locations2.csv
  
#Enter API token you get from the Teams Admin portal here.
$token = 'dsfasdfadsasd'
  
#Name and description of the networkplan.
$NetworkPlanName = "TEST"
$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){$SiteLocal.PSTNEgress}Else{"None"})",
"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
 $bodyWANEgressSite
$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){"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){$SiteLocalRemotePSTN.PSTNEgress}Else{"None"})",
"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){$SiteRemoteWan.PSTNEgress}Else{"None"})",
"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
 
  
  
}

4 thoughts on “Teams Network Planner Automated!

  1. Hi I am getting

    Invoke-RestMethod : The remote server returned an error: (401) Unauthorized.
    At line:121 char:15
    + … esourceID = Invoke-RestMethod -Uri $url2 -Method POST -Headers $Heade …
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-RestMethod], WebException
    + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand

    Invoke-RestMethod : The remote server returned an error: (401) Unauthorized.
    At line:91 char:16
    + … sourceID = (Invoke-RestMethod -Uri $url2 -Method POST -Headers $Heade …
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-RestMethod], WebException
    + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand

    Like

Leave a reply to Alexander Holmeset Cancel reply