Public/VoiceConfig/New-TeamsVoiceRoutingChain.ps1
# Module: TeamsFunctions # Function: Tenant Voice Configuration # Author: David Eberhardt # Updated: 19-FEB-2022 # Status: Live # Add ArgumentCompleter for Gateway and validate Others # Rework Country to Allow ArgumentCompleter with $global:TfAcSbTwoLetterCountryCode # If Country not selected, Unrestreicted does not work with Callrestriction & Matching pattern function New-TeamsVoiceRoutingChain { <# .SYNOPSIS Creates Voice Routing configuration existing Pstn Gateways .DESCRIPTION Creates OVP, OPU & OVR for all provided Gateways. A 1:1:1:1 connection is created. Use Connect-VoiceRoutingChain to establish link if not successful .PARAMETER Name Optional String. Proto-Name for the Routing Chain: Policy, PstnUsage and Voice Route. If provided will be deconstructed through other available particles. Prefixes, Interfixes and Suffixes are attached afterwards depending on Artifact, Country, Site & Call Restriction .PARAMETER Gateway Required String. OnlinePstnGateway used to connect the route for. Routes are created in order of provided Gateways. One or more FQDNs of OnlinePstnGateways already present and enabled in the Teams Tenant. .PARAMETER Region Optional selection of AMER, EMEA or APAC. Useful to indicate global geo-regional breakout Particle is added to the full name after the prefix and before the Name .PARAMETER Country String. ISO 3166-alpha2 or -alpha3 2/3-digit Country Code - Will be verified Required for Local Breakout and if non-default CallRestrictions are to be applied .PARAMETER Site Optional String (3-10 digits). Indicates a local breakout at a specific Site, adding the String to the Policy Name Required for Local Breakout and if non-default CallRestrictions are to be applied .PARAMETER CallRestriction Optional String. Indicates a Restriction to be applied to the Routes Level of Call Restriction to be applied: Unrestricted (default), International, National, EmergencyOnly, Custom Optional. Requires Country if not Unrestricted. .PARAMETER CustomRestrictionName Required String (3-10 digits) for Custom CallRestrictions only. .PARAMETER MatchingPattern Required for Custom CallRestrictions or to override specific restrictions Regex Pattern to match dialed numbers to. Required for Non-default CallRestrictions .EXAMPLE New-TeamsVoiceRoutingChain -Name TDR -Gateway PstnGatewayFqdn1.domain.com -Region AMER Creates one route per Gateway, one PstnUsage and one Policy: OVP-AMER-TDR No call restriction is applied. The Voice Route will receive a Matching pattern of ".*"" .EXAMPLE New-TeamsVoiceRoutingChain -Name TDR -Gateway PstnGatewayFqdn1.domain.com -Region AMER -Country CA -CallRestriction Unrestricted Creates one route per Gateway, one PstnUsage and one Policy: OVP-TDR-CA (Unrestricted is the default Policy) Call Restrictions (except Unrestricted) can only be used with/for a country as the dial string is dependent on location. No call restriction is applied. The Voice Route will receive a Matching pattern of ".*"" .EXAMPLE New-TeamsVoiceRoutingChain -Name TDR -Gateway PstnGatewayFqdn1.domain.com -Region AMER -Country CA -Site Toronto Creates one route per Gateway, one PstnUsage and one Policy: OVP-TDR-CA-Toronto .EXAMPLE New-TeamsVoiceRoutingChain -Name TDR -Gateway PstnGatewayFqdn1.domain.com,PstnGatewayFqdn2.domain.com -Region AMER -Country CA -CallRestriction National Creates one route per Gateway, one PstnUsage and one Policy: OVP-TDR-CA-National Applies CallRestriction to the Route with a Matching Pattern based on the dial code for the Country, i.E. "^\+1" Please note that this does not take non-normalised calls to Emergency Services or other Services providers into Account. If this is a requirement, please amend the Matching pattern manually or use the next example to provide it .EXAMPLE New-TeamsVoiceRoutingChain -Name TDR -Gateway PstnGatewayFqdn1.domain.com,PstnGatewayFqdn2.domain.com -Region AMER -Country CA -Site Toronto -CallRestriction National -MatchingPattern "^\+1" Creates one route per Gateway, one PstnUsage and one Policy: OVP-AMER-TDR-CA-Toronto-National Applies CallRestriction to the Route with the Matching Pattern provided .INPUTS System.String .OUTPUTS System.Object .NOTES Naming Convention: The provided name is interpreted as a proto-name. The final name is constructed as follows: Each artifact is prefixed indicating their use: "OVP-" for Online Voice Routing Policies, "OPU-" for Online Pstn Usages "OVR-" for Online Voice Routes. OVRs are suffixed with an index if multiple Gateways are provided Full naming flow: <Artifact>"-"<Region>["-"<Name>["-"<Country>]["-"<Site>]["-"<CallRestriction>]["-"<Index>] For Example: Name: "Standard", Region: "EMEA", Country: "France", Site: "Paris", CallRestriction: "National" with two Gateways provided Routing Policy: OVP-EMEA-Standard-FRA-Paris-National Pstn Usage: OPU-EMEA-Standard-FRA-Paris-National Voice Route: OVR-EMEA-Standard-FRA-Paris-National-1, OVR-EMEA-Standard-FRA-Paris-National-2 The first Gateway will be attached to Voice Route 1, the second Gateway to Voice Route 2, etc. .COMPONENT Tenant Voice Routing .FUNCTIONALITY Direct Routing .LINK https://github.com/DEberhardt/TeamsFunctions/tree/main/docs/New-TeamsVoiceRoutingChain.md .LINK https://github.com/DEberhardt/TeamsFunctions/tree/main/docs/about_VoiceConfiguration.md .LINK https://github.com/DEberhardt/TeamsFunctions/tree/main/docs/ #> [CmdletBinding(DefaultParameterSetName = 'Region', SupportsShouldProcess, ConfirmImpact = 'Medium')] [Alias('New-TeamsVRC')] [OutputType([System.Object])] param( [Parameter(HelpMessage = 'Proto Name for the Routing Chain')] #[ValidatePattern('^([A-z-_+])*[^\s]\1*$')] # Did not work with Call Restriction suffix attached? [ValidatePattern('^[a-zA-Z0-9-_]+$')] [String]$Name, [Parameter(Mandatory, HelpMessage = 'FQDN of the Gateway(s) to be targeted')] [ValidatePattern('(?=^.{1,254}$)(^(?:(?!\d+\.)[a-zA-Z0-9_\-]{1,63}\.?)+(?:[a-zA-Z]{2,})$)')] [ArgumentCompleter({ &$global:TfAcSbTeasmsPSTNGateway })] [String[]]$Gateway, [Parameter(Mandatory, ParameterSetName = 'Region', HelpMessage = 'Geo-Region to be added as a particle')] [Parameter(Mandatory, ParameterSetName = 'Unrestricted', HelpMessage = 'Geo-Region to be added as a particle')] [Parameter(Mandatory, ParameterSetName = 'Restricted', HelpMessage = 'Geo-Region to be added as a particle')] [Parameter(Mandatory, ParameterSetName = 'RestrictedCustom', HelpMessage = 'Geo-Region to be added as a particle')] [GeoRegion]$Region, [Parameter(Mandatory, ParameterSetName = 'Restricted', HelpMessage = 'ISO 3166-alpha2 Country Code')] [Parameter(Mandatory, ParameterSetName = 'RestrictedCustom', HelpMessage = 'ISO 3166-alpha2 Country Code')] [ValidatePattern('^([A-Z]){2,3}$')] [string]$Country, [Parameter(HelpMessage = 'Creates Site-Specific policies')] [ValidateLength(3, 10)] [String]$Site, [Parameter(ParameterSetName = 'Unrestricted', HelpMessage = 'Call Restriction Level')] [Parameter(Mandatory, ParameterSetName = 'Restricted', HelpMessage = 'Call Restriction Level')] [Parameter(Mandatory, ParameterSetName = 'RestrictedCustom', HelpMessage = 'Call Restriction Level')] [ValidateSet('Unrestricted', 'International', 'National', 'EmergencyOnly', 'Restricted', 'Custom')] [String]$CallRestriction, [Parameter(Mandatory, ParameterSetName = 'RestrictedCustom', HelpMessage = 'Custom Name for the Call Restriction Level')] [ValidateLength(3, 10)] [ValidatePattern('^([A-z])*[^\s]\1*$')] [String]$CustomRestrictionName, [Parameter(ParameterSetName = 'Restricted', HelpMessage = 'Matching pattern for the Voice Route')] [Parameter(Mandatory, ParameterSetName = 'RestrictedCustom', HelpMessage = 'Matching pattern for the Voice Route')] [ValidateScript( { ('.*' -or ($_.Substring(0, 1) -eq '^')) })] [String]$MatchingPattern ) begin { Show-FunctionStatus -Level Live Write-Verbose -Message "[BEGIN ] $($MyInvocation.MyCommand.Name)" Write-Verbose -Message "Need help? Online: $global:TeamsFunctionsHelpURLBase$($MyInvocation.MyCommand.Name)`.md" # Asserting MicrosoftTeams Connection if ( -not (Assert-MicrosoftTeamsConnection) ) { break } # Setting Preference Variables according to Upstream settings if (-not $PSBoundParameters.ContainsKey('Verbose')) { $VerbosePreference = $PSCmdlet.SessionState.PSVariable.GetValue('VerbosePreference') } if (-not $PSBoundParameters.ContainsKey('Confirm')) { $ConfirmPreference = $PSCmdlet.SessionState.PSVariable.GetValue('ConfirmPreference') } if (-not $PSBoundParameters.ContainsKey('WhatIf')) { $WhatIfPreference = $PSCmdlet.SessionState.PSVariable.GetValue('WhatIfPreference') } if (-not $PSBoundParameters.ContainsKey('Debug')) { $DebugPreference = $PSCmdlet.SessionState.PSVariable.GetValue('DebugPreference') } else { $DebugPreference = 'Continue' } if ( $PSBoundParameters.ContainsKey('InformationAction')) { $InformationPreference = $PSCmdlet.SessionState.PSVariable.GetValue('InformationAction') } else { $InformationPreference = 'Continue' } #Initialising Counters $private:StepsID0, $private:StepsID1 = Get-WriteBetterProgressSteps -Code $($MyInvocation.MyCommand.Definition) -MaxId 1 $private:ActivityID0 = $($MyInvocation.MyCommand.Name) [int] $private:CountID0 = [int] $private:CountID1 = 1 #region Data Validation $StatusID0 = 'Data validation' #region Country $CurrentOperationID0 = 'Processing Country' Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0 if ( $PSBoundParameters.ContainsKey('Country') ) { try { $CountryObject = switch ( $Country.length ) { 2 { Get-ISO3166Country -TwoLetterCode $Country } 3 { Get-ISO3166Country -ThreeLetterCode $Country } default { $null } } if ( $CountryObject ) { $CountryCode = switch ( $Country.length ) { 2 { $CountryObject.TwoLetterCode } 3 { $CountryObject.ThreeLetterCode } default { $null } } } else { throw } if ($PSBoundParameters.ContainsKey('Debug')) { " Function: $($MyInvocation.MyCommand.Name) - CountryObject:", ($CountryObject | Format-Table -AutoSize | Out-String).Trim() | Write-Debug } } catch { Write-Error "Country '$Country' not found as an ISO3166 Country Code. Please provide country code in alpha-2 or alpha-3 notation (find with Get-ISO3166Country)" -ErrorAction Stop } } #endregion #region Region $CurrentOperationID0 = 'Processing Region' Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0 if ( $PSBoundParameters.ContainsKey('Region') ) { if ($Region -ne $CountryObject.Region) { Write-Warning -Message "Determined Region '$($CountryObject.Region)' from Country and provided Region '$Region' do not match, this may be deliberate or an error." } [string]$VRCRegion = $Region Write-Information "INFO: Parameter Region provided. Using: '$VRCRegion'" } else { if ( $PSBoundParameters.ContainsKey('Country') ) { [string]$VRCRegion = $CountryObject.Region Write-Information "INFO: Site Region determined. Using: '$VRCRegion'" } else { Write-Error 'Region not determined. Please provide Region' continue } } #endregion #region Call Restriction $CurrentOperationID0 = 'Processing Call Restrictions' Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0 if (-not $PSBoundParameters.ContainsKey('CallRestriction')) { $CallRestriction = 'Unrestricted' Write-Information "INFO: Parameter CallRestriction not provided. Using: '$CallRestriction'" } else { if ( $CallRestriction -eq 'Restricted' ) { # Backwards compatibility $CallRestriction = 'Custom' $PSBoundParameters.Add('CustomRestrictionName', 'CallRestriction') } Write-Information "INFO: Parameter CallRestriction provided. Using: '$CallRestriction'" } #endregion #region Matching Pattern $CurrentOperationID0 = 'Processing Matching Pattern' Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0 if ( $PSBoundParameters.ContainsKey('MatchingPattern') ) { if ( $CallRestriction -eq 'Unrestricted') { if ( $MatchingPattern -NE '' ) { Write-Verbose -Message "MatchingPattern '$MatchingPattern' will be ignored. For Call Restriction '$CallRestriction' the Matching Pattern is Hardcoded." -Verbose } $MatchingPattern = '.*' Write-Information "INFO: MatchingPattern used: '$MatchingPattern'" } else { Write-Information "INFO: Parameter MatchingPattern provided for Call Restriction '$CallRestriction' in Country '$Country': Using: '$MatchingPattern'" } } else { if ( $CallRestriction -eq 'Custom' -or $PSCmdlet.ParameterSetName -eq 'RestrictedCustom' ) { Write-Error -Message "Parameter MatchingPattern not provided, no data for Call Restriction '$CallRestriction'. Please provide MatchingPattern" continue } else { if ( $CallRestriction -eq 'Unrestricted') { Write-Verbose -Message "MatchingPattern '$MatchingPattern' will be ignored. For Call Restriction '$CallRestriction' the Matching Pattern is Hardcoded." -Verbose $MatchingPattern = '.*' Write-Information "INFO: MatchingPattern used: '$MatchingPattern'" } else { Write-Verbose -Message "MatchingPattern queried through 'Get-MatchingPatternForCallRestriction' for Country '$Country'" $CallRestrictionPatterns = Get-MatchingPatternForCallRestriction -Country $CountryCode $MatchingPattern = $CallRestrictionPatterns."$CallRestriction`Pattern" Write-Information "INFO: MatchingPattern not provided, for Call Restriction '$CallRestriction' in Country '$Country': Using: '$MatchingPattern'" } } } #endregion #endregion #region Clean up of provided Name - Normalization step to reconstruct Policy Name proper $CurrentOperationID0 = 'Name normalization' Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0 $FormatVoiceRoutingChainInputParams = $null $FormatVoiceRoutingChainInputParams = @{ Name = $Name Region = $VRCRegion } if ( $PSBoundParameters.ContainsKey('Country') ) { $FormatVoiceRoutingChainInputParams += @{ Country = $CountryCode } } if ( $PSBoundParameters.ContainsKey('Site') ) { $FormatVoiceRoutingChainInputParams += @{ Site = $Site } } if ( $PSBoundParameters.ContainsKey('CallRestriction') ) { $FormatVoiceRoutingChainInputParams += @{ CallRestriction = $CallRestriction } } if ( $PSBoundParameters.ContainsKey('CustomRestrictionName') ) { $FormatVoiceRoutingChainInputParams += @{ CustomRestrictionName = $CustomRestrictionName } } $FormatVRCInput = Format-VoiceRoutingChainInput @FormatVoiceRoutingChainInputParams if ($PSBoundParameters.ContainsKey('Debug')) { " Function: $($MyInvocation.MyCommand.Name) - Voice Routing Chain Proto String:", ($FormatVRCInput | Format-Table -AutoSize | Out-String).Trim() | Write-Debug } if ( $FormmatVRCInput.Name.Length -gt 0 ) { # Not all elements of the provided Name could be stripped. this may be due to an issue (forgotten Site or Country parameter?) Write-Warning -Message "Proto String was reduced to '$($FormatVRCInput.Name)' (expected to be empty, but not exclusively) - This name will be padded to incorporate all please validate!" Write-Verbose -Message "Confirm preference set to High to let you confirm" $ConfirmPreference = 'High' } #Creating Policy Name $CurrentOperationID0 = 'Creating Proto String' Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0 $FormatVoiceRoutingChainOutputParams = $null $FormatVoiceRoutingChainOutputParams = @{ Object = $FormatVRCInput Region = $VRCRegion } if ( $PSBoundParameters.ContainsKey('Country') -and $CallRestriction -ne 'Unrestricted' ) { $FormatVoiceRoutingChainOutputParams += @{ Country = $CountryCode } } if ( $PSBoundParameters.ContainsKey('Site') ) { $FormatVoiceRoutingChainOutputParams += @{ Site = $Site } } if ( $PSBoundParameters.ContainsKey('CallRestriction') ) { $FormatVoiceRoutingChainOutputParams += @{ CallRestriction = $CallRestriction } } if ( $PSBoundParameters.ContainsKey('CustomRestrictionName') ) { $FormatVoiceRoutingChainOutputParams += @{ CustomRestrictionName = $CustomRestrictionName } } [string]$ProtoString = Format-VoiceRoutingChainOutput @FormatVoiceRoutingChainOutputParams # Constructing Names Write-Information "INFO: Common String used across OVP, OPU & OVR: '$ProtoString' - This will be pre-fixed depending on the artifact" $PolicyName = 'OVP-' + $ProtoString $UsageName = 'OPU-' + $ProtoString $RouteName = 'OVR-' + $ProtoString Write-Verbose -Message "OVP Name: '$PolicyName'" -Verbose Write-Verbose -Message "OPU Name: '$UsageName'" -Verbose Write-Verbose -Message "OVR Name: '$RouteName' (suffix will be attached if multiple Gateways are specified)" -Verbose #endregion } Process { # Initialising Objects [System.Collections.Generic.List[object]]$Usages = @() [System.Collections.Generic.List[object]]$Routes = @() [System.Collections.Generic.List[object]]$Policies = @() $StatusID0 = 'Processing' $CurrentOperationID0 = 'Online Voice Routes' Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0 #region Construction # Defining one Policy for Local breakout # Constructing Routes per Gateway $GatewayCounter = 0 [int]$GatewayCount = $Gateway.Count foreach ($GW in $Gateway) { # Fetching Gateways try { $OnlinePstnGateway = Get-CsOnlinePSTNGateway -Identity $GW -ErrorAction Stop } catch { Write-Error -Message "OnlinePstnGateway '$GW' not found. No route is being created. Please check Gateway name" -Category InvalidData continue } # Counter $GatewayCounter++ $Index = if ( $GatewayCount -gt 1 -and $GatewayCounter -le $GatewayCount) { $GatewayCounter } else { $null } if ( $Index ) { Write-Verbose -Message "Voice Route for Gateway '$GW' - Creating index suffix: '-$Index'" } # Constructing Route $DescriptionSuffix = ', ' + $(if ( $CallRestriction -ne 'Unrestricted' ) { 'Restricted to ' }) + $(if ( $CallRestriction -EQ 'Custom' ) { $CustomRestrictionName } else { $CallRestriction } ) $Description = $(if ($VRCRegion) { $VRCRegion + ' - ' }) + $(if ($CountryCode) { $CountryObject.Name }) + $(if ($Site) { ', ' + $Site }) + $DescriptionSuffix $CsOnlineVoiceRoute = $null $CsOnlineVoiceRoute = @{ Name = $RouteName + $( if ($Index) { '-' + $Index }) Description = 'Route for ' + $Description + $( if ($Index) { ' - ' + $Index }) NumberPattern = $MatchingPattern OnlinePstnUsages = @{add = $UsageName } OnlinePstnGatewayList = $OnlinePstnGateway.Fqdn } if ($PSBoundParameters.ContainsKey('Debug')) { " Function: $($MyInvocation.MyCommand.Name) - CsOnlineVoiceRoute:", ($CsOnlineVoiceRoute | Format-Table -AutoSize | Out-String).Trim() | Write-Debug } # Constructing Usage if ( $CsOnlineVoiceRoute.OnlinePstnGatewayList.Count -gt 0 ) { [Void]$Routes.Add($CsOnlineVoiceRoute) Write-Verbose -Message "Online Voice Route '$($CsOnlineVoiceRoute.Name)' added" if ( $Usages -notcontains $UsageName) { if ($PSBoundParameters.ContainsKey('Debug')) { " Function: $($MyInvocation.MyCommand.Name) - CsOnlinePstnUsage(Global):", ($UsageName | Format-Table -AutoSize | Out-String).Trim() | Write-Debug } # Avoiding duplication errors [Void]$Usages.Add($UsageName) Write-Verbose -Message "Online Pstn Usage '$UsageName' added" } } else { Write-Warning -Message "Online Voice Route '$($CsOnlineVoiceRoute.Name)' not added - No corresponding Gateway '$GW' found" } } # Constructing Policy $CurrentOperationID0 = 'Online Voice Routing Policies' Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0 $CsOnlineVoiceRoutingPolicy = $null $CsOnlineVoiceRoutingPolicy = @{ Identity = $PolicyName OnlinePstnUsages = $CsOnlineVoiceRoute.OnlinePstnUsages # Test $UsageName instead? Description = $Description } if ($PSBoundParameters.ContainsKey('Debug')) { " Function: $($MyInvocation.MyCommand.Name) - CsOnlineVoiceRoutingPolicy:", ($CsOnlineVoiceRoutingPolicy | Format-Table -AutoSize | Out-String).Trim() | Write-Debug } if ( $Policies.Identity -notcontains $CsOnlineVoiceRoutingPolicy.Identity) { [Void]$Policies.Add($CsOnlineVoiceRoutingPolicy) Write-Verbose -Message "Online Voice Routing Policy '$($CsOnlineVoiceRoutingPolicy.Identity)' added" } #endregion #region Action $StatusID0 = 'Creating' # 1 Add Usage $CurrentOperationID0 = 'Online Pstn Usages' Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0 if ($PSCmdlet.ShouldProcess("$Usages", 'Set-CsOnlinePstnUsage')) { foreach ($OPU in $Usages) { if ($PSBoundParameters.ContainsKey('Debug')) { " Function: $($MyInvocation.MyCommand.Name) - OPU:", ($OPU | Format-Table -AutoSize | Out-String).Trim() | Write-Debug } try { $null = Set-CsOnlinePstnUsage Global -Usage @{ add = "$OPU" } -ErrorAction Stop Write-Information "INFO: Online Pstn Usage '$OPU' created" } catch { if ($_.Exception.Message.Contains('duplicate key')) { Write-Warning -Message "Online Pstn Usage '$OPU' already exists - No Action taken" } else { Write-Error "$($_.Exception.Message)" -ErrorAction Stop continue } } } } # 2 Create Route add to Usage $CurrentOperationID0 = 'Online Voice Routes' Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0 foreach ($OVR in $Routes) { if ($PSCmdlet.ShouldProcess("$($OVR.Name)", 'New-CsOnlineVoiceRoute')) { try { $CreatedRoute = $null $CreatedRoute = New-CsOnlineVoiceRoute @OVR -ErrorAction Stop Write-Information "INFO: Online Voice Route '$($CreatedRoute.Identity)' created" } catch { if ($_.Exception.Message.Contains('already exists')) { Write-Warning -Message "Online Voice Route '$($OVR.Name)' already exists - No Action taken" } else { Write-Error "$($_.Exception.Message)" } } } } # 3 Create Policy, add to Usage # Creating the Policy while adding the Usage is dependent on O365 replication (this works immediately for Routes, but not for policies!?) # Attempting to add the Policy incl. the Usage, but if not successful, will just add the Policy and leave it unconnected. # Run this command again or Connect-VoiceRoutingChain to rectify $CurrentOperationID0 = 'Online Voice Routing Policies' Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0 if ($PSCmdlet.ShouldProcess("$($Policies.Identity)", 'New-CsOnlineVoiceRoutingPolicy')) { foreach ($OVP in $Policies) { try { $CreatedPolicy = $null $CreatedPolicy = New-CsOnlineVoiceRoutingPolicy @OVP -ErrorAction Stop Write-Information "INFO: Online Voice Routing Policy '$($CreatedPolicy.Identity)' created" } catch { if ($_.Exception.Message.Contains('already exists')) { $ExistingPolicy = Get-CsOnlineVoiceRoutingPolicy -Identity $OVP.Identity if ( $ExistingPolicy ) { Write-Warning -Message "Online Voice Routing Policy '$($OVP.Identity)' already exists - No Action taken" } } elseif ($_.Exception.Message.Contains('Cannot find specified Online PSTN usage')) { Write-Verbose -Message "Online Voice Routing Policy '$($OVP.Identity)' - PstnUsage cannot be found (yet) - retrying in 30s" -Verbose Start-Sleep -Seconds 30 try { $CreatedPolicy = $null $CreatedPolicy = New-CsOnlineVoiceRoutingPolicy @OVP -ErrorAction Stop Write-Information "INFO: Online Voice Routing Policy '$($CreatedPolicy.Identity)' created" } catch { $OVP.Remove('OnlinePstnUsages') Write-Verbose -Message "Online Voice Routing Policy '$($OVP.Identity)' - PstnUsage cannot be found (yet) - Please re-run this command in 5-10 mins" -Verbose try { $CreatedPolicy = $null $CreatedPolicy = New-CsOnlineVoiceRoutingPolicy @OVP -ErrorAction Stop Write-Information "INFO: Online Voice Routing Policy '$($CreatedPolicy.Identity)' created without attaching PstnUsage" Write-Verbose -Message "Policy created without attaching the PstnUsage. Please allow for replication, then run 'Connect-VoiceRoutingChain'" } catch { Write-Error "$($_.Exception.Message)" } } } else { Write-Error "$($_.Exception.Message)" } } #Output #Get-CsOnlineVoiceRoutingPolicy $($OVP.Identity) $CurrentOperationID0 = 'Querying output object' Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0 Get-TeamsVoiceRoutingChain $($OVP.Identity) Write-Progress -Id 0 -Activity $ActivityID0 -Completed } } #endregion } end { Write-Verbose -Message "[END ] $($MyInvocation.MyCommand)" } } # New-TeamsVoiceRoutingChain |