Module: SqlCmd
- Defined in:
- lib/sql_cmd/agent.rb,
lib/sql_cmd/azure.rb,
lib/sql_cmd/query.rb,
lib/sql_cmd/config.rb,
lib/sql_cmd/format.rb,
lib/sql_cmd/backups.rb,
lib/sql_cmd/database.rb,
lib/sql_cmd/security.rb,
lib/sql_cmd/always_on.rb
Defined Under Namespace
Modules: Agent, AlwaysOn, Azure, Database, Security
Class Method Summary collapse
- .apply_log_level ⇒ Object
-
.assign_database_roles(connection_string, database_name, user, roles = ['db_owner'], retries: 3, retry_delay: 5) ⇒ Object
roles: array of database roles.
-
.azure_blob_storage_list_backup_files(backup_url, backup_basename, storage_account_name, storage_access_key) ⇒ Object
Get a list of backup files from blob storage.
- .azure_blob_storage_url?(backup_url) ⇒ Boolean
-
.backup_basename_from_path(backup_path) ⇒ Object
get the basename of the backup based on a full file_path such as the SQL value from [backupmediafamily]..
-
.backup_fileset_names(backup_files) ⇒ Object
Returns a single string to be used for the source for a RESTORE command from an array of backup file paths.
- .backup_location_and_basename(start_time, connection_string, database_name, options = {}, backup_folder: nil, backup_url: nil, backup_basename: nil) ⇒ Object
- .backup_sets_from_backup_files(backup_files) ⇒ Object
-
.backup_sets_from_unc_path(sql_server_settings, backup_folder, backup_basename, log_only: false, database_backup_header: nil, restored_database_lsn: nil) ⇒ Object
sql_server_settings can be for any server that has network access to these files.
- .backup_sets_from_url(sql_server_settings, backup_url, backup_basename, options, log_only: false, database_backup_header: nil, restored_database_lsn: nil) ⇒ Object
-
.check_header_date(sql_backup_header, backup_start_time, messages = :none) ⇒ Object
messages: :none :prebackup || :prerestore - Output info messages pertaining to a backup or restore returns: :current :outdated :nobackup.
- .compress_all_tables(connection_string) ⇒ Object
- .config ⇒ Object
- .connection_string_accessible?(connection_string, suppress_failure: true, retries: 3, retry_delay: 5) ⇒ Boolean
- .connection_string_credentials_from_hash(**credentials_hash) ⇒ Object
-
.connection_string_from_hash(**connection_hash) ⇒ Object
generates a connection string from the hash provided.
-
.connection_string_part(connection_string, part, value_only: true) ⇒ Object
options: :server, :database, :credentials, :readonly.
- .connection_string_part_regex(part) ⇒ Object
-
.convert_powershell_tables_to_hash(json_string, return_type = :all_tables, at_timezone: 'UTC', string_to_time_by: '%Y-%m-%d %H:%M:%S %z') ⇒ Object
options: :all_tables, :first_table, :first_row.
-
.convert_powershell_time_objects(value, at_timezone: 'UTC', string_to_time_by: '%Y-%m-%d %H:%M:%S %z', timezone_override: nil, override_strings: false, utc_column: false) ⇒ Object
at_timezone: convert all times to this time zone timezone_override: overwrite the timezone on the time without changing the timestamp value - this occurs before :at_timezone string_to_time_by: Provide the format of the string to be parsed - see ruby-doc.org/stdlib-2.4.1/libdoc/time/rdoc/Time.html#method-c-strptime - Set it to nil or false to not parse any strings.
- .create_sql_login(connection_string, user, password, update_existing: false, retries: 3, retry_delay: 5) ⇒ Object
- .database_roles_assigned?(connection_string, database_name, user, roles, sid) ⇒ Boolean
- .defaults ⇒ Object
- .ensure_sql_agent_is_running(connection_string) ⇒ Object
-
.execute_query(connection_string, sql_query, return_type: :all_tables, values: nil, timeout: 172_800, readonly: false, ignore_missing_values: false, at_timezone: SqlCmd.config['environment']['timezone'], string_to_time_by: '%Y-%m-%d %H:%M:%S %z', retries: 0, retry_delay: 5) ⇒ Object
Execute a SQL query and return a dataset, table, row, or scalar, based on the return_type specified return_type: :all_tables, :first_table, :first_row, :scalar values: hash of values to replace sqlcmd style variables.
- .execute_script_file(connection_string, import_script_filename, values: nil, readonly: false, retries: 0, retry_delay: 15) ⇒ Object
- .export_logins(start_time, connection_string, database_name, remove_existing_logins: true) ⇒ Object
- .gap_in_log_backups?(sql_server_settings, database_backup_header, restored_database_lsn, backup_folder, backup_basename, options = {}) ⇒ Boolean
-
.get_backup_file_info(connection_string, backup_files, options) ⇒ Object
Returns the data and log file information contained in the backup files.
-
.get_backup_files(sql_server_settings, options = {}, backup_folder: nil, backup_url: nil, backup_basename: nil) ⇒ Object
get a list of backup files and the backup basename.
- .get_backup_logical_names(connection_string, backup_files, options) ⇒ Object
-
.get_backup_size(sql_backup_header) ⇒ Object
Get size of uncompressed database from backup header in MB.
- .get_backup_sql_server_settings(connection_string) ⇒ Object
-
.get_database_size(connection_string, database_name, log_only: false, retries: 3, retry_delay: 15) ⇒ Object
Returns the database size or log size in MB.
-
.get_sql_backup_headers(connection_string, backup_files, options = {}) ⇒ Object
Returns the headers from the backup set provided.
-
.get_sql_disk_space(connection_string, target_folder, retries: 3, retry_delay: 15) ⇒ Object
Returns a hash with the following fields: Available_MB, Total_MB, Percent_Free.
- .get_sql_server_settings(connection_string, retries: 3, retry_delay: 15) ⇒ Object
-
.get_unc_backup_files(sql_server_settings, backup_folder, backup_basename, log_only: false, all_time_stamps: false) ⇒ Object
sql_server_settings can be for any server that has network access to these files.
-
.get_url_backup_files(sql_server_settings, backup_url, backup_basename, options = {}, log_only: false, all_time_stamps: false) ⇒ Object
get a list of backup files from a URL.
- .hide_connection_string_password(connection_string) ⇒ Object
-
.insert_values(sql_query, values, case_sensitive: false) ⇒ Object
Substitute sqlcmd style variables with values provided in a hash.
- .migrate_logins(start_time, source_connection_string, destination_connection_string, database_name) ⇒ Object
- .most_recent_backup_files_and_basename(sql_server_settings, backup_files, backup_basename, options = {}) ⇒ Object
- .relevant_log_backup_sets(sql_server_settings, backup_files, database_backup_header, restored_database_lsn, options = {}) ⇒ Object
-
.remove_connection_string_part(connection_string, part) ⇒ Object
part options: :server, :database, :credentials, :applicationintent.
-
.replace_connection_string_part(connection_string, part, replacement_value) ⇒ Object
Supply entire value (EG: ‘user id=value;password=value;’ or ‘integrated security=SSPI;’) as the replacement_value if the part is :credentials or provide a hash containing a username and password { user: ‘someuser’, password: ‘somepassword’, }.
- .required_keys ⇒ Object
- .run_sql_as_job(connection_string, sql_script, sql_job_name, sql_job_owner = 'sa', values: nil, retries: 0, retry_delay: 15) ⇒ Object
- .sql_login_exists?(connection_string, login, retries: 3, retry_delay: 15) ⇒ Boolean
- .sql_script_dir ⇒ Object
- .sql_server_backup_files(sql_server_settings, backup_basename, log_only: false) ⇒ Object
-
.to_integrated_security(connection_string, server_only: false) ⇒ Object
Ensures a connection string is using integrated security instead of SQL Authentication.
-
.to_unc_path(path, server_name) ⇒ Object
converts a path to unc_path if it contains a drive letter.
-
.to_utf8(text) ⇒ Object
Convert encoding of a string to utf8.
- .unify_start_time(start_time, timezone: ) ⇒ Object
-
.update_sql_compatibility(connection_string, database_name, compatibility_level) ⇒ Object
compatibility_level options: :sql_2008, :sql_2012, :sql_2016, :sql_2017, :sql_2019.
- .validate_logins_script(connection_string, database_name) ⇒ Object
Class Method Details
.apply_log_level ⇒ Object
56 57 58 59 |
# File 'lib/sql_cmd/config.rb', line 56 def apply_log_level config_level = config['logging']['level'] EasyIO.logger.level = EasyIO.levels[config_level] end |
.assign_database_roles(connection_string, database_name, user, roles = ['db_owner'], retries: 3, retry_delay: 5) ⇒ Object
roles: array of database roles
301 302 303 304 305 306 307 308 |
# File 'lib/sql_cmd/query.rb', line 301 def assign_database_roles(connection_string, database_name, user, roles = ['db_owner'], retries: 3, retry_delay: 5) # roles: array of database roles values = { 'databasename' => database_name, 'user' => user, 'databaseroles' => roles.join(',') } sql_script = ::File.read("#{sql_script_dir}/Security/AssignDatabaseRoles.sql") EasyIO.logger.info "Assigning #{roles.join(', ')} access to [#{user}] for database [#{database_name}]..." result = execute_query(connection_string, sql_script, return_type: :first_row, values: values, retries: retries, retry_delay: retry_delay) raise "Failed to assign SQL database roles for user: [#{user}]!" if result.nil? || result['name'].nil? result end |
.azure_blob_storage_list_backup_files(backup_url, backup_basename, storage_account_name, storage_access_key) ⇒ Object
Get a list of backup files from blob storage
51 52 53 54 55 |
# File 'lib/sql_cmd/backups.rb', line 51 def azure_blob_storage_list_backup_files(backup_url, backup_basename, storage_account_name, storage_access_key) files = SqlCmd::Azure::AttachedStorage.list(storage_account_name, storage_access_key, storage_url: backup_url, filename_prefix: backup_basename) base_url = backup_url[%r{[a-z]+://[^/]+/[^/]+}i] # URL without directories files.map { |f, properties| ["#{base_url}/#{f}", properties[:last_modified]] }.to_h end |
.azure_blob_storage_url?(backup_url) ⇒ Boolean
45 46 47 48 |
# File 'lib/sql_cmd/backups.rb', line 45 def azure_blob_storage_url?(backup_url) return false if backup_url.nil? || backup_url.empty? (backup_url =~ /blob\.core\.windows\.net/i) >= 0 end |
.backup_basename_from_path(backup_path) ⇒ Object
get the basename of the backup based on a full file_path such as the SQL value from [backupmediafamily].
88 89 90 91 |
# File 'lib/sql_cmd/format.rb', line 88 def backup_basename_from_path(backup_path) return nil if backup_path.nil? || backup_path.empty? ::File.basename(backup_path).gsub(/(\.part\d+)?\.(bak|trn)/i, '') end |
.backup_fileset_names(backup_files) ⇒ Object
Returns a single string to be used for the source for a RESTORE command from an array of backup file paths
104 105 106 107 108 109 110 111 112 113 |
# File 'lib/sql_cmd/backups.rb', line 104 def backup_fileset_names(backup_files) result = '' backup_files.each do |backup_file| backup_type = backup_file =~ /^http/i ? 'URL' : 'DISK' current_file = " #{backup_type} = N''#{backup_file}''," current_file = backup_type =~ /url/i ? current_file.tr('\\', '/') : current_file.tr('/', '\\') result << current_file end result.chomp(',') end |
.backup_location_and_basename(start_time, connection_string, database_name, options = {}, backup_folder: nil, backup_url: nil, backup_basename: nil) ⇒ Object
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 |
# File 'lib/sql_cmd/backups.rb', line 57 def backup_location_and_basename(start_time, connection_string, database_name, = {}, backup_folder: nil, backup_url: nil, backup_basename: nil) start_time = SqlCmd.unify_start_time(start_time) database_info = SqlCmd::Database.info(connection_string, database_name) server_settings = get_sql_server_settings(connection_string) backup_type = backup_url.nil? || backup_url.empty? ? 'DISK' : 'URL' if database_info['DatabaseNotFound'] backup_basename = "#{database_name}_#{EasyTime.yyyymmdd(start_time)}" return [backup_url, backup_basename] if backup_type == 'URL' backup_unc_location = SqlCmd.config['sql_cmd']['backups']['backup_to_host_sql_server'] ? "\\\\#{server_settings['ServerName']}\\#{SqlCmd.config['sql_cmd']['backups']['default_backup_share']}" : SqlCmd.config['sql_cmd']['backups']['default_destination'] return Dir.glob("#{backup_unc_location}/#{backup_basename}*".tr('\\', '/')).empty? ? [nil, nil] : [backup_unc_location, backup_basename] end backup_file_path = database_info['BackupFileLocation'] if backup_file_path =~ /^\{.*\}$/ backup_basename ||= "#{database_name}_#{EasyTime.yyyymmdd(start_time)}" backup_files, backup_basename = SqlCmd::Database.existing_backup_files(server_settings, , backup_folder: backup_folder, backup_url: backup_url, backup_basename: backup_basename, log_only: ['logonly']) backup_file_path = backup_files.first end backup_file = ::File.basename(backup_file_path) backup_basename = backup_basename_from_path(backup_file) return [nil, backup_basename] if backup_type == 'URL' backup_unc_location = to_unc_path(::File.dirname(backup_file_path), server_settings['ServerName']) backup_folder = if ::File.exist?("#{backup_unc_location}/#{backup_file}") backup_unc_location elsif ::File.exist?("\\\\#{server_settings['ServerName']}\\#{SqlCmd.config['sql_cmd']['backups']['default_backup_share']}\\#{backup_file}") "\\\\#{server_settings['ServerName']}\\#{SqlCmd.config['sql_cmd']['backups']['default_backup_share']}" end return [nil, nil] unless defined?(backup_folder) [backup_folder, backup_basename] end |
.backup_sets_from_backup_files(backup_files) ⇒ Object
228 229 230 231 232 233 234 235 236 |
# File 'lib/sql_cmd/backups.rb', line 228 def backup_sets_from_backup_files(backup_files) backup_sets = {} backup_files.each do |file| current_basename = ::File.basename(file).sub(/(\.part\d+)?\.(bak|trn)$/i, '') # determine basename of current file backup_sets[current_basename] = [] if backup_sets[current_basename].nil? backup_sets[current_basename].push(file) end backup_sets end |
.backup_sets_from_unc_path(sql_server_settings, backup_folder, backup_basename, log_only: false, database_backup_header: nil, restored_database_lsn: nil) ⇒ Object
sql_server_settings can be for any server that has network access to these files
89 90 91 92 93 94 |
# File 'lib/sql_cmd/backups.rb', line 89 def backup_sets_from_unc_path(sql_server_settings, backup_folder, backup_basename, log_only: false, database_backup_header: nil, restored_database_lsn: nil) backup_files, backup_basename = get_unc_backup_files(sql_server_settings, backup_folder, backup_basename, log_only: log_only, all_time_stamps: log_only) backup_sets = log_only ? relevant_log_backup_sets(sql_server_settings, backup_files, database_backup_header, restored_database_lsn) : { backup_basename => backup_files } EasyIO.logger.debug "Database backup sets found: #{JSON.pretty_generate(backup_sets)}" backup_sets end |
.backup_sets_from_url(sql_server_settings, backup_url, backup_basename, options, log_only: false, database_backup_header: nil, restored_database_lsn: nil) ⇒ Object
96 97 98 99 100 101 |
# File 'lib/sql_cmd/backups.rb', line 96 def backup_sets_from_url(sql_server_settings, backup_url, backup_basename, , log_only: false, database_backup_header: nil, restored_database_lsn: nil) backup_files, backup_basename = get_url_backup_files(sql_server_settings, backup_url, backup_basename, , log_only: log_only, all_time_stamps: log_only) backup_sets = log_only ? relevant_log_backup_sets(sql_server_settings, backup_files, database_backup_header, restored_database_lsn) : { backup_basename => backup_files } EasyIO.logger.debug "Database backup sets found: #{JSON.pretty_generate(backup_sets)}" backup_sets end |
.check_header_date(sql_backup_header, backup_start_time, messages = :none) ⇒ Object
messages:
:none
:prebackup || :prerestore - Output info messages pertaining to a backup or restore
returns:
:current
:outdated
:nobackup
267 268 269 270 271 272 273 274 275 276 277 278 279 280 |
# File 'lib/sql_cmd/backups.rb', line 267 def check_header_date(sql_backup_header, backup_start_time, = :none) backup_start_time = SqlCmd.unify_start_time(backup_start_time) return :nobackup if sql_backup_header.nil? || sql_backup_header.empty? EasyIO.logger.info "Last backup for [#{sql_backup_header['DatabaseName']}] completed: #{sql_backup_header['BackupFinishDate']}" if [:prebackup, :prerestore].include?() backup_finish_time = sql_backup_header['BackupFinishDate'] raise "BackupFinishDate missing from backup header: #{sql_backup_header}" if backup_finish_time.nil? if backup_finish_time > backup_start_time EasyIO.logger.info 'Backup is current. Bypassing database backup.' if == :prebackup EasyIO.logger.info 'Backup is current. Proceeding with restore...' if == :prerestore :current else :outdated end end |
.compress_all_tables(connection_string) ⇒ Object
333 334 335 336 337 338 339 340 341 342 343 344 345 |
# File 'lib/sql_cmd/query.rb', line 333 def compress_all_tables(connection_string) uncompressed_count_script = ::File.read("#{sql_script_dir}/Status/UncompressedTableCount.sql") compress_tables_script = ::File.read("#{sql_script_dir}/Database/CompressAllTables.sql") EasyIO.logger.info 'Checking for uncompressed tables...' uncompressed_count = execute_query(connection_string, uncompressed_count_script, return_type: :scalar, retries: 3) if uncompressed_count > 0 EasyIO.logger.info "Compressing #{uncompressed_count} tables..." execute_query(connection_string, compress_tables_script) EasyIO.logger.info 'Compression complete.' else EasyIO.logger.info 'No uncompressed tables.' end end |
.config ⇒ Object
4 5 6 |
# File 'lib/sql_cmd/config.rb', line 4 def config @config ||= EasyJSON.config(defaults: defaults, required_keys: required_keys) end |
.connection_string_accessible?(connection_string, suppress_failure: true, retries: 3, retry_delay: 5) ⇒ Boolean
155 156 157 158 159 160 161 |
# File 'lib/sql_cmd/query.rb', line 155 def connection_string_accessible?(connection_string, suppress_failure: true, retries: 3, retry_delay: 5) sql_script = 'SELECT @@SERVERNAME AS [ServerName]' !execute_query(connection_string, sql_script, return_type: :scalar, retries: retries, retry_delay: retry_delay).nil? rescue raise unless suppress_failure false end |
.connection_string_credentials_from_hash(**credentials_hash) ⇒ Object
93 94 95 96 97 98 |
# File 'lib/sql_cmd/format.rb', line 93 def connection_string_credentials_from_hash(**credentials_hash) credentials_hash = Hashly.symbolize_all_keys(credentials_hash.dup) windows_authentication = credentials_hash[:windows_authentication] windows_authentication = credentials_hash[:user].nil? || credentials_hash[:password].nil? if windows_authentication.nil? # If windows authentication wasn't specified, set it if user or pass is nil windows_authentication ? 'integrated security=SSPI;' : "user id=#{credentials_hash[:user]};password=#{credentials_hash[:password]}" end |
.connection_string_from_hash(**connection_hash) ⇒ Object
generates a connection string from the hash provided. Example hash: { ‘server’ => ‘someservername’, ‘database’ => ‘somedb’, ‘user’ => ‘someuser’, ‘password’ => ‘somepass’, ‘windows_authentication’ => false }
101 102 103 104 105 106 107 |
# File 'lib/sql_cmd/format.rb', line 101 def connection_string_from_hash(**connection_hash) connection_hash = Hashly.symbolize_all_keys(connection_hash.dup) credentials_segment = connection_string_credentials_from_hash(**connection_hash) database_name = connection_hash[:database] database_segment = database_name.nil? || database_name.strip.empty? ? '' : "database=#{database_name};" "server=#{connection_hash[:server]};#{database_segment}#{credentials_segment}" end |
.connection_string_part(connection_string, part, value_only: true) ⇒ Object
options: :server, :database, :credentials, :readonly
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
# File 'lib/sql_cmd/format.rb', line 23 def connection_string_part(connection_string, part, value_only: true) # options: :server, :database, :credentials, :readonly raise 'Connection string provided is nil or empty!' if connection_string.nil? || connection_string.empty? case part when :server, :database, :applicationintent connection_string[connection_string_part_regex(part)] when :user, :password credentials = connection_string_part(connection_string, :credentials) return nil if credentials.nil? return credentials[part] when :credentials # array of user/password or integrated security connection_string[connection_string_part_regex(:user)] user = Regexp.last_match(3) connection_string[connection_string_part_regex(:password)] password = Regexp.last_match(3) return { user: user, password: password } unless user.nil? || password.nil? return connection_string[connection_string_part_regex(:integrated)] end return Regexp.last_match(3) if value_only result = (Regexp.last_match(1) || '') + '=' + (Regexp.last_match(3) || '') + (Regexp.last_match(4) || '') result.empty? ? nil : result end |
.connection_string_part_regex(part) ⇒ Object
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
# File 'lib/sql_cmd/format.rb', line 4 def connection_string_part_regex(part) case part when :server /(server|data source)(\s*\=\s*)([^;]*)(;)?/i when :database /(database|initial catalog)(\s*\=\s*)([^;]*)(;)?/i when :user # array of user/password or integrated security /(user id|uid)(\s*\=\s*)([^;]*)(;)?/i when :password /(password|pwd)(\s*\=\s*)([^;]*)(;)?/i when :integrated /integrated security\s*\=\s*[^;]*(;)?/i when :applicationintent /applicationintent\s*\=\s*[^;]*(;)?/i else raise "#{part} is not a supported connection string part!" end end |
.convert_powershell_tables_to_hash(json_string, return_type = :all_tables, at_timezone: 'UTC', string_to_time_by: '%Y-%m-%d %H:%M:%S %z') ⇒ Object
options: :all_tables, :first_table, :first_row
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
# File 'lib/sql_cmd/query.rb', line 100 def convert_powershell_tables_to_hash(json_string, return_type = :all_tables, at_timezone: 'UTC', string_to_time_by: '%Y-%m-%d %H:%M:%S %z') # options: :all_tables, :first_table, :first_row EasyIO.logger.debug "Output from sql command: #{json_string}" if SqlCmd.config['logging']['verbose'] parsed_json = JSON.parse(to_utf8(json_string.sub(/[^{\[]*/, ''))) # Ignore any leading characters other than '{' or '[' timezone_table = parsed_json.delete(parsed_json.keys.last) sqlserver_timezone = timezone_table.first.values.first # The last table should be the SQL server's time zone - get the value and remove it from the dataset result_hash = if json_string.empty? {} else convert_powershell_time_objects(parsed_json, at_timezone: at_timezone, string_to_time_by: string_to_time_by, timezone_override: sqlserver_timezone) end raise 'No tables were returned by specified sql query!' if result_hash.values.first.nil? && return_type != :all_tables case return_type when :first_table result_hash.values.first when :first_row result_hash.values.first.first when :scalar return nil if result_hash.values.first.first.nil? result_hash.values.first.first.values.first # Return first column of first row of first table else result_hash.values end end |
.convert_powershell_time_objects(value, at_timezone: 'UTC', string_to_time_by: '%Y-%m-%d %H:%M:%S %z', timezone_override: nil, override_strings: false, utc_column: false) ⇒ Object
at_timezone: convert all times to this time zone timezone_override: overwrite the timezone on the time without changing the timestamp value - this occurs before :at_timezone string_to_time_by: Provide the format of the string to be parsed - see ruby-doc.org/stdlib-2.4.1/libdoc/time/rdoc/Time.html#method-c-strptime
- Set it to nil or false to not parse any strings
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 |
# File 'lib/sql_cmd/query.rb', line 129 def convert_powershell_time_objects(value, at_timezone: 'UTC', string_to_time_by: '%Y-%m-%d %H:%M:%S %z', timezone_override: nil, override_strings: false, utc_column: false) case value when Array value.map { |v| convert_powershell_time_objects(v, at_timezone: at_timezone, string_to_time_by: string_to_time_by, timezone_override: timezone_override, utc_column: utc_column) } when Hash Hash[value.map { |k, v| [k, convert_powershell_time_objects(v, at_timezone: at_timezone, string_to_time_by: string_to_time_by, timezone_override: timezone_override, utc_column: k =~ /utc/i)] }] else timezone_override = 'UTC' if utc_column return value unless value.is_a?(String) time_from_js = EasyTime.from_javascript_format(value) if time_from_js # A java script time was found value = EasyTime.stomp_timezone(time_from_js, timezone_override) at_timezone ? EasyTime.at_timezone(value, at_timezone) : value else return value unless string_to_time_by begin value = Time.strptime(value, string_to_time_by) value = EasyTime.stomp_timezone(value, timezone_override) if override_strings at_timezone ? EasyTime.at_timezone(value, at_timezone) : value rescue ArgumentError # If an ArgumentError is thrown, the string was not in the :string_to_time_by format expected so it's probably not a time. Return it as is. value end end end end |
.create_sql_login(connection_string, user, password, update_existing: false, retries: 3, retry_delay: 5) ⇒ Object
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 |
# File 'lib/sql_cmd/query.rb', line 214 def create_sql_login(connection_string, user, password, update_existing: false, retries: 3, retry_delay: 5) sid_script = ::File.read("#{sql_script_dir}/Security/GetUserSID.sql") raise "SQL password for login [#{user}] must not be empty!" if password.nil? || password.empty? values = { 'user' => user, 'password' => password } EasyIO.logger.info "Checking for existing SqlLogin: #{user}..." login_sid = execute_query(connection_string, sid_script, return_type: :scalar, values: values, readonly: true, retries: retries, retry_delay: retry_delay) if login_sid.nil? sql_script = ::File.read("#{sql_script_dir}/Security/CreateSqlLogin.sql") EasyIO.logger.info "Creating SqlLogin: #{user}..." result = execute_query(connection_string, sql_script, return_type: :first_row, values: values, retries: retries, retry_delay: retry_delay) raise "Failed to create SQL login: [#{user}]!" if result.nil? || result['name'].nil? login_sid = result['sid'] elsif update_existing sql_script = ::File.read("#{sql_script_dir}/Security/UpdateSqlPassword.sql") EasyIO.logger.info "Login [#{user}] already exists... updating password." execute_query(connection_string, sql_script, return_type: :first_row, values: values, retries: retries, retry_delay: retry_delay) else EasyIO.logger.info "Login [#{user}] already exists..." end EasyIO.logger.debug "SqlLogin [#{user}] sid: #{login_sid}" login_sid end |
.database_roles_assigned?(connection_string, database_name, user, roles, sid) ⇒ Boolean
310 311 312 313 314 315 |
# File 'lib/sql_cmd/query.rb', line 310 def database_roles_assigned?(connection_string, database_name, user, roles, sid) values = { 'databasename' => database_name, 'user' => user, 'sid' => sid } validation_script = ::File.read("#{sql_script_dir}/Security/ValidateDatabaseRoles.sql") validation_result = execute_query(connection_string, validation_script, return_type: :first_table, values: values, readonly: true, retries: 3) roles.all? { |role| validation_result.any? { |row| row['name'].casecmp(role) == 0 } } end |
.defaults ⇒ Object
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 |
# File 'lib/sql_cmd/config.rb', line 19 def defaults { 'paths' => { 'cache' => Dir.tmpdir, }, 'environment' => { 'timezone' => 'UTC', }, 'logging' => { 'level' => 'info', 'verbose' => false, # show queries and returned json }, 'sql_cmd' => { 'backups' => { 'always_on_backup_temp_dir' => nil, # where backups will go when adding to availability groups and seeding_mode is manual or nil 'always_on_backup_url' => nil, # where backups will go when adding to availability groups and seeding_mode is manual or nil - supercedes temp dir above 'default_destination' => nil, # where backups will go by default 'backup_to_host_sql_server' => false, # if set to true, will backup databases to the SQL host instead of the default destination 'default_backup_share' => nil, # the name of the windows share relative to SQL hosts where backups go when set to backup to sql hosts 'free_space_threshold' => 5, # raises an exception if a backup or restore operation would bring free space on the target location below this threshold 'compress_backups' => false, }, 'exports' => { 'include_table_permissions' => false, }, 'paths' => { 'sql_script_dir' => ::File.("#{__dir__}/../../sql_scripts"), 'powershell_helper_script' => ::File.("#{__dir__}/sql_helper.ps1"), }, }, } end |
.ensure_sql_agent_is_running(connection_string) ⇒ Object
327 328 329 330 331 |
# File 'lib/sql_cmd/query.rb', line 327 def ensure_sql_agent_is_running(connection_string) sql_script = ::File.read("#{sql_script_dir}/Agent/SQLAgentStatus.sql") sql_server_settings = get_sql_server_settings(connection_string) raise "SQL Agent is not running on #{sql_server_settings['ServerName']}!" if execute_query(connection_string, sql_script, return_type: :scalar, retries: 3) == 0 end |
.execute_query(connection_string, sql_query, return_type: :all_tables, values: nil, timeout: 172_800, readonly: false, ignore_missing_values: false, at_timezone: SqlCmd.config['environment']['timezone'], string_to_time_by: '%Y-%m-%d %H:%M:%S %z', retries: 0, retry_delay: 5) ⇒ Object
Execute a SQL query and return a dataset, table, row, or scalar, based on the return_type specified
return_type: :all_tables, :first_table, :first_row, :scalar
values: hash of values to replace sqlcmd style variables.
EG: sql_query = SELECT * FROM $(databasename)
values = { 'databasename' => 'my_database_name' }
readonly: if true, sets the connection_string to readonly (Useful with AOAG)
retries: number of times to re-attempt failed queries
retry_delay: how many seconds to wait between retries
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 |
# File 'lib/sql_cmd/query.rb', line 15 def execute_query(connection_string, sql_query, return_type: :all_tables, values: nil, timeout: 172_800, readonly: false, ignore_missing_values: false, at_timezone: SqlCmd.config['environment']['timezone'], string_to_time_by: '%Y-%m-%d %H:%M:%S %z', retries: 0, retry_delay: 5) sql_query = insert_values(sql_query, values) unless values.nil? missing_values = sql_query.reverse.scan(/(\)[0-9a-z_]+\(\$)(?!.*--)/i).uniq.join(' ,').reverse # Don't include commented variables raise "sql_query has missing variables! Ensure that values are supplied for: #{missing_values}\n" unless missing_values.empty? || ignore_missing_values connection_string_updated = connection_string.dup connection_string_updated = replace_connection_string_part(connection_string, :applicationintent, 'readonly') if readonly raise 'Connection string is nil or incomplete' if connection_string_updated.nil? || connection_string_updated.empty? EasyIO.logger.debug "Executing query with connection_string: \n\t#{hide_connection_string_password(connection_string_updated)}" EasyIO.logger.debug sql_query if SqlCmd.config['logging']['verbose'] && sql_query.length < 8096 start_time = Time.now.utc.strftime('%y%m%d_%H%M%S-%L') ps_script = <<-EOS.strip . "#{@scripts_cache_windows}\\sql_helper_#{start_time}.ps1" $connectionString = '#{connection_string_updated}' $sqlCommand = '#{sql_query.gsub('\'', '\'\'')}' $dataset = Invoke-SQL -timeout #{timeout} -connectionString $connectionString -sqlCommand $sqlCommand ConvertSqlDatasetTo-Json -dataset $dataset EOS ps_script_file = "#{@scripts_cache}/ps_script-thread_id-#{Thread.current.object_id}.ps1" FileUtils.mkdir_p ::File.dirname(ps_script_file) FileUtils.cp(SqlCmd.config['sql_cmd']['paths']['powershell_helper_script'], "#{@scripts_cache}/sql_helper_#{start_time}.ps1") ::File.write(ps_script_file, ps_script) retry_count = 0 begin result = '' exit_status = '' Open3.popen3("powershell -File \"#{ps_script_file}\"") do |_stdin, stdout, stderr, wait_thread| buffers = [stdout, stderr] queued_buffers = IO.select(buffers) || [[]] queued_buffers.first.each do |buffer| case buffer when stdout while (line = buffer.gets) stdout_split = line.split('#return_data#:') raise "SQL exception: #{stdout_split.first} #{stdout.read} #{stderr.read}\nConnectionString: '#{hide_connection_string_password(connection_string)}'\n#{EasyIO::Terminal.line('=')}\n" if stdout_split.first =~ /error 50000, severity (1[1-9]|2[0-5])/i EasyIO.logger.info "SQL message: #{stdout_split.first.strip}" unless stdout_split.first.empty? result = stdout_split.last + buffer.read if stdout_split.count > 1 end when stderr = stderr.read raise "SQL exception: #{}\nConnectionString: '#{hide_connection_string_password(connection_string)}'\n#{'=' * 120}\n" unless .empty? end end exit_status = wait_thread.value end EasyIO.logger.debug "Script exit status: #{exit_status}" EasyIO.logger.debug "JSON result: #{result}" rescue = 'Executing SQL query failed! ' += if retries == 0 'No retries specified. Will not reattempt. ' else retry_count < retries ? "Retry #{(retry_count + 1)} of #{retries}" : "All #{retries} retries attempted." end EasyIO.logger.info if (retry_count += 1) <= retries EasyIO.logger.info "Retrying in #{retry_delay} seconds..." sleep(retry_delay) retry end raise end begin convert_powershell_tables_to_hash(result, return_type, at_timezone: at_timezone, string_to_time_by: string_to_time_by) rescue # Change it to use terminal size instead of 120 chars in the error here and above EasyIO.logger.fatal "Failed to convert SQL data to hash! ConnectionString: '#{hide_connection_string_password(connection_string)}'\n#{EasyIO::Terminal.line('=')}\n" raise end ensure retry_count = 0 retries = 3 retry_delay = 3 begin ::File.delete "#{@scripts_cache_windows}\\sql_helper_#{start_time}.ps1" if defined?(start_time) && ::File.exist?("#{@scripts_cache_windows}\\sql_helper_#{start_time}.ps1") rescue # Try to delete the file 3 times if (retry_count += 1) <= retries sleep(retry_delay) retry end raise end end |
.execute_script_file(connection_string, import_script_filename, values: nil, readonly: false, retries: 0, retry_delay: 15) ⇒ Object
295 296 297 298 299 |
# File 'lib/sql_cmd/query.rb', line 295 def execute_script_file(connection_string, import_script_filename, values: nil, readonly: false, retries: 0, retry_delay: 15) return nil if import_script_filename.nil? || import_script_filename.empty? sql_script = ::File.read(import_script_filename) execute_query(connection_string, sql_script, values: values, readonly: readonly, retries: retries, retry_delay: retry_delay) end |
.export_logins(start_time, connection_string, database_name, remove_existing_logins: true) ⇒ Object
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 |
# File 'lib/sql_cmd/query.rb', line 257 def export_logins(start_time, connection_string, database_name, remove_existing_logins: true) start_time = SqlCmd.unify_start_time(start_time) export_folder = "#{SqlCmd.config['paths']['cache']}/sql_cmd/logins" server_name = connection_string_part(connection_string, :server) import_script_filename = "#{export_folder}/#{EasyFormat::File.windows_friendly_name(server_name)}_#{database_name}_logins.sql" if SqlCmd::Database.info(connection_string, database_name)['DatabaseNotFound'] = 'Source database was not found' unless ::File.exist?(import_script_filename) EasyIO.logger.warn "#{}. Unable to export logins! Ensure logins are migrated or migrate them manually!" return import_script_filename end += if File.mtime(import_script_filename) >= start_time ' but the import logins script file already exists. Proceeding...' else ' and the import logins script file is out of date. Ensure logins are migrated or migrate them manually!' end EasyIO.logger.warn return import_script_filename # TODO: attempt to create logins anyway instead of returning a non-existent script file end sql_script = ::File.read("#{sql_script_dir}/Security/GenerateCreateLoginsScript.sql") values = { 'databasename' => database_name, 'removeexistinglogins' => remove_existing_logins } EasyIO.logger.info "Exporting logins associated with database: [#{database_name}] on [#{server_name}]..." import_script = execute_query(connection_string, sql_script, return_type: :scalar, values: values, readonly: true, retries: 3) return nil if import_script.nil? || import_script.empty? FileUtils.mkdir_p(export_folder) ::File.write(import_script_filename, import_script) import_script_filename end |
.gap_in_log_backups?(sql_server_settings, database_backup_header, restored_database_lsn, backup_folder, backup_basename, options = {}) ⇒ Boolean
126 127 128 129 130 131 132 133 134 135 |
# File 'lib/sql_cmd/backups.rb', line 126 def gap_in_log_backups?(sql_server_settings, database_backup_header, restored_database_lsn, backup_folder, backup_basename, = {}) last_full_backup_lsn = database_backup_header['LastLSN'] return true if last_full_backup_lsn.nil? # if the last full backup does not exist, behave as if there is a gap in the log backups backup_sets = backup_sets_from_unc_path(sql_server_settings, backup_folder, backup_basename, database_backup_header: database_backup_header, restored_database_lsn: restored_database_lsn, log_only: true) return true if backup_sets.nil? # nil is returned if the backup is too new for the restored database LSN, therefore there's a gap return false if backup_sets.empty? # if no log backup sets were current, behave as if there is no gap since a log backup hasn't yet been taken since the backup first_lsn_from_log_backups = get_sql_backup_headers(sql_server_settings['connection_string'], backup_sets.first.last, ).first['FirstLSN'] EasyIO.logger.debug "LastLSN from full backup: #{last_full_backup_lsn} | First LSN from log backups: #{first_lsn_from_log_backups}" last_full_backup_lsn < first_lsn_from_log_backups && restored_database_lsn < first_lsn_from_log_backups end |
.get_backup_file_info(connection_string, backup_files, options) ⇒ Object
Returns the data and log file information contained in the backup files.
138 139 140 141 142 143 144 145 |
# File 'lib/sql_cmd/backups.rb', line 138 def get_backup_file_info(connection_string, backup_files, ) EasyFormat.validate_parameters(method(__method__), binding) disk_backup_files = backup_fileset_names(backup_files) sql_script = ::File.read("#{sql_script_dir}/Database/GetFileInfoFromBackup.sql") ['bkupfiles'] = disk_backup_files ['credential'] ||= ['storage_account_name'] || '' execute_query(connection_string, sql_script, return_type: :first_table, values: , retries: 3) end |
.get_backup_files(sql_server_settings, options = {}, backup_folder: nil, backup_url: nil, backup_basename: nil) ⇒ Object
get a list of backup files and the backup basename
options:
log_only: determines whether to look for log backup files or normal backup files. (.trn vs .bak) default: false
all_time_stamps: returns all backup sets found matching the basename and not just the most recent. default: false
storage_account_name: (required if using Azure blob storage) Azure blob storage account name.
storage_access_key: (required if using Azure blob storage) Azure blob storage access key.
245 246 247 248 249 250 251 252 253 |
# File 'lib/sql_cmd/backups.rb', line 245 def get_backup_files(sql_server_settings, = {}, backup_folder: nil, backup_url: nil, backup_basename: nil) if !backup_url.nil? && !backup_url.empty? get_url_backup_files(sql_server_settings, backup_url, backup_basename, ) elsif backup_folder.start_with?('\\\\') get_unc_backup_files(sql_server_settings, backup_folder, backup_basename, log_only: ['log_only']) else sql_server_backup_files(sql_server_settings, backup_basename, log_only: log_only) end end |
.get_backup_logical_names(connection_string, backup_files, options) ⇒ Object
147 148 149 150 151 152 153 |
# File 'lib/sql_cmd/backups.rb', line 147 def get_backup_logical_names(connection_string, backup_files, ) EasyFormat.validate_parameters(method(__method__), binding) sql_backup_file_info = SqlCmd.get_backup_file_info(connection_string, backup_files, ) data_file_logical_name = sql_backup_file_info.select { |file| file['Type'] == 'D' }.first['LogicalName'] log_file_logical_name = sql_backup_file_info.select { |file| file['Type'] == 'L' }.first['LogicalName'] [data_file_logical_name, log_file_logical_name] end |
.get_backup_size(sql_backup_header) ⇒ Object
Get size of uncompressed database from backup header in MB
256 257 258 |
# File 'lib/sql_cmd/backups.rb', line 256 def get_backup_size(sql_backup_header) sql_backup_header['BackupSize'].to_f / 1024 / 1024 end |
.get_backup_sql_server_settings(connection_string) ⇒ Object
6 7 8 9 10 11 |
# File 'lib/sql_cmd/backups.rb', line 6 def get_backup_sql_server_settings(connection_string) sql_server_settings = get_sql_server_settings(connection_string) sql_server_settings = get_sql_server_settings(to_integrated_security(connection_string)) if sql_server_settings.nil? || sql_server_settings['BackupDir'].nil? || sql_server_settings['BackupDir'] == 'null' raise "FATAL: Current user #{ENV['user'] || ENV['username']} does not have access to backup database!" if sql_server_settings.nil? || sql_server_settings['BackupDir'].nil? || sql_server_settings['BackupDir'] == 'null' sql_server_settings end |
.get_database_size(connection_string, database_name, log_only: false, retries: 3, retry_delay: 15) ⇒ Object
Returns the database size or log size in MB
202 203 204 205 206 |
# File 'lib/sql_cmd/query.rb', line 202 def get_database_size(connection_string, database_name, log_only: false, retries: 3, retry_delay: 15) sql_script = ::File.read("#{sql_script_dir}/Status/DatabaseSize.sql") connection_string = remove_connection_string_part(connection_string, :database) execute_query(connection_string, sql_script, values: { 'databasename' => database_name, 'logonly' => log_only }, return_type: :scalar, readonly: true, retries: retries, retry_delay: retry_delay).to_f end |
.get_sql_backup_headers(connection_string, backup_files, options = {}) ⇒ Object
Returns the headers from the backup set provided. Pass an array of path strings to the backup files.
116 117 118 119 120 121 122 123 124 |
# File 'lib/sql_cmd/backups.rb', line 116 def get_sql_backup_headers(connection_string, backup_files, = {}) EasyFormat.validate_parameters(method(__method__), binding, %W(options)) disk_backup_files = backup_fileset_names(backup_files) sql_script = ::File.read("#{sql_script_dir}/Database/GetBackupHeaders.sql") ['bkupfiles'] = disk_backup_files ['credential'] ||= ['storage_account_name'] || '' SqlCmd::Security.create_credential(connection_string, ['credential'], ['storage_account_name'], ['storage_access_key'], ) unless ['credential'].nil? || ['credential'].empty? execute_query(connection_string, sql_script, return_type: :first_table, values: , retries: 3, retry_delay: 10) end |
.get_sql_disk_space(connection_string, target_folder, retries: 3, retry_delay: 15) ⇒ Object
Returns a hash with the following fields: Available_MB, Total_MB, Percent_Free
209 210 211 212 |
# File 'lib/sql_cmd/query.rb', line 209 def get_sql_disk_space(connection_string, target_folder, retries: 3, retry_delay: 15) sql_script = ::File.read("#{sql_script_dir}/Status/DiskSpace.sql") execute_query(connection_string, sql_script, return_type: :first_row, values: { 'targetfolder' => target_folder }, retries: retries, retry_delay: retry_delay) end |
.get_sql_server_settings(connection_string, retries: 3, retry_delay: 15) ⇒ Object
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 |
# File 'lib/sql_cmd/query.rb', line 163 def get_sql_server_settings(connection_string, retries: 3, retry_delay: 15) get_sql_settings_script = ::File.read("#{sql_script_dir}/Status/SQLSettings.sql") sql_server_settings = execute_query(SqlCmd.remove_connection_string_part(connection_string, :database), get_sql_settings_script, return_type: :first_row, retries: retries, retry_delay: retry_delay) EasyIO.logger.debug "sql_server_settings: \n#{JSON.pretty_generate(sql_server_settings)}" return nil if sql_server_settings.nil? || sql_server_settings['ServerName'].nil? direct_connection_string = connection_string.gsub(sql_server_settings['DataSource'], sql_server_settings['ServerName']) application_connection_string = connection_string.gsub(sql_server_settings['ServerName'], sql_server_settings['DataSource']) secondary_replica_connection_string = if sql_server_settings['SecondaryReplica'].nil? nil else cnstr = SqlCmd.replace_connection_string_part(connection_string, :server, sql_server_settings['SecondaryReplica']) SqlCmd.remove_connection_string_part(cnstr, :database) end sql_server_settings['direct_connection_string'] = direct_connection_string # Does not use AlwaysOn listener sql_server_settings['connection_string'] = application_connection_string sql_server_settings['secondary_replica_connection_string'] = secondary_replica_connection_string sql_server_settings['DataDir'] = EasyFormat::Directory.ensure_trailing_slash(sql_server_settings['DataDir']) sql_server_settings['LogDir'] = EasyFormat::Directory.ensure_trailing_slash(sql_server_settings['LogDir']) sql_server_settings['BackupDir'] = EasyFormat::Directory.ensure_trailing_slash(sql_server_settings['BackupDir']) sql_server_settings end |
.get_unc_backup_files(sql_server_settings, backup_folder, backup_basename, log_only: false, all_time_stamps: false) ⇒ Object
sql_server_settings can be for any server that has network access to these files
14 15 16 17 18 19 20 21 22 |
# File 'lib/sql_cmd/backups.rb', line 14 def get_unc_backup_files(sql_server_settings, backup_folder, backup_basename, log_only: false, all_time_stamps: false) backup_folder = EasyFormat::Directory.ensure_trailing_slash(backup_folder).tr('\\', '/') backup_file_extension = backup_basename.slice!(/\.(trn|bak)/i) backup_file_extension ||= log_only ? 'trn' : 'bak' backup_file_extension = backup_file_extension.reverse.chomp('.').reverse backup_files = Dir.glob("#{backup_folder}#{backup_basename}*").grep(/#{Regexp.escape(backup_basename)}(_\d{6})?(\.part\d+)?\.#{backup_file_extension}$/i) return [backup_files, backup_basename] if all_time_stamps most_recent_backup_files_and_basename(sql_server_settings, backup_files, backup_basename) end |
.get_url_backup_files(sql_server_settings, backup_url, backup_basename, options = {}, log_only: false, all_time_stamps: false) ⇒ Object
get a list of backup files from a URL
options:
log_only: determines whether to look for log backup files or normal backup files. (.trn vs .bak) default: false
all_time_stamps: returns all backup sets found matching the basename and not just the most recent. default: false
storage_account_name: (required if using Azure blob storage) Azure blob storage account name.
storage_access_key: (required if using Azure blob storage) Azure blob storage access key.
31 32 33 34 35 36 37 38 39 40 41 42 43 |
# File 'lib/sql_cmd/backups.rb', line 31 def get_url_backup_files(sql_server_settings, backup_url, backup_basename, = {}, log_only: false, all_time_stamps: false) if azure_blob_storage_url?(backup_url) backup_file_extension = backup_basename.slice!(/\.(trn|bak)/i) backup_file_extension ||= log_only ? 'trn' : 'bak' backup_file_extension = backup_file_extension.reverse.chomp('.').reverse backup_files = azure_blob_storage_list_backup_files(backup_url, backup_basename, ['storage_account_name'], ['storage_access_key']) backup_files.select! { |f, _modified| f =~ /#{Regexp.escape(backup_file_extension)}$/i } return [backup_files.keys, backup_basename] if ['all_time_stamps'] most_recent_backup_files_and_basename(sql_server_settings, backup_files.keys, backup_basename, ) else EasyIO.logger.warn 'Non-azure blob storage URLs are not currently supported.' end end |
.hide_connection_string_password(connection_string) ⇒ Object
52 53 54 55 56 57 |
# File 'lib/sql_cmd/format.rb', line 52 def hide_connection_string_password(connection_string) credentials = connection_string_part(connection_string, :credentials) credentials[:password] = credentials[:password].gsub(/(.)([^(.$)]*)(.$)/) { Regexp.last_match(1) + ('*' * Regexp.last_match(2).length) + Regexp.last_match(3) } if credentials.is_a?(Hash) && !credentials[:password].nil? && credentials[:password].length > 2 raise "Connection string missing authentication information! Connection string: '#{connection_string}'" if credentials.nil? || credentials.empty? replace_connection_string_part(connection_string, :credentials, credentials) end |
.insert_values(sql_query, values, case_sensitive: false) ⇒ Object
Substitute sqlcmd style variables with values provided in a hash. Don’t include $() Example values: ‘varname’ => ‘Some value’, ‘varname2’ => ‘some other value’
188 189 190 191 192 193 194 195 196 197 198 199 |
# File 'lib/sql_cmd/query.rb', line 188 def insert_values(sql_query, values, case_sensitive: false) return sql_query if values.nil? || values.all? { |i, _j| i.nil? || i.empty? } EasyIO.logger.debug "Inserting variable values into query: #{JSON.pretty_generate(values)}" sql_query = to_utf8(sql_query) values.each do |key, value| regexp_key = case_sensitive ? "$(#{key})" : /\$\(#{Regexp.escape(key)}\)/i sql_query.gsub!(regexp_key) { value } end sql_query end |
.migrate_logins(start_time, source_connection_string, destination_connection_string, database_name) ⇒ Object
245 246 247 248 249 250 251 252 253 254 255 |
# File 'lib/sql_cmd/query.rb', line 245 def migrate_logins(start_time, source_connection_string, destination_connection_string, database_name) start_time = SqlCmd.unify_start_time(start_time) import_script_filename = export_logins(start_time, source_connection_string, database_name) return if import_script_filename.nil? if ::File.exist?(import_script_filename) EasyIO.logger.info "Importing logins on [#{connection_string_part(destination_connection_string, :server)}]..." execute_script_file(destination_connection_string, import_script_filename) else EasyIO.logger.warn 'Unable to migrate logins. Ensure they exist or manually create them.' end end |
.most_recent_backup_files_and_basename(sql_server_settings, backup_files, backup_basename, options = {}) ⇒ Object
173 174 175 176 177 178 179 180 181 182 183 184 185 |
# File 'lib/sql_cmd/backups.rb', line 173 def most_recent_backup_files_and_basename(sql_server_settings, backup_files, backup_basename, = {}) backup_sets = backup_sets_from_backup_files(backup_files) if backup_sets.keys.count > 1 # if there is more than 1 backup set, find the most recent backup_headers = {} backup_sets.each do |basename, files| backup_headers[basename] = get_sql_backup_headers(sql_server_settings['connection_string'], files, ).first end backup_basename = backup_headers.max_by { |_basename, header| header['BackupFinishDate'] }.first elsif backup_sets.keys.count == 0 # if there are no backup sets, use an empty array backup_sets[backup_basename] = [] end [backup_sets[backup_basename], backup_basename] end |
.relevant_log_backup_sets(sql_server_settings, backup_files, database_backup_header, restored_database_lsn, options = {}) ⇒ Object
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 |
# File 'lib/sql_cmd/backups.rb', line 187 def relevant_log_backup_sets(sql_server_settings, backup_files, database_backup_header, restored_database_lsn, = {}) restored_database_lsn ||= 0 backup_sets = backup_sets_from_backup_files(backup_files) database_backup_lsn = database_backup_header['DatabaseBackupLSN'] EasyIO.logger.debug "Database backup LSN: #{database_backup_lsn}" backup_headers = backup_sets.each_with_object({}) { |(basename, files), headers| headers[basename] = get_sql_backup_headers(sql_server_settings['connection_string'], files, ).first } backup_sets = backup_sets.sort_by { |basename, _files| backup_headers[basename]['LastLSN'] }.to_h backup_headers = backup_headers.sort_by { |_basename, backup_header| backup_header['LastLSN'] }.to_h EasyIO.logger.debug "Backup sets after sorting: #{JSON.pretty_generate(backup_sets)}" backup_sets.each { |basename, _files| EasyIO.logger.debug "Backup header for #{basename}: FirstLSN: #{backup_headers[basename]['FirstLSN']} | LastLSN: #{backup_headers[basename]['LastLSN']}" } start_lsn = nil current_lsn = database_backup_header['LastLSN'] backup_headers.each do |basename, backup_header| start_lsn ||= backup_header['FirstLSN'] EasyIO.logger.debug "Current LSN: #{current_lsn}" EasyIO.logger.debug "Current header (#{basename}) - FirstLSN: #{backup_headers[basename]['FirstLSN']} | LastLSN: #{backup_headers[basename]['LastLSN']} | DatabaseBackupLSN: #{backup_headers[basename]['DatabaseBackupLSN']}" unless backup_header['DatabaseBackupLSN'] == database_backup_lsn EasyIO.logger.debug "Current backup is from a different database backup as the DatabaseBackupLSN (#{backup_header['DatabaseBackupLSN']}) doesn't match the database backup LSN (#{database_backup_lsn}). Removing backup set..." backup_sets.delete(basename) next end if backup_header['LastLSN'] < database_backup_lsn || backup_header['LastLSN'] < restored_database_lsn EasyIO.logger.debug "Current backup LastLSN (#{backup_header['FirstLSN']}) older than database backup LSN (#{database_backup_lsn}) or restored database LSN (#{restored_database_lsn}). Removing backup set..." backup_sets.delete(basename) next end if backup_header['FirstLSN'] > current_lsn # remove previous backup sets if there's a gap EasyIO.logger.debug "Gap found between previous backup LastLSN (#{current_lsn}) and current backup FirstLSN #{backup_header['FirstLSN']}. Updating starting point..." unless current_lsn == 0 start_lsn = backup_header['FirstLSN'] if start_lsn > restored_database_lsn # if the starting point is beyond the restored database LastLSN, no backups can be applied EasyIO.logger.warn "Gap found in log backups. The previous backup ends at LSN #{current_lsn} and the next log backup starts at LSN #{backup_header['FirstLSN']}!" return nil end end current_lsn = backup_header['LastLSN'] end backup_headers.each { |basename, backup_header| backup_sets.delete(basename) unless backup_header['LastLSN'] > start_lsn } # remove any obsolete backup sets EasyIO.logger.debug "Backup sets after removing obsolete sets: #{JSON.pretty_generate(backup_sets)}" backup_sets end |
.remove_connection_string_part(connection_string, part) ⇒ Object
part options: :server, :database, :credentials, :applicationintent
45 46 47 48 49 50 |
# File 'lib/sql_cmd/format.rb', line 45 def remove_connection_string_part(connection_string, part) # part options: :server, :database, :credentials, :applicationintent connection_string_new = connection_string.dup parts = part == :credentials ? [:user, :password, :integrated] : [part] parts.each { |p| connection_string_new.gsub!(connection_string_part_regex(p), '') } # unless full_part.nil? } connection_string_new end |
.replace_connection_string_part(connection_string, part, replacement_value) ⇒ Object
Supply entire value (EG: ‘user id=value;password=value;’ or ‘integrated security=SSPI;’) as the replacement_value if the part is :credentials or provide a hash containing a username and password { user: ‘someuser’, password: ‘somepassword’, }
67 68 69 70 71 72 73 74 75 76 77 78 79 |
# File 'lib/sql_cmd/format.rb', line 67 def replace_connection_string_part(connection_string, part, replacement_value) EasyFormat.validate_parameters(method(__method__), binding) new_connection_string = remove_connection_string_part(connection_string, part) new_connection_string = case part when :credentials replacement_value = "User Id=#{replacement_value[:user]};Password=#{replacement_value[:password]}" if replacement_value.is_a?(Hash) "#{new_connection_string};#{replacement_value}" else "#{part}=#{replacement_value};#{new_connection_string};" end new_connection_string.gsub!(/;+/, ';') new_connection_string end |
.required_keys ⇒ Object
8 9 10 11 12 13 14 15 16 17 |
# File 'lib/sql_cmd/config.rb', line 8 def required_keys { 'sql_cmd' => { 'backups' => { 'always_on_backup_temp_dir' => nil, 'default_destination' => nil, }, }, } end |
.run_sql_as_job(connection_string, sql_script, sql_job_name, sql_job_owner = 'sa', values: nil, retries: 0, retry_delay: 15) ⇒ Object
317 318 319 320 321 322 323 324 325 |
# File 'lib/sql_cmd/query.rb', line 317 def run_sql_as_job(connection_string, sql_script, sql_job_name, sql_job_owner = 'sa', values: nil, retries: 0, retry_delay: 15) sql_script = insert_values(sql_script, values) unless values.nil? ensure_sql_agent_is_running(connection_string) job_values = { 'sqlquery' => sql_script.gsub('\'', '\'\''), 'jobname' => sql_job_name, 'jobowner' => sql_job_owner } sql_job_script = ::File.read("#{sql_script_dir}/Agent/CreateSQLJob.sql") execute_query(connection_string, sql_job_script, values: job_values, retries: retries, retry_delay: retry_delay) end |
.sql_login_exists?(connection_string, login, retries: 3, retry_delay: 15) ⇒ Boolean
237 238 239 240 241 242 243 |
# File 'lib/sql_cmd/query.rb', line 237 def sql_login_exists?(connection_string, login, retries: 3, retry_delay: 15) sid_script = ::File.read("#{sql_script_dir}/Security/GetUserSID.sql") raise "SQL password for login [#{login}] must not be empty!" if password.nil? || password.empty? values = { 'user' => login, 'password' => password } EasyIO.logger.info "Checking for existing SqlLogin: #{login}..." execute_query(connection_string, sid_script, return_type: :scalar, values: values, readonly: true, retries: retries, retry_delay: retry_delay) end |
.sql_script_dir ⇒ Object
52 53 54 |
# File 'lib/sql_cmd/config.rb', line 52 def sql_script_dir SqlCmd.config['sql_cmd']['paths']['sql_script_dir'] end |
.sql_server_backup_files(sql_server_settings, backup_basename, log_only: false) ⇒ Object
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 |
# File 'lib/sql_cmd/backups.rb', line 155 def sql_server_backup_files(sql_server_settings, backup_basename, log_only: false) values = { 'targetfolder' => sql_server_settings['BackupDir'], 'bkupname' => backup_basename, 'logonly' => log_only } sql_script = ::File.read("#{sql_script_dir}/Database/GetBackupFiles.sql") backup_files_results = execute_query(sql_server_settings['connection_string'], sql_script, return_type: :first_table, values: values, retries: 3) backup_files = [] backup_files_results.each do |file| backup_files.push("#{EasyFormat::Directory.ensure_trailing_slash(sql_server_settings['BackupDir'])}#{file['FileName']}") end if log_only database_backup_files = sql_server_backup_files(sql_server_settings, backup_basename) database_backup_header = get_sql_backup_headers(sql_server_settings['connection_string'], database_backup_files).first return relevant_log_backup_sets(sql_server_settings, backup_files, database_backup_header, 0) end most_recent_backup_files_and_basename(sql_server_settings, backup_files, backup_basename) end |
.to_integrated_security(connection_string, server_only: false) ⇒ Object
Ensures a connection string is using integrated security instead of SQL Authentication.
110 111 112 113 114 115 116 117 118 |
# File 'lib/sql_cmd/format.rb', line 110 def to_integrated_security(connection_string, server_only: false) raise 'Failed to convert connection string to integrated security. Connection string is nil!' if connection_string.nil? parts = connection_string.split(';') new_connection_string = '' ommitted_parts = ['user id', 'uid', 'password', 'pwd', 'integrated security', 'trusted_connection'] ommitted_parts += ['database', 'initial catalog'] if server_only parts.each { |part| new_connection_string << "#{part};" unless part.downcase.strip.start_with?(*ommitted_parts) } # only keep parts not omitted "#{new_connection_string}Integrated Security=SSPI;" end |
.to_unc_path(path, server_name) ⇒ Object
converts a path to unc_path if it contains a drive letter. Uses the server name provided
82 83 84 85 |
# File 'lib/sql_cmd/format.rb', line 82 def to_unc_path(path, server_name) return nil if path.nil? || path.empty? || server_name.nil? || server_name.empty? path.gsub(/(\p{L})+(:\\)/i) { "\\\\#{server_name}\\#{Regexp.last_match(1)}$\\" } # replace local paths with network paths end |
.to_utf8(text) ⇒ Object
Convert encoding of a string to utf8
121 122 123 |
# File 'lib/sql_cmd/format.rb', line 121 def to_utf8(text) text.encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '') end |
.unify_start_time(start_time, timezone: ) ⇒ Object
59 60 61 62 63 |
# File 'lib/sql_cmd/format.rb', line 59 def unify_start_time(start_time, timezone: SqlCmd.config['environment']['timezone']) return EasyTime.at_timezone(Time.now, timezone) if start_time.nil? || start_time.to_s.strip.empty? return EasyTime.at_timezone(start_time, timezone) if start_time.is_a?(Time) EasyTime.stomp_timezone(start_time, timezone) # Stomp the timezone with the config timezone. If no start_time was provided, use the current time end |
.update_sql_compatibility(connection_string, database_name, compatibility_level) ⇒ Object
compatibility_level options: :sql_2008, :sql_2012, :sql_2016, :sql_2017, :sql_2019
347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 |
# File 'lib/sql_cmd/query.rb', line 347 def update_sql_compatibility(connection_string, database_name, compatibility_level) # compatibility_level options: :sql_2008, :sql_2012, :sql_2016, :sql_2017, :sql_2019 sql_compatibility_script = ::File.read("#{sql_script_dir}/Database/SetSQLCompatibility.sql") compatibility_levels = { sql_2008: 100, sql_2012: 110, sql_2014: 120, sql_2016: 130, sql_2017: 140, sql_2019: 150, } values = { 'databasename' => database_name, 'compatibility_level' => compatibility_levels[compatibility_level] } EasyIO.logger.info "Ensuring SQL compatibility is set to #{compatibility_level}..." compatibility_result = execute_query(connection_string, sql_compatibility_script, return_type: :scalar, values: values, retries: 3) end |
.validate_logins_script(connection_string, database_name) ⇒ Object
287 288 289 290 291 292 293 |
# File 'lib/sql_cmd/query.rb', line 287 def validate_logins_script(connection_string, database_name) server_name = connection_string_part(connection_string, :server) sql_script = ::File.read("#{sql_script_dir}/Security/GenerateValidateLoginsScript.sql") values = { 'databasename' => database_name } EasyIO.logger.debug "Creating validate logins script for logins associated with database: [#{database_name}] on [#{server_name}]..." execute_query(connection_string, sql_script, return_type: :scalar, values: values, readonly: true, retries: 3) end |