ll-innobackup 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. data/bin/ll-innobackup +6 -0
  2. data/lib/ll-innobackup.rb +307 -0
  3. metadata +64 -0
data/bin/ll-innobackup ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'll-innobackup'
4
+
5
+ LL::InnoBackup.new(LL::InnoBackup.options).backup
6
+
@@ -0,0 +1,307 @@
1
+ require 'json'
2
+ require 'date'
3
+
4
+ # Run Inmental or Full backups on MySQL
5
+ module LL
6
+ class InnoBackup
7
+ class << self
8
+ # Use this in case the log file is massive
9
+ def options
10
+ JSON.parse(File.read('/etc/mysql/innobackupex.json'))
11
+ rescue Errno::ENOENT
12
+ {}
13
+ end
14
+
15
+ def tail_file(path, n)
16
+ file = File.open(path, 'r')
17
+ buffer_s = 512
18
+ line_count = 0
19
+ file.seek(0, IO::SEEK_END)
20
+
21
+ offset = file.pos # we start at the end
22
+
23
+ while line_count <= n && offset > 0
24
+ to_read = if (offset - buffer_s) < 0
25
+ offset
26
+ else
27
+ buffer_s
28
+ end
29
+
30
+ file.seek(offset - to_read)
31
+ data = file.read(to_read)
32
+
33
+ data.reverse.each_char do |c|
34
+ if line_count > n
35
+ offset += 1
36
+ break
37
+ end
38
+ offset -= 1
39
+ line_count += 1 if c == "\n|"
40
+ end
41
+ end
42
+
43
+ file.seek(offset)
44
+ file.read
45
+ end
46
+
47
+ def state_file(t)
48
+ "/tmp/backup_#{t}_state"
49
+ end
50
+
51
+ def lock_file(type)
52
+ "/tmp/backup_#{type}.lock"
53
+ end
54
+
55
+ def innobackup_log(t)
56
+ "/tmp/backup_#{t}_innobackup_log"
57
+ end
58
+ end
59
+
60
+ attr_reader :type,
61
+ :now,
62
+ :date,
63
+ :state_files,
64
+ :lock_files,
65
+ :options
66
+
67
+ def initialize(options = {})
68
+ @now = Time.now
69
+ @date = @now.to_date
70
+ @options = options
71
+ @lock_files = {}
72
+ @state_files = {}
73
+ @type = backup_type
74
+ end
75
+
76
+ def aws_log
77
+ "/tmp/backup_#{type}_aws_log"
78
+ end
79
+
80
+ def innobackup_log
81
+ "/tmp/backup_#{type}_innobackup_log"
82
+ end
83
+
84
+ def lock?(t = type)
85
+ lock_files[t] ||= File.new(InnoBackup.lock_file(t), File::CREAT)
86
+ lock_files[t].flock(File::LOCK_NB | File::LOCK_EX).zero?
87
+ end
88
+
89
+ def state(t)
90
+ state_files[t] ||= JSON.parse(File.read(InnoBackup.state_file(t)))
91
+ rescue JSON::ParserError
92
+ puts 'unable to stat state file'
93
+ {}
94
+ end
95
+
96
+ def fully_backed_up_today?
97
+ require 'active_support/all'
98
+ date = state('full')['date']
99
+ Time.parse(date).today?
100
+ rescue Errno::ENOENT
101
+ puts 'unable to obtain last full backup state'
102
+ false
103
+ rescue NoMethodError
104
+ puts 'unable to obtain last backup state'
105
+ false
106
+ end
107
+
108
+ def can_full_backup?
109
+ !fully_backed_up_today? && lock?('full')
110
+ end
111
+
112
+ def full_backup_running?
113
+ !lock?('full')
114
+ end
115
+
116
+ def incremental_backup_running?
117
+ !lock?('incremental')
118
+ end
119
+
120
+ def backup_type
121
+ return 'full' unless fully_backed_up_today? || full_backup_running?
122
+ return 'incremental' unless incremental_backup_running?
123
+ raise 'Unable to backup as backups are running'
124
+ end
125
+
126
+ def backup_bin
127
+ @backup_bin = options['backup_bin'] ||= '/usr/bin/innobackupex'
128
+ end
129
+
130
+ def backup_parallel
131
+ @backup_parallel = options['backup_parallel'] ||= 4
132
+ end
133
+
134
+ def backup_compress_threads
135
+ @backup_compress_threads = options['backup_compress_threads'] ||= 4
136
+ end
137
+
138
+ def sql_backup_user
139
+ @sql_backup_user ||= options['sql_backup_user']
140
+ end
141
+
142
+ def sql_backup_password
143
+ @sql_backup_password ||= options['sql_backup_password']
144
+ end
145
+
146
+ def aws_bin
147
+ @aws_bin = options['aws_bin'] ||= '/usr/local/bin/aws'
148
+ end
149
+
150
+ def aws_bucket
151
+ @aws_bucket = options['aws_bucket'] ||= 'jim-test-mysql-stream1'
152
+ end
153
+
154
+ def expected_full_size
155
+ @expected_full_size = options['expected_full_size'] ||= 1_600_000_000
156
+ end
157
+
158
+ def sql_authentication
159
+ "--user=#{sql_backup_user} --password=#{sql_backup_password}"
160
+ end
161
+
162
+ def innobackup_options
163
+ "--parallel=#{backup_parallel} "\
164
+ "--compress-threads=#{backup_compress_threads} "\
165
+ '--stream=xbstream --compress'
166
+ end
167
+
168
+ def innobackup_command
169
+ "#{backup_bin} #{sql_authentication} "\
170
+ "#{incremental} #{innobackup_options} /tmp/sql"
171
+ end
172
+
173
+ def expires_date
174
+ require 'active_support/all'
175
+ # Keep incrementals for 2 days
176
+ return (@now + 2.days).iso8601 if type == 'incremental'
177
+ # Keep first backup of month for 180 days
178
+ return (@now + 6.months).iso8601 if @date.yesterday.month != @date.month
179
+ # Keep first backup of week for 31 days (monday)
180
+ return (@now + 1.month).iso8601 if @date.cwday == 1
181
+ # Keep daily backups for 14 days
182
+ (@now + 2.weeks).iso8601
183
+ end
184
+
185
+ def expires
186
+ ed = expires_date
187
+ "--expires=#{ed}" if ed
188
+ end
189
+
190
+ def expected_size
191
+ "--expected-size=#{expected_full_size}" if type == 'full'
192
+ end
193
+
194
+ def aws_command
195
+ "#{aws_bin} s3 cp - s3://#{aws_bucket}/#{aws_backup_file}"\
196
+ " #{expected_size} #{expires}"
197
+ end
198
+
199
+ def valid_commands?
200
+ File.exist?(backup_bin) && File.exist?(aws_bin)
201
+ end
202
+
203
+ def backup
204
+ require 'English'
205
+ exc = "#{innobackup_command} 2> #{innobackup_log} |
206
+ #{aws_command} 2> #{aws_log} >> #{aws_log}"
207
+ `#{exc}`if valid_commands?
208
+ @completed = $CHILD_STATUS == 0
209
+ return record if success? && completed?
210
+ revert_aws if valid_commands?
211
+ rescue InnoBackup::NoStateError => e
212
+ STDERR.puts e.message
213
+ ensure
214
+ report
215
+ end
216
+
217
+ def revert_aws
218
+ exc = "#{aws_bin} s3 rm s3://#{aws_bucket}/#{aws_backup_file} > /dev/null 2>/dev/null"
219
+ `#{exc}`
220
+ end
221
+
222
+ def success?
223
+ InnoBackup.tail_file(
224
+ innobackup_log,
225
+ 1
226
+ ) =~ /: completed OK/
227
+ rescue Errno::ENOENT
228
+ false
229
+ end
230
+
231
+ def record
232
+ File.write(
233
+ InnoBackup.state_file(type),
234
+ {
235
+ date: now,
236
+ lsn: lsn_from_backup_log,
237
+ file: aws_backup_file
238
+ }.to_json
239
+ )
240
+ end
241
+
242
+ def incremental
243
+ return unless backup_type == 'incremental'
244
+ "--incremental --incremental-lsn=#{lsn_from_state}"
245
+ end
246
+
247
+ def lsn_from_full_backup_state?
248
+ Time.parse(state('full')['date']) > Time.parse(state('incremental')['date'])
249
+ rescue Errno::ENOENT
250
+ true
251
+ end
252
+
253
+ def lsn_from_state
254
+ return state('full')['lsn'] if lsn_from_full_backup_state?
255
+ state('incremental')['lsn']
256
+ rescue NoMethodError
257
+ raise NoStateError, 'no state file for incremental backup'
258
+ end
259
+
260
+ def lsn_from_backup_log
261
+ matches = InnoBackup.tail_file(
262
+ InnoBackup.innobackup_log(type),
263
+ 30
264
+ ).match(/The latest check point \(for incremental\): '(\d+)'/)
265
+ matches[1] if matches
266
+ end
267
+
268
+ def hostname
269
+ return options[:hostname] if options[:hostname]
270
+ require 'socket'
271
+ Socket.gethostbyname(Socket.gethostname).first
272
+ end
273
+
274
+ def aws_backup_file
275
+ return "#{hostname}/#{now.to_time.utc.to_i}/percona_full_backup" if type == 'full'
276
+ "#{hostname}/#{Time.parse(state('full')['date']).to_i}/percona_incremental_#{now.to_time.utc.to_i}"
277
+ rescue NoMethodError
278
+ raise NoStateError, 'incremental state missing or corrupt'
279
+ end
280
+
281
+ def completed?
282
+ @completed == true
283
+ end
284
+
285
+ def report
286
+ # Eventually Tell Zabbix
287
+ if success? && completed?
288
+ STDERR.puts "#{$PROGRAM_NAME}: success: completed #{type} backup"
289
+ return
290
+ end
291
+ STDERR.puts "#{$PROGRAM_NAME}: failed"
292
+ STDERR.puts 'missing binaries' unless valid_commands?
293
+ inno_tail = InnoBackup.tail_file(innobackup_log, 10)
294
+ STDERR.puts 'invalid sql user' if inno_tail =~ /Option user requires an argument/
295
+ STDERR.puts 'unable to connect to DB' if inno_tail =~ /Access denied for user/
296
+ STDERR.puts 'insufficient file access' if inno_tail =~ /Can't change dir to/
297
+ aws_tail = InnoBackup.tail_file(aws_log, 10)
298
+ STDERR.puts 'bucket incorrect' if aws_tail =~ /The specified bucket does not exist/
299
+ STDERR.puts 'invalid AWS key' if aws_tail =~ /The AWS Access Key Id you/
300
+ STDERR.puts 'invalid Secret key' if aws_tail =~ /The request signature we calculated/
301
+ end
302
+
303
+ class NoStateError < StandardError
304
+ end
305
+ end
306
+ end
307
+ InnoBackup.new(InnoBackup.options).backup if $PROGRAM_NAME == __FILE__
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ll-innobackup
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Stuart Harland, LiveLink Technology Ltd
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2018-03-20 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activesupport
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - '='
20
+ - !ruby/object:Gem::Version
21
+ version: 4.2.6
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - '='
28
+ - !ruby/object:Gem::Version
29
+ version: 4.2.6
30
+ description: A program to conduct innobackup
31
+ email: essjayhch@gmail.com, infra@livelinktechnology.net
32
+ executables:
33
+ - ll-innobackup
34
+ extensions: []
35
+ extra_rdoc_files: []
36
+ files:
37
+ - lib/ll-innobackup.rb
38
+ - bin/ll-innobackup
39
+ homepage: http://rubygems.org/gems/hola
40
+ licenses:
41
+ - MIT
42
+ post_install_message:
43
+ rdoc_options: []
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ none: false
48
+ requirements:
49
+ - - ! '>='
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ! '>='
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ requirements: []
59
+ rubyforge_project:
60
+ rubygems_version: 1.8.23
61
+ signing_key:
62
+ specification_version: 3
63
+ summary: Livelink Innobackup Script
64
+ test_files: []