Class: LL::InnoBackup

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

Defined Under Namespace

Classes: NoStateError

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



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

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



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

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

#aws_bucketObject

Raises:



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

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

#aws_commandObject



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

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



226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/ll-innobackup.rb', line 226

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



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

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

#completed?Boolean

Returns:

  • (Boolean)


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

def completed?
  completed_aws? && completed_inno?
end

#completed_aws?Boolean

Returns:

  • (Boolean)


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

def completed_aws?
  @completed_aws == true
end

#completed_inno?Boolean

Returns:

  • (Boolean)


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

def completed_inno?
  @completed_inno == true
end

#encryption_keyObject



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

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

#expected_full_sizeObject



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

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



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

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

#expiresObject



207
208
209
210
# File 'lib/ll-innobackup.rb', line 207

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

#expires_dateObject



195
196
197
198
199
200
201
202
203
204
205
# File 'lib/ll-innobackup.rb', line 195

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



297
298
299
300
301
# File 'lib/ll-innobackup.rb', line 297

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

#incrementalObject



271
272
273
274
# File 'lib/ll-innobackup.rb', line 271

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



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

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



180
181
182
183
184
185
186
187
# File 'lib/ll-innobackup.rb', line 180

def innobackup_options
  [
   "--parallel=#{backup_parallel}",
   "--compress-threads=#{backup_compress_threads}",
   ("--encrypt=AES256 --encrypt-key=#{encryption_key}" 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



289
290
291
292
293
294
295
# File 'lib/ll-innobackup.rb', line 289

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)


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

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

#lsn_from_stateObject



282
283
284
285
286
287
# File 'lib/ll-innobackup.rb', line 282

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



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

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

#reportObject



332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
# File 'lib/ll-innobackup.rb', line 332

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



246
247
248
249
# File 'lib/ll-innobackup.rb', line 246

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

#sql_authenticationObject



176
177
178
# File 'lib/ll-innobackup.rb', line 176

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

#sql_backup_passwordObject



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

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

#sql_backup_userObject



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

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)


251
252
253
254
255
256
257
258
# File 'lib/ll-innobackup.rb', line 251

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

#valid_commands?Boolean

Returns:

  • (Boolean)


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

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

#working_directoryObject



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

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

#working_fileObject



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

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