sql_cmd 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +201 -0
  3. data/lib/optional_dependencies.rb +30 -0
  4. data/lib/sql_cmd/agent.rb +32 -0
  5. data/lib/sql_cmd/always_on.rb +267 -0
  6. data/lib/sql_cmd/azure.rb +80 -0
  7. data/lib/sql_cmd/backups.rb +276 -0
  8. data/lib/sql_cmd/config.rb +62 -0
  9. data/lib/sql_cmd/database.rb +618 -0
  10. data/lib/sql_cmd/format.rb +124 -0
  11. data/lib/sql_cmd/query.rb +350 -0
  12. data/lib/sql_cmd/security.rb +21 -0
  13. data/lib/sql_cmd/sql_helper.ps1 +89 -0
  14. data/lib/sql_cmd.rb +44 -0
  15. data/sql_scripts/Agent/CreateSQLJob.sql +81 -0
  16. data/sql_scripts/Agent/JobLastRunInfo.sql +70 -0
  17. data/sql_scripts/Agent/JobRunStatus.sql +21 -0
  18. data/sql_scripts/Agent/SQLAgentStatus.sql +8 -0
  19. data/sql_scripts/AlwaysOn/AddDatabaseToAvailabilityGroupOnSecondary.sql +72 -0
  20. data/sql_scripts/AlwaysOn/AddDatabaseToPrimaryAvailabilityGroup.sql +16 -0
  21. data/sql_scripts/AlwaysOn/AutomaticSeedingProgress.sql +34 -0
  22. data/sql_scripts/AlwaysOn/ConfigurePrimaryForAutomaticSeeding.sql +2 -0
  23. data/sql_scripts/AlwaysOn/ConfigurePrimaryForManualSeeding.sql +2 -0
  24. data/sql_scripts/AlwaysOn/ConfigureSecondaryForAutomaticSeeding.sql +1 -0
  25. data/sql_scripts/AlwaysOn/DropSecondary.sql +58 -0
  26. data/sql_scripts/AlwaysOn/RemoveDatabaseFromGroup.sql +2 -0
  27. data/sql_scripts/AlwaysOn/SynchronizationState.sql +14 -0
  28. data/sql_scripts/Database/BackupDatabase.sql +95 -0
  29. data/sql_scripts/Database/CompressAllTables.sql +100 -0
  30. data/sql_scripts/Database/CreateLogin.sql +16 -0
  31. data/sql_scripts/Database/DropDatabase.sql +51 -0
  32. data/sql_scripts/Database/GetBackupFiles.sql +31 -0
  33. data/sql_scripts/Database/GetBackupHeaders.sql +94 -0
  34. data/sql_scripts/Database/GetFileInfoFromBackup.sql +9 -0
  35. data/sql_scripts/Database/RestoreDatabase.sql +185 -0
  36. data/sql_scripts/Database/SetFullRecovery.sql +19 -0
  37. data/sql_scripts/Database/SetSQLCompatibility.sql +33 -0
  38. data/sql_scripts/Security/AssignDatabaseRoles.sql +44 -0
  39. data/sql_scripts/Security/CreateOrUpdateCredential.sql +11 -0
  40. data/sql_scripts/Security/CreateSqlLogin.sql +20 -0
  41. data/sql_scripts/Security/ExportDatabasePermissions.sql +757 -0
  42. data/sql_scripts/Security/GenerateCreateLoginsScript.sql +144 -0
  43. data/sql_scripts/Security/GenerateValidateLoginsScript.sql +83 -0
  44. data/sql_scripts/Security/GetUserSID.sql +3 -0
  45. data/sql_scripts/Security/UpdateSqlPassword.sql +24 -0
  46. data/sql_scripts/Security/ValidateDatabaseRoles.sql +12 -0
  47. data/sql_scripts/Status/ANSINullsOffTableCount.sql +13 -0
  48. data/sql_scripts/Status/ANSINullsOffTables.sql +9 -0
  49. data/sql_scripts/Status/BackupProgress.sql +17 -0
  50. data/sql_scripts/Status/DatabaseInfo.sql +199 -0
  51. data/sql_scripts/Status/DatabaseSize.sql +26 -0
  52. data/sql_scripts/Status/DiskSpace.sql +14 -0
  53. data/sql_scripts/Status/RestoreProgress.sql +17 -0
  54. data/sql_scripts/Status/SQLSettings.sql +182 -0
  55. data/sql_scripts/Status/UncompressedTableCount.sql +27 -0
  56. metadata +224 -0
