-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathRobustCloudCommand.psm1
442 lines (335 loc) · 35.8 KB
/
RobustCloudCommand.psm1
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
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
Function Start-RobustCloudCommand {
<#
.SYNOPSIS
Generic wrapper script that tries to ensure that a script block successfully finishes execution in O365 against a large object count.
Works well with intense operations that may cause throttling
.DESCRIPTION
Wrapper script that tries to ensure that a script block successfully finishes execution in O365 against a large object count.
It accomplishs this by doing the following:
* Monitors the health of the Remote powershell session and restarts it as needed.
* Restarts the session every X number seconds to ensure a valid connection.
* Attempts to work past session related errors and will skip objects that it can't process.
* Attempts to calculate throttle exhaustion and sleep a sufficient time to allow throttle recovery
.PARAMETER ActiveThrottle
Calculated value based on your tenants powershell recharge rate.
You tenant recharge rate can be calculated using a Micro Delay Warning message.
Look for the following line in your Micro Delay Warning Message
Balance: -1608289/2160000/-3000000
The middle value is the recharge rate.
Divide this value by the number of milliseconds in an hour (3600000)
And subtract the result from 1 to get your AutomaticThrottle value
1 - (2160000 / 3600000) = 0.4
Default Value is .25
.PARAMETER IdentifyingProperty
What property of the objects we are processing that will be used to identify them in the log file and host
If the value is not set by the user the script will attempt to determine if one of the following properties is present
"DisplayName","Name","Identity","PrimarySMTPAddress","Alias","GUID"
If the value is not set and we are not able to match a well known property the script will generate an error and terminate.
.PARAMETER LogFile
Location and file name for the log file.
.PARAMETER ManualThrottle
Manual delay of X number of milliseconds to sleep between each cmdlets call.
Should only be used if the AutomaticThrottle isn't working to introduce sufficent delay to prevent Micro Delays
.PARAMETER NonInteractive
Suppresses output to the screen. All output will still be in the log file.
.PARAMETER Recipients
Array of objects to operate on. This can be mailboxes or any other set of objects.
Input must be an array!
Anything comming in from the array can be accessed in the script block using $input.property
.PARAMETER ResetSeconds
How many seconds to run the script block before we rebuild the session with O365.
.PARAMETER ScriptBlock
The script that you want to robustly execute against the array of objects. The Recipient objects will be provided to the cmdlets in the script block
and can be accessed with $input as if you were pipelining the object.
.PARAMETER UserPrincipalName
UPN of the user that will be connecting to Exchange online. Required so that sessions can automatically be set up using cached tokens.
.LINK
https://github.com/Canthv0/RobustCloudCommand
.OUTPUTS
Creates the log file specified in -logfile. Logfile contains a record of all actions taken by the script.
.EXAMPLE
invoke-command -scriptblock {Get-mailbox -resultsize unlimited | select-object -property Displayname,PrimarySMTPAddress,Identity} -session (get-pssession) | export-csv c:\temp\mbx.csv
$mbx = import-csv c:\temp\mbx.csv
$cred = get-Credential
.\Start-RobustCloudCommand.ps1 -UserPrincipalName [email protected] -recipients $mbx -logfile C:\temp\out.log -ScriptBlock {Set-Clutter -identity $input.PrimarySMTPAddress.tostring() -enable:$false}
Gets all mailboxes from the service returning only Displayname,Identity, and PrimarySMTPAddress. Exports the results to a CSV
Imports the CSV into a variable
Gets your O365 Credential
Executes the script setting clutter to off using Legacy Credentials
.EXAMPLE
invoke-command -scriptblock {Get-mailbox -resultsize unlimited | select-object -property Displayname,PrimarySMTPAddress,Identity} -session (get-pssession) | export-csv c:\temp\recipients.csv
$recipients = import-csv c:\temp\recipients.csv
Start-RobustCloudCommand -UserPrincipalName [email protected] -recipients $recipients -logfile C:\temp\out.log -ScriptBlock {Get-MobileDeviceStatistics -mailbox $input.PrimarySMTPAddress.tostring() | Select-Object -Property @{Name = "PrimarySMTPAddress";Expression={$input.PrimarySMTPAddress.tostring()}},DeviceType,LastSuccessSync,FirstSyncTime | Export-Csv c:\temp\stats.csv -Append }
Gets All Recipients and exports them to a CSV (for restart ability)
Imports the CSV into a variable
Executes the script to gather EAS Device statistics and output them to a csv file using ADAL with support for MFA
#>
Param(
[Parameter(Mandatory = $true)]
[string]$LogFile,
[Parameter(Mandatory = $true)]
$Recipients,
[Parameter(Mandatory = $true)]
[ScriptBlock]$ScriptBlock,
[String]$UserPrincipalName,
[int]$ManualThrottle = 0,
[double]$ActiveThrottle = .25,
[int]$ResetSeconds = 870,
[string]$IdentifyingProperty,
[Switch]$NonInteractive,
[String]$Certificate,
[String]$AppID,
[string]$Organization
)
# Turns on strict mode https://technet.microsoft.com/library/03373bbe-2236-42c3-bf17-301632e0c428(v=wps.630).aspx
Set-StrictMode -Version 2
$InformationPreference = "Continue"
$Global:ErrorActionPreference = "Stop"
Write-Log ("Error Action Preference: " + $Global:ErrorActionPreference)
# Log the script block for debugging purposes
Write-log $ScriptBlock
# Setup our first session to O365
$ErrorCount = 0
New-CleanO365Session
# Get when we started the script for estimating time to completion
$ScriptStartTime = Get-Date
[int]$ObjectsProcessed = 0
[int]$ObjectCount = $Recipients.count
# If we don't have an identifying property then try to find one
if ([string]::IsNullOrEmpty($IdentifyingProperty)) {
# Call our function for finding an identifying property and pass in the first recipient object
$IdentifyingProperty = Get-ObjectIdentificationProperty -object $Recipients[0]
}
# Go thru each recipient object and execute the script block
foreach ($object in $Recipients) {
# Set our initial while statement values
$TryCommand = $true
$errorcount = 0
$Global:Error.clear()
# Try the command 3 times and exit out if we can't get it to work
# Record the error and restart the session each time it errors out
while ($TryCommand) {
Write-log ("Running scriptblock for " + ($object.$IdentifyingProperty).tostring())
# Test our connection and rebuild if needed
Write-Log "Testing Session"
Test-O365Session
# Invoke the script block
try {
Write-Log "Invoking Command"
Invoke-Command -InputObject $object -ScriptBlock $ScriptBlock -ErrorAction Stop
# Since we didn't get an error don't run again
$TryCommand = $false
# Increment the object processed count / Estimate time to completion
Write-Log "Updating object count"
$ObjectsProcessed = Get-EstimatedTimeToCompletion -ProcessedCount $ObjectsProcessed -TotalObjects $ObjectCount -StartTime $ScriptStartTime
} catch {
# Handle if we keep failing on the object
if ($errorcount -ge 3) {
Write-Log ("[ERROR] - Object `"" + ($object.$IdentifyingProperty).tostring() + "`" has failed three times!")
Write-Log ("[ERROR] - Skipping Object")
# Increment the object processed count / Estimate time to completion
$ObjectsProcessed = Get-EstimatedTimeToCompletion -ProcessedCount $ObjectsProcessed -StartTime $ScriptStartTime
# Set trycommand to false so we abort the while loop
$TryCommand = $false
}
# Otherwise try the command again
else {
if ($null -eq $Global:Error) {
Write-Log "Global Error Null"
Write-Log ("Local Error: " + $Error)
} else {
Write-Log $Global:Error
}
Write-Log ("Rebuilding session and trying again")
$ErrorCount++
# Create a new session in case the error was due to a session issue
New-CleanO365Session
}
}
}
}
Write-Log "Script Complete Destroying PS Sessions"
# Destroy any outstanding PS Session
Get-PSSession | Remove-PSSession -Confirm:$false
$Global:ErrorActionPreference = "Continue"
Write-Log ("Error Action Preference: " + $Global:ErrorActionPreference)
}
# Writes output to a log file with a time date stamp
Function Write-Log {
Param ([string]$string)
# Get the current date
[string]$date = Get-Date -Format G
# Write everything to our log file
( "[" + $date + "] - " + $string) | Out-File -FilePath $LogFile -Append
# If NonInteractive true then suppress host output
if (!($NonInteractive)) {
Write-Information ( "[" + $date + "] - " + $string)
}
}
# Sleeps X seconds and displays a progress bar
Function Start-SleepWithProgress {
Param([int]$sleeptime)
# Loop Number of seconds you want to sleep
For ($i = 0; $i -le $sleeptime; $i++) {
$timeleft = ($sleeptime - $i);
# Progress bar showing progress of the sleep
Write-Progress -Activity "Sleeping" -CurrentOperation "$Timeleft More Seconds" -PercentComplete (($i / $sleeptime) * 100) -Status " "
# Sleep 1 second
start-sleep 1
}
Write-Progress -Completed -Activity "Sleeping" -Status " "
}
# Setup a new O365 Powershell Session
Function New-CleanO365Session {
# Destroy any outstanding PS Session
Write-Log "Removing all PS Sessions"
Get-PSSession | Remove-PSSession -Confirm:$false
# Force Garbage collection just to try and keep things more agressively cleaned up due to some issue with large memory footprints
[System.GC]::Collect()
# Sleep 15s to allow the sessions to tear down fully
Write-Log ("Sleeping 15 seconds for Session Tear Down")
Start-SleepWithProgress -SleepTime 15
# Clear out all errors
$Error.Clear()
# Create the session
Write-Log "Connecting to Exchange Online"
if (![string]::IsNullOrEmpty($Certificate)) {
Write-Log "Using AppID: $AppID"
Connect-ExchangeOnline -CertificateThumbPrint $Certificate -AppID $AppID -Organization $Organization -UseRPSSession
} else {
Write-Log "Using UserName: $UserPrincipalName"
Connect-ExchangeOnline -UserPrincipalName $UserPrincipalName -ShowBanner:$false -UseRPSSession
}
# Check for an error while creating the session
if ($Error.Count -gt 0) {
Write-Log "[ERROR] - Error while setting up session"
Write-log $Error
# Increment our error count so we abort after so many attempts to set up the session
$ErrorCount++
# if we have failed to setup the session > 3 times then we need to abort because we are in a failure state
if ($ErrorCount -gt 3) {
Write-log "[ERROR] - Failed to setup session after multiple tries"
Write-log "[ERROR] - Aborting Script"
exit
}
# If we are not aborting then sleep 60s in the hope that the issue is transient
Write-Log "Sleeping 60s so that issue can potentially be resolved"
Start-SleepWithProgress -sleeptime 60
# Attempt to set up the sesion again
New-CleanO365Session
}
# If the session setup worked then we need to set $errorcount to 0
else {
$ErrorCount = 0
}
# Set the Start time for the current session
Set-Variable -Scope script -Name SessionStartTime -Value (Get-Date)
}
# Verifies that the connection is healthy
# Goes ahead and resets it every $ResetSeconds number of seconds either way
Function Test-O365Session {
# Get the time that we are working on this object to use later in testing
Write-Log "Getting Data"
$ObjectTime = Get-Date
# Reset and regather our session information
$SessionInfo = $null
Write-Log "Getting Session"
$SessionInfo = Get-PSSession
# Make sure we found a session
if ($null -eq $SessionInfo) {
Write-Log "[ERROR] - No Session Found"
Write-log "Recreating Session"
New-CleanO365Session
}
# Make sure it is in an opened state if not log and recreate
elseif ($SessionInfo.State -ne "Opened") {
Write-Log "[ERROR] - Session not in Open State"
Write-log ($SessionInfo | Format-List | Out-String )
Write-log "Recreating Session"
New-CleanO365Session
}
# If we have looped thru objects for an amount of time gt our reset seconds then tear the session down and recreate it
elseif (($ObjectTime - $SessionStartTime).totalseconds -gt $ResetSeconds) {
Write-Log ("Session Has been active for greater than " + $ResetSeconds + " seconds" )
Write-Log "Rebuilding Connection"
# Estimate the throttle delay needed since the last session rebuild
# Amount of time the session was allowed to run * our activethrottle value
# Divide by 2 to account for network time, script delays, and a fudge factor
# Subtract 15s from the results for the amount of time that we spend setting up the session anyway
[int]$DelayinSeconds = ((($ResetSeconds * $ActiveThrottle) / 2) - 15)
# If the delay is >15s then sleep that amount for throttle to recover
if ($DelayinSeconds -gt 0) {
Write-Log ("Sleeping " + $DelayinSeconds + " addtional seconds to allow throttle recovery")
Start-SleepWithProgress -SleepTime $DelayinSeconds
}
# If the delay is <15s then the sleep already built into New-CleanO365Session should take care of it
else {
Write-Log ("Active Delay calculated to be " + ($DelayinSeconds + 15) + " seconds no addtional delay needed")
}
# new O365 session and reset our object processed count
New-CleanO365Session
} else {
# If session is active and it hasn't been open too long then do nothing and keep going
}
# If we have a manual throttle value then sleep for that many milliseconds
if ($ManualThrottle -gt 0) {
Write-log ("Sleeping " + $ManualThrottle + " milliseconds")
Start-Sleep -Milliseconds $ManualThrottle
}
}
# If the $identifyingProperty has not been set then we attempt to locate a value for tracking modified objects
Function Get-ObjectIdentificationProperty {
Param($object)
Write-Log "Trying to identify a property for displaying per object progress"
# Common properties to check
[array]$PropertiesToCheck = "DisplayName", "Name", "Identity", "PrimarySMTPAddress", "Alias", "GUID"
# Set our counter to 0
$i = 0
[string]$PropertiesString = $null
[bool]$Found = $false
# While we haven't found an ID property continue checking
while ($found -eq $false) {
# If we have gone thru the list then we need to throw an error because we don't have Identity information
# Set the string to bogus just to ensure we will exit the while loop
if ($i -gt ($PropertiesToCheck.length - 1)) {
Write-Log "[ERROR] - Unable to find a common identity parameter in the input object"
# Create an error message that has all of the valid property names that we are looking for
ForEach ($value in $PropertiesToCheck) { [string]$PropertiesString = $PropertiesString + "`"" + $value + "`", " }
$PropertiesString = $PropertiesString.TrimEnd(", ")
[string]$errorstring = "Objects does not contain a common identity parameter " + $PropertiesString + " please use -IdentifyingProperty to set the identity value"
# Throw error
Write-Error -Message $errorstring -ErrorAction Stop
}
# Get the property we are testing out of our array
[string]$Property = $PropertiesToCheck[$i]
# Check the properties of the object to see if we have one that matches a well known name
# If we have found one set the value to that property
if ($null -ne $object.$Property) {
Write-log ("Found " + $Property + " to use for displaying per object progress")
$found = $true
Return $Property
}
# Increment our position counter
$i++
}
}
# Gather and print out information about how fast the script is running
Function Get-EstimatedTimeToCompletion {
param([int]$ProcessedCount, [int]$TotalObjects, [datetime]$StartTime)
# Increment our count of how many objects we have processed
$ProcessedCount++
# Every 100 we need to estimate our completion time and write that out
if (($ProcessedCount % 100) -eq 0) {
# Get the current date
$CurrentDate = Get-Date
# Average time per object in seconds
$AveragePerObject = (((($CurrentDate) - $StartTime).totalseconds) / $ProcessedCount)
# Write out session stats and estimated time to completion
Write-Log ("[STATS] - Total Number of Objects: " + $TotalObjects)
Write-Log ("[STATS] - Number of Objects processed: " + $ProcessedCount)
Write-Log ("[STATS] - Average seconds per object: " + $AveragePerObject)
Write-Log ("[STATS] - Estimated completion time: " + $CurrentDate.addseconds((($TotalObjects - $ProcessedCount) * $AveragePerObject)))
}
# Return number of objects processed so that the variable in incremented
return $ProcessedCount
}