functions/Invoke-DbaLogShippingRecovery.ps1

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
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
Function Invoke-DbaLogShippingRecovery
{
<#
    .SYNOPSIS
        Invoke-DbaLogShippingRecovery recovers log shipped databases to a normal state to act upon a migration or disaster.
     
    .DESCRIPTION
        By default all the databases for a particular instance are recovered.
        If the database is in the right state, either standby or recovering, the process will try to recover the database.
         
        At first the function will check if the backup source directory can still be reached.
        If so it will look up the last transaction log backup for the database. If that backup file is not the last copied file the log shipping copy job will be started.
        If the directory cannot be reached for the function will continue to the restoring process.
        After the copy job check is performed the job is disabled to prevent the job to run.
         
        For the restore the log shipping status is checked in the msdb database.
        If the last restored file is not the same as the last file name found, the log shipping restore job will be executed.
        After the restore job check is performed the job is disabled to prevent the job to run
         
        The last part is to set the databse online by restoring the databases with recovery
     
    .PARAMETER SqlInstance
        SQLServer name or SMO object representing the SQL Server to connect to
     
    .PARAMETER Database
        Database to perform the restore for. This value can also be piped enabling multiple databases to be recovered.
        If this value is not supplied all databases will be recovered.
     
    .PARAMETER SqlCredential
        Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. To use:
         
        $scred = Get-Credential, then pass $scred object to the -SqlCredential parameter.
         
        Windows Authentication will be used if SqlCredential is not specified. SQL Server does not accept Windows credentials being passed as credentials.
        To connect as a different Windows user, run PowerShell as that user.
     
    .PARAMETER NoRecovery
        Allows you to choose to not restore the database to a functional state (Normal) in the final steps of the process.
        By default the database is restored to a functional state (Normal).
     
    .PARAMETER Silent
        Use this switch to disable any kind of verbose messages
     
    .PARAMETER Force
        Use this parameter to force the function to continue and perform any adjusting actions to successfully execute
     
    .PARAMETER Delay
        Set the delay in seconds to wait for the copy and/or restore jobs.
        By default the delay is 5 seconds
     
    .PARAMETER WhatIf
        Shows what would happen if the command were to run. No actions are actually performed.
     
    .PARAMETER Confirm
        Prompts you for confirmation before executing any changing operations within the command.
     
    .EXAMPLE
        Invoke-DbaLogShippingRecovery -SqlServer server1
         
        Recovers all the databases on the instance that are enabled for log shiping
     
    .EXAMPLE
        Invoke-DbaLogShippingRecovery -SqlServer server1 -SqlCredential $cred -Verbose
         
        Recovers all the databases on the instance that are enabled for log shiping using a credential
     
    .EXAMPLE
        Invoke-DbaLogShippingRecovery -SqlServer server1 -database db_logship -Verbose
         
        Recovers the database "db_logship" to a normal status
     
    .EXAMPLE
        db1, db2, db3, db4 | Invoke-DbaLogShippingRecovery -SqlServer server1 -Verbose
         
        Recovers the database db1, db2, db3, db4 to a normal status
     
    .EXAMPLE
        Invoke-DbaLogShippingRecovery -SqlServer server1 -WhatIf
         
        Shows what would happen if the command were executed.
     
    .NOTES
        Author: Sander Stad (@sqlstad), sqlstad.nl
        Requires: sysadmin access on SQL Servers
         
        dbatools PowerShell module (https://dbatools.io, clemaire@gmail.com)
        Copyright (C) 2016 Chrissy LeMaire
         
        This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
         
        This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
         
        You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>.
     
    .LINK
        https://dbatools.io/Invoke-DbaLogShippingRecovery
#>

    [CmdletBinding(SupportsShouldProcess = $true)]
    param
    (
        [Parameter(Position = 1)]
        [Alias("ServerInstance", "SqlServer")]
        [object]$SqlInstance,
        [Parameter(Mandatory = $false, Position = 2, ValueFromPipeline = $true)]
        [object[]]$Database,
        [Parameter(Mandatory = $false, Position = 3)]
        [PSCredential]$SqlCredential,
        [Parameter(Mandatory = $false, Position = 4)]
        [switch]$NoRecovery,
        [Parameter(Mandatory = $false, Position = 5)]
        [switch]$Silent,
        [Parameter(Mandatory = $false, Position = 6)]
        [switch]$Force,
        [Parameter(Mandatory = $false, Position = 7)]
        [int]$Delay = 5
    )
    
    begin
    {
        # Setting the CIM session options
        $SessionOption = New-CimSessionOption -Protocol DCom
    }
    # TODO - ADD -All switch and docs
    process
    {
        if (!$sqlinstance -and !$database.name)
        {
            # You can prolly do this with
            Stop-Function -Message "You must pipe an SMO database object or specify SqlInstance"
            return
        }
        
        if ($sqlinstance)
        {
            # Check the instance if it is a named instance
            $servername, $instancename = $sqlinstance.Split("\")
            
            if ($null -eq $instancename)
            {
                $instancename = "MSSQLSERVER"
            }
            
            $islocal = $false
            
            # Check if it's local or remote
            if ($servername -in ".", "localhost", $env:ServerNamename, "127.0.0.1")
            {
                $islocal = $true
            }
            
            Write-Message -Message "Attempting to connect to Sql Server" -Level 2
            try
            {
                $server = Connect-SqlInstance -SqlInstance $sqlinstance -SqlCredential $SqlCredential
            }
            catch
            {
                Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $instance -Continue
                return
            }
            
            if (!$database)
            {
                $database = $server.databases
            }
            else
            {
                $database = $server.databases | Where-Object Name -in $database
            }
        }
        else
        {
            # get the connected SMO object from the $databases
            $firstdb = $database[0]
            $server = $firstdb.Parent
            $servername, $instancename = $server.Name.Split("\")
            
            # Check if it's local or remote
            if ($servername -in ".", "localhost", $env:ServerNamename, "127.0.0.1")
            {
                $islocal = $true
            }
            
            if ($null -eq $instancename)
            {
                $instancename = "MSSQLSERVER"
            }
        }
        
        #region CIM Session creation
        $servername = $server.netname
        Write-Message -Level Verbose -Message "Creating CimSession on $servername over WSMan"
        
        # in case it's piped and already connected
        if ($cimsession.id -eq $null)
        {
            # Check if the CIM session needs to be created using a credential
            if (!$Credential)
            {
                $cimsession = New-CimSession -ComputerName $servername -ErrorAction SilentlyContinue
            }
            else
            {
                $cimsession = New-CimSession -ComputerName $servername -ErrorAction SilentlyContinue -Credential $SqlCredential
            }
            
            # Create a CIM session if it's not yet created
            if ($cimsession.id -eq $null)
            {
                Write-Message -Level Verbose -Message "Creating CimSession on $servername over WSMan failed. Creating CimSession on $servername over DCom"
                
                if (!$Credential)
                {
                    $cimsession = New-CimSession -ComputerName $servername -SessionOption $SessionOption -ErrorAction SilentlyContinue -Credential $SqlCredential
                }
                else
                {
                    $cimsession = New-CimSession -ComputerName $servername -SessionOption $SessionOption -ErrorAction SilentlyContinue
                }
            }
            
            # Check if the CIM session was created successfully
            if ($cimsession.id -eq $null)
            {
                Stop-Function -Message "Can't create CimSession on $servername"
                return
            }
        }
        # Checking the status of the SQL Server Agent service
        Write-Message -Message "Retrieving the status of the SQL Server Agent $secondarydb" -Level 2
        
        # Get the agent service from the CIM session
        try
        {
            $agentservice = Get-CimInstance -CimSession $cimsession -classname Win32_Service -ErrorAction Stop | Where-Object { $_.DisplayName -eq "SQL Server Agent ($instancename)" }
        }
        catch
        {
            if ($_.Exception -match "namespace")
            {
                Stop-Function -Message "Can't get SQL Server Agent Info for '$server'. Unsupported operating system."
            }
            else
            {
                Stop-Function -Message "Can't get SQL Server Agent Info for '$server'. Check logs for more details."
            }
        }
        #endregion CIM Session creation
        
        #region SQL Server Agent check
        # Check if the service is running
        if ($agentservice.State -ne 'Running')
        {
            # Check if the service needs to be started forcefully
            if ($Force)
            {
                try
                {
                    # Start the service
                    Invoke-CimMethod -MethodName StartService -CimSession $cimsession -InputObject $agentservice
                }
                catch
                {
                    # Stop the funcion when the service was unable to start
                    Stop-Function -Message "Unable to start SQL Server Agent Service" -InnerErrorRecord $_ -Target $sqlinstance
                    return
                }
            }
            
            # If the force switch and the silent switch are not set
            if (!$Force -and !$Silent)
            {
                # Set up the parts for the user choice
                $Title = "SQL Server Agent is not running"
                $Info = "Do you want to start the SQL Server Agent service?"
                
                $Options = [System.Management.Automation.Host.ChoiceDescription[]] @("&Start", "&Quit")
                [int]$Defaultchoice = 0
                $choice = $host.UI.PromptForChoice($Title, $Info, $Options, $Defaultchoice)
                
                # Check the given option
                if ($choice -eq 0)
                {
                    try
                    {
                        # Start the service
                         Invoke-CimMethod -MethodName StartService -CimSession $cimsession -InputObject $agentservice
                    }
                    catch
                    {
                        # Stop the funcion when the service was unable to start
                        Stop-Function -Message "Unable to start SQL Server Agent Service" -InnerErrorRecord $_ -Target $sqlinstance
                        return
                    }
                }
                else
                {
                    Stop-Function -Message "The SQL Server Agent service needs to be started to be able to recover the databases" -InnerErrorRecord $_ -Target $sqlinstance
                    return
                }
            }
            
            # If the force switch it not set and the silent switch is set
            if (!$Force -and $Silent)
            {
                Stop-Function -Message "The SQL Server Agent service needs to be started to be able to recover the databases" -InnerErrorRecord $_ -Target $sqlinstance
                return
            }
        }
        #endregion SQL Server Agent check
        
        Write-Message -Message "Started Log Shipping Recovery" -Level 2
        foreach ($db in $database)
        {
            # Query for retrieving the log shipping information
            $query = "SELECT lss.primary_server, lss.primary_database, lsd.secondary_database, lss.backup_source_directory,
            lss.backup_destination_directory, lss.last_copied_file, lss.last_copied_date,
            lsd.last_restored_file, sj1.name AS 'copyjob', sj2.name AS 'restorejob'
    FROM msdb.dbo.log_shipping_secondary AS lss
            INNER JOIN msdb.dbo.log_shipping_secondary_databases AS lsd ON lsd.secondary_id = lss.secondary_id
      INNER JOIN msdb.dbo.sysjobs AS sj1 ON sj1.job_id = lss.copy_job_id
      INNER JOIN msdb.dbo.sysjobs AS sj2 ON sj2.job_id = lss.restore_job_id
    WHERE lsd.secondary_database = '$db'"

            
            
            # Retrieve the log shipping information from the secondary instance
            try
            {
                Write-Message -Message "Retrieving log shipping information from the secondary instance" -Level 5
                $logshipping_details = Invoke-Sqlcmd2 -ServerInstance $server -Database msdb -Query $query
            }
            catch
            {
                Stop-Function -Message "Error retrieving the log shipping details: $($_.Exception.Message)" -InnerErrorRecord $_ -Target $sqlinstance
                
                return
            }
            
            #region Log Shipping Recovery
            # Check if the agent is running
            if ($agentservice.State -ne 'Running')
            {
                Stop-Function -Message "The SQL Server Agent service needs to be started to be able to recover the databases" -InnerErrorRecord $_ -Target $sqlinstance
                return
            }
            else
            {
                # Check if there are any databases to recover
                if ($logshipping_details -eq $null)
                {
                    Stop-Function -Message "The database $db is not configured for log shipping." -Continue
                }
                else
                {
                    # Loop through each of the log shipped databases
                    foreach ($ls in $logshipping_details)
                    {
                        $secondarydb = $ls.secondary_database
                        
                        # Check if the database is in the right state
                        if ($server.Databases[$secondarydb].Status -notin ('Normal, Standby', 'Standby', 'Restoring'))
                        {
                            Write-Message "The database $db doesn't have the right status to be recovered" -Level 3
                        }
                        else
                        {
                            Write-Message -Message "Started Recovery for $secondarydb" -Level 2
                            
                            #region Copy of remaining backup files
                            # Check if the backup source directory can be reached
                            if (Test-Path $ls.backup_source_directory)
                            {
                                # Get the last file from the backup source directory
                                $latestfile = Get-ChildItem -Path $ls.backup_source_directory -filter ("*" + $ls.primary_database + "*") | Where-Object { ($_.Extension -eq '.trn') } | Sort-Object LastWriteTime -Descending | Select-Object -First 1
                                
                                # Check if the latest file is also the latest copied file
                                if ($latestfile.Name -ne ([string]$ls.last_copied_file).Split('\')[-1])
                                {
                                    Write-Message -Message "Synchronizing the latest transaction log backup file(s)" -Level 5
                                    
                                    # Start the job to get the latest files
                                    if ($PSCmdlet.ShouldProcess($sqlinstance, ("Starting copy job $($ls.copyjob)")))
                                    {
                                        $server.JobServer.Jobs[$ls.copyjob].Start()
                                        
                                        Write-Message -Message "Copying files to $($ls.backup_destination_directory)" -Level 5
                                        
                                        # Check if the file has been copied
                                        $query = "SELECT last_copied_file FROM msdb.dbo.log_shipping_secondary WHERE primary_database = '$($ls.primary_database)'"
                                        $latestcopy = Invoke-Sqlcmd2 -ServerInstance $server -Database msdb -Query $query
                                        
                                        Write-Message -Message "Waiting for the copy action to complete.." -Level 5
                                        
                                        while ($latestfile.Name -ne ([string]$latestcopy.last_copied_file).Split('\')[-1])
                                        {
                                            # Sleep for while to let the files be copied
                                            Start-Sleep -Seconds $Delay
                                            
                                            # Again get the latest file to check if the process can continue
                                            $latestcopy = Invoke-Sqlcmd2 -ServerInstance $server -Database msdb -Query $query
                                        }
                                    }
                                    Write-Message -Message "Copying of backup files finished" -Level 5
                                }
                            }
                            else
                            {
                                Write-Message -Message "Couldn't reach the backup source directory. Continuing..." -Level 5
                            }
                            #endregion Copy of remaining backup files
                            
                            # Disable the log shipping copy job on the secondary instance
                            if ($PSCmdlet.ShouldProcess($sqlinstance, "Disabling copy job $($ls.copyjob)"))
                            {
                                Write-Message -Message "Disabling copy job $($ls.copyjob)" -Level 5
                                $server.JobServer.Jobs[$ls.copyjob].IsEnabled = $false
                                $server.JobServer.Jobs[$ls.copyjob].Alter()
                            }
                            
                            # Check if the file has been copied
                            $query = "SELECT last_restored_file FROM dbo.log_shipping_secondary_databases WHERE secondary_database = '$secondarydb'"
                            $latestrestore = Invoke-Sqlcmd2 -ServerInstance $server -Database msdb -Query $query
                            
                            #region Restore of remaining backup files
                            # Check if the last copied file is newer than the last restored file
                            if ($latestfile.Name -ne ([string]$latestrestore.last_restored_file).Split('\')[-1])
                            {
                                Write-Message -Message "Last file found has not yet been restored yet" -Level 5
                                # Start the restore job
                                if ($PSCmdlet.ShouldProcess($sqlinstance, ("Starting restore job " + $ls.restorejob)))
                                {
                                    Write-Message -Message "Starting restore job $($ls.restorejob)" -Level 5
                                    $server.JobServer.Jobs[$ls.restorejob].Start()
                                    
                                    Write-Message -Message "Waiting for the restore action to complete.." -Level 5
                                    
                                    # Check if the jobs is still running
                                    while ($latestfile.Name -ne ([string]$latestrestore.last_restored_file).Split('\')[-1])
                                    {
                                        # Sleep for while to let the files be copied
                                        Start-Sleep -Seconds $Delay
                                        
                                        # Again get the latest file to check if the process can continue
                                        $latestrestore = Invoke-Sqlcmd2 -ServerInstance $server -Database msdb -Query $query
                                    }
                                }
                            }
                            #endregion Restore of remaining backup files
                            
                            # Disable the log shipping restore job on the secondary instance
                            if ($PSCmdlet.ShouldProcess($sqlinstance, "Disabling restore job $($ls.restorejob)"))
                            {
                                Write-Message -Message ("Disabling restore job " + $ls.restorejob) -Level 5
                                $server.JobServer.Jobs[$ls.restorejob].IsEnabled = $false
                                $server.JobServer.Jobs[$ls.restorejob].Alter()
                            }
                            
                            #region Restore database to normal state
                            # Check for the last time if everything is up-to-date
                            if ($latestfile.Name -eq [string]$latestrestore.last_restored_file.Split('\')[-1])
                            {
                                # Check if the database needs to recovered to it's notmal state
                                if ($NoRecovery -eq $false)
                                {
                                    if ($PSCmdlet.ShouldProcess($secondarydb, "Restoring database with recovery"))
                                    {
                                        Write-Message -Message "Restoring the database to it's normal state" -Level 2
                                        $query = "RESTORE DATABASE [$secondarydb] WITH RECOVERY"
                                        Invoke-Sqlcmd2 -ServerInstance $server -Database master -Query $query
                                    }
                                }
                                elseif ($NoRecovery -eq $true) # what else could norecovery be?
                                {
                                    Write-Message -Message "Skipping restore with recovery" -Level 2
                                }
                            }
                            #endregion Restore database to normal state
                        }
                    }
                }
                
                Write-Message -Message ("Finished Recovery for $secondarydb") -Level 2
                
                # Reset the log ship details
                $logshipping_details = $null
            }
            #endregion Log Shipping Recovery
        }
    }
}