Class: LL::InnoBackup

Inherits:
Object
  • Object
show all
Defined in:
lib/ll-innobackup.rb

Defined Under Namespace

Classes: NoStateError, StateError

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ InnoBackup

Returns a new instance of InnoBackup.



69
70
71
72
73
74
75
76
77
# File 'lib/ll-innobackup.rb', line 69

def initialize(options = {})
  @now = Time.now
  @date = @now.to_date
  @options = options
  @lock_files = {}
  @state_files = {}
  @type = backup_type
  @s3 = Aws::S3::Resource.new()
end

Instance Attribute Details

#dateObject (readonly)

Returns the value of attribute date.



61
62
63
# File 'lib/ll-innobackup.rb', line 61

def date
  @date
end

#lock_filesObject (readonly)

Returns the value of attribute lock_files.



61
62
63
# File 'lib/ll-innobackup.rb', line 61

def lock_files
  @lock_files
end

#nowObject (readonly)

Returns the value of attribute now.



61
62
63
# File 'lib/ll-innobackup.rb', line 61

def now
  @now
end

#optionsObject (readonly)

Returns the value of attribute options.



61
62
63
# File 'lib/ll-innobackup.rb', line 61

def options
  @options
end

#s3Object (readonly)

Returns the value of attribute s3.



61
62
63
# File 'lib/ll-innobackup.rb', line 61

def s3
  @s3
end

#state_filesObject (readonly)

Returns the value of attribute state_files.



61
62
63
# File 'lib/ll-innobackup.rb', line 61

def state_files
  @state_files
end

#typeObject (readonly)

Returns the value of attribute type.



61
62
63
# File 'lib/ll-innobackup.rb', line 61

def type
  @type
end

Class Method Details

.innobackup_log(t) ⇒ Object



56
57
58
# File 'lib/ll-innobackup.rb', line 56

def innobackup_log(t)
  "/tmp/backup_#{t}_innobackup_log"
end

.lock_file(type) ⇒ Object



52
53
54
# File 'lib/ll-innobackup.rb', line 52

def lock_file(type)
  "/tmp/backup_#{type}.lock"
end

.optionsObject

Use this in case the log file is massive



10
11
12
13
14
# File 'lib/ll-innobackup.rb', line 10

def options
  JSON.parse(File.read('/etc/mysql/innobackupex.json'))
rescue Errno::ENOENT
  {}
end

.state_file(t) ⇒ Object



48
49
50
# File 'lib/ll-innobackup.rb', line 48

def state_file(t)
  "/tmp/backup_#{t}_state"
end

.tail_file(path, n) ⇒ Object



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
# File 'lib/ll-innobackup.rb', line 16

def tail_file(path, n)
  file = File.open(path, 'r')
  buffer_s = 512
  line_count = 0
  file.seek(0, IO::SEEK_END)

  offset = file.pos # we start at the end

  while line_count <= n && offset > 0
    to_read = if (offset - buffer_s) < 0
                offset
              else
                buffer_s
              end

    file.seek(offset - to_read)
    data = file.read(to_read)

    data.reverse.each_char do |c|
      if line_count > n
        offset += 1
        break
      end
      offset -= 1
      line_count += 1 if c == "\n|"
    end
  end

  file.seek(offset)
  file.read
end

Instance Method Details

#aws_backup_fileObject



317
318
319
320
321
322
# File 'lib/ll-innobackup.rb', line 317

def aws_backup_file
  return "#{hostname}/#{now.iso8601}/percona_full_backup" if type == 'full'
  "#{hostname}/#{Time.parse(state('full')['date']).iso8601}/percona_incremental_#{now.iso8601}"
rescue NoMethodError
  raise NoStateError, 'incremental state missing or corrupt'
end

#aws_binObject



161
162
163
# File 'lib/ll-innobackup.rb', line 161

def aws_bin
  @aws_bin = options['aws_bin'] ||= '/usr/local/bin/aws'
end

#aws_bucketObject

Raises:



165
166
167
168
# File 'lib/ll-innobackup.rb', line 165

def aws_bucket
  raise NoStateError, 'aws_bucket not provided' unless options['aws_bucket']
  @aws_bucket = options['aws_bucket']
