Public/VoiceConfig/Remove-TeamsVoiceRoutingChain.ps1

# Module: TeamsFunctions
# Function: Tenant Voice Configuration
# Author: David Eberhardt
# Updated: 19-FEB-2022
# Status: LIVE




function Remove-TeamsVoiceRoutingChain {
  <#
  .SYNOPSIS
    Removes an Online Voice Routing Policy and all associated Objects dependent on configuration
  .DESCRIPTION
    Determines whether an Online Voice Routing Policy is used and its associated Objects (Online Pstn Usage & Online Voice Routes)
    Will determine an action plan:
    If the determined Voice Route(s) is not used in other Pstn Usages will prompt to remove the Voice Route
    If the determined Pstn Usage is not used elsewhere, will remove the Usage from the Voice Route and the Policy
    If the Voice Policy is used (assigned to Users) will prompt for confirmation (unless overridden with Force)
    If consent is given, will attempt to remove the whole assignment chain.
  .PARAMETER Identity
    String. Identifying the Online Voice Routing Policy
    Required to identify the correct OVP and start the determination chain
  .PARAMETER Force
    Switch. If used, will suppress confirmation dialogs to warn of "Policy is in use". Handle with care!
  .EXAMPLE
    Remove-TeamsVoiceRoutingChain -Identity "OVP-AMER-GSIP-MX"
 
    Queries the Online Voice Routing Policy OVP-AMER-GSIP-MX and determines its usage (assignment to User Object)
    Determines Pstn Usages nested in this Policy and Voice Routes that reference this Pstn Usage.
    Provided a Unique assignment chain is found, the Policy, PstnUsage & corresponding Routes are then removed.
  .EXAMPLE
    Remove-TeamsVoiceRoutingChain -Identity "OVP-AMER-GSIP-MX" -Force
 
    Queries the Online Voice Routing Policy OVP-AMER-GSIP-MX and determines its usage (assignment to User Object)
    If the Policy is still in use, no query is presented, but only a warning is displayed.
    Determines Pstn Usages nested in this Policy and Voice Routes that reference this Pstn Usage.
    Provided a Unique assignment chain is found, the Policy, PstnUsage & corresponding Routes are then removed.
  .INPUTS
    System.String
  .OUTPUTS
    System.Void
  .NOTES
    General notes
  .COMPONENT
    Tenant Voice Routing
  .FUNCTIONALITY
    Direct Routing
  .LINK
    https://github.com/DEberhardt/TeamsFunctions/tree/main/docs/Remove-TeamsVoiceRoutingChain.md
  .LINK
    https://github.com/DEberhardt/TeamsFunctions/tree/main/docs/about_VoiceConfiguration.md
  .LINK
    https://github.com/DEberhardt/TeamsFunctions/tree/main/docs/
    #>

  [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
  [Alias('Remove-TeamsVRC')]
  param(
    [Parameter(Mandatory, ValueFromPipeline, HelpMessage = 'Name of the Online Voice Routing Policy')]
    [Alias('Name')]
    [ValidateScript( {
        if ($_ -in $(&$global:TfAcSbVoiceRoutingPolicy)) { return $true } else {
          throw [System.Management.Automation.ValidationMetadataException] 'Value must be a valid Policy in the Tenant. Use Intellisense for options'
        } })]
    [ArgumentCompleter({ &$global:TfAcSbVoiceRoutingPolicy })]
    [string[]]$Identity,

    [Parameter(HelpMessage = 'Overrides Confirm to remove assigned policies')]
    [switch]$Force

  )

  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
  }

  Process {
    foreach ($Id in $Identity) {
      # Initialising Objects
      [System.Collections.Generic.List[object]]$OVRtoRemove = @()
      [System.Collections.Generic.List[object]]$OPUtoRemove = @()

      # Querying Object
      $StatusID0 = 'Information Gathering'
      $CurrentOperationID0 = 'Querying Online Voice Routing Policies'
      Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
      try {
        $CsOnlineVoiceRoutingPolicy = Get-CsOnlineVoiceRoutingPolicy -Identity $Id -ErrorAction Stop
      }
      catch {
        Write-Error "$($_.Exception.Message)"
        Continue
      }

      #region Defining Scope (OPUs and OVRs)
      if ( $CsOnlineVoiceRoutingPolicy) {
        # Determine PSTN Usages
        $CurrentOperationID0 = 'Querying Online Pstn Usages'
        Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
        Write-Verbose "Querying Voice Routing Path for Online Voice Routing Policy '$($CsOnlineVoiceRoutingPolicy.Identity)'"
        $OPUs = $null
        $OPUs = (Get-CsOnlineVoiceRoutingPolicy -Identity $($CsOnlineVoiceRoutingPolicy.Identity) -ErrorAction Stop).OnlinePstnUsages

        # Determining Routes
        if ($OPUs) {
          $CurrentOperationID0 = 'Querying Online Voice Routing Routes'
          Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0

          Write-Information "INFO: OVP '$($CsOnlineVoiceRoutingPolicy.Identity)' - $($OPUs.Count) Online PSTN Usages found: $($OPUs -join ', ')"
          [System.Collections.Generic.List[object]]$VoiceRoutes = @()
          foreach ($OPU in $OPUs) {
            $RouteContainingOPU = Get-CsOnlineVoiceRoute | Where-Object { $_.OnlinePstnUsages -contains $OPU } | Select-Object *, @{label = 'PSTNUsage'; Expression = { $OPU } }
            foreach ( $Route in $RouteContainingOPU ) {
              if ( $VoiceRoutes.Identity -contains $Route.Identity ) {
                Write-Verbose "OVR '$($Route.Identity)' already captured, omitting"
              }
              else {
                $VoiceRoutes += $Route
              }
            }
            #$VoiceRoutes = $VoiceRoutes | Get-Unique
          }
          if ($VoiceRoutes) {
            Write-Information "INFO: OVP '$($CsOnlineVoiceRoutingPolicy.Identity)' - $($VoiceRoutes.Count) Online Voice Routes found: $($VoiceRoutes.Identity)"
            if ($PSBoundParameters.ContainsKey('Debug') -or $DebugPreference -eq 'Continue') {
              "Function: $($MyInvocation.MyCommand.Name) - VoiceRoutes", ( $VoiceRoutes | Select-Object Identity, Priority, Description, OnlinePstnUsages | Format-Table -AutoSize | Out-String).Trim() | Write-Debug
            }
          }
          else {
            Write-Verbose -Message "OVP '$($CsOnlineVoiceRoutingPolicy.Identity)' - No Online Voice Routes have been found" -Verbose
          }
        }
        else {
          Write-Verbose -Message "OVP '$($CsOnlineVoiceRoutingPolicy.Identity)' - No Online PSTN Usages have been found" -Verbose
        }
      }
      else {
        throw "OnlineVoiceRoutingPolicy '$Id' not found"
      }
      #endregion

      #region Handling assigned OVPs
      $CurrentOperationID0 = 'Determining Impact (assigned Users)'
      Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
      $UsersAssigned = Get-CsOnlineUser -Filter ("OnlineVoiceRoutingPolicy -eq '{0}'" -f $Id)
      if ( $UsersAssigned) {
        $global:AffectedUsers = $null
        $global:AffectedUsers = [Management.Automation.PSSerializer]::DeSerialize([Management.Automation.PSSerializer]::Serialize($UsersAssigned.UserPrincipalName))
        Write-Error -Message "OVP '$($CsOnlineVoiceRoutingPolicy.Identity)' - Policy is assigned to $($AffectedUsers.Count) users. To prevent orphaned object references, the policy cannot be removed. Assign a different policy to the users before removing this one."
        Write-Verbose -Message 'The Variable "$AffectedUsers" has been populated showing the impact' -Verbose
        continue
      }
      #endregion

      #region Determining Removeability
      $CurrentOperationID0 = 'Determining constraints on OPUs'
      Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
      if ( $OPUs ) {
        #$OPUtoRemove = $OPUs | ForEach-Object { $_ }
        $OPUtoRemove = [Management.Automation.PSSerializer]::DeSerialize([Management.Automation.PSSerializer]::Serialize($OPUs))
        #$OVRtoRemove = $VoiceRoutes | ForEach-Object { $_ }
        $OVRtoRemove = [Management.Automation.PSSerializer]::DeSerialize([Management.Automation.PSSerializer]::Serialize($VoiceRoutes))
        foreach ($OPU in $OPUs) {
          # Determination of OVPs using the $OPU
          Write-Verbose -Message "Processing OPU '$OPU' - Determining nesting in Online Voice Routing Policies"
          foreach ($Usage in $CsOnlineVoiceRoutingPolicy.OnlinePstnUsages) {
            $OVPsUsingOPU = Get-CsOnlineVoiceRoutingPolicy | Where-Object OnlinePstnUsages -Contains $Usage
            if ($PSBoundParameters.ContainsKey('Debug') -or $DebugPreference -eq 'Continue') {
              "Function: $($MyInvocation.MyCommand.Name) - OVPsUsingOPU", ( $OVPsUsingOPU | Select-Object Identity, Description, OnlinePstnUsages | Format-Table -AutoSize | Out-String).Trim() | Write-Debug
            }
            foreach ( $OVP in $OVPsUsingOPU) {
              if ( $OVP.Identity -ne $CsOnlineVoiceRoutingPolicy.Identity ) {
                Write-Warning -Message "OPU '$OPU' is used in OVP '$($OVP.Identity)' and cannot be removed!"
                [void]$OPUtoRemove.Remove($OPU)
              }
            }
          }
          if ($PSBoundParameters.ContainsKey('Debug') -or $DebugPreference -eq 'Continue') {
            "Function: $($MyInvocation.MyCommand.Name) - Remaining OPUtoRemove", ( $OPUtoRemove -join ', ').Trim() | Write-Debug
          }

          # Determination of OVRs using the $OPU
          $CurrentOperationID0 = 'Determining constraints on OVRs'
          Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
          if ( $VoiceRoutes ) {
            foreach ($OVR in $VoiceRoutes) {
              Write-Verbose -Message "Processing OVR '$($OVR.Identity)' - Determining nesting in Online PstnUsages"
              foreach ($OtherOPU in $OVR.OnlinePstnUsages) {
                if ( [string]$OtherOPU -eq [string]$OPU ) {
                  Write-Verbose -Message "OVR '$($OVR.Identity)' is used in OPU '$OPU' - OK"
                  continue
                }
                else {
                  Write-Warning -Message "OVR '$($OVR.Identity)' is used in OPU '$OtherOPU' and cannot be removed!"
                  [void]$OVRtoRemove.Remove($OVR)
                }
              }
            }
            if ($PSBoundParameters.ContainsKey('Debug') -or $DebugPreference -eq 'Continue') {
              "Function: $($MyInvocation.MyCommand.Name) - Remaining OVRtoRemove", ( $OVRtoRemove.Name -join ', ').Trim() | Write-Debug
            }
          }
        }
        Write-Verbose -Message 'Processing complete'
      }
      #endregion

      #Final Scope
      Write-Warning -Message 'The following configuration has been discovered as removable (and will not impact other OVP/OPU/OVRs)'
      $OVPtoRemove = $CsOnlineVoiceRoutingPolicy.Identity
      Write-Information "$($PsStstyle.Foreground.Green)INFO: Online Voice Routes to be removed:$($PsStstyle.Reset)"
      Write-Information -MessageData ( $OVRtoRemove.Name | Format-Table -AutoSize | Out-String).Trim()
      Write-Information "$($PsStstyle.Foreground.Green)INFO: Online PSTN Usages to be removed:$($PsStstyle.Reset)"
      Write-Information -MessageData ( $OPUtoRemove | Format-Table -AutoSize | Out-String).Trim()
      Write-Information "$($PsStstyle.Foreground.Green)INFO: Online Voice Routing Policies to be removed:$($PsStstyle.Reset)"
      Write-Information -MessageData ( $OVPtoRemove | Format-Table -AutoSize | Out-String).Trim()

      #region Action
      #if ($PSCmdlet.ShouldProcess('WARNING: The following Sequence will be performed in order: Removing OVRs, Removing OVP, Removing OPUs')) {
      if ( -not $Force ) {
        Write-Verbose -Message 'Confirmation is sought for each step (override with -Force if desired)' -Verbose
      }

      # 1 Remove Route
      $CurrentOperationID0 = 'Removing Routes'
      Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
      foreach ($OVRName in $OVRtoRemove.Identity) {
        if ($Force -or $PSCmdlet.ShouldProcess("$OVRName", 'Remove-CsOnlineVoiceRoute')) {
          try {
            Remove-CsOnlineVoiceRoute -Identity $OVRName -ErrorAction Stop
            Write-Information "Online Voice Route '$OVRName' removed"
          }
          catch {
            Write-Error "$($_.Exception.Message)"
          }
        }
      }

      # 2 Remove Policy
      $CurrentOperationID0 = 'Removing Policies'
      Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
      foreach ($OVPName in $OVPtoRemove) {
        if ($Force -or $PSCmdlet.ShouldProcess("$OVPName", 'Remove-CsOnlineVoiceRoutingPolicy')) {
          try {
            Remove-CsOnlineVoiceRoutingPolicy -Identity $OVPName -ErrorAction Stop
            Write-Information "Online Voice Routing Policy '$OVPName' removed"
          }
          catch {
            Write-Error "$($_.Exception.Message)"
          }
        }
      }

      # 3 Remove Usages
      $CurrentOperationID0 = 'Removing Pstn Usages'
      Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
      foreach ($OPU in $OPUtoRemove) {
        if ($Force -or $PSCmdlet.ShouldProcess("$OPU", 'Set-CsOnlinePstnUsage')) {
          try {
            $null = Set-CsOnlinePstnUsage Global -Usage @{remove = $OPU } -ErrorAction Stop
            Write-Information "Online Pstn Usage '$OPU' removed"
          }
          catch {
            Write-Error "$($_.Exception.Message)"
          }
        }
      }

      Write-Progress -Id 0 -Activity $ActivityID0 -Completed
      #}
      #endregion
    }
  }

  end {
    Write-Verbose -Message "[END ] $($MyInvocation.MyCommand)"
  }
} # Remove-TeamsVoiceRoutingChain