private/Measure-UcmOnPremNumberRange.ps1
#PerformScriptSigning Function Measure-UcmOnPremNumberRange { <# .SYNOPSIS A tool to audit Skype for Business Number ranges ready for migration .DESCRIPTION An Auditing tool, The tool will run through all your PSTN numbers, find number blocks, then report on what objects are in each. It will also highlight what it thinks are "weird" number blocks (Blocks with only one number in it, usually caused by a typing error) or optionally, number blocks that don't follow your numbering format. (IE, if you use 10 digit numbers and a 3 digit number is found, it will be highlighted) .EXAMPLE Measure-UcmOnPremNumberRange -NormalLength 12 -Report .PARAMETER NormalLength (Optional) Used to check all numbers are this long, will report on any that arent if set .PARAMETER Report (Optional) Uses the Write-UcmReport functions to generate a HTML and CSV report of the results in the current folder .INPUTS This function does not accept any inputs .REQUIRED FUNCTIONS/MODULES Modules Lync (Lync Management Tools) or SkypeforBusiness (Skype for Business Management Tools) UcmPSTools (Install-Module UcmPsTools) Includes Cmdlets below. Cmdlets Write-UcmLog: https://github.com/Atreidae/UcmPsTools/blob/main/public/Write-UcmLog.ps1 Write-HTMLReport: https://github.com/Atreidae/UcmPsTools/blob/main/public/Write-HTMLReport.ps1 (optional) .REQUIRED PERMISSIONS 'CSReadOnlyAdministrator' or better .LINK https://www.UcMadScientist.com https://github.com/Atreidae/UcmPSTools .NOTES Version: 1.0 Date: 02/03/2022 .VERSION HISTORY 1.0: Initial Public Release #> Param ( [Parameter(Position = 1)] $NormalLength = 0, [Parameter(Position = 2)] [bool]$Debuging = $false, [Parameter(Position = 3)] [bool]$Offline = $false, [Parameter(Position = 4)] [bool]$Report = $false ) #region FunctionSetup, Set Default Variables for HTML Reporting and Write Log $function = 'Measure-CsNumberRange' [hashtable]$Return = @{} $return.Function = $function $return.Status = 'Unknown' $return.Message = 'Function did not return a status message' # Log why we were called Write-UcmLog -Message "$($MyInvocation.InvocationName) called with $($MyInvocation.Line)" -Severity 1 -Component $function Write-UcmLog -Message 'Parameters' -Severity 1 -Component $function -LogOnly Write-UcmLog -Message "$($PsBoundParameters.Keys)" -Severity 1 -Component $function -LogOnly Write-UcmLog -Message 'Parameters Values' -Severity 1 -Component $function -LogOnly Write-UcmLog -Message "$($PsBoundParameters.Values)" -Severity 1 -Component $function -LogOnly Write-UcmLog -Message 'Optional Arguments' -Severity 1 -Component $function -LogOnly Write-UcmLog -Message "$Args" -Severity 1 -Component $function -LogOnly Write-Host '' #Insert a blank line to make reading output easier on loops #endregion FunctionSetup #region FunctionWork Write-UcmLog -Message 'Initializing variables and reports' -Severity 2 -Component $function #Global Variables [int]$global:100NumberBlocks = 0 [int]$global:EVwithNoNumber = 0 [int]$global:OnPremUsersWithoutEV = 0 [int]$global:OnPremUsersWithEV = 0 [int]$global:CloudUsersWithoutEV = 0 [int]$global:CloudUsersWithEV = 0 [int]$global:CloudRooms = 0 [int]$global:NumberAndDisabled = 0 [int]$global:NumberAndNoEV = 0 [int]$global:IncorrectLength = 0 [int]$global:ValidNumbers = 0 [int]$global:BadObjects = 0 #Declare a new "NumberObject" Class Class NumberObject { [string]$ObjectType [string]$SipAddress [string]$Identity [string]$DisplayName [string]$PstnNumber [string]$NumberRange [bool]$IsWeird = $false [string]$WhyWeird [string]$Source } Class NumberBlock { [String]$Identity [String]$Users [String]$MeetingRooms [String]$AnalogDevices [String]$CommonAreaPhones [String]$ExchangeUmContacts [String]$DialInConferencingAccessNumber [String]$TrustedAppEndpoints [String]$RgsWorkflow [String]$RgsAgents [String]$TotalNumbersUsed } #Now create an empty "Number Hashtable" $global:NumberObjects = @() $global:NumberBlocks = @() #Todo init html report If ($report) { Initialize-UcmReport -Title 'Number Range Audit' -Subtitle 'Existing Number Ranges found in the Skype for Business Environment' } #Build a hashtable of every number in the platform and store it in a useful format Write-UcmLog -Message 'Obtaining Skype User data, this make take some time...' -Severity 2 -Component $function Write-Progress -Activity 'Obtaining Skype Enviroment Data' -Status 'Obtain User Data' -PercentComplete (((1) / 8) * 100) If ($Debuging -or $offline) { $Userlist = Import-Csv -Path userlist.csv Write-Progress -Activity 'Obtaining Skype Enviroment Data' -Status 'Meeting Rooms' -PercentComplete (((1) / 8) * 100) $MeetingRooms = Import-Csv -Path Meetingroom.csv Write-Progress -Activity 'Obtaining Skype Enviroment Data'-Status 'Analog Devices' -PercentComplete (((2) / 8) * 100) $AnalogDevices = Import-Csv -Path AnalogDevice.csv Write-Progress -Activity 'Obtaining Skype Enviroment Data'-Status 'Common Area Phones' -PercentComplete (((3) / 8) * 100) $CommonAreaPhones = Import-Csv -Path CommonArea.csv Write-Progress -Activity 'Obtaining Skype Enviroment Data'-Status 'Exchange UM Endpoints' -PercentComplete (((4) / 8) * 100) $ExchangeUM = Import-Csv -Path Exchange.csv Write-Progress -Activity 'Obtaining Skype Enviroment Data'-Status 'Dial In Conferencing Numbers' -PercentComplete (((5) / 8) * 100) $DialInConf = Import-Csv -Path DialinConf.csv Write-Progress -Activity 'Obtaining Skype Enviroment Data'-Status 'Trusted App Endpoints' -PercentComplete (((6) / 8) * 100) $TrustedAppEndpoints = Import-Csv -Path TrustedApp.csv Write-Progress -Activity 'Obtaining Skype Enviroment Data' -Status 'Response Group Data' -PercentComplete (((7) / 8) * 100) $ResponseGroupWorkflows = Import-Csv -Path RgsWorkflow.csv $ResponseGroupAgents = Import-Csv -Path Agents.csv Write-UcmLog -Message 'Done.' -Severity 2 -Component $function } Else { { $Userlist = Get-CsUser Write-Progress -Activity 'Obtaining Skype Enviroment Data' -Status 'Meeting Rooms' -PercentComplete (((1) / 8) * 100) $MeetingRooms = Get-CsMeetingRoom Write-Progress -Activity 'Obtaining Skype Enviroment Data'-Status 'Analog Devices' -PercentComplete (((2) / 8) * 100) $AnalogDevices = Get-CsAnalogDevice Write-Progress -Activity 'Obtaining Skype Enviroment Data'-Status 'Common Area Phones' -PercentComplete (((3) / 8) * 100) $CommonAreaPhones = Get-CsCommonAreaPhone Write-Progress -Activity 'Obtaining Skype Enviroment Data'-Status 'Exchange UM Endpoints' -PercentComplete (((4) / 8) * 100) $ExchangeUM = Get-CsExUmContact Write-Progress -Activity 'Obtaining Skype Enviroment Data'-Status 'Dial In Conferencing Numbers' -PercentComplete (((5) / 8) * 100) $DialInConf = Get-CsDialInConferencingAccessNumber Write-Progress -Activity 'Obtaining Skype Enviroment Data'-Status 'Trusted App Endpoints' -PercentComplete (((6) / 8) * 100) $TrustedAppEndpoints = Get-CsTrustedApplicationEndpoint #todo Write-Progress -Activity 'Obtaining Skype Enviroment Data' -Status 'Response Group Data' -PercentComplete (((7) / 8) * 100) $ResponseGroupWorkflows = Import-Csv -Path RgsWorkflow.csv $ResponseGroupAgents = Import-Csv -Path Agents.csv Write-UcmLog -Message 'Done.' -Severity 2 -Component $function } } #region Process Data Merge-CsNumberObject -Objects $Userlist -ObjectType 'Users' Merge-CsNumberObject -Objects $MeetingRooms -ObjectType 'MeetingRooms' Merge-CsNumberObject -Objects $AnalogDevices -ObjectType 'AnalogDevices' Merge-CsNumberObject -Objects $CommonAreaPhones -ObjectType 'CommonAreaPhones' Merge-CsNumberObject -Objects $ExchangeUM -ObjectType 'ExchangeUmContacts' Merge-CsNumberObject -Objects $DialInConf -ObjectType 'DialInConferencingAccessNumber' Merge-CsNumberObject -Objects $TrustedAppEndpoints -ObjectType 'TrustedAppEndpoints' Merge-CsNumberObject -Objects $ResponseGroupWorkflows -ObjectType 'RgsWorkflow' #Merge-CsNumberObject -Objects $ResponseGroupAgents -ObjectType 'RgsAgents' #todo, this is a bit more complex\ #endregion Process Data # Calculate the number of blocks $HundredNumberBlocks = ($NumberObjects.PstnNumber -replace '(.*)\d{2}$', '$1' | Select-Object -Unique) #Create a Progress Report for the filtering loop #Setup progress variables $ObjectCount = $HundredNumberBlocks.count $CurrentObject = 0 $maxI = 250 $StartTime = Get-Date Foreach ($HundredNumberBlock in $HundredNumberBlocks) #Numberblock Loop { Write-UcmLog -Message "Auditing Numberblock $HundredNumberBlock" -Severity 1 -Component $function #Capture data for time estimate, we have to do this now as we might exit early. if ($CurrentObject -ge 1) { $ElapsedTime = $(Get-Date) - $StartTime #do the ratios and "the math" to compute the Estimated Time Of Completion $EstimatedTotalSeconds = $ObjectCount / $CurrentObject * $ElapsedTime.TotalSeconds $EstimatedTotalSecondsTS = New-TimeSpan -Seconds $EstimatedTotalSeconds $EstimatedCompletionTime = $StartTime + $EstimatedTotalSecondsTS $EstimatedTimeLeft = $EstimatedCompletionTime - (Get-Date) #Give us a human readable time $Eta = ($EstimatedTimeLeft.ToString('mm\:ss')) } $CurrentObject ++ Write-Progress -Activity "Processing $NumberRange" -Status "$ObjectType $CurrentObject of $ObjectCount., Remaining Time $ETA / Completion @ $EstimatedCompletionTime" -PercentComplete ((($CurrentObject) / $ObjectCount) * 100) Measure-UcmNumberBlock -NumberRange $HundredNumberBlock } #Report on each of the 100 number blocks #Summarize findings Write-UcmLog -Message '' -Severity 2 -Component $function Write-UcmLog -Message 'Number Check Complete' -Severity 2 -Component $function Write-UcmLog -Message 'Summary' -Severity 2 -Component $function Write-UcmLog -Message "Processed $($global:NumberObjects.count) Number objects, $ValidNumbers of which appear to be valid phone numbers" -Severity 2 -Component $function Write-UcmLog -Message "Located a total of $($HundredNumberBlocks.count) unique number ranges" -Severity 2 -Component $function Write-UcmLog -Message "Found $($Userlist.count) users, $($MeetingRooms.count) MeetingRooms, $($AnalogDevices.count) AnalogDevices, $($CommonAreaPhones.count) CommonAreaPhones, $($ExchangeUM.count) ExchangeUmContacts, $($DialInConf.count) DialInConferencingAccessNumbers, $($TrustedAppEndpoints.count) and TrustedAppEndpoints, $($ResponseGroupWorkflows.count) ResponseGroupWorkflows" -Severity 2 -Component $function Write-UcmLog -Message "$global:CloudUsersWithoutEV cloud hosted users without EV" -Severity 2 -Component $function Write-UcmLog -Message "$global:CloudUsersWithEV cloud hosted users with EV" -Severity 2 -Component $function Write-UcmLog -Message '' -Severity 2 -Component $function Write-UcmLog -Message 'Cautions' -Severity 2 -Component $function Write-UcmLog -Message "Found $global:NumberAndDisabled On-Prem users with a phone number and disabled in Skype/Lync" -Severity 2 -Component $function Write-UcmLog -Message "Found $global:NumberAndNoEV On-Prem users with a phone number, but Enterprise Voice is Disabled" -Severity 2 -Component $function Write-UcmLog -Message "Found $global:EVwithNoNumber On-Prem users with Enterprise Voice Enabled, but no phone number set" -Severity 2 -Component $function Write-UcmLog -Message "Found $global:BadObjects Objects with errors" -Severity 2 -Component $function If ($NormalLength -ne 0) { Write-UcmLog -Message "$global:IncorrectLength users with a phone number that is not $NormalLength digits long" -Severity 2 -Component $function } if ($Debuging) { $NumberObjects | Export-Csv -Path NumberObjects.csv -NoTypeInformation $NumberObjects | Out-GridView $NumberBlocks | Export-Csv -Path NumberBlocks.csv -NoTypeInformation $NumberBlocks | Out-GridView $NumberBlocks | Format-Table * } }#End of main function Function Merge-CsNumberObject { Param ( [Parameter(Position = 1)] $Objects, [Parameter(Position = 2)] [ValidateSet('Users', 'MeetingRooms', 'AnalogDevices', 'CommonAreaPhones', 'ExchangeUmContacts', 'DialInConferencingAccessNumber', 'TrustedAppEndpoints', 'RgsWorkflow', 'RgsAgents')] $ObjectType ) #check we actually got something If ($Objects.count -eq 0) { Write-UcmLog -Message "No $ObjectType Found, Skipping..." -Severity 2 -Component $function Return } #Create a Progress Report for the filtering loop #Setup progress variables $ObjectCount = $objects.count $CurrentObject = 0 $maxI = 250 $StartTime = Get-Date Write-UcmLog -Message "Categorizing $ObjectType data..." -Severity 2 -Component $function :ObjectFilterLoop Foreach ($Object in $Objects) #Object Loop { $HumanNumber = $null #Capture data for time estimate, we have to do this now as we might exit early. if ($CurrentObject -ge 1) { $ElapsedTime = $(Get-Date) - $StartTime #do the ratios and "the math" to compute the Estimated Time Of Completion $EstimatedTotalSeconds = $ObjectCount / $CurrentObject * $ElapsedTime.TotalSeconds $EstimatedTotalSecondsTS = New-TimeSpan -Seconds $EstimatedTotalSeconds $EstimatedCompletionTime = $StartTime + $EstimatedTotalSecondsTS $EstimatedTimeLeft = $EstimatedCompletionTime - (Get-Date) #Give us a human readable time $Eta = ($EstimatedTimeLeft.ToString('mm\:ss')) } $CurrentObject ++ Write-Progress -Activity "Processing $ObjectType" -Status "$ObjectType $CurrentObject of $ObjectCount., Remaining Time $ETA / Completion @ $EstimatedCompletionTime" -PercentComplete ((($CurrentObject) / $ObjectCount) * 100) #User Specific Code If ($ObjectType -eq 'Users') { if ($Object.HostingProvider -ne 'SRV:') { #Check to see if object is actually is in SFBO/Teams, if ($Object.HostingProvider -ne 'sipfed.online.lync.com') { $global:BadObjects ++ Write-UcmLog -Message "Object $($Object.SipAddress) has an invalid Hosting Provider! is '$($Object.HostingProvider)' Should be 'SRV:' for OnPrem and 'sipfed.online.lync.com' for Cloud Hosted. Skipping Object!" -Severity 3 -Component $function Continue ObjectFilterLoop #Breaks out of Filter Loop } #object is in SFBO/Teams, check their number If (($Object.Lineuri -eq '') -and ($Object.PrivateLine -eq '')) { $global:CloudUsersWithoutEV ++ $global:NumberObjects += [NumberObject]@{ SipAddress = $Object.SipAddress; Identity = $Object.Identity; DisplayName = $Object.DisplayName; PstnNumber = "$null" ; NumberRange = "$null"; IsWeird = $false ; WhyWeird = ''; Source = 'Get-CsUser'; ObjectType = $ObjectType } Continue ObjectFilterLoop #Breaks out of Filter Loop } else { #We have a number, Covert to a human readable format $HumanNumber = $Object.Lineuri.replace('tel:+', '') $HumanNumber = ($HumanNumber -Split (';'))[0] $global:CloudUsersWithEV ++ $global:NumberObjects += [NumberObject]@{ SipAddress = $Object.SipAddress; Identity = $Object.Identity; DisplayName = $Object.DisplayName; PstnNumber = $humannumber ; NumberRange = ($humannumber.Substring(0, $humannumber.length - 2)) ; IsWeird = $False ; WhyWeird = ''; Source = 'Get-CsUser' ; ObjectType = $ObjectType } Continue ObjectFilterLoop #Breaks out of Filter Loop } } } else { #Do nothing, we will handle it below. } #check to see if we actually have a number If ($Object.Lineuri -eq '') { #Object doesnt have a number, check to see if it has EV If ($Object.EnterpriseVoiceEnabled -eq $true) { $global:NumberObjects += [NumberObject]@{ SipAddress = $Object.SipAddress; Identity = $Object.Identity; DisplayName = $Object.DisplayName; PstnNumber = "$null" ; NumberRange = "$null"; IsWeird = $true ; WhyWeird = "$Object with no number but EV"; ObjectType = $ObjectType } Write-UcmLog -Message "$ObjectType $($Object.Sipaddress) is an on-prem user without a number, but an EV licence. Either remove the EV licence or assign a number" -Severity 3 -Component $function $global:EVwithNoNumber ++ Continue ObjectFilterLoop #Breaks out of Filter Loop } else #On-prem object without a number or EV { $global:NumberObjects += [NumberObject]@{ SipAddress = $Object.SipAddress; Identity = $Object.Identity; DisplayName = $Object.DisplayName; PstnNumber = "$null" ; NumberRange = "$null"; IsWeird = $false ; WhyWeird = ''; ObjectType = $ObjectType } $global:OnPremUsersWithoutEV ++ Continue ObjectFilterLoop #Breaks out of Filter Loop } } #We have a number, Covert to a human readable format $HumanNumber = $Object.Lineuri.replace('tel:+', '') $HumanNumber = ($HumanNumber -Split (';'))[0] #check for PrimaryURI (Used in RGS and Dialin Conf) If ($Object.PrimaryUri) { $Object | Add-Member -NotePropertyName SipAddress -NotePropertyValue $Object.PrimaryUri } #todo, deal with Private line! #Okay, check to see if the object is Disabled, or Not Ev Licenced. if ($ObjectType -eq 'Users' -or $ObjectType -eq 'MeetingRooms' -or $ObjectType -eq 'CommonAreaPhones' -or $ObjectType -eq 'AnalogDevices') { If ($Object.EnterpriseVoiceEnabled -ne $true) { $global:NumberObjects += [NumberObject]@{ SipAddress = $Object.SipAddress; Identity = $Object.Identity; DisplayName = $Object.DisplayName; PstnNumber = $humannumber ; NumberRange = ($humannumber.Substring(0, $humannumber.length - 2)); IsWeird = $true ; WhyWeird = 'Object with Number but no EV'; ObjectType = $ObjectType } Write-UcmLog -Message "Object $($Object.Sipaddress) has an assigned number, but no EV in S4B. The number $HumanNumber can probably be reclaimed" -Severity 2 -Component $function $global:NumberAndNoEV ++ Continue ObjectFilterLoop #No EV, break out } If ($Object.Enabled -ne $true) { $global:NumberObjects += [NumberObject]@{ SipAddress = $Object.SipAddress; Identity = $Object.Identity; DisplayName = $Object.DisplayName; PstnNumber = $humannumber ; NumberRange = ($humannumber.Substring(0, $humannumber.length - 2)); IsWeird = $true ; WhyWeird = 'Disabled Object with Number'; ObjectType = $ObjectType } Write-UcmLog -Message "Object $($Object.Sipaddress) has an assigned number, but is disabled in S4B. The number $HumanNumber can probably be reclaimed" -Severity 2 -Component $function $global:NumberAndDisabled ++ Continue ObjectFilterLoop #Not enabled, break out } } #Check Number Length If ($NormalLength -ne 0) { #Check the length of the human readable number if ($HumanNumber.Length -ne $NormalLength) { $global:NumberObjects += [NumberObject]@{ SipAddress = $Object.SipAddress; Identity = $Object.Identity; DisplayName = $Object.DisplayName; PstnNumber = $humannumber ; NumberRange = ($humannumber.Substring(0, $humannumber.length - 2)); IsWeird = $true ; WhyWeird = 'Number Length Inconsistent'; ObjectType = $ObjectType } Write-UcmLog -Message "Object $($Object.Sipaddress)'s number $humannumber does not match the specified NormalLength of $normalLength" -Severity 2 -Component $function $global:IncorrectLength ++ Continue ObjectFilterLoop #Object stored, break out } } #Number seems okay, mark it as okay and move on $global:NumberObjects += [NumberObject]@{ SipAddress = $Object.SipAddress; Identity = $Object.Identity; DisplayName = $Object.DisplayName; PstnNumber = $humannumber ; NumberRange = ($humannumber.Substring(0, $humannumber.length - 2)); IsWeird = $False ; WhyWeird = ''; ObjectType = $ObjectType } $global:ValidNumbers++ } #End of Object For Each Loop #Cleanup Objects to ensure we dont have problems $Objects = $null } Function Measure-UcmNumberBlock { Param ( [Parameter(Position = 1)] $NumberRange ) #stuff the numberblock full of number objects $NumberRangeContents = ($NumberObjects | where-object -Property NumberRange -eq $NumberRange) [string]$Users = ($NumberRangeContents | where-object -Property ObjectType -eq 'Users').count [string]$MeetingRooms = ($NumberRangeContents | where-object -Property ObjectType -eq 'MeetingRooms').count [string]$AnalogDevices = ($NumberRangeContents | where-object -Property ObjectType -eq 'AnalogDevices').count [string]$CommonAreaPhones = ($NumberRangeContents | where-object -Property ObjectType -eq 'CommonAreaPhones').count [string]$ExchangeUmContacts = ($NumberRangeContents | where-object -Property ObjectType -eq 'ExchangeUmContacts').count [string]$DialInConferencingAccessNumber = ($NumberRangeContents | where-object -Property ObjectType -eq 'DialInConferencingAccessNumber').count [string]$TrustedAppEndpoints = ($NumberRangeContents | where-object -Property ObjectType -eq 'TrustedAppEndpoints').count [string]$RgsWorkflow = ($NumberRangeContents | where-object -Property ObjectType -eq 'RgsWorkflow').count [string]$TotalNumbersUsed = $NumberRangeContents.count #[NumberObject]$RgsAgents = $NumberRangeContents | where-object -Property ObjectType -eq 'RgsAgents' #Now put it into numberblocks $global:NumberBlocks += [NumberBlock]@{Identity = $NumberRange; Users = $Users; MeetingRooms = $MeetingRooms; AnalogDevices = $AnalogDevices ; CommonAreaPhones = $CommonAreaPhones ; ExchangeUmContacts = $ExchangeUmContacts ; DialInConferencingAccessNumber = $DialInConferencingAccessNumber; TrustedAppEndpoints = $TrustedAppEndpoints ; RgsWorkflow = $RgsWorkflow; TotalNumbersUsed = $TotalNumbersUsed} #Old way trying to cast the whole object for deeper diving, mybe we make a new function for auditing? this is just measuring after all <# [NumberObject]$Users = ($NumberRangeContents | where-object -Property ObjectType -eq 'Users') [NumberObject]$MeetingRooms = ($NumberRangeContents | where-object -Property ObjectType -eq 'MeetingRooms') [NumberObject]$AnalogDevices = ($NumberRangeContents | where-object -Property ObjectType -eq 'AnalogDevices') [NumberObject]$CommonAreaPhones = ($NumberRangeContents | where-object -Property ObjectType -eq 'CommonAreaPhones') [NumberObject]$ExchangeUmContacts = ($NumberRangeContents | where-object -Property ObjectType -eq 'ExchangeUmContacts') [NumberObject]$DialInConferencingAccessNumber = ($NumberRangeContents | where-object -Property ObjectType -eq 'DialInConferencingAccessNumber') [NumberObject]$TrustedAppEndpoints = ($NumberRangeContents | where-object -Property ObjectType -eq 'TrustedAppEndpoints') [NumberObject]$RgsWorkflow = ($NumberRangeContents | where-object -Property ObjectType -eq 'RgsWorkflow') #[NumberObject]$RgsAgents = $NumberRangeContents | where-object -Property ObjectType -eq 'RgsAgents' #> } |