end

#aws_logObject



79
80
81
# File 'lib/ll-innobackup.rb', line 79

def aws_log
  "/tmp/backup_#{type}_aws_log"
end

#backupObject



237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/ll-innobackup.rb', line 237

def backup
  require 'English'

  return unless valid_commands?
  `#{innobackup_command}`
  @completed_inno = $CHILD_STATUS == 0
  raise InnoBackup::StateError, 'Unable to run innobackup correctly' unless @completed_inno
  @completed_aws = s3object_uploaded?(aws_bucket, aws_backup_file, working_file)
  raise InnoBackup::StateError, 'Unable to run aws upload correctly' unless @completed_aws
  return record if success? && completed?
rescue InnoBackup::StateError => e
  revert_aws
rescue InnoBackup::NoStateError => e
  STDERR.puts e.message
ensure
  report
  cleanup
end

#backup_binObject



133
134
135
# File 'lib/ll-innobackup.rb', line 133

def backup_bin
  @backup_bin = options['backup_bin'] ||= '/usr/bin/innobackupex'
end

#backup_compress_threadsObject



141
142
143
# File 'lib/ll-innobackup.rb', line 141

def backup_compress_threads
  @backup_compress_threads = options['backup_compress_threads'] ||= 4
end

#backup_parallelObject



137
138
139
# File 'lib/ll-innobackup.rb', line 137

def backup_parallel
  @backup_parallel = options['backup_parallel'] ||= 4
end

#backup_typeObject



127
128
129
130
131
# File 'lib/ll-innobackup.rb', line 127

def backup_type
  return 'full' unless fully_backed_up_today? || full_backup_running?
  return 'incremental' unless incremental_backup_running?
  raise 'Unable to backup as backups are running'
end

#can_full_backup?Boolean

Returns:

  • (Boolean)


115
116
117
# File 'lib/ll-innobackup.rb', line 115

def can_full_backup?
  !fully_backed_up_today? && lock?('full')
end

#cleanupObject



336
337
338
339
340
# File 'lib/ll-innobackup.rb', line 336

def cleanup
  File.unlink working_file
rescue StandardError => e
  STDERR.puts "Caught exception #{e} when trying to cleanup"
end

#completed?Boolean

Returns:

  • (Boolean)


324
325
326
# File 'lib/ll-innobackup.rb', line 324

def completed?
  completed_aws? && completed_inno?
end

#completed_aws?Boolean

Returns:

  • (Boolean)


328
329
330
# File 'lib/ll-innobackup.rb', line 328

def completed_aws?
  @completed_aws == true
end

#completed_inno?Boolean

Returns:

  • (Boolean)


332
333
334
# File 'lib/ll-innobackup.rb', line 332

def completed_inno?
  @completed_inno == true
end

#encryption_keyObject



157
158
159
# File 'lib/ll-innobackup.rb', line 157

def encryption_key
  @encryption_key ||= options['encryption_key']
end

#encryption_threadsObject



145
146
147
# File 'lib/ll-innobackup.rb', line 145

def encryption_threads
  @encryption_threads = options['encryption_threads'] ||= 4
end

#expected_full_sizeObject



175
176
177
178
179
180
181
# File 'lib/ll-innobackup.rb', line 175

def expected_full_size
  @expected_full_size ||= -> do
    return File.size(working_file) if File.exist?(working_file)
    return options['expected_full_size'] if options['expected_full_size']
    1_600_000_000
  end.call
end

#expected_sizeObject



219
220
221
# File 'lib/ll-innobackup.rb', line 219

def expected_size
  "--expected-size=#{expected_full_size}" if type == 'full'
end

#expiresObject



214
215
216
217
# File 'lib/ll-innobackup.rb', line 214

def expires
  ed = expires_date
  "--expires=#{ed}" if ed
end

#expires_dateObject



202
203
204
205
206
207
208
209
210
211
212
# File 'lib/ll-innobackup.rb', line 202