@@ -0,0 +1,618 @@
1
+ module SqlCmd
2
+ module Database
3
+ module_function
4
+
5
+ def backup(backup_start_time, connection_string, database_name, backup_folder: nil, backup_url: nil, backup_basename: nil, asynchronous: false, options: {})
6
+ EasyIO.logger.header "#{options['logonly'] ? 'Log' : 'Database'} #{options['copyonly'] ? 'Full Backup' : 'Backup'}"
7
+ backup_start_time = SqlCmd.unify_start_time(backup_start_time)
8
+ database_info = SqlCmd::Database.info(connection_string, database_name) # TODO: 3 seconds
9
+ sql_server_settings = SqlCmd.get_backup_sql_server_settings(connection_string) # TODO: 3 seconds
10
+ raise "Failed to backup database! [#{database_name}] was not found on [#{sql_server_settings['ServerName']}]!" if database_info['DatabaseNotFound']
11
+ raise 'Backup attempted before scheduled time!' if Time.now < backup_start_time
12
+
13
+ options = default_backup_options.merge(options)
14
+ free_space_threshold = SqlCmd.config['sql_cmd']['backups']['free_space_threshold']
15
+ options['compressbackup'] ||= sql_server_settings['CompressBackup'] ? SqlCmd.config['sql_cmd']['backups']['compress_backups'] : false
16
+ options['credential'] ||= options['storage_account_name'] || ''
17
+ backup_basename = "#{database_name}_#{EasyTime.yyyymmdd(backup_start_time)}" if backup_basename.nil? || backup_basename.empty?
18
+ original_basename = backup_basename
19
+
20
+ # Set backup_folder to default_destination if it is not set and the backup_to_host_sql_server flag is not set
21
+ # If the backup_to_host_sql_server flag is set, use the server's default location
22
+ if (backup_folder.nil? || backup_folder.empty?) && (backup_url.nil? || backup_url.empty?)
23
+ backup_folder = SqlCmd.config['sql_cmd']['backups']['backup_to_host_sql_server'] ? sql_server_settings['BackupDir'] : SqlCmd.config['sql_cmd']['backups']['default_destination']
24
+ end
25
+ # Check if a backup is currently running
26
+ job_status = SqlCmd::Agent::Job.status(connection_string, "Backup: #{database_name}")['LastRunStatus'] # TODO: 7 seconds
27
+ monitor_backup(backup_start_time, connection_string, database_name, backup_folder: backup_folder, backup_url: backup_url, backup_basename: backup_basename, log_only: options['logonly']) if job_status == 'Running'
28
+
29
+ # Check if there's a current backup and if so, return without creating another backup
30
+ backup_files, backup_basename = existing_backup_files(sql_server_settings, options, backup_folder: backup_folder, backup_url: backup_url, backup_basename: backup_basename, log_only: options['logonly'])
31
+ unless backup_files.empty?
32
+ sql_backup_header = SqlCmd.get_sql_backup_headers(connection_string, backup_files, options).first # TODO: 4 seconds
33
+ return :current if SqlCmd.check_header_date(sql_backup_header, backup_start_time, :prebackup) == :current
34
+ backup_basename = "#{original_basename}_#{EasyTime.hhmmss}" # use a unique name if backup already exists
35
+ end
36
+ backup_basename = original_basename if options['init'] # use original backup name if init (overwrite) is specified
37
+
38
+ EasyIO.logger.info "Checking size of [#{database_name}] database..."
39
+ backup_size = SqlCmd.get_database_size(connection_string, database_name, log_only: options['logonly'])
40
+ EasyIO.logger.info "#{options['logonly'] ? 'Log' : 'Database'} size: #{backup_size == 0 ? 'Not found' : backup_size.round(2)} MB"
41
+
42
+ # Check disk space if not using backup url
43
+ if backup_url.nil? || backup_url.empty?
44
+ if backup_folder == sql_server_settings['BackupDir']
45
+ EasyIO.logger.info "Checking disk space on #{sql_server_settings['DataSource']}..."
46
+ sql_server_disk_space = SqlCmd.get_sql_disk_space(sql_server_settings['connection_string'], sql_server_settings['BackupDir'])
47
+ sql_server_free_space = sql_server_disk_space['Available_MB'].to_f
48
+ sql_server_disk_size = sql_server_disk_space['Total_MB'].to_f
49
+ backup_folder = sql_server_settings['default_destination'] unless sufficient_free_space?(sql_server_disk_size, sql_server_free_space, backup_size, free_space_threshold)
50
+ end
51
+
52
+ if backup_folder != sql_server_settings['BackupDir'] # If the backup folder is not the host sql box or there was not enough space on the sql box, check for free space where specified
53
+ EasyIO.logger.info "Checking free space for #{backup_folder}..."
54
+ specified_backup_folder_free_space = EasyIO::Disk.free_space(backup_folder)
55
+ specified_backup_folder_disk_size = EasyIO::Disk.size(backup_folder)
56
+ sufficient_space = sufficient_free_space?(specified_backup_folder_disk_size, specified_backup_folder_free_space, backup_size, free_space_threshold)
57
+ raise "Failed to backup database #{database_name} due to insufficient space! Must have greater than #{free_space_threshold}% space remaining after backup." unless sufficient_space
58
+ end
59
+ end
60
+
61
+ run_backup_as_job(connection_string, database_name, backup_folder: backup_folder, backup_url: backup_url, backup_basename: backup_basename, options: options)
62
+ monitor_backup(backup_start_time, connection_string, database_name, options, backup_folder: backup_folder, backup_url: backup_url, backup_basename: backup_basename, log_only: options['logonly']) unless asynchronous
63
+ end
64
+
65
+ def sufficient_free_space?(disk_size, free_space, backup_size, free_space_threshold)
66
+ free_space_percentage = disk_size.nil? ? 'unknown ' : (free_space / disk_size) * 100
67
+ free_space_post_backup = free_space - backup_size
68
+ free_space_percentage_post_backup = disk_size.nil? ? 'unknown ' : (free_space_post_backup / disk_size) * 100
69
+ EasyIO.logger.info "Free space on backup drive: #{free_space.round(2)} MB / #{free_space_percentage.round(2)}%"
70
+ EasyIO.logger.info "Estimated free space on backup drive after backup: #{free_space_post_backup.round(2)} MB / #{free_space_percentage_post_backup.round(2)}%"
71
+ free_space_percentage_post_backup >= free_space_threshold
72
+ end
73
+
74
+ def delete_backup_and_restore_history(connection_string, database_name)
75
+ sql_script = "EXEC msdb.dbo.sp_delete_database_backuphistory @database_name = N'#{database_name}'"
76
+ SqlCmd.execute_query(connection_string, sql_script, retries: 3)
77
+ end
78
+
79
+ def existing_backup_files(sql_server_settings, options = {}, backup_folder: nil, backup_url: nil, backup_basename: nil, log_only: false)
80
+ if backup_url && !backup_url.empty?
81
+ SqlCmd.get_url_backup_files(sql_server_settings, backup_url, backup_basename, options)
82
+ elsif backup_folder.nil? || backup_folder.empty?
83
+ default_destination = SqlCmd.config['sql_cmd']['backups']['default_destination']
84
+ EasyIO.logger.info "Checking for existing backup files in #{default_destination}..."
85
+ primary_backup_files, backup_basename = SqlCmd.get_unc_backup_files(sql_server_settings, default_destination, backup_basename, log_only: log_only)
86
+ return [primary_backup_files, backup_basename] unless primary_backup_files.empty?
87
+ EasyIO.logger.info "Checking for existing backup files on #{sql_server_settings['DataSource']}..."
88
+ SqlCmd.sql_server_backup_files(sql_server_settings, backup_basename, log_only: log_only)
89
+ else
90
+ SqlCmd.get_unc_backup_files(sql_server_settings, backup_folder, backup_basename, log_only: log_only)
91
+ end
92
+ end
93
+
94
+ def migrate(start_time, source_connection_string, database_name, destination_connection_string, backup_folder: nil, backup_url: nil, backup_basename: nil, permissions_only: false, force_restore: false, full_backup_method: nil, options: {})
95
+ EasyIO.logger.header 'Database Migration'
96
+ start_time = SqlCmd.unify_start_time(start_time)
97
+ source_connection_string = SqlCmd.remove_connection_string_part(source_connection_string, :database)
98
+ database_info = SqlCmd::Database.info(source_connection_string, database_name)
99
+ destination_database_info = SqlCmd::Database.info(destination_connection_string, database_name)
100
+ destination_server_name = SqlCmd.connection_string_part(destination_connection_string, :server)
101
+ source_server_name = SqlCmd.get_sql_server_settings(source_connection_string)[destination_server_name.include?(',') ? 'DataSource' : 'ServerName']
102
+ source_sql_version = SqlCmd.get_sql_server_settings(source_connection_string)['SQLVersion']
103
+ destination_sql_version = SqlCmd.get_sql_server_settings(destination_connection_string)['SQLVersion']
104
+ validate_restorability(source_sql_version, destination_sql_version, source_type: :database) unless permissions_only
105
+ raise "Failed to migrate database! Destination and source servers are the same! (#{source_server_name})" if source_server_name =~ /#{Regexp.escape(destination_server_name)}/i
106
+ if database_info['DatabaseNotFound'] && (destination_database_info['DatabaseNotFound'] || destination_database_info['DatabaseRestoring'] || destination_database_info['LastRestoreDate'] < start_time)
107
+ raise "Failed to migrate database. Database [#{database_name}] does not exist on [#{source_server_name}]!"
108
+ end
109
+ SqlCmd::Database.duplicate(start_time, source_connection_string, database_name, destination_connection_string, database_name, backup_folder: backup_folder, backup_url: backup_url, backup_basename: backup_basename, force_restore: force_restore, full_backup_method: full_backup_method, options: options) unless permissions_only
110
+ SqlCmd.migrate_logins(start_time, source_connection_string, destination_connection_string, database_name)
111
+
112
+ replication_active = SqlCmd::Database.info(source_connection_string, database_name)['ReplicationActive'] # Refresh database_info to see if the source still has replication enabled
113
+ if replication_active
114
+ return if options['return_at_replication_active']
115
+ raise "Replication must now be dropped from [#{source_server_name}] in order to proceed. Before dropping replication from the source server, script out replication and script it into the migrated database on [#{destination_server_name}] if you wish to preserve replication. Then rerun the migration."
116
+ end
117
+ SqlCmd::AlwaysOn.remove_from_availability_group(source_connection_string, database_name)
118
+ SqlCmd::Database.drop(source_connection_string, database_name)
119
+ end
120
+
121
+ # permissions:
122
+ # :keep_existing - If the database already exists before being restored, exports existing database permissions and imports them after restoring
123
+ # :no_permissions - Does not do anything with permissions, only restores the database
124
+ # :export_only - If the database already exists before being restored, exports exiting database permissions but does not import them after restoring
125
+ # options:
126
+ # TODO: see link
127
+ # :fail_on_synchronized_database - Raise an error if the database is a member of an AlwaysOn Availability Group
128
+ def restore(start_time, connection_string, database_name, backup_folder: nil, backup_url: nil, backup_basename: nil, full_backup_method: nil, force_restore: false, asynchronous: false, overwrite: true, permissions: :keep_existing, options: {})
129
+ raise 'backup_basename parameter is required when restoring a database!' if backup_basename.nil?
130
+ EasyIO.logger.header "#{options['logonly'] ? 'Log' : 'Database'} Restore"
131
+ start_time = SqlCmd.unify_start_time(start_time)
132
+ backup_folder ||= SqlCmd.config['sql_cmd']['backups']['default_destination']
133
+ backup_type = backup_url.nil? || backup_url.empty? ? 'DISK' : 'URL'
134
+ backup_source = backup_type == 'DISK' ? backup_folder : backup_url
135
+ options = default_restore_options.merge(options)
136
+ options['replace'] ||= overwrite
137
+ connection_string = SqlCmd.remove_connection_string_part(connection_string, :database)
138
+ sql_server = SqlCmd.connection_string_part(connection_string, :server)
139
+ database_info = info(connection_string, database_name)
140
+ import_script_path = nil
141
+ unless database_info['DatabaseNotFound']
142
+ raise "Failed to restore database: [#{database_name}] on [#{sql_server}]! Database already exists!" unless overwrite || force_restore
143
+ unless options['logonly'] || database_info['state_desc'] != 'ONLINE' || permissions == :no_permissions
144
+ EasyIO.logger.info "Database already exists before restore on [#{sql_server}]. Saving existing database security permissions..."
145
+ import_script_path = export_security(start_time, connection_string, database_name, backup_url, options)
146
+ end
147
+ end
148
+ free_space_threshold = SqlCmd.config['sql_cmd']['backups']['free_space_threshold']
149
+ EasyIO.logger.debug "Getting SQL server settings for #{sql_server}..."
150
+ sql_server_settings = SqlCmd.get_backup_sql_server_settings(connection_string)
151
+ EasyIO.logger.info "Reading backup files in #{backup_source}..."
152
+ non_log_backup_files = if backup_type == 'DISK'
153
+ SqlCmd.backup_sets_from_unc_path(sql_server_settings, backup_folder, backup_basename).values.first
154
+ else
155
+ SqlCmd.backup_sets_from_url(sql_server_settings, backup_url, backup_basename, options).values.first
156
+ end
157
+ raise "Backup files do not exist in #{backup_source} named #{backup_basename}.bak or .partX.bak" if non_log_backup_files.nil? || non_log_backup_files.empty?
158
+ database_backup_header = SqlCmd.get_sql_backup_headers(connection_string, non_log_backup_files, options).first
159
+ source_sql_version = "#{database_backup_header['SoftwareVersionMajor']}.#{database_backup_header['SoftwareVersionMinor']}"
160
+ destination_sql_version = sql_server_settings['SQLVersion']
161
+ validate_restorability(source_sql_version, destination_sql_version, source_type: :backup)
162
+ backup_sets = if backup_type == 'DISK'
163
+ SqlCmd.backup_sets_from_unc_path(sql_server_settings, backup_folder, backup_basename, log_only: options['logonly'], database_backup_header: database_backup_header, restored_database_lsn: database_info['LastRestoreLSN'])
164
+ else
165
+ SqlCmd.backup_sets_from_url(sql_server_settings, backup_url, backup_basename, options, log_only: options['logonly'], database_backup_header: database_backup_header, restored_database_lsn: database_info['LastRestoreLSN'])
166
+ end
167
+ backup_extension = options['logonly'] ? '.trn' : '.bak'
168
+ raise 'Log backups are not complete. Restore missing log backup or delete existing backups and try again' if backup_sets.nil?
169
+ raise "Backup files do not exist in #{backup_source} named #{backup_basename}.#{backup_extension} or .partX.#{backup_extension}" if backup_sets.empty?
170
+ if options['logonly']
171
+ raise 'No current log backups were found' if backup_sets.nil? || backup_sets.empty?
172
+ first_log_backup_lsn = SqlCmd.get_sql_backup_headers(connection_string, backup_sets.first.last, options).first['FirstLSN']
173
+ raise "First log backup is too recent to apply. First log backup LSN: #{first_log_backup_lsn} - Restored database LSN: #{database_info['LastRestoreLSN']}" if first_log_backup_lsn > [database_info['LastRestoreLSN'], database_info['LastLogRestoreLSN'] || 0].max
174
+ end
175
+ EasyIO.logger.debug "Backup sets to restore: #{JSON.pretty_generate(backup_sets)}"
176
+ backup_sets.each do |backup_set_basename, backup_files|
177
+ EasyIO.logger.info "Preparing to restore #{backup_set_basename}..."
178
+ EasyIO.logger.debug "Backup files: #{JSON.pretty_generate(backup_files)}"
179
+ raise "Backup #{backup_set_basename} does not exist! Backup file(s) source: \n #{backup_source}" if backup_files.nil? || backup_files.empty?
180
+ sql_backup_header = SqlCmd.get_sql_backup_headers(connection_string, backup_files, options).first
181
+ raise "Backup #{backup_set_basename} could not be read! It may be from a newer SQL server version or corrupt." if sql_backup_header.nil?
182
+ raise "Backup #{backup_set_basename} is not current! Backup file(s) source: \n #{backup_source}" unless SqlCmd.check_header_date(sql_backup_header, start_time, :prerestore)
183
+
184
+ minimum_restore_date = sql_backup_header['BackupFinishDate'] > start_time ? sql_backup_header['BackupFinishDate'] : start_time
185
+ next unless force_restore || !restore_up_to_date?(start_time, connection_string, database_name, backup_files, options, pre_restore: true)
186
+
187
+ raise "Unable to restore database [#{database_name}] to [#{sql_server}]! The database is being used for replication." if database_info['ReplicationActive']
188
+ if !database_info['DatabaseNotFound'] && SqlCmd::AlwaysOn.database_synchronized?(connection_string, database_name)
189
+ raise "Failed to restore database: [#{database_name}] is part of an AlwaysOn Availability group on [#{sql_server}]." if options['fail_on_synchronized_database']
190
+ unless options['secondaryreplica'] # unless we're restoring to the secondary replica, remove the DB from the AlwaysOn Availability Group
191
+ EasyIO.logger.info "Database [#{database_name}] is part of an availability group and will be removed..."
192
+ SqlCmd::AlwaysOn.remove_from_availability_group(connection_string, database_name)
193
+ end
194
+ end
195
+ EasyIO.logger.debug "Restoring backup #{backup_set_basename} with header: #{JSON.pretty_generate(sql_backup_header)}"
196
+
197
+ unless force_restore
198
+ # Calculate disk space
199
+ database_size = options['logonly'] ? 0 : database_info['DatabaseSize'] || 0
200
+ EasyIO.logger.info "Existing target database size: #{database_size == 0 ? 'No existing database' : "#{database_size.round(2)} MB"}" unless options['logonly']
201
+ restored_backup_size = SqlCmd.get_backup_size(sql_backup_header)
202
+ EasyIO.logger.info "Size of backup #{backup_set_basename}: #{restored_backup_size.round(2)} MB"
203
+ database_size_difference = restored_backup_size - database_size
204
+ EasyIO.logger.debug "Checking disk space on [#{sql_server_settings['ServerName']}]..."
205
+ sql_server_disk_space = SqlCmd.get_sql_disk_space(sql_server_settings['connection_string'], sql_server_settings['DataDir'])
206
+ sql_server_free_space = sql_server_disk_space['Available_MB'].to_f
207
+ sql_server_disk_size = sql_server_disk_space['Total_MB'].to_f
208
+ sql_server_free_space_after_restore = sql_server_free_space - database_size_difference
209
+ sql_server_free_space_percentage = sql_server_disk_size.nil? ? 'unknown ' : (sql_server_free_space / sql_server_disk_size) * 100
210
+ sql_server_free_space_percentage_post_restore = sql_server_disk_size.nil? ? 'unknown ' : (sql_server_free_space_after_restore / sql_server_disk_size) * 100
211
+ EasyIO.logger.info "Free space on [#{sql_server_settings['ServerName']}] before restore: #{sql_server_free_space.round(2)} MB / #{sql_server_free_space_percentage.round(2)}%"
212
+ EasyIO.logger.info "Estimated free space on [#{sql_server_settings['ServerName']}] after restore: #{sql_server_free_space_after_restore.round(2)} MB / #{sql_server_free_space_percentage_post_restore.round(2)}%"
213
+ insufficient_space = sql_server_free_space_percentage_post_restore < free_space_threshold && database_size_difference > 0
214
+ raise "Insufficient free space on #{sql_server} to restore database! Must have greater than #{free_space_threshold}% space remaining after restore." if insufficient_space
215
+ end
216
+
217
+ run_restore_as_job(connection_string, sql_server_settings, backup_files, database_name, options: options)
218
+ monitor_restore(minimum_restore_date, connection_string, database_name, backup_files, options) unless asynchronous
219
+ end
220
+ import_security(connection_string, database_name, import_script_path, backup_url, options) unless import_script_path.nil? || [:no_permissions, :export_only].include?(permissions)
221
+ SqlCmd.update_sql_compatibility(connection_string, database_name, options['compatibility_level']) if options['compatibility_level']
222
+ apply_recovery_model(connection_string, database_name, options) if options['recovery_model']
223
+ SqlCmd::AlwaysOn.add_to_availability_group(connection_string, database_name, full_backup_method: full_backup_method) if sql_server_settings['AlwaysOnEnabled'] && !options['secondaryreplica'] && !options['skip_always_on']
224
+ ensure_full_backup_has_occurred(connection_string, database_name, full_backup_method: full_backup_method, database_info: database_info) unless options['secondaryreplica'] || full_backup_method == :skip
225
+ end
226
+
227
+ def validate_restorability(source_sql_version, destination_sql_version, source_type: :backup)
228
+ sql_versions_valid = ::Gem::Version.new(source_sql_version.split('.')[0...1].join('.')) <= ::Gem::Version.new(destination_sql_version.split('.')[0...1].join('.'))
229
+ not_valid_msg = "Unable to restore database. Destination server (#{destination_sql_version}) is on an older version of SQL than the source #{source_type == :backup ? 'backup' : 'server'} (#{source_sql_version})!"
230
+ raise not_valid_msg unless sql_versions_valid
231
+ end
232
+
233
+ def backup_up_to_date?(start_time, connection_string, database_name, last_backup_date_key)
234
+ start_time = SqlCmd.unify_start_time(start_time)
235
+ (SqlCmd::Database.info(connection_string, database_name)[last_backup_date_key] || Time.at(0)) >= start_time
236
+ end
237
+
238
+ def restore_up_to_date?(start_time, connection_string, database_name, backup_files, options = {}, pre_restore: false)
239
+ start_time = SqlCmd.unify_start_time(start_time)
240
+ database_info = info(connection_string, database_name)
241
+ restore_date_key, restore_lsn_key = backup_files.any? { |f| f =~ /\.trn/i } ? %w(LastLogRestoreDate LastLogRestoreLSN) : %w(LastRestoreDate LastRestoreLSN)
242
+ last_restore_database_backup_lsn = database_info['LastRestoreDatabaseBackupLSN'] || 0
243
+ return false if last_restore_database_backup_lsn == 0 || database_info[restore_date_key].nil?
244
+ job_status = SqlCmd::Agent::Job.status(connection_string, "Restore: #{database_name}")['LastRunStatus']
245
+ return false if job_status == 'Running'
246
+ backup_header = SqlCmd.get_sql_backup_headers(connection_string, backup_files, options).first
247
+ minimum_restore_date = backup_header['BackupFinishDate'] > start_time ? backup_header['BackupFinishDate'] : start_time
248
+ EasyIO.logger.debug "LastLSN from restored database: #{database_info[restore_lsn_key]}"
249
+ EasyIO.logger.debug "LastLSN in backup header : #{backup_header['LastLSN']}"
250
+ EasyIO.logger.debug "DatabaseBackupLSN of restored database: #{last_restore_database_backup_lsn}"
251
+ EasyIO.logger.debug "DatabaseBackupLSN of backup set: #{backup_header['DatabaseBackupLSN']}"
252
+ EasyIO.logger.debug "Restore date key: #{restore_date_key}"
253
+ up_to_date = database_info[restore_date_key] > minimum_restore_date && last_restore_database_backup_lsn == backup_header['DatabaseBackupLSN'] &&
254
+ database_info[restore_lsn_key] >= backup_header['LastLSN']
255
+ unless pre_restore
256
+ raise "The restore job status is '#{job_status}'. Check the 'Restore: #{database_name}' job history for more details." unless job_status == 'NoJob' || job_status == 'Succeeded'
257
+ EasyIO.logger.warn "The restored date in the database (#{database_info[restore_date_key]}) is older than the database backup or the scheduled start time (#{minimum_restore_date})!" unless database_info[restore_date_key] > minimum_restore_date
258
+ EasyIO.logger.warn "The DatabaseBackupLSN for the database (#{last_restore_database_backup_lsn}) doesn't match that of the database backup (#{backup_header['DatabaseBackupLSN']})!" unless last_restore_database_backup_lsn == backup_header['DatabaseBackupLSN']
259
+ EasyIO.logger.warn "The #{restore_lsn_key} for the database (#{database_info[restore_lsn_key]}) is lower than the LastLSN of the database backup (#{backup_header['LastLSN']})!" unless database_info[restore_lsn_key] >= backup_header['LastLSN']
260
+ end
261
+ EasyIO.logger.info "Restored database #{database_name} is up to date." if up_to_date
262
+ up_to_date
263
+ end
264
+
265
+ # deletes a Sql database
266
+ def drop(connection_string, database_name)
267
+ EasyIO.logger.header 'Drop Database'
268
+ connection_string = SqlCmd.remove_connection_string_part(connection_string, :database)
269
+ sql_server_settings = SqlCmd.get_sql_server_settings(connection_string)
270
+ database_information = info(connection_string, database_name)
271
+ if database_information['DatabaseNotFound']
272
+ EasyIO.logger.info 'The database was not found. Skipping drop of database...'
273
+ return
274
+ end
275
+ raise "Unable to drop database [#{database_name}] from [#{sql_server_settings['ServerName']}]! The database is being used for replication." if database_information['ReplicationActive']
276
+ EasyIO.logger.info "Dropping database [#{database_name}] from [#{sql_server_settings['ServerName']}]..."
277
+
278
+ sql_script = ::File.read("#{SqlCmd.sql_script_dir}/Database/DropDatabase.sql")
279
+ SqlCmd.execute_query(connection_string, sql_script, values: { 'databasename' => database_name }, retries: 3)
280
+
281
+ database_information = info(connection_string, database_name)
282
+ raise "Failed to drop database [#{database_name}] from [#{sql_server_settings['ServerName']}]!" unless database_information['DatabaseNotFound']
283
+ end
284
+
285
+ # Creates and starts a SQL job to backup a database
286
+ # * See default_backup_options method for default options
287
+ def run_backup_as_job(connection_string, database_name, backup_folder: nil, backup_url: nil, backup_basename:, options: {})
288
+ raise 'Backup folder or url must be specified!' if (backup_folder.nil? || backup_folder.empty?) && (backup_url.nil? || backup_url.empty?)
289
+ backup_status_script = ::File.read("#{SqlCmd.sql_script_dir}/Status/BackupProgress.sql")
290
+ return unless SqlCmd.execute_query(connection_string, backup_status_script, return_type: :first_table, values: { 'databasename' => database_name }, retries: 3).empty?
291
+
292
+ values = default_backup_options.merge(options)
293
+ values['bkupdbname'] = database_name
294
+ values['bkupname'] = backup_basename
295
+ values['bkuppartmaxsize'] ||= 58800
296
+ if backup_url.nil? || backup_url.empty?
297
+ values['bkupdest'] = backup_folder
298
+ values['bkuptype'] = 'DISK'
299
+ else
300
+ backup_url = "#{backup_url}/" unless backup_url.end_with?('/')
301
+ values['bkupdest'] = backup_url
302
+ values['bkuptype'] = 'URL'
303
+ SqlCmd::Security.create_credential(connection_string, values['credential'], options['storage_account_name'], options['storage_access_key'], options) if SqlCmd.azure_blob_storage_url?(backup_url)
304
+ end
305
+
306
+ sql_backup_script = ::File.read("#{SqlCmd.sql_script_dir}/Database/BackupDatabase.sql")
307
+ EasyIO.logger.info "Backing up #{options['logonly'] ? 'log for' : 'database'} #{database_name} to: #{values['bkupdest']}..." # TODO: Update log message with url
308
+ EasyIO.logger.debug "Backup basename: #{backup_basename}"
309
+ EasyIO.logger.debug "Database name: #{database_name}"
310
+ EasyIO.logger.debug "Compress backup: #{values['compressbackup']}"
311
+ EasyIO.logger.debug "Copy only: #{values['copyonly']}"
312
+ ::FileUtils.mkdir_p(backup_folder) unless values['bkuptype'] == 'URL' || ::File.directory?(backup_folder) # create the destination folder if it does not exist
313
+ SqlCmd.run_sql_as_job(connection_string, sql_backup_script, "Backup: #{backup_basename}", values: values, retries: 1, retry_delay: 30)
314
+ end
315
+
316
+ def monitor_backup(backup_start_time, connection_string, database_name, options = {}, backup_folder: nil, backup_url: nil, backup_basename: nil, log_only: false, retries: 3, retry_delay: 10)
317
+ backup_start_time = SqlCmd.unify_start_time(backup_start_time)
318
+ backup_status_script = ::File.read("#{SqlCmd.sql_script_dir}/Status/BackupProgress.sql")
319
+ job_name = "Backup: #{database_name}"
320
+ EasyIO.logger.info 'Checking backup status...'
321
+ EasyIO.logger.debug "Backup start time: #{backup_start_time}"
322
+ sleep(3) # Give the job time to start
323
+ timeout = 60 # After the SQLAgent job exits, check the backup up to date algorithm for this long to update before failing
324
+ monitoring_start_time = Time.now
325
+ timer_interval = 15
326
+ last_backup_date_key = log_only ? 'LastLogOnlyBackupDate' : 'LastFullBackupDate'
327
+
328
+ # Initialize variables so they persists through retries
329
+ job_started ||= false
330
+ job_completion_time ||= nil
331
+ begin
332
+ loop do
333
+ job_started = true if !job_started && SqlCmd::Agent::Job.exists?(connection_string, job_name) # TODO: 5 seconds?
334
+ status_row = SqlCmd.execute_query(connection_string, backup_status_script, return_type: :first_row, values: { 'databasename' => database_name }, retries: 3) # TODO: 5 seconds?
335
+ if status_row.nil?
336
+ job_status = SqlCmd::Agent::Job.status(connection_string, job_name)['LastRunStatus'] # TODO: 5 seconds?
337
+ break if backup_up_to_date?(backup_start_time, connection_string, database_name, last_backup_date_key) # TODO: 4 seconds?
338
+ next if job_status == 'Running'
339
+ # TODO: check job history for errors if not :current
340
+ # job_message = job_history_message(connection_string, job_name) unless result == :current
341
+ if job_started # Check if job has timed out after stopping without completing
342
+ job_completion_time ||= Time.now
343
+ _raise_backup_failure(connection_string, database_name, last_backup_date_key, backup_start_time, backup_basename, job_status: job_status, job_started: job_started) if Time.now > job_completion_time + timeout
344
+ elsif Time.now > monitoring_start_time + timeout # Job never started and timed out
345
+ _raise_backup_failure(connection_string, database_name, last_backup_date_key, backup_start_time, backup_basename, job_status: job_status, job_started: job_started)
346
+ end
347
+ sleep(timer_interval)
348
+ next
349
+ end
350
+ job_started = true
351
+ EasyIO.logger.info "Percent complete: #{status_row['Percent Complete']} / Elapsed min: #{status_row['Elapsed Min']} / Min remaining: #{status_row['ETA Min']} / ETA: #{status_row['ETA Completion Time']}"
352
+ sleep(timer_interval)
353
+ false # if we got here, conditions were not met. Keep looping...
354
+ end
355
+ rescue
356
+ sleep(retry_delay)
357
+ retry if (retries -= 1) >= 0
358
+ raise
359
+ end
360
+ begin
361
+ unless backup_basename.nil? # Don't validate backup files if backup folder or basename was not provided
362
+ sql_server_settings = SqlCmd.get_sql_server_settings(connection_string) # TODO: 4 seconds
363
+ backup_destination = backup_url.nil? || backup_url.empty? ? backup_folder : backup_url
364
+ backup_files, backup_basename = SqlCmd.get_backup_files(sql_server_settings, options, backup_folder: backup_folder, backup_url: backup_url, backup_basename: backup_basename)
365
+ if backup_files.empty?
366
+ EasyIO.logger.warn "Unable to verify backup files. No backup files found or backup destination '#{backup_destination}' is inaccessible!"
367
+ return :current
368
+ end
369
+ sql_backup_header = SqlCmd.get_sql_backup_headers(connection_string, backup_files, options).first # TODO: 3 seconds
370
+ result = SqlCmd.check_header_date(sql_backup_header, backup_start_time)
371
+ raise 'WARNING! Backup files are not current!' if result == :outdated
372
+ raise 'WARNING! Backup files could not be read!' if result == :nobackup
373
+ end
374
+ rescue
375
+ sleep(retry_delay)
376
+ retry if (retries -= 1) >= 0
377
+ raise
378
+ end
379
+ EasyIO.logger.info 'Backup complete.'
380
+ result
381
+ end
382
+
383
+ def _raise_backup_failure(connection_string, database_name, last_backup_date_key, backup_start_time, backup_basename, job_status: nil, job_started: false)
384
+ server_name = SqlCmd.connection_string_part(connection_string, :server)
385
+ if job_started
386
+ failure_message = "Backup may have failed as the backup has stopped and the last backup time shows #{SqlCmd::Database.info(connection_string, database_name)[last_backup_date_key]} " \
387
+ "but the backup should be newer than #{backup_start_time}! "
388
+ failure_message += job_status == 'NoJob' ? 'The job exited with success and so does not exist!' : "Check sql job 'Backup: #{backup_basename}' history on [#{server_name}] for details. \n"
389
+ raise failure_message + "Last backup time retrieved from: #{server_name}\\#{database_name}"
390
+ end
391
+ failure_message = "Backup appears to have failed! The last backup time shows #{SqlCmd::Database.info(connection_string, database_name)[last_backup_date_key]} " \
392
+ "but the backup should be newer than #{backup_start_time}! "
393
+ failure_message += job_status == 'NoJob' ? 'The backup job could not be found!' : "The job did not start in time. Check sql job 'Backup: #{backup_basename}' history on [#{server_name}] for details."
394
+ raise failure_message + "Last backup time retrieved from: #{server_name}\\#{database_name}"
395
+ end
396
+
397
+ # Creates and starts a SQL job to restore a database.
398
+ def run_restore_as_job(connection_string, sql_server_settings, backup_files, database_name, options: {})
399
+ disk_backup_files = SqlCmd.backup_fileset_names(backup_files)
400
+ restore_status_script = ::File.read("#{SqlCmd.sql_script_dir}/Status/RestoreProgress.sql")
401
+ server_connection_string = SqlCmd.remove_connection_string_part(connection_string, :database)
402
+ data_file_logical_name, log_file_logical_name = options['logonly'] ? ['', ''] : SqlCmd.get_backup_logical_names(server_connection_string, backup_files, options)
403
+
404
+ values = default_restore_options.merge(options)
405
+ values['databasename'] = database_name
406
+ values['bkupfiles'] = disk_backup_files
407
+ values['datafile'] ||= "#{sql_server_settings['DataDir']}#{database_name}.mdf"
408
+ values['logfile'] ||= "#{sql_server_settings['LogDir']}#{database_name}.ldf"
409
+ values['datafilelogicalname'] ||= data_file_logical_name
410
+ values['logfilelogicalname'] ||= log_file_logical_name
411
+
412
+ return unless SqlCmd.execute_query(server_connection_string, restore_status_script, return_type: :first_table, values: values, retries: 3).empty? # Do nothing if restore is in progress
413
+
414
+ # TODO: implement: Replication.script_replication(connection_string, database_name)
415
+ # TODO: implement: Replication.remove_replication(connection_string, database_name)
416
+ SqlCmd::AlwaysOn.remove_from_availability_group(server_connection_string, database_name) unless options['secondaryreplica']
417
+ sql_restore_script = ::File.read("#{SqlCmd.sql_script_dir}/Database/RestoreDatabase.sql")
418
+ EasyIO.logger.info "Restoring #{options['logonly'] ? 'log for' : 'database'} [#{database_name}] on [#{SqlCmd.connection_string_part(connection_string, :server)}]..."
419
+ SqlCmd.run_sql_as_job(server_connection_string, sql_restore_script, "Restore: #{database_name}", values: values, retries: 1, retry_delay: 30)
420
+ end
421
+
422
+ def monitor_restore(start_time, connection_string, database_name, backup_files, options = {}, retries: 3, retry_delay: 15)
423
+ start_time = SqlCmd.unify_start_time(start_time)
424
+ restore_status_script = ::File.read("#{SqlCmd.sql_script_dir}/Status/RestoreProgress.sql")
425
+ job_name = "Restore: #{database_name}"
426
+ server_connection_string = SqlCmd.remove_connection_string_part(connection_string, :database)
427
+ values = { 'databasename' => database_name }
428
+ EasyIO.logger.info 'Checking restore status...'
429
+ sleep(5)
430
+ timeout = 60
431
+ monitoring_start_time = Time.now
432
+ timer_interval = 15
433
+ restore_date_key = backup_files.any? { |f| f =~ /\.trn/i } ? 'LastLogRestoreDate' : 'LastRestoreDate'
434
+
435
+ # Initialize variables so they persists through retries
436
+ job_started ||= false
437
+ job_completion_time ||= nil
438
+ begin
439
+ loop do
440
+ job_started = true if !job_started && SqlCmd::Agent::Job.exists?(connection_string, job_name)
441
+ status_row = SqlCmd.execute_query(server_connection_string, restore_status_script, return_type: :first_row, values: values, retries: 3)
442
+ if status_row.nil?
443
+ break if restore_up_to_date?(start_time, connection_string, database_name, backup_files, options)
444
+ job_status = SqlCmd::Agent::Job.status(connection_string, job_name)['LastRunStatus']
445
+ next if job_status == 'Running'
446
+ # TODO: check job history for errors if not :current
447
+ # job_message = job_history_message(connection_string, job_name) unless result == :current
448
+ if job_started # check if job has timed out after stopping but not completing
449
+ job_completion_time ||= Time.now
450
+ _raise_restore_failure(connection_string, database_name, restore_date_key, start_time, job_status: job_status, job_started: job_started) if Time.now > job_completion_time + timeout
451
+ elsif Time.now > monitoring_start_time + timeout # Job never started and timed out
452
+ _raise_restore_failure(connection_string, database_name, restore_date_key, start_time, job_status: job_status, job_started: job_started)
453
+ end
454
+ sleep(timer_interval)
455
+ next
456
+ end
457
+ job_started = true
458
+ EasyIO.logger.info "Percent complete: #{status_row['Percent Complete']} / Elapsed min: #{status_row['Elapsed Min']} / Min remaining: #{status_row['ETA Min']} / ETA: #{status_row['ETA Completion Time']}"
459
+ sleep(timer_interval)
460
+ false # if we got here, conditions were not met. Keep looping...
461
+ end
462
+ EasyIO.logger.info 'Restore complete.'
463
+ rescue
464
+ sleep(retry_delay)
465
+ retry if (retries -= 1) >= 0
466
+ raise
467
+ end
468
+ end
469
+
470
+ def _raise_restore_failure(connection_string, database_name, restore_date_key, start_time, job_status: nil, job_started: false)
471
+ server_name = SqlCmd.connection_string_part(connection_string, :server)
472
+ if job_started
473
+ failure_message = "Restore may have failed as the restore has stopped and the last restore time shows #{SqlCmd::Database.info(connection_string, database_name)[restore_date_key]} " \
474
+ "but the restore should be newer than #{start_time}! "
475
+ failure_message += job_status == 'NoJob' ? 'The job exited with success and so does not exist!' : "Check sql job 'Restore: #{database_name}' history on [#{server_name}] for details."
476
+ raise failure_message + "Last restore time retrieved from: #{server_name}\\#{database_name}"
477
+ end
478
+ failure_message = 'Restore appears to have failed! '
479
+ failure_message += job_status == 'NoJob' ? 'The job could not be found and the restored database is not up to date!' : "The job did not start in time. Check sql job 'Restore: #{database_name}' history on [#{server_name}] for details."
480
+ raise failure_message + "Restore destination: #{server_name}\\#{database_name}"
481
+ end
482
+
483
+ def check_restore_date(start_time, connection_string, database_name, messages = :none, log_only: false)
484
+ start_time = SqlCmd.unify_start_time(start_time)
485
+ database_information = info(connection_string, database_name)
486
+ last_restore_date_key = log_only ? 'LastLogRestoreDate' : 'LastRestoreDate'
487
+ return :unknown if database_information.nil?
488
+ return :notfound if database_information['DatabaseNotFound']
489
+ return :restoring if database_information['state_desc'] == 'RESTORING'
490
+ return :nodate if database_information[last_restore_date_key].nil?
491
+ return :outdated if database_information[last_restore_date_key] < start_time
492
+ if [:prerestore].include?(messages)
493
+ EasyIO.logger.info "Last restore for [#{database_name}] completed: #{database_information[last_restore_date_key]}"
494
+ EasyIO.logger.info 'Restored database is current.'
495
+ end
496
+ :current
497
+ end
498
+
499
+ def ensure_full_backup_has_occurred(connection_string, database_name, force_backup: false, full_backup_method: nil, database_info: nil)
500
+ database_info = info(connection_string, database_name) if database_info.nil?
501
+ server_name = SqlCmd.connection_string_part(connection_string, :server)
502
+ EasyIO.logger.info "Ensuring full backup has taken place for [#{server_name}].[#{database_name}]..."
503
+ if force_backup || database_info['LastNonCopyOnlyFullBackupDate'].nil? || # Ensure last full backup occurred AFTER the DB was last restored
504
+ (!database_info['LastRestoreDate'].nil? && database_info['LastNonCopyOnlyFullBackupDate'] < database_info['LastRestoreDate'])
505
+ EasyIO.logger.info 'Running full backup...'
506
+ backup_basename = "full_backup-#{database_name}_#{EasyTime.yyyymmdd}" # If a full_backup_method was not provided, use this name for the database backup for clarity
507
+ full_backup_method.nil? ? SqlCmd::Database.backup(Time.now, connection_string, database_name, backup_basename: backup_basename, options: { 'copyonly' => false }) : full_backup_method.call
508
+ end
509
+ end
510
+
511
+ def duplicate(start_time, source_connection_string, source_database_name, destination_connection_string, destination_database_name, backup_folder: nil, backup_url: nil, backup_basename: nil, force_restore: false, full_backup_method: nil, options: {})
512
+ start_time = SqlCmd.unify_start_time(start_time)
513
+ backup(start_time, source_connection_string, source_database_name, backup_folder: backup_folder, backup_url: backup_url, backup_basename: backup_basename, options: options) unless info(source_connection_string, source_database_name)['DatabaseNotFound']
514
+ backup_folder, backup_basename = SqlCmd.backup_location_and_basename(start_time, source_connection_string, source_database_name, options, backup_url: backup_url) # TODO: rework for URL
515
+ if (backup_folder.nil? && backup_url.nil?) || backup_basename.nil?
516
+ source_server = SqlCmd.connection_string_part(source_connection_string, :server)
517
+ destination_server = SqlCmd.connection_string_part(destination_connection_string, :server)
518
+ database_info = SqlCmd::Database.info(destination_connection_string, destination_database_name)
519
+ raise "Backup files could not be found while duplicating #{source_database_name} from #{source_server} to #{destination_server}. Manually restore the database and run again." if database_info['LastRestoreDate'].nil? || database_info['LastRestoreDate'] < start_time
520
+ EasyIO.logger.warn 'Backup files could not be found but restore appears to be current. Skipping restore...'
521
+ return
522
+ end
523
+ restore(start_time, destination_connection_string, destination_database_name, backup_folder: backup_folder, backup_url: backup_url, backup_basename: backup_basename, force_restore: force_restore, full_backup_method: full_backup_method, options: options)
524
+ end
525
+
526
+ def info(connection_string, database_name, retries: 3, retry_delay: 5)
527
+ raise 'Failed to get database information! The database_name argument must be specified.' if database_name.nil? || database_name.empty?
528
+ raise 'Failed to get database information! The connection_string argument must be specified.' if connection_string.nil? || connection_string.empty?
529
+ sql_script = ::File.read("#{SqlCmd.sql_script_dir}/Status/DatabaseInfo.sql")
530
+ server_connection_string = SqlCmd.remove_connection_string_part(connection_string, :database)
531
+ result = SqlCmd.execute_query(server_connection_string, sql_script, return_type: :first_row, values: { 'databasename' => database_name }, readonly: true, retries: retries, retry_delay: retry_delay) || {}
532
+ return result if result.empty?
533
+ result['DatabaseName'] ||= database_name
534
+ result
535
+ end
536
+
537
+ def apply_recovery_model(connection_string, database_name, options)
538
+ return if recovery_model_set?(connection_string, database_name, options)
539
+ options['recovery_model'] ||= 'FULL'
540
+ options['rollback'] ||= 'ROLLBACK IMMEDIATE' # other options: ROLLBACK AFTER 30, NO_WAIT
541
+ sql_script = "ALTER DATABASE [#{database_name}] SET RECOVERY #{options['recovery_model']} WITH #{options['rollback']}"
542
+ SqlCmd.execute_query(connection_string, sql_script) || {}
543
+ failure_message = <<-EOS
544
+ Failed to set recovery model to '#{options['recovery_model']}'!\n
545
+ Command attempted: #{sql_script}\n
546
+ ConnectionString: '#{SqlCmd.hide_connection_string_password(connection_string)}'
547
+ #{'=' * 120}\n"
548
+ EOS
549
+ raise failure_message unless recovery_model_set?(connection_string, database_name, options)
550
+ end
551
+
552
+ def recovery_model_set?(connection_string, database_name, options)
553
+ recovery_model = options['recovery_model'] || 'FULL'
554
+ sql_script = "SELECT 1 FROM master.sys.databases WHERE recovery_model_desc LIKE '#{recovery_model}' and name = '#{database_name}'"
555
+ SqlCmd.execute_query(connection_string, sql_script, return_type: :scalar, readonly: true) || false
556
+ end
557
+
558
+ def export_security(start_time, connection_string, database_name, storage_url = nil, options = {})
559
+ start_time = SqlCmd.unify_start_time(start_time)
560
+ server_name = SqlCmd.connection_string_part(connection_string, :server)
561
+ export_folder = "#{SqlCmd.config['paths']['cache']}/sql_cmd/logins"
562
+ basename_prefix = storage_url.nil? ? "#{EasyFormat::File.windows_friendly_name(server_name)}_" : ''
563
+ import_script_path = "#{export_folder}/#{basename_prefix}#{database_name}_database_permissions_#{EasyTime.yyyymmdd(start_time)}.sql"
564
+ if ::File.exist?(import_script_path) && ::File.mtime(import_script_path) > start_time
565
+ content = ::File.read(import_script_path)
566
+ SqlCmd::Azure::AttachedStorage.upload(::File.basename(import_script_path), content, options['storage_account_name'], options['storage_access_key'], storage_url: storage_url) unless storage_url.nil?
567
+ return import_script_path
568
+ end
569
+
570
+ sql_script = ::File.read("#{SqlCmd.sql_script_dir}/Security/ExportDatabasePermissions.sql")
571
+ values = { 'databasename' => database_name, 'output' => 'CreateOnly', 'includetablepermissions' => SqlCmd.config['sql_cmd']['exports']['include_table_permissions'] ? 1 : 0 }
572
+ EasyIO.logger.info "Exporting database permissions for: [#{database_name}] on [#{server_name}]..."
573
+ import_script = SqlCmd.execute_query(connection_string, sql_script, return_type: :scalar, values: values, readonly: true, retries: 3)
574
+ return nil if import_script.nil? || import_script.empty?
575
+ FileUtils.mkdir_p(export_folder)
576
+ ::File.write(import_script_path, import_script)
577
+ EasyIO.logger.info "Permissions exported to: #{import_script_path}"
578
+ EasyIO.logger.debug "Resulting import script: #{import_script}"
579
+ SqlCmd::Azure::AttachedStorage.upload(::File.basename(import_script_path), import_script, options['storage_account_name'], options['storage_access_key'], storage_url: storage_url) unless storage_url.nil?
580
+ import_script_path
581
+ end
582
+
583
+ def import_security(connection_string, database_name, import_script_path = nil, storage_url = nil, options = {})
584
+ EasyIO.logger.info 'Restoring previous security configuration...'
585
+ export_folder = "#{SqlCmd.config['paths']['cache']}/sql_cmd/logins"
586
+ start_time = options['start_time'] || SqlCmd.unify_start_time(nil)
587
+ import_script_path ||= "#{export_folder}/#{database_name}_database_permissions_#{EasyTime.yyyymmdd(start_time)}.sql"
588
+ SqlCmd::Azure::AttachedStorage.download(::File.basename(import_script_path), import_script_path, options['storage_account_name'], options['storage_access_key'], storage_url: storage_url) unless storage_url.nil?
589
+ SqlCmd.execute_script_file(connection_string, import_script_path, values: { 'databasename' => database_name })
590
+ end
591
+
592
+ def default_backup_options
593
+ {
594
+ # 'compressbackup' => false, # Uses server default if not specified
595
+ 'splitfiles' => true, # Split files for large databases
596
+ 'logonly' => false, # Does a log only backup
597
+ 'formatbackup' => false, # Specifies that a new media set be created. Overwrites media header
598
+ 'copyonly' => true, # Specifies not to affect normal sequence of backups
599
+ 'init' => false, # Specifies that backup sets should be overwritten but preserves the media header
600
+ 'skip' => true, # Skips checking backup expiration date and name before overwriting
601
+ 'rewind' => false, # Specifies that SQL server releases and rewinds the tape
602
+ 'unload' => false, # Specifies that the tape is automatically rewound and unloaded after completion
603
+ 'stats' => 5, # Specifies how often sql server reports progress by percentage
604
+ }
605
+ end
606
+
607
+ def default_restore_options
608
+ {
609
+ 'logonly' => false, # Restore a log backup
610
+ 'recovery' => true, # Recovers the database after restoring - Should be used unless additional log files are to be restored
611
+ 'replace' => false, # Overwrites the existing database
612
+ 'keepreplication' => false, # Restore replication
613
+ 'unload' => false, # Specifies that the tape is automatically rewound and unloaded after completion
614
+ 'stats' => 5, # Specifies how often sql server reports progress by percentage
615
+ }
616
+ end
617
+ end
618
+ end