Public/Functions/Support/Backup/Restore-TeamsEV.ps1

# Module: TeamsFunctions
# Function: Backup
# Author: Ken Lasko
# Updated: 01-JUN-2020
# Status: Unmanaged




function Restore-TeamsEV {
  <#
  .SYNOPSIS
    A script to automatically restore a backed-up Teams Enterprise Voice configuration.
  .DESCRIPTION
    A script to automatically restore a backed-up Teams Enterprise Voice configuration. Requires a backup run using Backup-TeamsEV.ps1 in the same directory as the script. Will restore the following items:
    - Dialplans and associated normalization rules
    - Voice routes
    - Voice routing policies
    - PSTN usages
    - Outbound translation rules
  .PARAMETER File
    REQUIRED. Path to the zip file containing the backed up Teams EV config to restore
  .PARAMETER KeepExisting
    OPTIONAL. Will not erase existing Enterprise Voice configuration before restoring.
  .PARAMETER OverrideAdminDomain
    OPTIONAL: The FQDN your Office365 tenant. Use if your admin account is not in the same domain as your tenant (ie. doesn't use a @tenantname.onmicrosoft.com address)
  .EXAMPLE
    Restore-TeamsEV -File C:\Temp\Backup.ZIP
    Restores the Teams Enterprise Voice Configuration from Backup.ZIP file.
  .INPUTS
    System.File
  .OUTPUTS
    None
  .NOTES
    Version 1.10
    Build: Feb 04, 2020
 
    Copyright © 2020 Ken Lasko
    klasko@ucdialplans.com
    https://www.ucdialplans.com
  .COMPONENT
    SupportingFunction
  .FUNCTIONALITY
    Restoring a backup of the Configuration in the Teams Tenant
  .LINK
    https://github.com/DEberhardt/TeamsFunctions/tree/master/docs/Restore-TeamsEV.md
  .LINK
    https://github.com/DEberhardt/TeamsFunctions/tree/master/docs/about_Supporting_Functions.md
  .LINK
    https://github.com/DEberhardt/TeamsFunctions/tree/master/docs/
  #>


  [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = 'Colourful feedback required to emphasise feedback for script executors')]
  [CmdletBinding(ConfirmImpact = 'Medium', SupportsShouldProcess)]
  param(
    [Parameter(Mandatory, ValueFromPipelineByPropertyName, HelpMessage = 'Path to the zip file containing the backed up Teams EV config to restore')]
    [ArgumentCompleter( { 'C:\Temp\' })]
    [string]$File,

    [switch]$KeepExisting,

    [string]$OverrideAdminDomain

  ) #param

  begin {
    Show-FunctionStatus -Level Unmanaged
    Write-Verbose -Message "[BEGIN ] $($MyInvocation.MyCommand)"
    Write-Verbose -Message "Need help? Online: $global:TeamsFunctionsHelpURLBase$($MyInvocation.MyCommand)`.md"

    Try {
      $ZipPath = (Resolve-Path -Path $File)
      $null = (Add-Type -AssemblyName System.IO.Compression.FileSystem)
      $ZipStream = [io.compression.zipfile]::OpenRead($ZipPath)
    }
    Catch {
      Write-Error -Message 'Could not open zip archive.' -ErrorAction Stop
      return
    }

    If ((Get-PSSession -WarningAction SilentlyContinue | Where-Object -FilterScript { $_.ComputerName -eq 'api.interfaces.records.teams.microsoft.com|online.lync.com' }).State -eq 'Opened') {
      Write-Host -Object 'Using existing session credentials'
    }
    Else {
      Write-Host -Object 'Logging into Office 365...'

      If ($OverrideAdminDomain) {
        $O365Session = (New-CsOnlineSession -OverrideAdminDomain $OverrideAdminDomain)
      }
      Else {
        $O365Session = (New-CsOnlineSession)
      }
      $null = (Import-PSSession -Session $O365Session -AllowClobber)
    }

  } #begin

  process {
    Write-Verbose -Message "[PROCESS] $($MyInvocation.MyCommand)"
    $EV_Entities = 'Dialplans', 'VoiceRoutes', 'VoiceRoutingPolicies', 'PSTNUsages', 'TranslationRules', 'PSTNGateways'

    Write-Host -Object 'Validating backup files.'

    ForEach ($EV_Entity in $EV_Entities) {
      Try {
        $ZipItem = $ZipStream.GetEntry("$EV_Entity.txt")
        $ItemReader = (New-Object -TypeName System.IO.StreamReader -ArgumentList ($ZipItem.Open()))

        $null = (Set-Variable -Name $EV_Entity -Value ($ItemReader.ReadToEnd() | ConvertFrom-Json))

        # Throw error if there is no Identity field, which indicates this isn't a proper backup file
        If ($null -eq ((Get-Variable -Name $EV_Entity).Value[0].Identity)) {
          $null = (Set-Variable -Name $EV_Entity -Value $NULL)
          Throw ('Error')
        }
      }
      Catch {
        Write-Error -Message ($EV_Entity + '.txt could not be found, was empty or could not be parsed. ' + $EV_Entity + ' will not be restored.') -ErrorAction Continue
      }
      $ItemReader.Close()
    }

    If (!$KeepExisting) {
      $Confirm = Read-Host -Prompt 'WARNING: This will ERASE all existing dialplans/voice routes/policies etc prior to restoring from backup. Continue (Y/N)?'
      If ($Confirm -notmatch '^[Yy]$') {
        Write-Host -Object 'returning without making changes.'
        return
      }

      Write-Host -Object 'Erasing all existing dialplans/voice routes/policies etc.'

      Write-Verbose 'Erasing all tenant dialplans'
      $null = (Get-CsTenantDialPlan -WarningAction SilentlyContinue -ErrorAction SilentlyContinue | Remove-CsTenantDialPlan -ErrorAction SilentlyContinue)
      Write-Verbose 'Erasing all online voice routes'
      $null = (Get-CsOnlineVoiceRoute -WarningAction SilentlyContinue -ErrorAction SilentlyContinue | Remove-CsOnlineVoiceRoute -ErrorAction SilentlyContinue)
      Write-Verbose 'Erasing all online voice routing policies'
      $null = (Get-CsOnlineVoiceRoutingPolicy -WarningAction SilentlyContinue -ErrorAction SilentlyContinue | Remove-CsOnlineVoiceRoutingPolicy -ErrorAction SilentlyContinue)
      Write-Verbose 'Erasing all PSTN usages'
      $null = (Set-CsOnlinePstnUsage -Identity Global -Usage $NULL -ErrorAction SilentlyContinue)
      Write-Verbose 'Removing translation rules from PSTN gateways'
      $null = (Get-CsOnlinePSTNGateway -WarningAction SilentlyContinue -ErrorAction SilentlyContinue | Set-CsOnlinePSTNGateway -OutbundTeamsNumberTranslationRules $NULL -OutboundPstnNumberTranslationRules $NULL -ErrorAction SilentlyContinue)
      Write-Verbose 'Removing translation rules'
      $null = (Get-CsTeamsTranslationRule -WarningAction SilentlyContinue -ErrorAction SilentlyContinue | Remove-CsTeamsTranslationRule -ErrorAction SilentlyContinue)
    }

    # Rebuild tenant dialplans from backup
    Write-Host -Object 'Restoring tenant dialplans'

    ForEach ($Dialplan in $Dialplans) {
      Write-Verbose -Message "Restoring $($Dialplan.Identity) dialplan"
      $DPExists = (Get-CsTenantDialPlan -Identity $Dialplan.Identity -WarningAction SilentlyContinue -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Identity)

      $DPDetails = @{
        Identity              = $Dialplan.Identity
        OptimizeDeviceDialing = $Dialplan.OptimizeDeviceDialing
        Description           = $Dialplan.Description
      }

      # Only include the external access prefix if one is defined. MS throws an error if you pass a null/empty ExternalAccessPrefix
      If ($Dialplan.ExternalAccessPrefix) {
        [void]$DPDetails.Add('ExternalAccessPrefix', $Dialplan.ExternalAccessPrefix)
      }

      If ($DPExists) {
        $null = (Set-CsTenantDialPlan @DPDetails)
      }
      Else {
        $null = (New-CsTenantDialPlan @DPDetails)
      }

      # Create a new Object
      $NormRules = @()

      ForEach ($NormRule in $Dialplan.NormalizationRules) {
        $NRDetails = @{
          Parent              = $Dialplan.Identity
          Name                = [regex]::Match($NormRule, '(?ms)Name=(.*?);').Groups[1].Value
          Pattern             = [regex]::Match($NormRule, '(?ms)Pattern=(.*?);').Groups[1].Value
          Translation         = [regex]::Match($NormRule, '(?ms)Translation=(.*?);').Groups[1].Value
          Description         = [regex]::Match($NormRule, '(?ms)^Description=(.*?);').Groups[1].Value
          IsInternalExtension = [Convert]::ToBoolean([regex]::Match($NormRule, '(?ms)IsInternalExtension=(.*?)$').Groups[1].Value)
        }
        $NormRules += New-CsVoiceNormalizationRule @NRDetails -InMemory
      }
      $null = (Set-CsTenantDialPlan -Identity $Dialplan.Identity -NormalizationRules $NormRules)
    }

    # Rebuild PSTN usages from backup
    Write-Host -Object 'Restoring PSTN usages'

    ForEach ($PSTNUsage in $PSTNUsages.Usage) {
      Write-Verbose -Message "Restoring $PSTNUsage PSTN usage"
      $null = (Set-CsOnlinePstnUsage -Identity Global -Usage @{Add = $PSTNUsage } -WarningAction SilentlyContinue -ErrorAction SilentlyContinue)
    }

    # Rebuild voice routes from backup
    Write-Host -Object 'Restoring voice routes'

    ForEach ($VoiceRoute in $VoiceRoutes) {
      Write-Verbose -Message "Restoring $($VoiceRoute.Identity) voice route"
      $VRExists = (Get-CsOnlineVoiceRoute -Identity $VoiceRoute.Identity -WarningAction SilentlyContinue -ErrorAction SilentlyContinue).Identity

      $VRDetails = @{
        Identity              = $VoiceRoute.Identity
        NumberPattern         = $VoiceRoute.NumberPattern
        Priority              = $VoiceRoute.Priority
        OnlinePstnUsages      = $VoiceRoute.OnlinePstnUsages
        OnlinePstnGatewayList = $VoiceRoute.OnlinePstnGatewayList
        Description           = $VoiceRoute.Description
      }

      If ($VRExists) {
        $null = (Set-CsOnlineVoiceRoute @VRDetails)
      }
      Else {
        $null = (New-CsOnlineVoiceRoute @VRDetails)
      }
    }

    # Rebuild voice routing policies from backup
    Write-Host -Object 'Restoring voice routing policies'

    ForEach ($VoiceRoutingPolicy in $VoiceRoutingPolicies) {
      Write-Verbose -Message "Restoring $($VoiceRoutingPolicy.Identity) voice routing policy"
      $VPExists = (Get-CsOnlineVoiceRoutingPolicy -Identity $VoiceRoutingPolicy.Identity -ErrorAction SilentlyContinue).Identity

      $VPDetails = @{
        Identity         = $VoiceRoutingPolicy.Identity
        OnlinePstnUsages = $VoiceRoutingPolicy.OnlinePstnUsages
        Description      = $VoiceRoutingPolicy.Description
      }

      If ($VPExists) {
        $null = (Set-CsOnlineVoiceRoutingPolicy @VPDetails)
      }
      Else {
        $null = (New-CsOnlineVoiceRoutingPolicy @VPDetails)
      }
    }

    # Rebuild outbound translation rules from backup
    Write-Host -Object 'Restoring outbound translation rules'

    ForEach ($TranslationRule in $TranslationRules) {
      Write-Verbose -Message "Restoring $($TranslationRule.Identity) translation rule"
      $TRExists = (Get-CsTeamsTranslationRule -Identity $TranslationRule.Identity -WarningAction SilentlyContinue -ErrorAction SilentlyContinue).Identity

      $TRDetails = @{
        Identity    = $TranslationRule.Identity
        Pattern     = $TranslationRule.Pattern
        Translation = $TranslationRule.Translation
        Description = $TranslationRule.Description
      }

      If ($TRExists) {
        $null = (Set-CsTeamsTranslationRule @TRDetails)
      }
      Else {
        $null = (New-CsTeamsTranslationRule @TRDetails)
      }
    }

    # Re-add translation rules to PSTN gateways
    Write-Host -Object 'Re-adding translation rules to PSTN gateways'

    ForEach ($PSTNGateway in $PSTNGateways) {
      Write-Verbose -Message "Restoring translation rules to $($PSTNGateway.Identity)"
      $GWExists = (Get-CsOnlinePSTNGateway -Identity $PSTNGateway.Identity -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Identity)

      $GWDetails = @{
        Identity                           = $PSTNGateway.Identity
        OutbundTeamsNumberTranslationRules = $PSTNGateway.OutbundTeamsNumberTranslationRules #Sadly Outbund isn't a spelling mistake here. That's what the command uses.
        OutboundPstnNumberTranslationRules = $PSTNGateway.OutboundPstnNumberTranslationRules
        InboundTeamsNumberTranslationRules = $PSTNGateway.InboundTeamsNumberTranslationRules
        InboundPstnNumberTranslationRules  = $PSTNGateway.InboundPstnNumberTranslationRules
      }
      If ($GWExists) {
        $null = (Set-CsOnlinePSTNGateway @GWDetails)
      }
    }
  } #process

  end {
    Write-Verbose -Message "[END ] $($MyInvocation.MyCommand)"
    Write-Host -Object 'Finished!'
  } #end
} #Restore-TeamsEV