forked from NunoFilipeMota/PublicScripts
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathGetMeetingRoomStats_GraphAPI.ps1
More file actions
424 lines (354 loc) · 20.5 KB
/
GetMeetingRoomStats_GraphAPI.ps1
File metadata and controls
424 lines (354 loc) · 20.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
# Script: GetMeetingRoomStats_GraphAPI.ps1
# Purpose: Gather statistics regarding meeting room usage from Exchange Online
# Author: Nuno Mota
# Date: December 2020
# Version: 0.1 - 20200224 - First draft
# 0.2 - 20201231 - Updated to use "list places" instead of findRoom (https://docs.microsoft.com/en-us/graph/api/place-list?view=graph-rest-beta&tabs=http)
# 0.3 - 20210927 - Updated to query URI to retrieve the details of more than 100 meeting rooms
<#
.SYNOPSIS
Gather statistics regarding meeting room usage from Exchange Online
.DESCRIPTION
This script uses Graph API to connect to one or more meeting rooms and gather statistics regarding their usage between specific dates.
Although the script is targeted at meeting rooms, it will work with any mailbox default calendar in Exchange Online.
IMPORTANT:
- To analyze a particular meeting room, specify one or more primary SMTP addresses in the format: 'room1@domain.com, room2@domain.com'. Alternatively, analyze all meeting rooms by using the "-All" switch;
- You will need to have, or create, an 'app registration' in Azure and create a 'client secret';
- The app registration will need the following API permissions to Graph API: 'User.Read.All', 'Calendars.Read', and 'Place.Read.All', all of type 'Application';
- Maximum range to search is 1825 days (5 years);
- You can enter the dates in the format "22/02/2020", "22/02/2020 15:00", or in ISO 8601 format such as "2020-02-22T15:00:00", or even "2020-02-22T15:00:00-08:00" to specify an offset to UTC (time zone).
The script gathers and exports the following stats for each meeting room for the given date range:
- RoomName: the display name of the meeting room (when using -All). When using -RoomListSMTP, this will be the room's SMTP address;
- RoomSMTP: the SMTP address of the meeting room;
- From: the start of the date range to search the calendar;
- To: the end of the date range to search the calendar;
- totalMeetings: the total number of meetings;
- totalDuration: the total number of minutes for all meetings;
- totalAttendees: the total number of attendees invited across all meetings;
- totalUniqueOrganizers: the number of unique meeting organizers;
- totalUniqueAttendees: the number of unique attendees;
- totalReqAttendees: the total number of required attendees;
- totalOptAttendees: the total number of optional attendees;
- Top5Organizers: the email address of the top 5 meeting organizers, and how many meetings each scheduled;
- Top5Attendees: the email address of the top 5 meeting attendees, and how many meetings each attended;
- totalAllDay: the total number of 'all-day' meetings;
- totalAM: the total number of meetings that started in the morning (this excludes all-day meetings);
- totalPM: the total number of meetings that started in the afternoon;
- totalRecurring: the total number of recurring meetings;
- totalSingle: the total number of non-recurring meetings (single instance/occurrence).
.PARAMETER From
The start of the date range to search the calendar.
You can enter the dates in the format "22/02/2020", "22/02/2020 15:00", or in ISO 8601 format such as "2020-02-22T15:00:00", or even "2020-02-22T15:00:00-08:00" to specify an offset to UTC (time zone).
.PARAMETER To
The end of the date range to search the calendar.
You can enter the dates in the format "22/02/2020", "22/02/2020 15:00", or in ISO 8601 format such as "2020-02-22T15:00:00", or even "2020-02-22T15:00:00-08:00" to specify an offset to UTC (time zone).
.PARAMETER All
When using this switch, the scripts retrieves all the rooms in the tenant using the "list places" method (as oposed to using "findRooms" as in a previous version of the script;
.PARAMETER RoomListSMTP
The SMTP address of one or more meeting rooms to process.
When using this parameter, only the default calendar for the meeting room will be analyzed.
.PARAMETER ClientID
The Application (client) ID of the app registration in Azure AD with required permissions.
.PARAMETER ClientSecret
The secret string that the application uses to prove its identity when requesting a token. Also can be referred to as application password.
.PARAMETER TenantID
The Azure Directory (tenant) ID.
.OUTPUTS
The script prints to the screen the stats for each meeting room, and exports them to a CSV file in the same location of the script.
.LINK
Online version: https://github.com/NunoFilipeMota/PublicScripts/blob/main/GetMeetingRoomStats_GraphAPI.ps1
.EXAMPLE
C:\PS> .\Get-MeetingRoomStats_GraphAPI.ps1 -All -From "01/01/2020" -To "01/02/2020"
Description
-----------
This command will:
1. Process up to 100 meetings rooms in the environment;
2. Gather statistics for all rooms for the month of January (please be aware of your date format: day/month vs month/day), using UTC format for the time;
3. Print all stats to the screen and export them to a CSV file.
.EXAMPLE
C:\PS> .\GetMeetingRoomStats_GraphAPI.ps1 -RoomListSMTP "room.1@domain.com" -From "2020-02-01T00:00:00-08:00" -To "2020-03-01T00:00:00-08:00"
Description
-----------
This command will:
1. Process room.1@domain.com meeting room;
2. Gather statistics for the month of February, with a time offset of -8h compared to UTC;
3. Print all stats to the screen and export them to a CSV file.
.EXAMPLE
C:\PS> .\GetMeetingRoomStats_GraphAPI.ps1 -RoomListSMTP "room.1@domain.com, "room.2@domain.com" -From "2020-02-01T00:00:00-08:00" -To "2020-03-01T00:00:00-08:00"
Description
-----------
This command will:
1. Process the meeting rooms 'room.1@domain.com' and 'room.2@domain.com';
2. Gather statistics for each room for the month of February, with a time offset of -8h compared to UTC;
3. Print all stats to the screen and export them to a CSV file.
.EXAMPLE
C:\PS> Get-Help .\GetMeetingRoomStats_GraphAPI.ps1 -Full
Description
-----------
Shows this help manual.
#>
[CmdletBinding()]
Param (
[Parameter(Mandatory = $False)]
[String] $From = "2021-03-01T00:00:00",
[Parameter(Mandatory = $False)]
[String] $To = "2021-03-31T00:00:00",
[Parameter(Mandatory = $False)]
[Switch] $All,
[Parameter(Mandatory = $False)]
[String] $RoomListSMTP,
[Parameter(Mandatory = $False)]
[String] $ClientID = "",
[Parameter(Mandatory = $False)]
[String] $ClientSecret = "",
[Parameter(Mandatory = $False)]
[String] $TenantID = ""
)
#####################################################################################################
# Function to write all the actions performed by the script to a log file
#####################################################################################################
Function Write-Log {
[CmdletBinding()]
Param ([String] $Type, [String] $Message)
$Logfile = $PSScriptRoot + "\GetMeetingRoomStats_Log_$(Get-Date -f 'yyyyMM').txt"
If (!(Test-Path $Logfile)) {New-Item $Logfile -Force -ItemType File | Out-Null}
$timeStamp = (Get-Date).toString("yyyy/MM/dd HH:mm:ss")
"$timeStamp $Type $Message" | Out-File -FilePath $Logfile -Append
Switch ($Type) {
"INF" {Write-Host $Message -ForegroundColor Green -BackgroundColor Black}
"WRN" {Write-Host $Message -ForegroundColor Yellow -BackgroundColor Black}
"ERR" {Write-Host $Message -ForegroundColor Red -BackgroundColor Black}
default {Write-Host $Message}
}
}
#####################################################################################################
# Function to get OAuth Token
#####################################################################################################
Function Get-OAuthToken {
# Construct URI for OAuth Token and Body for OAuth Token
$uri = "https://login.microsoftonline.com/$TenantID/oauth2/v2.0/token"
$body = @{
client_id = $ClientID
scope = "https://graph.microsoft.com/.default"
client_secret = $ClientSecret
grant_type = "client_credentials"
}
# Get OAuth Token
Try {
$tokenRequest = Invoke-RestMethod -Method Post -Uri $uri -ContentType "application/x-www-form-urlencoded" -Body $body -ErrorAction Stop
Write-Log -Type "INF" -Message "Retrieved OAuth Token"
# Get token expiration date and time so we can renew it 2 minutes before it expires
$global:tokenExpireDateTime = ((Get-Date).AddSeconds($tokenRequest.expires_in)).AddSeconds(-120)
Return $tokenRequest.access_token
} Catch [System.Net.WebException] {
Write-Log -Type "ERR" -Message "Unable to get token: '$($_.Exception.Message)'"
Exit
}
}
#####################################################################################################
# Function to query Graph API
#####################################################################################################
Function Query-GraphAPI {
Param ($uri, $token)
# Check if we need to renew our token
If ((Get-Date) -gt $global:tokenExpireDateTime) {$token = Get-OAuthToken}
[Bool] $stopLoop = $False
[Int32] $retryCount = 1
Do {
Try {
$response = Invoke-RestMethod -Method Get -Uri $uri -ContentType "application/json" -Headers @{Authorization = "Bearer $token"} -ErrorAction Stop
$stopLoop = $True
} Catch {
# If we get throttled, then we sleep for 15, 30, or 45 seconds before giving up
If ($_.Exception.Response.StatusCode -eq 429) {
If ($retryCount -ge 3){
Write-Log -Type "ERR" -Message "Unable to query Graph API: '$($_.Exception.Message)'"
$stopLoop = $True
Return $False
} Else {
Write-Log -Type "WRN" -Message "Unable to query Graph API: '$($_.Exception.Message)'. Retrying in $($retryCount * 15) seconds."
Start-Sleep -Seconds $($retryCount * 15)
$retryCount++
}
} ElseIf ($_ -match "REST API is not yet supported for this mailbox.") {
# This error means that the meeting room hasn't been migrated to Exchange Online yet
Write-Log -Type "WRN" -Message "$($room.nickname) has not yet been migrated to Exchange Online."
} Else {
Write-Log -Type "ERR" -Message "Unable to query Graph API: '$($_.Exception.Message)'"
}
# Write-Host $_ # Uncomment if you want to print further error details
Return $False
}
} While (!$stopLoop)
# Check if there are more results to retrieve (paging). The 'findRooms' method is limited to 100...
$fullResponse = @()
If ($response.Value) {
$fullResponse += $response.Value
If ($response."@odata.nextLink") {
Do {
[Bool] $stopLoop = $False
[Int32] $retryCount = 1
Do {
Try {
$response = Invoke-RestMethod -Uri $response."@odata.nextLink" -Headers @{Authorization = "Bearer $token"} -ErrorAction Stop
$stopLoop = $True
} Catch {
# If we get throttled, then we sleep for 15, 30, or 45 seconds before giving up
If ($_.Exception.Response.StatusCode -eq 429) {
If ($retryCount -ge 3){
Write-Log -Type "ERR" -Message "Unable to query Graph API: '$($_.Exception.Message)'"
$stopLoop = $True
$fullResponse += $response.Value
Return $fullResponse # Return incomplete results or no results at all?
} Else {
Write-Log -Type "WRN" -Message "Unable to query Graph API: '$($_.Exception.Message)'. Retrying in $($retryCount * 15) seconds."
Start-Sleep -Seconds $($retryCount * 15)
$retryCount++
}
} ElseIf ($_ -match "REST API is not yet supported for this mailbox.") {
# This error means that the meeting room hasn't been migrated to Exchange Online yet
Write-Log -Type "WRN" -Message "$($room.name) has not yet been migrated to Exchange Online."
} Else {
Write-Log -Type "ERR" -Message "Unable to query Graph API: '$($_.Exception.Message)'"
}
# Write-Host $_ # Uncomment if you want to print further error details
Return $False
}
} While (!$stopLoop)
$fullResponse += $response.Value
} While ($response."@odata.nextLink")
}
} Else {$fullResponse = $response.Value}
Return $fullResponse
}
#####################################################################################################
# Script Start
#####################################################################################################
Write-Log -Type "INF" -Message "--------------------------------------------------------------------------"
Write-Log -Type "INF" -Message "START. Running under '$($env:UserName)' from '$($env:ComputerName)'."
#####################################################################################################
# Basic parameter validation
If (!$ClientID -OR !$ClientSecret -OR !$TenantID) {
Write-Log -Type "ERR" -Message "You must use the -ClientID -ClientSecret AND -TenantID parameters. Exiting Script."
Exit
}
If (!$All -and !$RoomListSMTP) {
Write-Log -Type "ERR" -Message "Please use -All or -RoomListSMTP parameters. Exiting Script."
Exit
} ElseIf ($All -and $RoomListSMTP) {
Write-Log -Type "ERR" -Message "Please use only -All or -RoomListSMTP parameters, not both. Exiting Script."
Exit
}
# Validate date range and convert date to ISO 8601 format, if not already in that format
If (((Get-Date $To) - (Get-Date $From)).TotalDays -gt 1825) {Write-Log -Type "ERR" -Message "The range between the start and end dates cannot be greater then 1,825 days (5 years)! Exiting script."; Exit}
Try {
$From = Get-Date $From -Format s -ErrorAction Stop
$To = Get-Date $To -Format s -ErrorAction Stop
} Catch {
Write-Log -Type "ERR" -Message "Unable to convert date to ISO 8601 format: '$($_.Exception.Message)'"
Exit
}
#####################################################################################################
# Retrieve OAuth Token
$token = Get-OAuthToken
#####################################################################################################
# If user only wants to analyse specific rooms, save the rooms details into another variable. This is
# just so we don't have to check which option was used and keep the ForEach simple
If ($RoomListSMTP) {
[Array] $allRooms = @()
ForEach ($room in $RoomListSMTP.Split(",") -replace (" ", "")) {
$allRooms += [PSCustomObject] @{emailAddress = $room; nickname = $room}
}
}
# If user selected -All, then gather all metting rooms in the tenant
If ($All) {
# Retrieve all meeting rooms from the tenant
# $allRooms = Query-GraphAPI -URI "https://graph.microsoft.com/beta/users/$user/findRooms" -Token $token
$allRooms = Query-GraphAPI -URI "https://graph.microsoft.com/beta/places/microsoft.graph.room?top=999" -Token $token
Write-Log -Type "INF" -Message "Retrieved $(($allRooms | Measure).Count) Meeting Rooms"
If (!$allRooms) {Exit}
}
#####################################################################################################
# Gather the meetings for the selected room(s)
[Int] $count = 0
ForEach ($room in $allRooms) {
Write-Progress -Activity "Processing Meeting Room Calendars" -Status "Processed ($("{0:N0}" -f $count) / $("{0:N0}" -f $($allRooms | Measure).Count)). Current calendar: '$($room.name)'"
# Check if we need to renew our token
If ((Get-Date) -gt $global:tokenExpireDateTime) {$token = Get-OAuthToken}
# Get all room meetings for the given time period
$allMeetings = Query-GraphAPI -URI "https://graph.microsoft.com/beta/users/$($room.emailAddress)/calendar/calendarView?startDateTime=$From&endDateTime=$To" -Token $token
If (!$allMeetings) {Continue}
$totalMeetings = ($allMeetings | Measure).Count
If ($totalMeetings -eq 0) {
Write-Log -Type "WRN" -Message "0 meetings retrieved from '$($room.nickname)'"
Continue
} Else {
Write-Log -Type "INF" -Message "$totalMeetings meetings retrieved from '$($room.nickname)'"
}
[Int] $totalDuration = $totalAttendees = $totalReqAttendees = $totalOptAttendees = $totalAllDay = $totalAM = $totalPM = $totalRecurring = $totalSingle = 0
$topOrganizers = @{}
$topAttendees = @{}
ForEach ($meeting in $allMeetings) {
# Top Organizers
$organizer = $meeting.organizer.emailAddress.address
If ($organizer -and $topOrganizers.ContainsKey($organizer)) {
$topOrganizers.Set_Item($organizer, $topOrganizers.Get_Item($organizer) + 1)
} Else {
$topOrganizers.Add($organizer, 1)
}
# Top Required Attendees
ForEach ($attendee in ($meeting.attendees | ? {$_.Type -ne "resource"})) {
$attendee = $attendee.emailAddress | % {$_.address}
If (!$attendee) {Continue}
If ($topAttendees.ContainsKey($attendee)) {
$topAttendees.Set_Item($attendee, $topAttendees.Get_Item($attendee) + 1)
} Else {
$topAttendees.Add($attendee, 1)
}
}
# Gather other stats
$totalDuration += ((Get-Date $meeting.end.dateTime) - (Get-Date $meeting.start.dateTime)).TotalMinutes
$totalAttendees += ($meeting.attendees | ? {$_.Type -ne "resource"} | Measure).Count
$totalReqAttendees += ($meeting.attendees | ? {$_.Type -eq "required"} | Measure).Count
$totalOptAttendees += ($meeting.attendees | ? {$_.Type -eq "optional"} | Measure).Count
If ($meeting.isAllDay) {$totalAllDay++} Else {If ((Get-Date $meeting.start.dateTime -UFormat %p) -eq "AM") {$totalAM++} Else {$totalPM++}}
If ($meeting.type -eq "occurrence") {$totalRecurring++} Else {$totalSingle++}
# # If you want to capture the details of each meeting
# [PSCustomObject] @{
# Start = (Get-Date $meeting.start.dateTime).ToString() + " " + $meeting.start.timeZone
# End = (Get-Date $meeting.end.dateTime).ToString() + " " + $meeting.end.timeZone
# Subject = $meeting.subject
# importance = $meeting.importance
# isAllDay = $meeting.isAllDay
# MeetingRoom = $meeting.locations.locationUri -Join "; "
# attendees = ($meeting.attendees.emailAddress | % {$_.address}) -Join "; "
# organizer = $meeting.organizer.emailAddress.address
# type = $meeting.type
# hasAttachments = $meeting.hasAttachments
# }
}
$roomsObj = [PSCustomObject] @{
RoomName = $room.nickname
RoomSMTP = $room.emailAddress
From = Get-Date $From
To = Get-Date $To
totalMeetings = "{0:N0}" -f $totalMeetings
totalDuration = "{0:N0}" -f $totalDuration
totalAttendees = "{0:N0}" -f $totalAttendees
totalUniqueOrganizers = If ($topOrganizers) {($topOrganizers.GetEnumerator() | Select Name | Measure).Count} Else {""}
totalUniqueAttendees = If ($topAttendees) {($topAttendees.GetEnumerator() | Select Name | Measure).Count} Else {""}
totalReqAttendees = $totalReqAttendees
totalOptAttendees = $totalOptAttendees
Top5Organizers = If ($topOrganizers) {($topOrganizers.GetEnumerator() | Sort -Property Value -Descending | Select -First 5 | % {"$($_.Key) ($($_.Value))"}) -Join ", "} Else {""}
Top5Attendees = If ($topAttendees) {($topAttendees.GetEnumerator() | Sort -Property Value -Descending | Select -First 5 | % {"$($_.Key) ($($_.Value))"}) -Join ", "} Else {""}
totalAllDay = "{0:N0}" -f $totalAllDay
totalAM = "{0:N0}" -f $totalAM
totalPM = "{0:N0}" -f $totalPM
totalRecurring = "{0:N0}" -f $totalRecurring
totalSingle = "{0:N0}" -f $totalSingle
}
$roomsObj
$roomsObj | Export-CSV "GetMeetingRoomStats_$(Get-Date -f 'yyyyMMdd').csv" -NoType -Append
$count++
}
Write-Log -Type "INF" -Message "END."