ridoku 0.1.0

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.
@@ -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