Hello Friends –
Back to you all with my second blog of year 2017 with Logshipping automation. This time picked up a little deviated topics like year 2016. Being a DBA background monitoring, configuring & managing high availability & disaster recovery solution were a day to day responsibility for me, like most of us have/had at some point in time in career. Recently I’d been notified with a challenge for a customer managing highly confidential data. So here is the problem statement.
“While we’re expanding our business, some of high confidential data needs to be procured in secured way from a highly skilled & specialized organization involved into business for years. Challenge is data procured from vendor needs to be up-to-date on daily basis in customer environment, of course without compromising a bit of security.”
First things comes to an experienced DBA in mind, what kind of security model/concerns are being refrained here.
- Secured Bridge between vendor & customer
- Method to push data
- Access to data
- Method of accessing data
- Etc
- Etc
- ….
List is gone be unfathomable at moments. If proper & secured infrastructure is in place, most of the stuff comes to DBA in setting up pull data from vendor environment, push to customer in timely fashion so further analysis and respective tasks take place, seamlessly.
Here is quick flow of purposed solution from my side.
It’s working perfectly for a while and let me provide required set of tools excluding SQL Server Engine, Database & SQL Agent. Pretty much everything is being used in this solution is freeware and doesn’t carry any security issues.
- PowerShell 2.0 and above (free with windows)
- WinSCP (free tool)
- 7-zip (free tool)
***Assumptions
- Vendor is providing daily zip file with a suffix of YYYYMMDD format to distinguish file names.
- SFTP download is running with PPK shared by vendor to customer.
- SQL DB Engine & SQL Agent is running under domain & authentication is well placed.
- Listed tools are installed correctly and able to run seamless. Better to do a test run with wizard mode.
- A DBA database exists on SQL instance to hold historical sync information for back tracking. In my case DB name is SQLMaint.
- Database Mail configuration is in place to send success & failure notification.
Completed PowerShell & T-SQL scripts are mentioned below.
PowerShell script does inline task.
- Download T-log backups consolidated zip file securely from vendor sftp location. Following cases script gets terminated
- SFTP folder doesn’t exists
- Local folder doesn’t exists
- Zip file already downloaded
- Zip file is corrupted
- Unzip downloaded file at given location.
- Kick off T-SQL agent job if expected no of log files unzipped from daily zip file
try { # Load WinSCP .NET assembly Add-Type -Path "C:\Program Files (x86)\WinSCP\WinSCPnet.dll" $sessionOptions = New-Object WinSCP.SessionOptions $sessionOptions.Protocol = [WinSCP.Protocol]::Sftp $sessionOptions.HostName = "192.168.100.121" # Replace with actual host ip address for SFTP $sessionOptions.UserName = "SecuredData" $sessionOptions.Password = "" $sessionOptions.PortNumber = "15000" $sessionOptions.SshPrivateKeyPath = "D:\LogShippingData\SecureDataLinkFile.ppk" $sessionOptions.SshHostKeyFingerprint = "ssh-rsa 2048 d0:03:6g:ef:45:gg:66:3e:22:d5:82:ad:82:ac:10:10" $servername = "XYZ\ABC" $session = New-Object WinSCP.Session try { # Connect $session.Open($sessionOptions) # Variables declarati $stamp = ((Get-Date).AddDays(0).ToString("yyyMMdd")) $fileName = "SecuredData_$stamp.zip" $remotePath = "/VendorSFTPPath/$fileName" $localPath = "D:\LogShippingData\$fileName" #change path if NOT "D:\LogShippingData\" (for PROD run) $UnzipFolderUNCPath = "D:\LogInbox" $UnzipFolderUNCPathfiletype = "*.TRN" if ($session.FileExists($remotePath)) { if (!(Test-Path $localPath)) { Write-Host ( "File {0} exists, local backup {1} does not" -f $remotePath, $localPath) $download = $True } else { $remoteWriteTime = $session.GetFileInfo($remotePath).LastWriteTime $localWriteTime = (Get-Item $localPath).LastWriteTime if ($remoteWriteTime -gt $localWriteTime) { Write-Host ( ("File {0} as well as local backup {1} exist, " + "but remote file is newer ({2}) than local backup ({3})") -f $remotePath, $localPath, $remoteWriteTime, $localWriteTime) $download = $True } else { Write-Host ( ("File {0} as well as local backup {1} exist, " + "but remote file is not newer ({2}) than local backup ({3})") -f $remotePath, $localPath, $remoteWriteTime, $localWriteTime) $download = $False } } # Ensuring download from FTP is completed w/o issues and executing following pieces. if ($download) { # Download the file and throw on any error $session.GetFiles($remotePath, $localPath).Check() Write-Host "Download to backup file done." # Ensuring destination of unzipped files is Empty $Files = Get-ChildItem $UnzipFolderUNCPath -include $UnzipFolderUNCPathfiletype -recurse #| where {$_.CreationTime -le $DelDate_Backup } if ( $Files -eq "" -or $Files.Count -eq 0 ) { write-host " No files to be deleted..." #<-- this doesn't print when no files } else { foreach ($File in $Files) { write-host “Removing file [” $File "] from -->" $UnzipFolderUNCPath Remove-Item $File | out-null } } #Unzipping latest downloaded zip file from eClinical SFTP write-host " Unzipping today's downloaded file i.e. " $fileName $shell = new-object -com shell.application $zip = $shell.NameSpace($localPath) foreach($item in $zip.items()) { $shell.Namespace($UnzipFolderUNCPath).copyhere($item) } $Files = Get-ChildItem $UnzipFolderUNCPath -Recurse | Where-Object {!$_.PSIsContainer} | Measure-Object if ( $Files.Count -ge 90) { write-host " Unzipping completed, kicking off Daily DB Sync job.." #Kick of Daily Sync Job on [XYZ\ABC] SQL instance $SB = "Integrated Security=SSPI;Persist Security Info=False;Initial Catalog=msdb;Data Source=$servername" $SQLcon = New-object system.data.sqlclient.SqlConnection $SQLcon.ConnectionString = $SB $SelectCMD = New-object system.data.sqlclient.SqlCommand $SelectCMD.CommandTimeout = 30 $SelectCMD.Connection = $SQLCon $SelectCMD.CommandText = "exec sp_start_job 'Daily Log Shipping Sync'" $da = new-object System.Data.SqlClient.SQLDataAdapter($SelectCMD) $ds = new-object System.Data.dataset $da.fill($ds) write-host " Daily DB Sync job kicked off at SQL level, please look at [XYZ\ABC]... All steps completed under this execution." $sqlcon.Close() } } } else { Write-Host ("File {0} does not exist yet" -f $remotePath) } } finally { # Disconnect, clean up $session.Dispose() } exit 0 } catch [Exception] { Write-Host $_.Exception.Message exit 1 }
Below T-SQL script does inline task.
- Read all unzipped TRN backup files sequentially to avoid termination due to log sequence no.
- Put header information in a table.
- Restore T-Log backup to Log shipping database based upon sequence no.
- Send alert on success & failure respectively.
USE [msdb] GO BEGIN TRANSACTION DECLARE @ReturnCode INT SELECT @ReturnCode = 0 IF NOT EXISTS (SELECT name FROM msdb.dbo.syscategories WHERE name=N'[Uncategorized (Local)]' AND category_class=1) BEGIN EXEC @ReturnCode = msdb.dbo.sp_add_category @class=N'JOB', @type=N'LOCAL', @name=N'[Uncategorized (Local)]' IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback END DECLARE @jobId BINARY(16) EXEC @ReturnCode = msdb.dbo.sp_add_job @job_name=N'Daily SecuredDB Sync', @enabled=1, @notify_level_eventlog=0, @notify_level_email=0, @notify_level_netsend=0, @notify_level_page=0, @delete_level=0, @description=N'This job reads all T-log backup files in sequential order and push information to Log shipping database.', @category_name=N'[Uncategorized (Local)]', @owner_login_name=N'sa', @job_id = @jobId OUTPUT IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback EXEC @ReturnCode = msdb.dbo.sp_add_jobstep @job_id=@jobId, @step_name=N'Starting Email Notification', @step_id=1, @cmdexec_success_code=0, @on_success_action=3, @on_success_step_id=0, @on_fail_action=2, @on_fail_step_id=0, @retry_attempts=0, @retry_interval=0, @os_run_priority=0, @subsystem=N'TSQL', @command=N'DECLARE @rptdate datetime ,@subject NVARCHAR(200) ,@message NVARCHAR(1000) ,@emails as nvarchar(500) ,@emailsCC as nvarchar(500) ,@dt as varchar(20) SET @dt= REPLACE(CONVERT(NVARCHAR,CAST(getdate() AS DATETIME), 106), '' '', ''-'') --PRINt @dt SET @rptdate = convert(datetime, convert(char, GETDATE(), 101)) SET @subject = N''Daily DB Syc Started: '' + convert(char, GETDATE(), 101) SET @emails = ''Avanish@sqlservergeeks.com'' SET @emailsCC = ''Avanish@sqlservergeeks.com'' SET @message = ''<body> <p><font color="#000080">Please</font> <font color="#FF0000"> DO NOT </font><font color="#000080"> use [SecuredDB] DB till futher notice.</p> <p><font color="#000080">An automated notification will go out based upon completion status.</p> <p><i>This is an automated email, please do not reply back to this email account.</i></p> </body>'' EXEC msdb.dbo.sp_send_dbmail @profile_name = ''SQL Monitor'' ,@body_format = HTML ,@recipients = @emails ,@copy_recipients = @emailsCC ,@body = @message ,@subject = @subject ', @database_name=N'master', @flags=0 IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback EXEC @ReturnCode = msdb.dbo.sp_add_jobstep @job_id=@jobId, @step_name=N'DBSync', @step_id=2, @cmdexec_success_code=0, @on_success_action=4, @on_success_step_id=4, @on_fail_action=4, @on_fail_step_id=3, @retry_attempts=0, @retry_interval=0, @os_run_priority=0, @subsystem=N'TSQL', @command=N'SET NOCOUNT ON USE [SQLMaint] GO --1. Truncate tables before any data load TRUNCATE TABLE [StageFiles] TRUNCATE TABLE [TransLog_Files] --DROP TABLE [HeaderMaster] -- Declare variables -- DECLARE @InsertFileDetails as varchar(1000) ,@LogFileName varchar(500) ,@LogDate varchar(50) ,@getTRNFileName CURSOR ,@getRestoreFile CURSOR DECLARE @headers table ( BackupName varchar(256), BackupDescription varchar(256), BackupType varchar(256), ExpirationDate varchar(256), Compressed varchar(256), Position varchar(256), DeviceType varchar(256), UserName varchar(256), ServerName varchar(256), DatabaseName varchar(256), DatabaseVersion varchar(256), DatabaseCreationDate varchar(256), BackupSize varchar(256), FirstLSN varchar(256), LastLSN varchar(256), CheckpointLSN varchar(256), DatabaseBackupLSN varchar(256), BackupStartDate varchar(256), BackupFinishDate varchar(256), SortOrder varchar(256), CodePage varchar(256), UnicodeLocaleId varchar(256), UnicodeComparisonStyle varchar(256), CompatibilityLevel varchar(256), SoftwareVendorId varchar(256), SoftwareVersionMajor varchar(256), SoftwareVersionMinor varchar(256), SoftwareVersionBuild varchar(256), MachineName varchar(256), Flags varchar(256), BindingID varchar(256), RecoveryForkID varchar(256), Collation varchar(256), FamilyGUID varchar(256), HasBulkLoggedData varchar(256), IsSnapshot varchar(256), IsReadOnly varchar(256), IsSingleUser varchar(256), HasBackupChecksums varchar(256), IsDamaged varchar(256), BeginsLogChain varchar(256), HasIncompleteMetaData varchar(256), IsForceOffline varchar(256), IsCopyOnly varchar(256), FirstRecoveryForkID varchar(256), ForkPointLSN varchar(256), RecoveryModel varchar(256), DifferentialBaseLSN varchar(256), DifferentialBaseGUID varchar(256), BackupTypeDescription varchar(256), BackupSetGUID varchar(256), CompressedBackupSize varchar(256), Containment varchar(256), -- Additional Filed to retain order by Seq int NOT NULL identity(1,1) ); CREATE TABLE #HeaderMaster ( TRNFileName varchar(1000), DatabaseName varchar(256), BackupName varchar(256), BackupDescription varchar(256), BackupType varchar(256), ExpirationDate varchar(256), Compressed varchar(256), BackupSize varchar(256), FirstLSN varchar(256), LastLSN varchar(256), CheckpointLSN varchar(256), DatabaseBackupLSN varchar(256), BackupStartDate varchar(256), BackupFinishDate varchar(256), Seq int NOT NULL identity(1,1) ); --2. Load files into a staging table INSERT INTO [StageFiles] EXEC xp_cmdshell ''dir D:\LogShippingData\*.trn''; --3. Insert files into a better formatted table sorted by date. INSERT INTO [TransLog_Files] SELECT CAST(left(Filename,20) as datetime) AS LogDate ,REVERSE(SUBSTRING(REVERSE(Filename),0,CHARINDEX('' '',REVERSE(Filename)))) as filename FROM [StageFiles] WHERE isnumeric(left(Filename,1))=1 ORDER BY CAST(left(Filename,20) as datetime) asc SET @getTRNFileName = CURSOR FOR SELECT * FROM [TransLog_Files] OPEN @getTRNFileName FETCH NEXT FROM @getTRNFileName INTO @LogDate,@LogFileName WHILE @@FETCH_STATUS = 0 BEGIN SET @InsertFileDetails =''RESTORE HEADERONLY FROM DISK = ''''D:\LogShippingData\''+ @LogFileName +'''''''' INSERT INTO @headers EXEC (@InsertFileDetails) INSERT INTO #HeaderMaster ( TRNFileName, DatabaseName, BackupName, BackupDescription, BackupType, ExpirationDate, Compressed, BackupSize, FirstLSN, LastLSN, CheckpointLSN, DatabaseBackupLSN, BackupStartDate, BackupFinishDate ) SELECT @LogFileName, DatabaseName, BackupName, BackupDescription, BackupType, ExpirationDate, Compressed, BackupSize, FirstLSN, LastLSN, CheckpointLSN, DatabaseBackupLSN, BackupStartDate, BackupFinishDate FROM @headers DELETE FROM @headers SET @InsertFileDetails ='''' FETCH NEXT FROM @getTRNFileName INTO @LogDate,@LogFileName END CLOSE @getTRNFileName DEALLOCATE @getTRNFileName DECLARE @TRNFileName varchar(1000) , @DatabaseName varchar(20) SET @getTRNFileName = CURSOR FOR SELECT TRNFileName FROM [#HeaderMaster] order by LastLSN asc OPEN @getTRNFileName FETCH NEXT FROM @getTRNFileName INTO @LogFileName WHILE @@FETCH_STATUS = 0 BEGIN SET @InsertFileDetails = ''RESTORE LOG [SecuredDB] '' + ''FROM DISK = N''+ '''''''' + ''D:\LogShippingData\'' + @LogFileName + '''''''' + ''WITH FILE = 1, STANDBY = N'' + '''''''' + ''D:\Backup\'' + @LogFileName + ''.BAK'' + '''''''' + '','' + ''NOUNLOAD, STATS = 10;'' --PRINT @InsertFileDetails EXEC (@InsertFileDetails) FETCH NEXT FROM @getTRNFileName INTO @LogFileName END CLOSE @getTRNFileName DEALLOCATE @getTRNFileName --DROP TABLE #HeaderMaster SET NOCOUNT OFF --SELECT * from #HeaderMaster order by LastLSN asc', @database_name=N'master', @output_file_name=N'D:\LOGFILES\DailyDBSync.log', @flags=14 IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback EXEC @ReturnCode = msdb.dbo.sp_add_jobstep @job_id=@jobId, @step_name=N'Failed', @step_id=3, @cmdexec_success_code=0, @on_success_action=2, @on_success_step_id=0, @on_fail_action=2, @on_fail_step_id=0, @retry_attempts=0, @retry_interval=0, @os_run_priority=0, @subsystem=N'TSQL', @command=N'DECLARE @rptdate datetime ,@subject NVARCHAR(200) ,@message NVARCHAR(1000) ,@emails as nvarchar(500) ,@emailsCC as nvarchar(500) ,@dt as varchar(20) SET @dt= REPLACE(CONVERT(NVARCHAR,CAST(getdate() AS DATETIME), 106), '' '', ''-'') --PRINt @dt SET @rptdate = convert(datetime, convert(char, GETDATE(), 101)) SET @subject = N''Daily SecuredDB DB Syc : '' + convert(char, GETDATE(), 101) SET @emails = ''Avanish@sqlservergeeks.com'' SET @emailsCC = ''Avanish@sqlservergeeks.com'' SET @message = ''<body> <p><font color="#000080">DB Sync </font> <font color="#FF0000"> FAILED </font><font color="#000080"> for '' + @dt + ''.</p> <p><font color="#000080">DBA team will work for resolution. Please wait for communication.</p> <p><i>This is an automated email, please not reply back to this email account.</i></p> </body>'' EXEC msdb.dbo.sp_send_dbmail @profile_name = ''SQL Monitor'' ,@body_format = HTML ,@recipients = @emails ,@copy_recipients = @emailsCC ,@body = @message ,@subject = @subject ', @database_name=N'master', @flags=0 IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback EXEC @ReturnCode = msdb.dbo.sp_add_jobstep @job_id=@jobId, @step_name=N'Success', @step_id=4, @cmdexec_success_code=0, @on_success_action=1, @on_success_step_id=0, @on_fail_action=2, @on_fail_step_id=0, @retry_attempts=0, @retry_interval=0, @os_run_priority=0, @subsystem=N'TSQL', @command=N'DECLARE @rptdate datetime ,@subject NVARCHAR(200) ,@message NVARCHAR(1000) ,@emails as nvarchar(500) ,@emailsCC as nvarchar(500) ,@dt as varchar(20) SET @dt= REPLACE(CONVERT(NVARCHAR,CAST(getdate() AS DATETIME), 106), '' '', ''-'') --PRINt @dt SET @rptdate = convert(datetime, convert(char, GETDATE(), 101)) SET @subject = N''Daily SecuredDB DB Syc : '' + convert(char, GETDATE(), 101) SET @emails = ''Avanish@sqlservergeeks.com'' SET @emailsCC = ''Avanish@sqlservergeeks.com'' SET @message = ''<body> <p><font color="#000080">DB Sync completed </font> <font color="#437C17"> SUCCESSFULLY </font><font color="#000080"> for '' + @dt + ''.</p> <p><font color="#000080">Daily Process job kicked off. Please monitor same.</p> <p><i>This is an automated email, please do not reply back to this email account.</i></p> </body>'' EXEC msdb.dbo.sp_send_dbmail @profile_name = ''SQL Monitor'' ,@body_format = HTML ,@recipients = @emails ,@copy_recipients = @emailsCC ,@body = @message ,@subject = @subject GO ', @database_name=N'master', @flags=0 IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback EXEC @ReturnCode = msdb.dbo.sp_update_job @job_id = @jobId, @start_step_id = 1 IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback EXEC @ReturnCode = msdb.dbo.sp_add_jobserver @job_id = @jobId, @server_name = N'(local)' IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback COMMIT TRANSACTION GOTO EndSave QuitWithRollback: IF (@@TRANCOUNT > 0) ROLLBACK TRANSACTION EndSave: GO
I strongly believe, there is always chance of betterment, so suggestions are most welcome.
Happy learning.
Thank you!
Avanish Panchal
Regional Head – DataPlatformGeeks & DPS2017 Core Team Member
Like us on FaceBook | Join the fastest growing SQL Server group on FaceBook
Follow Avanish Panchal on Twitter | Follow Avanish Panchal on FaceBook