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