ridoku 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +23 -0
- data/AUTHORS +3 -0
- data/Gemfile +24 -0
- data/Gemfile.lock +45 -0
- data/LICENSE.txt +23 -0
- data/README.md +271 -0
- data/Rakefile +1 -0
- data/bin/rid +3 -0
- data/bin/ridoku +350 -0
- data/lib/helpers.rb +13 -0
- data/lib/io-colorize.rb +35 -0
- data/lib/options.rb +142 -0
- data/lib/ridoku.rb +11 -0
- data/lib/ridoku/backup.rb +393 -0
- data/lib/ridoku/base.rb +717 -0
- data/lib/ridoku/config_wizard.rb +195 -0
- data/lib/ridoku/cook.rb +103 -0
- data/lib/ridoku/create.rb +101 -0
- data/lib/ridoku/cron.rb +227 -0
- data/lib/ridoku/db.rb +235 -0
- data/lib/ridoku/defaults.rb +68 -0
- data/lib/ridoku/deploy.rb +157 -0
- data/lib/ridoku/domain.rb +124 -0
- data/lib/ridoku/dump.rb +132 -0
- data/lib/ridoku/env.rb +118 -0
- data/lib/ridoku/list.rb +168 -0
- data/lib/ridoku/log.rb +77 -0
- data/lib/ridoku/maintenance.rb +76 -0
- data/lib/ridoku/packages.rb +93 -0
- data/lib/ridoku/rails_defaults.rb +160 -0
- data/lib/ridoku/run.rb +137 -0
- data/lib/ridoku/service.rb +158 -0
- data/lib/ridoku/services/postgres.rb +77 -0
- data/lib/ridoku/services/rabbitmq.rb +48 -0
- data/lib/ridoku/version.rb +5 -0
- data/lib/ridoku/workers.rb +138 -0
- data/ridoku.gemspec +32 -0
- metadata +211 -0
data/lib/ridoku/base.rb
ADDED
@@ -0,0 +1,717 @@
|
|
1
|
+
#
|
2
|
+
# Base Ridoku class for running commands
|
3
|
+
|
4
|
+
require 'aws'
|
5
|
+
require 'active_support/inflector'
|
6
|
+
require 'securerandom'
|
7
|
+
require 'restclient'
|
8
|
+
|
9
|
+
module Ridoku
|
10
|
+
class InvalidConfig < StandardError
|
11
|
+
attr_accessor :type, :error
|
12
|
+
|
13
|
+
def initialize(type, error)
|
14
|
+
self.type = type
|
15
|
+
self.error = error
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class NoSshAccess < StandardError; end
|
20
|
+
|
21
|
+
class Base
|
22
|
+
class << self
|
23
|
+
attr_accessor :config, :aws_client, :iam_client, :stack, :custom_json,
|
24
|
+
:app, :layers, :instances, :account, :permissions, :stack_list,
|
25
|
+
:app_list, :layer_list, :instance_list, :account_id, :ec2_client
|
26
|
+
|
27
|
+
@config = {}
|
28
|
+
|
29
|
+
POSTGRES_GROUP_NAME = 'Ridoku-PostgreSQL-Server'
|
30
|
+
|
31
|
+
def load_config(path)
|
32
|
+
if File.exists?(path)
|
33
|
+
File.open(path, 'r') do |file|
|
34
|
+
self.config = JSON.parse(file.read, symbolize_names: true)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
(self.config ||= {}).tap do |default|
|
39
|
+
default[:wait] = true
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def save_config(path, limit = [:app, :stack, :ssh_key, :local_init,
|
44
|
+
:shell_user, :service_arn, :instance_arn, :backup_bucket])
|
45
|
+
save = {}
|
46
|
+
if limit.length
|
47
|
+
limit.each do |lc|
|
48
|
+
save[lc] = config[lc]
|
49
|
+
end
|
50
|
+
else
|
51
|
+
save = config
|
52
|
+
end
|
53
|
+
File.open(path, 'w') do |file|
|
54
|
+
file.write(save.to_json)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def configure_opsworks_client
|
59
|
+
opsworks = AWS::OpsWorks.new
|
60
|
+
self.aws_client = opsworks.client
|
61
|
+
end
|
62
|
+
|
63
|
+
def fetch_stack(options = {})
|
64
|
+
return stack if stack && !options[:force]
|
65
|
+
|
66
|
+
configure_opsworks_client
|
67
|
+
|
68
|
+
stack_name = config[:stack]
|
69
|
+
|
70
|
+
fail InvalidConfig.new(:stack, :none) unless stack_name ||
|
71
|
+
!options[:force]
|
72
|
+
|
73
|
+
self.stack_list = aws_client.describe_stacks[:stacks]
|
74
|
+
self.stack = nil
|
75
|
+
|
76
|
+
stack_list.each do |stck|
|
77
|
+
self.stack = stck if stack_name == stck[:name]
|
78
|
+
end
|
79
|
+
|
80
|
+
fail InvalidConfig.new(:stack, :invalid) if !stack &&
|
81
|
+
!options[:force]
|
82
|
+
|
83
|
+
self.custom_json = JSON.parse(stack[:custom_json]) if stack
|
84
|
+
|
85
|
+
return stack
|
86
|
+
end
|
87
|
+
|
88
|
+
def save_stack
|
89
|
+
aws_client.update_stack(
|
90
|
+
stack_id: stack[:stack_id],
|
91
|
+
custom_json: custom_json.to_json,
|
92
|
+
service_role_arn: stack[:service_role_arn]
|
93
|
+
) if stack
|
94
|
+
end
|
95
|
+
|
96
|
+
def fetch_app(options = {})
|
97
|
+
return app if app && !options[:force]
|
98
|
+
|
99
|
+
fetch_stack
|
100
|
+
app_name = config[:app]
|
101
|
+
|
102
|
+
fail InvalidConfig.new(:app, :none) unless app_name
|
103
|
+
|
104
|
+
self.app_list = aws_client.describe_apps(stack_id: stack[:stack_id])[:apps]
|
105
|
+
self.app = nil
|
106
|
+
|
107
|
+
app_list.each do |sapp|
|
108
|
+
self.app = sapp if app_name == sapp[:name]
|
109
|
+
end
|
110
|
+
|
111
|
+
fail InvalidConfig.new(:app, :invalid) unless app
|
112
|
+
|
113
|
+
return app
|
114
|
+
end
|
115
|
+
|
116
|
+
def save_app(values)
|
117
|
+
values = [values] unless values.is_a?(Array)
|
118
|
+
unless app
|
119
|
+
$stderr.puts "Unable to save information because no app is " +
|
120
|
+
"specified."
|
121
|
+
return
|
122
|
+
end
|
123
|
+
|
124
|
+
save_info = {
|
125
|
+
app_id: app[:app_id]
|
126
|
+
}
|
127
|
+
|
128
|
+
save_info.tap do |info|
|
129
|
+
values.each do |val|
|
130
|
+
info[val] = app[val]
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
aws_client.update_app(save_info)
|
135
|
+
end
|
136
|
+
|
137
|
+
def fetch_layer(shortname = :all, options = {})
|
138
|
+
return layers if layers && !options[:force]
|
139
|
+
fetch_stack
|
140
|
+
|
141
|
+
unless self.layer_list
|
142
|
+
self.layers = self.layer_list = aws_client.describe_layers(
|
143
|
+
stack_id: stack[:stack_id])[:layers]
|
144
|
+
end
|
145
|
+
|
146
|
+
if shortname != :all
|
147
|
+
shortname = [shortname] unless shortname.is_a?(Array)
|
148
|
+
self.layers = []
|
149
|
+
|
150
|
+
shortname.each do |short|
|
151
|
+
self.layers << self.layer_list.select do |layer|
|
152
|
+
layer[:shortname] == short
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
self.layers.flatten!
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def get_layer_ids(shortname)
|
161
|
+
fetch_stack
|
162
|
+
layers = aws_client.describe_layers(stack_id: stack[:stack_id])[:layers]
|
163
|
+
layers.select { |l| l[:shortname] == shortname }
|
164
|
+
.map { |l| l[:layer_id] }
|
165
|
+
end
|
166
|
+
|
167
|
+
def save_layer(layer, values)
|
168
|
+
values = [values] unless values.is_a?(Array)
|
169
|
+
|
170
|
+
return unless values.length > 0
|
171
|
+
|
172
|
+
save_info = {
|
173
|
+
layer_id: layer[:layer_id]
|
174
|
+
}
|
175
|
+
|
176
|
+
save_info.tap do |info|
|
177
|
+
values.each do |val|
|
178
|
+
info[val] = layer[val]
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
aws_client.update_layer(save_info)
|
183
|
+
end
|
184
|
+
|
185
|
+
def instance_by_id(id)
|
186
|
+
fetch_instance
|
187
|
+
instance_list.select { |is| is[:instance_id] == id }.first
|
188
|
+
end
|
189
|
+
|
190
|
+
# 'lb' - load balancing layers
|
191
|
+
# 'rails-app'
|
192
|
+
# 'custom'
|
193
|
+
def fetch_instance(shortname = :all, options = {})
|
194
|
+
return instances if instances && !options[:force]
|
195
|
+
|
196
|
+
fetch_stack
|
197
|
+
unless instance_list
|
198
|
+
self.instance_list = self.instances =
|
199
|
+
aws_client.describe_instances(stack_id: stack[:stack_id])[:instances]
|
200
|
+
end
|
201
|
+
|
202
|
+
if shortname != :all
|
203
|
+
fetch_layer(shortname, force: true)
|
204
|
+
self.instances = []
|
205
|
+
|
206
|
+
layers.each do |layer|
|
207
|
+
instance = aws_client.describe_instances(
|
208
|
+
layer_id: layer[:layer_id])
|
209
|
+
self.instances << instance[:instances]
|
210
|
+
end
|
211
|
+
|
212
|
+
self.instances.flatten!
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def get_instances_for_layer(layer)
|
217
|
+
layer_ids = get_layer_ids(layer)
|
218
|
+
instances = aws_client
|
219
|
+
.describe_instances(stack_id: stack[:stack_id])[:instances]
|
220
|
+
ret = []
|
221
|
+
layer_ids.each do |id|
|
222
|
+
instances.each do |inst|
|
223
|
+
ret << inst if inst[:layer_ids].include?(id)
|
224
|
+
end
|
225
|
+
end
|
226
|
+
ret
|
227
|
+
end
|
228
|
+
|
229
|
+
def configure_iam_client
|
230
|
+
return if self.iam_client
|
231
|
+
|
232
|
+
iam = AWS::IAM.new
|
233
|
+
self.iam_client = iam.client
|
234
|
+
end
|
235
|
+
|
236
|
+
def configure_ec2_client
|
237
|
+
return if self.ec2_client
|
238
|
+
|
239
|
+
self.ec2_client = AWS::EC2.new
|
240
|
+
end
|
241
|
+
|
242
|
+
def postgresql_group_exists?(region = 'us-west-1')
|
243
|
+
configure_ec2_client
|
244
|
+
|
245
|
+
ec2_client.security_groups.filter('group-name', POSTGRES_GROUP_NAME).length > 0
|
246
|
+
end
|
247
|
+
|
248
|
+
def update_pg_security_groups_in_all_regions
|
249
|
+
AWS.regions.each do |region|
|
250
|
+
$stdout.puts "Checking region: #{region.name}"
|
251
|
+
update_pg_security_group(region.ec2)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
def update_pg_security_group(client = self.ec2_client)
|
256
|
+
fetch_stack
|
257
|
+
|
258
|
+
port = 5432
|
259
|
+
|
260
|
+
if custom_json.key?('postgresql') &&
|
261
|
+
custom_json['postgresql'].key?('config')
|
262
|
+
custom_json['postgresql']['config'].key?('port')
|
263
|
+
port = custom_json['postgresql']['config']['port']
|
264
|
+
end
|
265
|
+
|
266
|
+
perm_match = false
|
267
|
+
group = client.security_groups.filter('group-name', POSTGRES_GROUP_NAME).first
|
268
|
+
|
269
|
+
unless group
|
270
|
+
$stdout.puts "Creating security group: #{POSTGRES_GROUP_NAME} in #{client.regions.first.name}"
|
271
|
+
group = client.security_groups.create(POSTGRES_GROUP_NAME)
|
272
|
+
else
|
273
|
+
group.ingress_ip_permissions.each do |ipperm|
|
274
|
+
if ipperm.protocol == :tcp && ipperm.port_range == port..port
|
275
|
+
perm_match = true
|
276
|
+
else
|
277
|
+
ipperm.revoke
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
group.authorize_ingress(:tcp, port) unless perm_match
|
283
|
+
end
|
284
|
+
|
285
|
+
def fetch_account(options = {})
|
286
|
+
return account if account && !options[:force]
|
287
|
+
|
288
|
+
configure_iam_client
|
289
|
+
|
290
|
+
self.account = iam_client.get_user
|
291
|
+
|
292
|
+
self.account_id = nil
|
293
|
+
|
294
|
+
account[:user][:arn].match(/.*:.*:.*:.*:([0-9]+)/) do |m|
|
295
|
+
self.account_id = m[1]
|
296
|
+
end
|
297
|
+
|
298
|
+
fail StandardError.new('Failed to determine account ID from user info (it was me not you!)!') unless
|
299
|
+
account_id
|
300
|
+
|
301
|
+
account
|
302
|
+
end
|
303
|
+
|
304
|
+
|
305
|
+
def fetch_permissions(options = {})
|
306
|
+
fetch_stack
|
307
|
+
fetch_account
|
308
|
+
|
309
|
+
return permissions if permissions && !options[:force]
|
310
|
+
|
311
|
+
self.permissions = aws_client.describe_permissions(
|
312
|
+
iam_user_arn: account[:user][:arn],
|
313
|
+
stack_id: stack[:stack_id]
|
314
|
+
)
|
315
|
+
end
|
316
|
+
|
317
|
+
def fetch_roles
|
318
|
+
configure_iam_client
|
319
|
+
|
320
|
+
service = 'aws-opsworks-service-role'
|
321
|
+
instance = 'aws-opsworks-ec2-role'
|
322
|
+
|
323
|
+
iam_client.list_roles[:roles].each do |role|
|
324
|
+
config[:instance_arn] = role[:arn] if role[:role_name] == instance && !config.key?(:instance_arn)
|
325
|
+
config[:service_arn] = role[:arn] if role[:role_name] == service && !config.key?(:service_arn)
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
def if_debug?(&block)
|
330
|
+
yield if config[:debug]
|
331
|
+
end
|
332
|
+
|
333
|
+
def roles_configured?
|
334
|
+
fetch_roles
|
335
|
+
service_role_configured? && instance_role_configured?
|
336
|
+
end
|
337
|
+
|
338
|
+
def service_role_configured?
|
339
|
+
fetch_roles
|
340
|
+
config.key?(:service_arn) && config[:service_arn] != nil
|
341
|
+
end
|
342
|
+
|
343
|
+
def instance_role_configured?
|
344
|
+
fetch_roles
|
345
|
+
config.key?(:instance_arn) && config[:instance_arn] != nil
|
346
|
+
end
|
347
|
+
|
348
|
+
def configure_roles
|
349
|
+
configure_service_roles
|
350
|
+
configure_instance_roles
|
351
|
+
end
|
352
|
+
|
353
|
+
def configure_instance_roles
|
354
|
+
return true if instance_role_configured?
|
355
|
+
fetch_account
|
356
|
+
|
357
|
+
instance_role = "%7B%22Version%22%3A%222008-10-17%22%2C%22Statement%22%3A%5B%7B%22Sid%22%3A%22%22%2C%22Effect%22%3A%22Allow%22%2C%22Principal%22%3A%7B%22Service%22%3A%22ec2.amazonaws.com%22%7D%2C%22Action%22%3A%22sts%3AAssumeRole%22%7D%5D%7D"
|
358
|
+
instance_resource = 'role/aws-opsworks-ec2-role'
|
359
|
+
instance_role_arn = "arn:aws:iam::#{account_id}:#{instance_resource}"
|
360
|
+
end
|
361
|
+
|
362
|
+
def configure_service_roles
|
363
|
+
return true if service_role_configured?
|
364
|
+
fetch_account
|
365
|
+
|
366
|
+
opsworks_role = "%7B%22Version%22%3A%222008-10-17%22%2C%22Statement%22%3A%5B%7B%22Sid%22%3A%22%22%2C%22Effect%22%3A%22Allow%22%2C%22Principal%22%3A%7B%22Service%22%3A%22opsworks.amazonaws.com%22%7D%2C%22Action%22%3A%22sts%3AAssumeRole%22%7D%5D%7D"
|
367
|
+
opsworks_resource = 'role/aws-opsworks-service-role'
|
368
|
+
opsworks_role_arn = "arn:aws:iam::#{account_id}:#{opsworks_resource}"
|
369
|
+
end
|
370
|
+
|
371
|
+
def create_role(conf)
|
372
|
+
if config[:practice]
|
373
|
+
puts conf.to_s
|
374
|
+
else
|
375
|
+
iam_client.create_role(conf)
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
def create_app(conf)
|
380
|
+
conf[:stack_id] = stack[:stack_id]
|
381
|
+
|
382
|
+
# Ensure key exists
|
383
|
+
key_file = conf[:app_source][:ssh_key]
|
384
|
+
|
385
|
+
fail ArgumentError.new('Key file doesn\'t exist.') unless
|
386
|
+
File.exists?(key_file)
|
387
|
+
|
388
|
+
File.open(key_file, 'r') { |f| conf[:app_source][:ssh_key] = f.read }
|
389
|
+
|
390
|
+
# Config[:attributes] must be a hash of <string,string> type.
|
391
|
+
conf[:attributes].tap do |opt|
|
392
|
+
opt.keys.each do |k|
|
393
|
+
opt[k.to_s.camelize] = opt.delete(k).to_s unless k.is_a?(String)
|
394
|
+
end
|
395
|
+
end
|
396
|
+
|
397
|
+
# Ensure attribute 'rails_env' is specified
|
398
|
+
fail ArgumentError.new('attribute:rails_env must be specified.') unless
|
399
|
+
conf[:attributes]['RailsEnv'].length > 0
|
400
|
+
|
401
|
+
if config[:practice]
|
402
|
+
$stdout.puts conf.to_s
|
403
|
+
else
|
404
|
+
aws_client.create_app(conf)
|
405
|
+
initialize_app_environment(conf)
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
def initialize_app_environment(conf)
|
410
|
+
fetch_stack
|
411
|
+
fetch_layer
|
412
|
+
fetch_instance
|
413
|
+
|
414
|
+
app_layer = layer_list.select do |lyr|
|
415
|
+
lyr[:shortname] == 'rails-app'
|
416
|
+
end.first
|
417
|
+
|
418
|
+
db_layer = layer_list.select do |lyr|
|
419
|
+
lyr[:shortname] == 'postgresql'
|
420
|
+
end.first
|
421
|
+
|
422
|
+
deploy_info = custom_json['deploy']
|
423
|
+
|
424
|
+
app = conf[:shortname]
|
425
|
+
|
426
|
+
instance = instances.select do |inst|
|
427
|
+
inst[:status] == 'online' &&
|
428
|
+
inst[:layer_ids].index(app_layer[:layer_id]) != nil
|
429
|
+
end.first
|
430
|
+
|
431
|
+
db_instance = instances.select do |inst|
|
432
|
+
inst[:layer_ids].index(db_layer[:layer_id]) != nil
|
433
|
+
end.first
|
434
|
+
|
435
|
+
dbase_info = {
|
436
|
+
database: app,
|
437
|
+
username: SecureRandom.hex(12),
|
438
|
+
user_password: SecureRandom.hex(12)
|
439
|
+
}
|
440
|
+
|
441
|
+
((custom_json['postgresql'] ||= {})['databases'] ||= []) << dbase_info
|
442
|
+
|
443
|
+
deploy_info[app] = {
|
444
|
+
auto_assets_precompile_on_deploy: true,
|
445
|
+
assetmaster: instance[:hostname],
|
446
|
+
app_env: {
|
447
|
+
'RAILS_ENV' => conf[:attributes]['RailsEnv']
|
448
|
+
},
|
449
|
+
database: {
|
450
|
+
adapter: 'postgresql',
|
451
|
+
username: dbase_info[:username],
|
452
|
+
database: dbase_info[:database],
|
453
|
+
host: db_instance[:public_ip],
|
454
|
+
password: dbase_info[:user_password],
|
455
|
+
port: custom_json['postgresql']['config']['port']
|
456
|
+
}
|
457
|
+
}
|
458
|
+
|
459
|
+
save_stack
|
460
|
+
|
461
|
+
# Update add our changes to the database.
|
462
|
+
run_command({
|
463
|
+
instance_ids: [db_instance[:instance_id]],
|
464
|
+
command: {
|
465
|
+
name: 'execute_recipes',
|
466
|
+
args: { 'recipes' => 'postgresql::create_databases' }
|
467
|
+
}
|
468
|
+
})
|
469
|
+
end
|
470
|
+
|
471
|
+
def valid_instances?(args)
|
472
|
+
args = [args] unless args.is_a?(Array)
|
473
|
+
|
474
|
+
return false if args.length == 0
|
475
|
+
|
476
|
+
fetch_instance
|
477
|
+
|
478
|
+
inst_names = instances.map do |inst|
|
479
|
+
# if requested is stop, its definitely invalid.
|
480
|
+
return false if args.index(inst[:hostname]) != nil &&
|
481
|
+
inst[:status] == 'stopped'
|
482
|
+
|
483
|
+
inst[:hostname]
|
484
|
+
end
|
485
|
+
|
486
|
+
# if a requested is not in the list, then its an invalid list.
|
487
|
+
args.each do |arg|
|
488
|
+
return false if inst_names.index(arg) == nil
|
489
|
+
end
|
490
|
+
|
491
|
+
true
|
492
|
+
end
|
493
|
+
|
494
|
+
def select_instances(args)
|
495
|
+
fetch_instance
|
496
|
+
return instance_list unless args
|
497
|
+
|
498
|
+
args = [args] unless args.is_a?(Array)
|
499
|
+
return nil if args.length == 0
|
500
|
+
|
501
|
+
self.instances = instance_list.select do |inst|
|
502
|
+
args.index(inst[:hostname]) != nil
|
503
|
+
end
|
504
|
+
end
|
505
|
+
|
506
|
+
def pretty_instances(io)
|
507
|
+
inststr = []
|
508
|
+
|
509
|
+
instances.each do |inst|
|
510
|
+
val = "#{inst[:hostname]} [#{inst[:status]}]"
|
511
|
+
inststr << io.colorize(val,
|
512
|
+
[:bold, inst[:status] == 'online' ? :green : :red])
|
513
|
+
end
|
514
|
+
|
515
|
+
inststr
|
516
|
+
end
|
517
|
+
|
518
|
+
def run_command(deployment)
|
519
|
+
fetch_stack
|
520
|
+
fetch_app
|
521
|
+
|
522
|
+
deployment[:stack_id] = stack[:stack_id]
|
523
|
+
|
524
|
+
if config[:practice]
|
525
|
+
$stdout.puts "Would run command: #{deployment[:command][:name]}"
|
526
|
+
$stdout.puts 'On instances:'
|
527
|
+
instances.each do |inst|
|
528
|
+
next unless
|
529
|
+
deployment[:instance_ids].index(inst[:instance_id]) != nil
|
530
|
+
|
531
|
+
$stdout.puts " #{inst[:hostname]}: #{$stdout.colorize(
|
532
|
+
inst[:status], inst[:status] == 'online' ? :green : :red)}"
|
533
|
+
|
534
|
+
end
|
535
|
+
|
536
|
+
if deployment.key?(:custom_json)
|
537
|
+
$stdout.puts 'With custom_json:'
|
538
|
+
$stdout.puts JSON.pretty_generate(deployment[:custom_json])
|
539
|
+
end
|
540
|
+
else
|
541
|
+
if deployment.key?(:custom_json)
|
542
|
+
deployment[:custom_json] = JSON.generate(deployment[:custom_json])
|
543
|
+
end
|
544
|
+
|
545
|
+
depid = aws_client.create_deployment(deployment)[:deployment_id]
|
546
|
+
|
547
|
+
$stdout.puts $stdout.colorize('Command Sent', :green) if
|
548
|
+
config[:verbose]
|
549
|
+
|
550
|
+
monitor_deployment(depid) if config[:wait]
|
551
|
+
end
|
552
|
+
end
|
553
|
+
|
554
|
+
def extract_instance_ids(layers = nil)
|
555
|
+
Base.fetch_instance(layers || Base.config[:layers] || :all, force: true)
|
556
|
+
|
557
|
+
names = Base.config[:instances] || []
|
558
|
+
instances = Base.instances.select do |inst|
|
559
|
+
if names.length > 0
|
560
|
+
names.index(inst[:hostname]) != nil && inst[:status] != 'offline'
|
561
|
+
else
|
562
|
+
inst[:status] == 'online'
|
563
|
+
end
|
564
|
+
end
|
565
|
+
|
566
|
+
instances.map do |inst|
|
567
|
+
inst[:instance_id]
|
568
|
+
end
|
569
|
+
end
|
570
|
+
|
571
|
+
def base_command(app_id, instance_ids, comment)
|
572
|
+
fail ArgumentError.new('[ERROR] No instances selected.') if
|
573
|
+
!instance_ids.is_a?(Array) || instance_ids.empty?
|
574
|
+
|
575
|
+
{}.tap do |cmd|
|
576
|
+
cmd[:instance_ids] = instance_ids
|
577
|
+
cmd[:app_id] = app_id if app_id
|
578
|
+
cmd[:comment] = comment if comment
|
579
|
+
end
|
580
|
+
end
|
581
|
+
|
582
|
+
def update_cookbooks(instance_ids)
|
583
|
+
command = Base.base_command(nil, instance_ids,
|
584
|
+
Base.config[:comment])
|
585
|
+
command[:command] = { name: 'update_custom_cookbooks' }
|
586
|
+
command
|
587
|
+
end
|
588
|
+
|
589
|
+
def execute_recipes(app_id, instance_ids, comment, recipes,
|
590
|
+
custom_json = nil)
|
591
|
+
base_command(app_id, instance_ids, comment).tap do |cmd|
|
592
|
+
cmd[:command] = {
|
593
|
+
name: 'execute_recipes',
|
594
|
+
args: { 'recipes' => [recipes].flatten }
|
595
|
+
}
|
596
|
+
cmd[:custom_json] = custom_json if custom_json
|
597
|
+
end
|
598
|
+
end
|
599
|
+
|
600
|
+
def deploy(app_id, instance_ids, comment, custom_json = nil)
|
601
|
+
base_command(app_id, instance_ids, comment).tap do |cmd|
|
602
|
+
cmd[:command] = {
|
603
|
+
name: 'deploy'
|
604
|
+
}
|
605
|
+
cmd[:custom_json] = custom_json if custom_json
|
606
|
+
end
|
607
|
+
end
|
608
|
+
|
609
|
+
def rollback(app_id, instance_ids, comment, custom_json = nil)
|
610
|
+
dep = deploy(app_id, instance_ids, comment, custom_json)
|
611
|
+
dep[:command] = { name: 'rollback' }
|
612
|
+
|
613
|
+
dep
|
614
|
+
end
|
615
|
+
|
616
|
+
def standard_deploy(layer = :all, custom_json = nil)
|
617
|
+
fetch_instance(layer, force: true)
|
618
|
+
fetch_app
|
619
|
+
|
620
|
+
instances.select! { |inst| inst[:status] == 'online' }
|
621
|
+
instance_ids = instances.map { |inst| inst[:instance_id] }
|
622
|
+
|
623
|
+
unless config[:quiet]
|
624
|
+
$stdout.puts "Application:"
|
625
|
+
$stdout.puts " #{$stdout.colorize(app[:name], :bold)}"
|
626
|
+
|
627
|
+
$stdout.puts "#{instances.length} instance(s):"
|
628
|
+
|
629
|
+
pretty_instances($stdout).each do |inst|
|
630
|
+
$stdout.puts " #{inst}"
|
631
|
+
end
|
632
|
+
|
633
|
+
$stdout.puts "Repository:"
|
634
|
+
$stdout.puts " #{$stdout.colorize(app[:app_source][:url], :bold)}"\
|
635
|
+
" @ #{$stdout.colorize(app[:app_source][:revision], :bold)}"
|
636
|
+
end
|
637
|
+
|
638
|
+
run_command(deploy(app[:app_id], instance_ids, config[:comment],
|
639
|
+
custom_json))
|
640
|
+
end
|
641
|
+
|
642
|
+
def color_code_logs(logs)
|
643
|
+
$stderr.puts(logs.gsub(%r((?<color>\[[0-9]{1,2}m)),"\e\\k<color>"))
|
644
|
+
end
|
645
|
+
|
646
|
+
def monitor_deployment(dep_ids)
|
647
|
+
cmds = aws_client.describe_commands(deployment_id: dep_ids)
|
648
|
+
|
649
|
+
commands = cmds[:commands].map do |cmd|
|
650
|
+
{ command: cmd, instance: instance_by_id(cmd[:instance_id]) }
|
651
|
+
end
|
652
|
+
|
653
|
+
$stdout.puts "Command issued to #{commands.length} instances:"
|
654
|
+
commands.each do |cmd|
|
655
|
+
$stdout.puts " #{$stdout.colorize(cmd[:instance][:hostname],
|
656
|
+
:green)}"
|
657
|
+
end
|
658
|
+
|
659
|
+
# Iterate a reasonable number of times... 100*5 => 500 seconds
|
660
|
+
20.times do |time|
|
661
|
+
cmds = aws_client.describe_commands(deployment_id: dep_ids)
|
662
|
+
|
663
|
+
success = cmds[:commands].select do |cmd|
|
664
|
+
cmd[:status] == 'successful'
|
665
|
+
end
|
666
|
+
|
667
|
+
# Show we are still thinking...
|
668
|
+
case time % 4
|
669
|
+
when 0
|
670
|
+
print "\\\r"
|
671
|
+
when 1
|
672
|
+
print "|\r"
|
673
|
+
when 2
|
674
|
+
print "/\r"
|
675
|
+
when 3
|
676
|
+
print "-\r"
|
677
|
+
end
|
678
|
+
|
679
|
+
if cmds.length == success.length
|
680
|
+
$stdout.puts 'Command executed successfully.'
|
681
|
+
return
|
682
|
+
end
|
683
|
+
|
684
|
+
# Collect the non-[running,pending,successful] command entries
|
685
|
+
not_ok = cmds[:commands].select do |cmd|
|
686
|
+
['running', 'pending', 'successful'].index(cmd[:status]) == nil
|
687
|
+
end.map do |cmd|
|
688
|
+
{
|
689
|
+
command: cmd,
|
690
|
+
instance: instance_by_id(cmd[:instance_id])
|
691
|
+
}
|
692
|
+
end
|
693
|
+
|
694
|
+
# Print each one that has failed.
|
695
|
+
not_ok.each do |item|
|
696
|
+
$stderr.puts "#{item[:instance][:hostname]}"
|
697
|
+
$stderr.puts " Status: " +
|
698
|
+
$stderr.colorize(item[:command][:status], :red)
|
699
|
+
$stderr.puts " Url: " + item[:command][:log_url]
|
700
|
+
color_code_logs(RestClient.get(item[:command][:log_url]))
|
701
|
+
exit 1
|
702
|
+
end
|
703
|
+
|
704
|
+
sleep 5
|
705
|
+
end
|
706
|
+
end
|
707
|
+
end
|
708
|
+
end
|
709
|
+
end
|
710
|
+
|
711
|
+
BYTE_UNITS2 =[[1073741824, "GB"], [1048576, "MB"], [1024, "KB"], [0,
|
712
|
+
"B"]]
|
713
|
+
|
714
|
+
def nice_bytes(n)
|
715
|
+
unit = BYTE_UNITS2.detect{ |u| n > u[0] }
|
716
|
+
"#{n/unit[0]} #{unit[1]}"
|
717
|
+
end
|