ridoku 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,13 @@
1
+ # encoding: utf-8
2
+
3
+ class String
4
+ def present?
5
+ length > 0
6
+ end
7
+ end
8
+
9
+ class NilClass
10
+ def present?
11
+ false
12
+ end
13
+ end
@@ -0,0 +1,35 @@
1
+ # Add colorize method to the IO class.
2
+
3
+ class IO
4
+ def colorize(input, args)
5
+ args = [args] unless args.is_a?(Array)
6
+ colors = {
7
+ black: ["\033[30m", "\033[0m"],
8
+ red: ["\033[31m", "\033[0m"],
9
+ green: ["\033[32m", "\033[0m"],
10
+ brown: ["\033[33m", "\033[0m"],
11
+ blue: ["\033[34m", "\033[0m"],
12
+ magenta: ["\033[35m", "\033[0m"],
13
+ cyan: ["\033[36m", "\033[0m"],
14
+ gray: ["\033[37m", "\033[0m"],
15
+ bg_black: ["\033[40m", "\0330m"],
16
+ bg_red: ["\033[41m", "\033[0m"],
17
+ bg_green: ["\033[42m", "\033[0m"],
18
+ bg_brown: ["\033[43m", "\033[0m"],
19
+ bg_blue: ["\033[44m", "\033[0m"],
20
+ bg_magenta: ["\033[45m", "\033[0m"],
21
+ bg_cyan: ["\033[46m", "\033[0m"],
22
+ bg_gray: ["\033[47m", "\033[0m"],
23
+ bold: ["\033[1m", "\033[22m"],
24
+ reverse_color: ["\033[7m", "\033[27m"]
25
+ }
26
+ return input unless self.isatty
27
+
28
+ args.each do |ar|
29
+ next unless colors.key?(ar)
30
+ input = "#{colors[ar].first}#{input}#{colors[ar].last}"
31
+ end
32
+
33
+ input
34
+ end
35
+ end
@@ -0,0 +1,142 @@
1
+ require 'getoptlong'
2
+
3
+ module Ridoku
4
+ def self.init_options
5
+ @options = [
6
+ [ '--debug', '-D', GetoptLong::NO_ARGUMENT,<<-EOF
7
+
8
+ Turn on debugging outputs (for AWS and Exceptions).
9
+ EOF
10
+ ],
11
+ [ '--no-wait', '-n', GetoptLong::NO_ARGUMENT,<<-EOF
12
+
13
+ When issuing a command, do not wait for the command to return.
14
+ EOF
15
+ ],
16
+ [ '--key', '-k', GetoptLong::REQUIRED_ARGUMENT,<<-EOF
17
+ <key>
18
+ Use the specified key as the AWS_ACCESS_KEY
19
+ EOF
20
+ ],
21
+ [ '--force', '-F', GetoptLong::NO_ARGUMENT,<<-EOF
22
+
23
+ Used to force an operation (e.g., deploy)
24
+ EOF
25
+ ],
26
+ [ '--secret', '-s', GetoptLong::REQUIRED_ARGUMENT,<<-EOF
27
+ <secret>
28
+ Use the specified secret as the AWS_SECRET_KEY
29
+ EOF
30
+ ],
31
+ [ '--set-app', '-A', GetoptLong::REQUIRED_ARGUMENT,<<-EOF
32
+ <app>
33
+ Use the specified App as the default Application.
34
+ EOF
35
+ ],
36
+ [ '--set-backup-bucket', '-B', GetoptLong::REQUIRED_ARGUMENT,<<-EOF
37
+ <bucket name>
38
+ Use the specified bucket name as the default Backup Bucket.
39
+ EOF
40
+ ],
41
+ [ '--backup-bucket', '-b', GetoptLong::REQUIRED_ARGUMENT,<<-EOF
42
+ <bucket name>
43
+ Use the specified bucket name as the current Backup Bucket.
44
+ EOF
45
+ ],
46
+ [ '--set-stack', '-S', GetoptLong::REQUIRED_ARGUMENT,<<-EOF
47
+ <stack>
48
+ Use the specified Stack as the default Stack.
49
+ EOF
50
+ ],
51
+ [ '--set-user', '-U', GetoptLong::REQUIRED_ARGUMENT,<<-EOF
52
+ <user>
53
+ Use the specified user as the default login user in 'run:shell'.
54
+ EOF
55
+ ],
56
+ [ '--set-ssh-key', '-K', GetoptLong::REQUIRED_ARGUMENT,<<-EOF
57
+ <key file>
58
+ Use the specified file as the default ssh key file.
59
+ EOF
60
+ ],
61
+ [ '--ssh-key', '-f', GetoptLong::REQUIRED_ARGUMENT,<<-EOF
62
+ <key file>
63
+ Override the default ssh key file for this call.
64
+ EOF
65
+ ],
66
+ [ '--app', '-a', GetoptLong::REQUIRED_ARGUMENT,<<-EOF
67
+ <app>
68
+ Override the default App name for this call.
69
+ EOF
70
+ ],
71
+ [ '--stack', '-t', GetoptLong::REQUIRED_ARGUMENT,<<-EOF
72
+ <stack>
73
+ Override the default Stack name for this call.
74
+ EOF
75
+ ],
76
+ [ '--instances', '-i', GetoptLong::REQUIRED_ARGUMENT,<<-EOF
77
+ <instances>
78
+ Run command on specified instances; valid delimiters: ',' or ':'
79
+ example:
80
+ ridoku deploy --instances mukujara,tanuki
81
+ EOF
82
+ ],
83
+ [ '--user', '-u', GetoptLong::REQUIRED_ARGUMENT,<<-EOF
84
+ <user>
85
+ Override the default user name for this call.
86
+ EOF
87
+ ],
88
+ [ '--comment', '-m', GetoptLong::REQUIRED_ARGUMENT,<<-EOF
89
+ <message>
90
+ Optional for: #{$stderr.colorize('deploy', :bold)}
91
+ Add the specified message to the deploy:* action.
92
+ EOF
93
+ ],
94
+ [ '--domains', '-d', GetoptLong::REQUIRED_ARGUMENT,<<-EOF
95
+ <domains>
96
+ Optional for: #{$stderr.colorize('create:app', :bold)}
97
+ Add the specified domains to the newly created application.
98
+ EOF
99
+ ],
100
+ [ '--lines', '-L', GetoptLong::REQUIRED_ARGUMENT,<<-EOF
101
+ <lines>
102
+ Optional for: #{$stderr.colorize('log:*', :bold)}
103
+ Print the specified number of lines.
104
+ EOF
105
+ ],
106
+ [ '--migrate', '-M', GetoptLong::NO_ARGUMENT,<<-EOF
107
+
108
+ Optional for: #{$stderr.colorize('deploy', :bold)}
109
+ Migrate the database after deploying the source.
110
+ EOF
111
+ ],
112
+ [ '--layer', '-l', GetoptLong::REQUIRED_ARGUMENT,<<-EOF
113
+ EOF
114
+ ],
115
+ [ '--repo', '-r', GetoptLong::REQUIRED_ARGUMENT,<<-EOF
116
+ EOF
117
+ ],
118
+ [ '--service-arn', '-V', GetoptLong::REQUIRED_ARGUMENT,<<-EOF
119
+ EOF
120
+ ],
121
+ [ '--instance-arn', '-N', GetoptLong::REQUIRED_ARGUMENT,<<-EOF
122
+ EOF
123
+ ],
124
+ [ '--practice', '-p', GetoptLong::NO_ARGUMENT,<<-EOF
125
+ EOF
126
+ ],
127
+ [ '--wizard', '-w', GetoptLong::NO_ARGUMENT,<<-EOF
128
+ EOF
129
+ ],
130
+ ]
131
+ end
132
+
133
+ def self.add_options(opts)
134
+ @options<< opts
135
+ end
136
+
137
+ def self.options
138
+ @options
139
+ end
140
+
141
+ init_options
142
+ end
@@ -0,0 +1,11 @@
1
+ # encoding: utf-8
2
+
3
+ module Ridoku
4
+ def self.register(name)
5
+ (@commands ||= []) << name.to_s
6
+ end
7
+
8
+ def self.commands
9
+ @commands
10
+ end
11
+ end
@@ -0,0 +1,393 @@
1
+ #
2
+ # Command: backup
3
+ # Description: List/Modify the current apps database backups
4
+ # backup - lists the database backups
5
+ #
6
+
7
+ require 'ridoku/base'
8
+
9
+ module Ridoku
10
+ register :backup
11
+
12
+ class Backup < Base
13
+ attr_accessor :dbase
14
+
15
+ def run
16
+ cline = Base.config[:command]
17
+ current = cline.shift
18
+ command = cline.shift
19
+ sub = cline.shift
20
+
21
+ case command
22
+ when 'list', nil, 'info'
23
+ list
24
+ when 'init'
25
+ init
26
+ when 'capture'
27
+ capture(sub)
28
+ when 'url'
29
+ url(ARGV.shift)
30
+ when 'restore'
31
+ restore(sub, ARGV.shift)
32
+ when 'delete', 'remove', 'rm'
33
+ remove(ARGV.shift)
34
+ else
35
+ print_backup_help
36
+ end
37
+ end
38
+
39
+ protected
40
+
41
+ def load_database
42
+ Base.fetch_stack
43
+ Base.fetch_layer
44
+ self.dbase =
45
+ Base.custom_json['deploy'][Base.config[:app]]['database']
46
+ end
47
+
48
+ def print_backup_help
49
+ $stderr.puts <<-EOF
50
+ Command: backup
51
+
52
+ List/Modify the current app's database backups.
53
+ backup[:list] lists the stored database backups.
54
+ backup:init initialize backup capability (check S3 permissions, and
55
+ generate required S3 buckets).
56
+ backup:rm <name> remove specified backup by name.
57
+ backup:capture capture a backup form the specified application database.
58
+ backup:url <name> get a download URL for the specified database backup.
59
+
60
+ Development:
61
+ backup:capture:local <pg_dump path>
62
+ Capture a backup locally using the apps config. Include the path the
63
+ desired 'pg_dump' command. Version 9.2 or above is required.
64
+ backup:restore:local <pg_restore path>
65
+ Restore a backup locally using the apps config. Include the path the
66
+ desired 'pg_restore' command. Version 9.2 or above is required.
67
+
68
+ EOF
69
+ end
70
+
71
+ def objects_from_bucket(bucket)
72
+ s3 = AWS::S3.new
73
+
74
+ bucket = s3.buckets[bucket]
75
+
76
+ # hack: couldn't find how to get an object count...
77
+ objects = 0
78
+
79
+ bucket.objects.map do |obj|
80
+ { key: obj.key, size: obj.content_length, type: obj.content_type }
81
+ end
82
+ end
83
+
84
+ def object_list_to_hash(list)
85
+ final = {}
86
+ max = 0
87
+
88
+ list.each do |obj|
89
+ comp = obj[:key].match(
90
+ %r(^(.*)-([0-9]{4})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2}).sqd$)
91
+ )
92
+
93
+ max = obj[:key].length if obj[:key].length > max
94
+
95
+ unless comp.nil?
96
+ app = comp[1]
97
+ final[app] ||= []
98
+ obj[:app] = app
99
+ obj[:date] = "#{comp[3]}/#{comp[4]}/#{comp[2]} #{comp[5]}:#{comp[6]}"
100
+ final[app] << obj
101
+ end
102
+ end
103
+
104
+ [ final, max ]
105
+ end
106
+
107
+ def pspace(str, max)
108
+ str + ' ' * (max - str.length)
109
+ end
110
+
111
+ def list
112
+ bucket_exists!
113
+
114
+ list = objects_from_bucket(Base.config[:backup_bucket])
115
+ app_hash, max = object_list_to_hash(list)
116
+
117
+ app_hash.each do |key, value|
118
+ $stdout.puts "#{$stdout.colorize(key, :green)}:"
119
+ value.each do |obj|
120
+ $stdout.puts " #{pspace(obj[:key], max)} "\
121
+ "#{obj[:date]}\t#{obj[:type]}"
122
+ end
123
+ end
124
+ end
125
+
126
+ def init
127
+ fail_undefined_bucket! if Base.config[:backup_bucket].nil?
128
+
129
+ s3 = AWS::S3.new
130
+ bucket = s3.buckets[Base.config[:backup_bucket]]
131
+ $stdout.puts "Checking for '#{$stdout.colorize(bucket.name, :bold)}' "\
132
+ 'S3 Buckets...'
133
+ if bucket.exists?
134
+ $stdout.puts "Bucket exists!"
135
+ else
136
+ $stdout.puts "Bucket does not exist. Generating..."
137
+ begin
138
+ bucket = s3.buckets.create(Base.config[:backup_bucket],
139
+ acl: :bucket_owner_full_control)
140
+ rescue => e
141
+ $stderr.puts "Oops! Something went wrong when trying to create your bucket!"
142
+ raise e
143
+ end
144
+
145
+ $stdout.puts 'Bucket created successfully!'
146
+ $stdout.puts "#{$stdout.colorize(bucket.name, [:bold, :green])}: owner read/write."
147
+ end
148
+ end
149
+
150
+ def bucket_exists!
151
+ # Fail if bucket not set
152
+ fail_undefined_bucket! if Base.config[:backup_bucket].nil?
153
+
154
+ s3 = AWS::S3.new
155
+ bucket = s3.buckets[Base.config[:backup_bucket]]
156
+
157
+ $stdout.print "Checking for '#{$stdout.colorize(bucket.name, :bold)}' "\
158
+ 'S3 Buckets... ' if $stdout.tty?
159
+
160
+ fail_uninitialized_bucket! unless bucket.exists?
161
+
162
+ $stdout.puts $stdout.colorize('Found!', :green) if $stdout.tty?
163
+ end
164
+
165
+ def object_exists!(sub, action)
166
+ fail_undefined_object!(sub, action) if sub.nil? || !sub.present?
167
+
168
+ s3 = AWS::S3.new
169
+ bucket = s3.buckets[Base.config[:backup_bucket]]
170
+ $stdout.print "Checking for '#{$stdout.colorize(sub, :bold)}' "\
171
+ 'in bucket... ' if $stdout.tty?
172
+
173
+ object = bucket.objects[sub]
174
+ fail_object_not_found! unless object.exists?
175
+
176
+ $stdout.puts $stdout.colorize('Found!', :green) if $stdout.tty?
177
+
178
+ object
179
+ end
180
+
181
+ def dump_local()
182
+ load_database
183
+
184
+ command = ARGV.shift
185
+
186
+ unless m = `#{command} --version`.match(/(9[.][23]([.][0-9]+)?)/)
187
+ $stderr.puts "Invalid pg_dump version #{m[1]}."
188
+ return
189
+ end
190
+
191
+ backup_file = "#{Base.config[:app]}-"\
192
+ "#{Time.now.utc.strftime("%Y%m%d%H%M%S")}.sql"
193
+
194
+ $stdout.puts "pg_dump version: #{$stdout.colorize(m[1], :green)}"
195
+ $stdout.puts "Creating backup file: #{backup_file}"
196
+
197
+ # Add PGPASSWORD to the environment.
198
+ ENV['PGPASSWORD'] = dbase['password']
199
+
200
+ system(["#{command}",
201
+ "-Fc",
202
+ "-h #{dbase['host']}",
203
+ "-U #{dbase['username']}",
204
+ "-p #{dbase['port']}",
205
+ "#{dbase['database']}",
206
+ "> #{backup_file}"].join(' '))
207
+
208
+ $stdout.puts $stdout.colorize("pg_dump complete.", :green)
209
+ $stdout.puts "File size: #{::File.size(backup_file)}"
210
+ ENV['PGPASSWORD'] = nil
211
+ end
212
+
213
+ def capture(opt)
214
+ return dump_local if opt == 'local'
215
+
216
+ bucket_exists!
217
+
218
+ recipe_data = {
219
+ backup: {
220
+ databases: Base.config[:app].downcase.split(','),
221
+ dump: {
222
+ type: 's3',
223
+ region: 'us-west-1',
224
+ bucket: Base.config[:backup_bucket]
225
+ }
226
+ }
227
+ }
228
+
229
+ Base.fetch_app
230
+ Base.fetch_instance
231
+ instances = Base.get_instances_for_layer('postgresql')
232
+ instance_ids = instances.map { |inst| inst[:instance_id] }
233
+
234
+ command = Base.execute_recipes(Base.app[:app_id], instance_ids,
235
+ 'Capturing application DB backup.', ['s3',
236
+ 'postgresql::backup_database'], recipe_data)
237
+
238
+ Base.run_command(command)
239
+ end
240
+
241
+ def restore_local(file)
242
+ load_database
243
+
244
+ command = ARGV.shift
245
+
246
+ unless m = `#{command} --version`.match(/(9[.][23]([.][0-9]+)?)/)
247
+ $stderr.puts "Invalid pg_restore version #{m[1]}."
248
+ return
249
+ end
250
+
251
+ $stdout.puts "pg_restore version: #{$stdout.colorize(m[1], :green)}"
252
+ $stdout.puts "Using backup file: #{file}"
253
+
254
+ # Add PGPASSWORD to the environment.
255
+ ENV['PGPASSWORD'] = dbase['password']
256
+
257
+ system(["#{command}",
258
+ "--clean",
259
+ "#{'--single-transaction' unless Base.config[:force]}",
260
+ "-h #{dbase['host']}",
261
+ "-U #{dbase['username']}",
262
+ "-p #{dbase['port']}",
263
+ "-d #{dbase['database']}",
264
+ "#{file}"].join(' '))
265
+
266
+ $stdout.puts $stdout.colorize("pg_restore complete.", :green)
267
+ $stdout.puts "File size: #{::File.size(backup_file)}"
268
+ ENV['PGPASSWORD'] = nil
269
+ end
270
+
271
+ def restore(sub, arg)
272
+ return restore_local(file) if sub == 'local'
273
+
274
+ bucket_exists!
275
+ object_exists!(arg, 'restore')
276
+ $stdout.puts 'Are you sure you want to restore this database dump? [yes/N]'
277
+ res = $stdin.gets.chomp
278
+
279
+ if res.present? && res == 'yes'
280
+ recipe_data = {
281
+ backup: {
282
+ databases: Base.config[:app].downcase.split(','),
283
+ force: Base.config[:force] || false,
284
+ dump: {
285
+ type: 's3',
286
+ region: 'us-west-1',
287
+ bucket: Base.config[:backup_bucket],
288
+ key: arg
289
+ }
290
+ }
291
+ }
292
+
293
+ Base.fetch_app
294
+ Base.fetch_instance
295
+ layer_ids = Base.get_layer_ids('postgresql')
296
+
297
+ Base.instances.select! do |inst|
298
+ next false unless inst[:status] == 'online'
299
+ next layer_ids.each do |id|
300
+ break true if inst[:layer_ids].include?(id)
301
+ end || false
302
+ end
303
+
304
+ instance_ids = Base.instances.map { |inst| inst[:instance_id] }
305
+
306
+ command = Base.execute_recipes(Base.app[:app_id], instance_ids,
307
+ 'Restoring application DB backup.', ['s3',
308
+ 'postgresql::restore_database'], recipe_data)
309
+
310
+ Base.run_command(command)
311
+ else
312
+ $stdout.puts 'Aborting.'
313
+ end
314
+ end
315
+
316
+ def url(sub)
317
+ bucket_exists!
318
+ object = object_exists!(sub, 'get url')
319
+
320
+ $stdout.puts object.url_for(:read, expires: 60)
321
+ end
322
+
323
+ def remove(sub)
324
+ bucket_exists!
325
+ object = object_exists!(sub, 'remove')
326
+
327
+ $stdout.puts 'Are you sure you want to delete this file? [yes/N]'
328
+ res = $stdin.gets.chomp
329
+
330
+ if res.present? && res == 'yes'
331
+ s3 = AWS::S3.new
332
+ object.delete
333
+
334
+ $stdout.puts 'Request object deleted.'
335
+ else
336
+ $stdout.puts 'Aborting.'
337
+ end
338
+ end
339
+
340
+ protected
341
+
342
+ def fail_undefined_bucket!
343
+ fail ArgumentError.new(<<EOF
344
+ Your Backup S3 bucket name is undefined.
345
+ Please set it by passing in --set-backup-bucket followed by the desired bucket
346
+ name.
347
+
348
+ Example:
349
+ $ ridoku --set-backup-bucket zv1ns-database-backups
350
+
351
+ EOF
352
+ )
353
+ end
354
+
355
+ def fail_uninitialized_bucket!
356
+ # Fail if bucket doesn't exist.
357
+ fail ArgumentError.new(<<EOF
358
+
359
+ The specified S3 backup bucket does not yet exist. Please create it manually,
360
+ or by running ([] represents optional arguments):
361
+
362
+ $ ridoku backup:init [--backup-bucket <bucket name>]
363
+
364
+ EOF
365
+ )
366
+ end
367
+
368
+ def fail_undefined_object!(sub, action)
369
+ # Fail if object name not set
370
+ fail ArgumentError.new(<<EOF
371
+ The specified backup (#{Base.config[:backup_bucket]}/#{sub}) doesn't exist!
372
+
373
+ Example:
374
+ $ ridoku backup:#{action} <name>
375
+
376
+ EOF
377
+ )
378
+ end
379
+
380
+ def fail_object_not_found!
381
+ # Fail if bucket doesn't exist.
382
+ fail ArgumentError.new(<<EOF
383
+
384
+ The specified S3 backup object does not yet exist. Please create it manually,
385
+ or by running ([] represents optional arguments):
386
+
387
+ $ ridoku backup:capture [--backup-bucket <bucket name> --app <app name>]
388
+
389
+ EOF
390
+ )
391
+ end
392
+ end
393
+ end