def expires_date
  require 'active_support/all'
  # Keep incrementals for 2 days
  return (@now + 2.days).iso8601 if type == 'incremental'
  # Keep first backup of month for 180 days
  return (@now + 6.months).iso8601 if @date.yesterday.month != @date.month
  # Keep first backup of week for 31 days (monday)
  return (@now + 1.month).iso8601 if @date.cwday == 1
  # Keep daily backups for 14 days
  (@now + 2.weeks).iso8601
end

#full_backup_running?Boolean

Returns:

  • (Boolean)


119
120
121
# File 'lib/ll-innobackup.rb', line 119

def full_backup_running?
  !lock?('full')
end

#fully_backed_up_today?Boolean

Returns:

  • (Boolean)


99
100
101
102
103
104
105
106
107
108
109
# File 'lib/ll-innobackup.rb', line 99

def fully_backed_up_today?
  require 'active_support/all'
  date = state('full')['date']
  Time.parse(date).today?
rescue Errno::ENOENT
  puts 'unable to obtain last full backup state'
  false
rescue NoMethodError
  puts 'unable to obtain last backup state'
  false
end

#hostnameObject



307
308
309
310
311
# File 'lib/ll-innobackup.rb', line 307

def hostname
  return options['hostname'] if options['hostname']
  require 'socket'
  Socket.gethostbyname(Socket.gethostname).first
end

#incrementalObject



281
282
283
284
# File 'lib/ll-innobackup.rb', line 281

def incremental
  return unless backup_type == 'incremental'
  "--incremental --incremental-lsn=#{lsn_from_state}"
end

#incremental_backup_running?Boolean

Returns:

  • (Boolean)


123
124
125
# File 'lib/ll-innobackup.rb', line 123

def incremental_backup_running?
  !lock?('incremental')
end

#innobackup_commandObject



196
197
198
199
200
# File 'lib/ll-innobackup.rb', line 196

def innobackup_command
  "#{backup_bin} #{sql_authentication} "\
  "#{incremental} #{innobackup_options} /tmp/sql "\
  "2> #{innobackup_log} > #{working_file}"
end

#innobackup_logObject



83
84
85
# File 'lib/ll-innobackup.rb', line 83

def innobackup_log
  "/tmp/backup_#{type}_innobackup_log"
end

#innobackup_optionsObject



187
188
189
190
191
192
193
194
# File 'lib/ll-innobackup.rb', line 187

def innobackup_options
  [
   "--parallel=#{backup_parallel}",
   "--compress-threads=#{backup_compress_threads}",
   ("--encrypt=AES256 --encrypt-key=#{encryption_key} --encrypt-threads=#{encryption_threads}" if is_encrypted?),
   '--stream=xbstream --compress'
  ].join(" ")
end

#is_encrypted?Boolean

Returns:

  • (Boolean)


111
112
113
# File 'lib/ll-innobackup.rb', line 111

def is_encrypted?
  !options['encryption_key'].empty?
end

#lock?(t = type) ⇒ Boolean

Returns:

  • (Boolean)


87
88
89
90
# File 'lib/ll-innobackup.rb', line 87

def lock?(t = type)
  lock_files[t] ||= File.new(InnoBackup.lock_file(t), File::CREAT)
  lock_files[t].flock(File::LOCK_NB | File::LOCK_EX).zero?
end

#lsn_from_backup_logObject



299
300
301
302
303
304
305
# File 'lib/ll-innobackup.rb', line 299

def lsn_from_backup_log
  matches = InnoBackup.tail_file(
    InnoBackup.innobackup_log(type),
    30
  ).match(/The latest check point \(for incremental\): '(\d+)'/)
  matches[1] if matches
end

#lsn_from_full_backup_state?Boolean

Returns:

  • (Boolean)


286
287
288
289
290
# File 'lib/ll-innobackup.rb', line 286

def lsn_from_full_backup_state?
  Time.parse(state('full')['date']) > Time.parse(state('incremental')['date'])
rescue Errno::ENOENT
  true
end

#lsn_from_stateObject



292
293
294
295
296
297
# File 'lib/ll-innobackup.rb', line 292

def lsn_from_state
  return state('full')['lsn'] if lsn_from_full_backup_state?
  state('incremental')['lsn']
rescue NoMethodError
  raise NoStateError, 'no state file for incremental backup'
end

#recordObject



