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.



67
68
69
70
71
72
73
74
# File 'lib/ll-innobackup.rb', line 67

def initialize(options = {})
  @now = Time.now
  @date = @now.to_date
  @options = options
  @lock_files = {}
  @state_files = {}
  @type = backup_type
end

Instance Attribute Details

#dateObject (readonly)

Returns the value of attribute date.



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

def date
  @date
end

#lock_filesObject (readonly)

Returns the value of attribute lock_files.



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

def lock_files
  @lock_files
end

#nowObject (readonly)

Returns the value of attribute now.



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

def now
  @now
end

#optionsObject (readonly)

Returns the value of attribute options.



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

def options
  @options
end

#state_filesObject (readonly)

Returns the value of attribute state_files.



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

def state_files
  @state_files
end

#typeObject (readonly)

Returns the value of attribute type.



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

def type
  @type
end

Class Method Details

.innobackup_log(t) ⇒ Object



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

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

.lock_file(type) ⇒ Object



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

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

.optionsObject

Use this in case the log file is massive



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

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

.state_file(t) ⇒ Object



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

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

.tail_file(path, n) ⇒ Object



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

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



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

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



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

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

#aws_bucketObject

Raises:



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

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

#aws_commandObject



220
221
222
223
224
# File 'lib/ll-innobackup.rb', line 220

def aws_command
  "#{aws_bin} s3 cp #{working_file} s3://#{aws_bucket}/#{aws_backup_file} "\
  "#{expected_size} #{expires} "\
  "2> #{aws_log} >> #{aws_log}"
end

#aws_logObject



76
77
78
# File 'lib/ll-innobackup.rb', line 76

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

#backupObject



230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/ll-innobackup.rb', line 230

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
  `#{aws_command}`
  @completed_aws = $CHILD_STATUS == 0
  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



130
131
132
# File 'lib/ll-innobackup.rb', line 130

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

#backup_compress_threadsObject



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

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

#backup_parallelObject



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

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

#backup_typeObject



124
125
126
127
128
# File 'lib/ll-innobackup.rb', line 124

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)


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

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

#cleanupObject



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

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

#completed?Boolean

Returns:

  • (Boolean)


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

def completed?
  completed_aws? && completed_inno?
end

#completed_aws?Boolean

Returns:

  • (Boolean)


322
323
324
# File 'lib/ll-innobackup.rb', line 322

def completed_aws?
  @completed_aws == true
end

#completed_inno?Boolean

Returns:

  • (Boolean)


326
327
328
# File 'lib/ll-innobackup.rb', line 326

def completed_inno?
  @completed_inno == true
end

#encryption_keyObject



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

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

#encryption_threadsObject



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

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

#expected_full_sizeObject



172
173
174
175
176
177
178
# File 'lib/ll-innobackup.rb', line 172

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



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

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

#expiresObject



211
212
213
214
# File 'lib/ll-innobackup.rb', line 211

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

#expires_dateObject



199
200
201
202
203
204
205
206
207
208
209
# File 'lib/ll-innobackup.rb', line 199

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)


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

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

#fully_backed_up_today?Boolean

Returns:

  • (Boolean)


96
97
98
99
100
101
102
103
104
105
106
# File 'lib/ll-innobackup.rb', line 96

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



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

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

#incrementalObject



275
276
277
278
# File 'lib/ll-innobackup.rb', line 275

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

#incremental_backup_running?Boolean

Returns:

  • (Boolean)


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

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

#innobackup_commandObject



193
194
195
196
197
# File 'lib/ll-innobackup.rb', line 193

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

#innobackup_logObject



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

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

#innobackup_optionsObject



184
185
186
187
188
189
190
191
# File 'lib/ll-innobackup.rb', line 184

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)


108
109
110
# File 'lib/ll-innobackup.rb', line 108

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

#lock?(t = type) ⇒ Boolean

Returns:

  • (Boolean)


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

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



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

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)


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

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

#lsn_from_stateObject



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

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



264
265
266
267
268
269
270
271
272
273
# File 'lib/ll-innobackup.rb', line 264

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

#reportObject



336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
# File 'lib/ll-innobackup.rb', line 336

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



250
251
252
253
# File 'lib/ll-innobackup.rb', line 250

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

#sql_authenticationObject



180
181
182
# File 'lib/ll-innobackup.rb', line 180

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

#sql_backup_passwordObject



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

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

#sql_backup_userObject



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

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

#state(t) ⇒ Object



89
90
91
92
93
94
# File 'lib/ll-innobackup.rb', line 89

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)


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

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

#valid_commands?Boolean

Returns:

  • (Boolean)


226
227
228
# File 'lib/ll-innobackup.rb', line 226

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

#working_directoryObject



167
168
169
170
# File 'lib/ll-innobackup.rb', line 167

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

#working_fileObject



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

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