270
271
272
273
274
275
276
277
278
279
# File 'lib/ll-innobackup.rb', line 270

def record
  File.write(
    InnoBackup.state_file(type),
    {
      date: now,
      lsn: lsn_from_backup_log,
      file: aws_backup_file
    }.to_json
  )
end

#reportObject



342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
# File 'lib/ll-innobackup.rb', line 342

def report
  # Eventually Tell Zabbix
  if success? && completed?
    STDERR.puts "#{$PROGRAM_NAME}: success: completed #{type} backup"
    return
  end
  STDERR.puts "Unable to run innobackup" unless completed_inno?
  STDERR.puts "Unable to run aws s3 command" unless completed_aws?
  STDERR.puts "#{$PROGRAM_NAME}: failed"
  STDERR.puts 'missing binaries' unless valid_commands?
  inno_tail = InnoBackup.tail_file(innobackup_log, 10)
  STDERR.puts 'invalid sql user' if inno_tail =~ /Option user requires an argument/
  STDERR.puts 'unable to connect to DB' if inno_tail =~ /Access denied for user/
  STDERR.puts 'insufficient file access' if inno_tail =~ /Can't change dir to/
  aws_tail = InnoBackup.tail_file(aws_log, 10)
  STDERR.puts 'bucket incorrect' if aws_tail =~ /The specified bucket does not exist/
  STDERR.puts 'invalid AWS key' if aws_tail =~ /The AWS Access Key Id you/
  STDERR.puts 'invalid Secret key' if aws_tail =~ /The request signature we calculated/
end

#revert_awsObject



256
257
258
259
# File 'lib/ll-innobackup.rb', line 256

def revert_aws
  exc = "#{aws_bin} s3 rm s3://#{aws_bucket}/#{aws_backup_file} > /dev/null 2>/dev/null"
  `#{exc}`
end

#s3object_uploaded?(bucket_name, object_key, file_path) ⇒ Boolean

Returns:

  • (Boolean)


223
224
225
226
227
228
229
230
231
# File 'lib/ll-innobackup.rb', line 223

def s3object_uploaded?(bucket_name, object_key, file_path)
  object = @s3.bucket(bucket_name).object(object_key)
    object.upload_file(file_path, {expires: expires_date, thread_count: @options['thread_count']}) do |r|
      return true
    end
rescue StandardError => e
  STDERR.puts "Error uploading object: #{e.message}"
  return false
end

#sql_authenticationObject



183
184
185
# File 'lib/ll-innobackup.rb', line 183

def sql_authentication
  "--user=#{sql_backup_user} --password=#{sql_backup_password}"
end

#sql_backup_passwordObject



153
154
155
# File 'lib/ll-innobackup.rb', line 153

def sql_backup_password
  @sql_backup_password ||= options['sql_backup_password']
end

#sql_backup_userObject



149
150
151
# File 'lib/ll-innobackup.rb', line 149

def sql_backup_user
  @sql_backup_user ||= options['sql_backup_user']
end

#state(t) ⇒ Object



92
93
94
95
96
97
# File 'lib/ll-innobackup.rb', line 92

def state(t)
  state_files[t] ||= JSON.parse(File.read(InnoBackup.state_file(t)))
rescue JSON::ParserError
  puts 'unable to stat state file'
  {}
end

#success?Boolean

Returns:

  • (Boolean)


261
262
263
264
265
266
267
268
# File 'lib/ll-innobackup.rb', line 261

def success?
  InnoBackup.tail_file(
    innobackup_log,
    1
  ) =~ / completed OK/
rescue Errno::ENOENT
  false
end

#valid_commands?Boolean

Returns:

  • (Boolean)


233
234
235
# File 'lib/ll-innobackup.rb', line 233

def valid_commands?
  File.exist?(backup_bin) && File.exist?(aws_bin)
end

#working_directoryObject



170
171
172
173
# File 'lib/ll-innobackup.rb', line 170

def working_directory
  return options['working_directory'] if options['working_directory']
  '/tmp'
end

#working_fileObject



313
314
315
# File 'lib/ll-innobackup.rb', line 313

def working_file
  @working_file ||= File.join working_directory, "#{now.iso8601}-percona_backup"
end