aerosol 0.5.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.
- checksums.yaml +7 -0
- data/.cane +3 -0
- data/.gitignore +15 -0
- data/.rspec +3 -0
- data/.travis.yml +6 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +20 -0
- data/README.md +399 -0
- data/Rakefile +18 -0
- data/aerosol.gemspec +32 -0
- data/bin/aerosol +8 -0
- data/img/aerosol.pdf +3898 -11
- data/img/aerosol.png +0 -0
- data/lib/aerosol/auto_scaling.rb +240 -0
- data/lib/aerosol/aws.rb +62 -0
- data/lib/aerosol/aws_model.rb +93 -0
- data/lib/aerosol/cli.rb +41 -0
- data/lib/aerosol/connection.rb +39 -0
- data/lib/aerosol/deploy.rb +105 -0
- data/lib/aerosol/env.rb +6 -0
- data/lib/aerosol/instance.rb +55 -0
- data/lib/aerosol/launch_configuration.rb +106 -0
- data/lib/aerosol/rake_task.rb +141 -0
- data/lib/aerosol/runner.rb +329 -0
- data/lib/aerosol/util.rb +41 -0
- data/lib/aerosol/version.rb +5 -0
- data/lib/aerosol.rb +83 -0
- data/spec/aerosol/auto_scaling_spec.rb +420 -0
- data/spec/aerosol/aws_spec.rb +24 -0
- data/spec/aerosol/cli_spec.rb +10 -0
- data/spec/aerosol/connection_spec.rb +94 -0
- data/spec/aerosol/deploy_spec.rb +192 -0
- data/spec/aerosol/env_spec.rb +16 -0
- data/spec/aerosol/instance_spec.rb +57 -0
- data/spec/aerosol/launch_configuration_spec.rb +328 -0
- data/spec/aerosol/rake_task_spec.rb +19 -0
- data/spec/aerosol/runner_spec.rb +482 -0
- data/spec/aerosol_spec.rb +41 -0
- data/spec/fixtures/Procfile +1 -0
- data/spec/fixtures/Rakefile +17 -0
- data/spec/fixtures/not_a_tar-2.txt +1 -0
- data/spec/fixtures/not_a_tar.txt +1 -0
- data/spec/fixtures/tar-2.tar +0 -0
- data/spec/fixtures/test-1.tar +0 -0
- data/spec/fixtures/test-2.tar.gz +0 -0
- data/spec/spec_helper.rb +22 -0
- data/spec/support/vcr.rb +11 -0
- data/spec/vcr/Deployz_Docker/_fetch_import/when_both_import_and_name_are_present_present/and_it_points_to_a_non-S3_url/pulls_the_file.yml +214 -0
- metadata +312 -0
@@ -0,0 +1,106 @@
|
|
1
|
+
class Aerosol::LaunchConfiguration
|
2
|
+
include Aerosol::AWSModel
|
3
|
+
include Dockly::Util::Logger::Mixin
|
4
|
+
|
5
|
+
logger_prefix '[aerosol launch_configuration]'
|
6
|
+
aws_attribute :aws_identifier => 'LaunchConfigurationName',
|
7
|
+
:ami => 'ImageId',
|
8
|
+
:instance_type => 'InstanceType',
|
9
|
+
:security_groups => 'SecurityGroups',
|
10
|
+
:user_data => 'UserData',
|
11
|
+
:iam_role => 'IamInstanceProfile',
|
12
|
+
:kernel_id => 'KernelId',
|
13
|
+
:key_name => 'KeyName',
|
14
|
+
:spot_price => 'SpotPrice',
|
15
|
+
:created_time => 'CreatedTime',
|
16
|
+
:associate_public_ip_address => 'AssociatePublicIpAddress'
|
17
|
+
|
18
|
+
primary_key :aws_identifier
|
19
|
+
default_value(:security_groups) { [] }
|
20
|
+
|
21
|
+
def aws_identifier(arg = nil)
|
22
|
+
if arg
|
23
|
+
raise "You cannot set the aws_identifer directly" unless from_aws
|
24
|
+
@aws_identifier = arg
|
25
|
+
else
|
26
|
+
@aws_identifier || default_identifier
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def security_group(group)
|
31
|
+
security_groups << group
|
32
|
+
end
|
33
|
+
|
34
|
+
def create!
|
35
|
+
ensure_present! :ami, :instance_type
|
36
|
+
|
37
|
+
info self.to_s
|
38
|
+
conn.create_launch_configuration(ami, instance_type, aws_identifier, create_options)
|
39
|
+
sleep 10 # TODO: switch to fog models and .wait_for { ready? }
|
40
|
+
end
|
41
|
+
|
42
|
+
def destroy!
|
43
|
+
info self.to_s
|
44
|
+
conn.delete_launch_configuration(aws_identifier)
|
45
|
+
end
|
46
|
+
|
47
|
+
def all_instances
|
48
|
+
Aerosol::Instance.all.select { |instance|
|
49
|
+
!instance.launch_configuration.nil? &&
|
50
|
+
(instance.launch_configuration.aws_identifier == self.aws_identifier)
|
51
|
+
}.each(&:description)
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.request_all_for_token(next_token)
|
55
|
+
options = next_token.nil? ? {} : { 'NextToken' => next_token }
|
56
|
+
Aerosol::AWS.auto_scaling
|
57
|
+
.describe_launch_configurations(options)
|
58
|
+
.body
|
59
|
+
.[]('DescribeLaunchConfigurationsResult')
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.request_all
|
63
|
+
next_token = nil
|
64
|
+
lcs = []
|
65
|
+
|
66
|
+
begin
|
67
|
+
new_lcs = request_all_for_token(next_token)
|
68
|
+
lcs.concat(new_lcs['LaunchConfigurations'])
|
69
|
+
next_token = new_lcs['NextToken']
|
70
|
+
end while !next_token.nil?
|
71
|
+
|
72
|
+
lcs
|
73
|
+
end
|
74
|
+
|
75
|
+
def to_s
|
76
|
+
%{Aerosol::LaunchConfiguration { \
|
77
|
+
"aws_identifier" => "#{aws_identifier}", \
|
78
|
+
"ami" => "#{ami}", \
|
79
|
+
"instance_type" => "#{instance_type}", \
|
80
|
+
"security_groups" => #{security_groups.to_s}, \
|
81
|
+
"user_data" => "#{user_data}", \
|
82
|
+
"iam_role" => "#{iam_role}", \
|
83
|
+
"kernel_id" => "#{kernel_id}", \
|
84
|
+
"key_name" => "#{key_name}", \
|
85
|
+
"spot_price" => "#{spot_price}", \
|
86
|
+
"created_time" => "#{created_time}" \
|
87
|
+
}}
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
def create_options
|
92
|
+
{ # TODO Add dsl so that 'BlockDeviceMappings' may be specified
|
93
|
+
'IamInstanceProfile' => iam_role,
|
94
|
+
'KernelId' => kernel_id,
|
95
|
+
'KeyName' => key_name,
|
96
|
+
'SecurityGroups' => security_groups,
|
97
|
+
'SpotPrice' => spot_price,
|
98
|
+
'UserData' => Aerosol::Util.strip_heredoc(user_data || ''),
|
99
|
+
'AssociatePublicIpAddress' => associate_public_ip_address
|
100
|
+
}.reject { |k, v| v.nil? }
|
101
|
+
end
|
102
|
+
|
103
|
+
def conn
|
104
|
+
Aerosol::AWS.auto_scaling
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'aerosol'
|
3
|
+
|
4
|
+
$rake_task_logger = Dockly::Util::Logger.new('[aerosol rake_task]', STDOUT, false)
|
5
|
+
|
6
|
+
class Rake::AutoScalingTask < Rake::Task
|
7
|
+
def needed?
|
8
|
+
!auto_scaling.exists?
|
9
|
+
end
|
10
|
+
|
11
|
+
def auto_scaling
|
12
|
+
Aerosol.auto_scaling(name.split(':').last.to_sym)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module Rake::DSL
|
17
|
+
def auto_scaling(*args, &block)
|
18
|
+
Rake::AutoScalingTask.define_task(*args, &block)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
namespace :aerosol do
|
23
|
+
desc "Verify an aerosol.rb file exists"
|
24
|
+
task :load do
|
25
|
+
raise "No aerosol.rb found!" unless File.exist?('aerosol.rb')
|
26
|
+
end
|
27
|
+
|
28
|
+
namespace :auto_scaling do
|
29
|
+
Aerosol.auto_scalings.values.reject(&:from_aws).each do |inst|
|
30
|
+
auto_scaling inst.name => 'aerosol:load' do |name|
|
31
|
+
Thread.current[:rake_task] = name
|
32
|
+
inst.create
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
namespace :ssh do
|
38
|
+
Aerosol.deploys.values.each do |inst|
|
39
|
+
desc "Prints out ssh command to all instances of the latest deploy of #{inst.name}"
|
40
|
+
task inst.name do |name|
|
41
|
+
Thread.current[:rake_task] = name
|
42
|
+
inst.generate_ssh_commands.each do |ssh_command|
|
43
|
+
puts ssh_command
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
all_deploy_tasks = []
|
50
|
+
all_asynch_deploy_tasks = []
|
51
|
+
|
52
|
+
namespace :env do
|
53
|
+
Aerosol.envs.values.each do |env|
|
54
|
+
desc "Run all of the deploys for #{env.name} in parallel"
|
55
|
+
multitask env.name => env.deploy.map { |dep| "aerosol:#{dep.name}:all" }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
Aerosol.deploys.values.each do |inst|
|
61
|
+
namespace :"#{inst.name}" do
|
62
|
+
desc "Runs the ActiveRecord migration through the SSH connection given"
|
63
|
+
task :run_migration => 'aerosol:load' do |name|
|
64
|
+
Thread.current[:rake_task] = name
|
65
|
+
Aerosol::Runner.new.with_deploy(inst.name) do |runner|
|
66
|
+
runner.run_migration
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
desc "Creates a new auto scaling group for the current git hash"
|
71
|
+
task :create_auto_scaling_group => "aerosol:auto_scaling:#{inst.auto_scaling.name}"
|
72
|
+
|
73
|
+
desc "Waits for instances of the new autoscaling groups to start up"
|
74
|
+
task :wait_for_new_instances => 'aerosol:load' do |name|
|
75
|
+
Thread.current[:rake_task] = name
|
76
|
+
Aerosol::Runner.new.with_deploy(inst.name) do |runner|
|
77
|
+
runner.wait_for_new_instances
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
desc "Runs command to shut down the application on the old instances instead of just terminating"
|
82
|
+
task :stop_old_app => 'aerosol:load' do |name|
|
83
|
+
Thread.current[:rake_task] = name
|
84
|
+
Aerosol::Runner.new.with_deploy(inst.name) do |runner|
|
85
|
+
runner.stop_app
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
desc "Terminates instances with the current tag and different git hash"
|
90
|
+
task :destroy_old_auto_scaling_groups => 'aerosol:load' do |name|
|
91
|
+
Thread.current[:rake_task] = name
|
92
|
+
Aerosol::Runner.new.with_deploy(inst.name) do |runner|
|
93
|
+
runner.destroy_old_auto_scaling_groups
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
desc "Terminates instances with the current tag and current git hash"
|
98
|
+
task :destroy_new_auto_scaling_groups => 'aerosol:load' do |name|
|
99
|
+
Thread.current[:rake_task] = name
|
100
|
+
Aerosol::Runner.new.with_deploy(inst.name) do |runner|
|
101
|
+
runner.destroy_new_auto_scaling_groups
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
desc "Runs a post deploy command"
|
106
|
+
task :run_post_deploy => 'aerosol:load' do |name|
|
107
|
+
Thread.current[:rake_task] = name
|
108
|
+
inst.run_post_deploy
|
109
|
+
end
|
110
|
+
|
111
|
+
##
|
112
|
+
|
113
|
+
desc "Runs migration and creates auto scaling groups"
|
114
|
+
task :all_prep => [:run_migration, :create_auto_scaling_group]
|
115
|
+
|
116
|
+
desc "Waits for new instances, stops old application, destroys old auto scaling groups "\
|
117
|
+
"and runs the post deploy command"
|
118
|
+
task :all_release => [:wait_for_new_instances, :stop_old_app, :destroy_old_auto_scaling_groups, :run_post_deploy]
|
119
|
+
|
120
|
+
desc "Run migration, create auto scaling group, wait for instances, stop old application, "\
|
121
|
+
"destroy old auto scaling groups and run the post deploy command"
|
122
|
+
task :all => [:all_prep, :all_release]
|
123
|
+
all_deploy_tasks << "aerosol:#{inst.name}:all"
|
124
|
+
|
125
|
+
##
|
126
|
+
|
127
|
+
desc "Runs migration and creates auto scaling groups in parallel"
|
128
|
+
multitask :all_asynch_prep => [:run_migration, :create_auto_scaling_group]
|
129
|
+
|
130
|
+
desc "Same as `all` but runs the migration and creates auto scaling groups in parallel"
|
131
|
+
task :all_asynch => [:all_asynch_prep, :all_release]
|
132
|
+
all_asynch_deploy_tasks << "aerosol:#{inst.name}:all_asynch"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
desc "Runs all the all deploy tasks in the aerosol.rb"
|
137
|
+
task :deploy_all => all_deploy_tasks
|
138
|
+
|
139
|
+
desc "Runs all the all deploy tasks in the aerosol.rb in parallel"
|
140
|
+
multitask :deploy_all_asynch => all_asynch_deploy_tasks
|
141
|
+
end
|
@@ -0,0 +1,329 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'active_record'
|
3
|
+
require 'grit'
|
4
|
+
require 'timeout'
|
5
|
+
|
6
|
+
class Aerosol::Runner
|
7
|
+
extend Dockly::Util::Delegate
|
8
|
+
include Dockly::Util::Logger::Mixin
|
9
|
+
|
10
|
+
logger_prefix '[aerosol runner]'
|
11
|
+
attr_reader :deploy, :log_pids
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@log_pids = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def run_migration
|
18
|
+
require_deploy!
|
19
|
+
return unless deploy.migrate?
|
20
|
+
raise 'To run a migration, $RAILS_ENV must be set.' if ENV['RAILS_ENV'].nil?
|
21
|
+
|
22
|
+
info "running migration"
|
23
|
+
begin
|
24
|
+
info "loading config for env #{ENV['RAILS_ENV']}"
|
25
|
+
original_config = YAML.load(ERB.new(File.read(db_config_path)).result)[ENV['RAILS_ENV']]
|
26
|
+
debug "creating ssh tunnel"
|
27
|
+
migration_ssh.with_connection do |session|
|
28
|
+
# session.logger.sev_threshold=Logger::Severity::DEBUG
|
29
|
+
debug "finding free port"
|
30
|
+
port = random_open_port
|
31
|
+
db_port = original_config['port'] || 3306 # TODO: get default port from DB driver
|
32
|
+
host = original_config['host']
|
33
|
+
info "forwarding 127.0.0.1:#{port} --> #{host}:#{db_port}"
|
34
|
+
session.forward.local(port, host, db_port)
|
35
|
+
child = fork do
|
36
|
+
GC.disable
|
37
|
+
with_prefix('child:') do |logger|
|
38
|
+
logger.debug "establishing connection"
|
39
|
+
ActiveRecord::Base.establish_connection(original_config.merge(
|
40
|
+
'host' => '127.0.0.1',
|
41
|
+
'port' => port
|
42
|
+
))
|
43
|
+
logger.info "running migration"
|
44
|
+
ActiveRecord::Migrator.migrate(%w[db/migrate])
|
45
|
+
end
|
46
|
+
end
|
47
|
+
debug "waiting for child"
|
48
|
+
exitstatus = nil
|
49
|
+
session.loop(0.1) do
|
50
|
+
pid = Process.waitpid(child, Process::WNOHANG)
|
51
|
+
exitstatus = $?.exitstatus if !pid.nil?
|
52
|
+
pid.nil?
|
53
|
+
end
|
54
|
+
raise "migration failed: #{exitstatus}" unless exitstatus == 0
|
55
|
+
end
|
56
|
+
info "complete"
|
57
|
+
ensure
|
58
|
+
ActiveRecord::Base.clear_all_connections!
|
59
|
+
end
|
60
|
+
info "migration ran"
|
61
|
+
end
|
62
|
+
|
63
|
+
def wait_for_new_instances
|
64
|
+
require_deploy!
|
65
|
+
info "waiting for new instances"
|
66
|
+
|
67
|
+
live_instances = []
|
68
|
+
Timeout.timeout(instance_live_grace_period) do
|
69
|
+
loop do
|
70
|
+
current_instances = new_instances
|
71
|
+
remaining_instances = current_instances - live_instances
|
72
|
+
info "waiting for instances to be live (#{remaining_instances.count} remaining)"
|
73
|
+
debug "current instances: #{current_instances.map(&:id)}"
|
74
|
+
debug "live instances: #{live_instances.map(&:id)}"
|
75
|
+
live_instances.concat(remaining_instances.select { |instance| healthy?(instance) })
|
76
|
+
break if (current_instances - live_instances).empty?
|
77
|
+
debug 'sleeping for 10 seconds'
|
78
|
+
sleep(10)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
info 'new instances are up'
|
83
|
+
rescue Timeout::Error
|
84
|
+
raise "[aerosol runner] site live check timed out after #{instance_live_grace_period} seconds"
|
85
|
+
ensure
|
86
|
+
log_pids.each do |instance_id, fork|
|
87
|
+
debug "Killing tailing for #{instance_id}: #{Time.now}"
|
88
|
+
Process.kill('HUP', fork)
|
89
|
+
debug "Killed process for #{instance_id}: #{Time.now}"
|
90
|
+
debug "Waiting for process to die"
|
91
|
+
Process.wait(fork)
|
92
|
+
debug "Process ended for #{instance_id}: #{Time.now}"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def healthy?(instance)
|
97
|
+
debug "Checking if #{instance.id} is healthy"
|
98
|
+
|
99
|
+
unless instance.live?
|
100
|
+
debug "#{instance.id} is not live"
|
101
|
+
return false
|
102
|
+
end
|
103
|
+
|
104
|
+
debug "trying to SSH to #{instance.id}"
|
105
|
+
success = false
|
106
|
+
ssh.with_connection(instance) do |session|
|
107
|
+
start_tailing_logs(ssh, instance) if log_pids[instance.id].nil?
|
108
|
+
debug "checking if #{instance.id} is healthy"
|
109
|
+
success = if is_alive?.nil?
|
110
|
+
debug 'Using default site live check'
|
111
|
+
check_site_live(session)
|
112
|
+
else
|
113
|
+
debug 'Using custom site live check'
|
114
|
+
is_alive?.call(session, self)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
if success
|
119
|
+
debug "#{instance.id} is healthy"
|
120
|
+
else
|
121
|
+
debug "#{instance.id} is not healthy"
|
122
|
+
end
|
123
|
+
success
|
124
|
+
rescue => ex
|
125
|
+
debug "#{instance.id} is not healthy: #{ex.message}"
|
126
|
+
false
|
127
|
+
end
|
128
|
+
|
129
|
+
def check_site_live(session)
|
130
|
+
command = [
|
131
|
+
'wget',
|
132
|
+
'-q',
|
133
|
+
# Since we're hitting localhost, the cert will always be invalid, so don't try to check it.
|
134
|
+
deploy.ssl ? '--no-check-certificate' : nil,
|
135
|
+
"'#{deploy.live_check_url}'",
|
136
|
+
'-O',
|
137
|
+
'/dev/null'
|
138
|
+
].compact.join(' ')
|
139
|
+
|
140
|
+
debug "running #{command}"
|
141
|
+
ret = ssh_exec!(session, command)
|
142
|
+
debug "finished running #{command}"
|
143
|
+
ret[:exit_status].zero?
|
144
|
+
end
|
145
|
+
|
146
|
+
def start_tailing_logs(ssh, instance)
|
147
|
+
if tail_logs && log_files.length > 0
|
148
|
+
command = [
|
149
|
+
'sudo', 'tail', '-f', *log_files
|
150
|
+
].join(' ')
|
151
|
+
|
152
|
+
log_pids[instance.id] ||= ssh_fork(command, ssh, instance)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def ssh_fork(command, ssh, instance)
|
157
|
+
debug 'starting ssh fork'
|
158
|
+
fork do
|
159
|
+
Signal.trap('HUP') do
|
160
|
+
debug 'Killing tailing session'
|
161
|
+
Process.exit!
|
162
|
+
end
|
163
|
+
debug 'starting tail'
|
164
|
+
begin
|
165
|
+
ssh.with_connection(instance) do |session|
|
166
|
+
debug 'tailing session connected'
|
167
|
+
buffer = ''
|
168
|
+
ssh_exec!(session, command) do |stream, data|
|
169
|
+
data.lines.each do |line|
|
170
|
+
if line.end_with?($/)
|
171
|
+
debug "[#{instance.id}] #{stream}: #{buffer + line}"
|
172
|
+
buffer = ''
|
173
|
+
else
|
174
|
+
buffer = line
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
rescue => ex
|
180
|
+
error "#{ex.class}: #{ex.message}"
|
181
|
+
error "#{ex.backtrace.join("\n")}"
|
182
|
+
ensure
|
183
|
+
debug 'finished'
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def stop_app
|
189
|
+
info "stopping old app"
|
190
|
+
to_stop = old_instances
|
191
|
+
|
192
|
+
info "starting with #{to_stop.length} instances to stop"
|
193
|
+
|
194
|
+
stop_app_retries.succ.times do |n|
|
195
|
+
break if to_stop.empty?
|
196
|
+
debug "stop app: #{to_stop.length} instances remaining"
|
197
|
+
to_stop.reject! { |instance| stop_one_app(instance) }
|
198
|
+
end
|
199
|
+
|
200
|
+
if to_stop.length.zero?
|
201
|
+
info "successfully stopped the app on each old instance"
|
202
|
+
elsif !continue_if_stop_app_fails
|
203
|
+
raise "Failed to stop app on #{to_stop.length} instances"
|
204
|
+
end
|
205
|
+
info "stopped old app"
|
206
|
+
end
|
207
|
+
|
208
|
+
def destroy_old_auto_scaling_groups
|
209
|
+
require_deploy!
|
210
|
+
info "destroying old autoscaling groups"
|
211
|
+
sleep deploy.sleep_before_termination
|
212
|
+
old_auto_scaling_groups.map(&:destroy)
|
213
|
+
info "destroyed old autoscaling groups"
|
214
|
+
end
|
215
|
+
|
216
|
+
def destroy_new_auto_scaling_groups
|
217
|
+
require_deploy!
|
218
|
+
info "destroying autoscaling groups created for this sha"
|
219
|
+
new_auto_scaling_groups.map(&:destroy)
|
220
|
+
info "destroyed new autoscaling groups"
|
221
|
+
end
|
222
|
+
|
223
|
+
def old_instances
|
224
|
+
require_deploy!
|
225
|
+
old_auto_scaling_groups.map(&:launch_configuration).compact.map(&:all_instances).flatten.compact
|
226
|
+
end
|
227
|
+
|
228
|
+
def old_auto_scaling_groups
|
229
|
+
select_auto_scaling_groups { |asg| asg.tags['GitSha'] != auto_scaling.tags['GitSha'] }
|
230
|
+
end
|
231
|
+
|
232
|
+
def new_auto_scaling_groups
|
233
|
+
select_auto_scaling_groups { |asg| asg.tags['GitSha'] == auto_scaling.tags['GitSha'] }
|
234
|
+
end
|
235
|
+
|
236
|
+
def select_auto_scaling_groups(&block)
|
237
|
+
require_deploy!
|
238
|
+
Aerosol::LaunchConfiguration.all # load all of the launch configurations first
|
239
|
+
Aerosol::AutoScaling.all.select { |asg|
|
240
|
+
(asg.tags['Deploy'].to_s == auto_scaling.tags['Deploy']) &&
|
241
|
+
(block.nil? ? true : block.call(asg))
|
242
|
+
}
|
243
|
+
end
|
244
|
+
|
245
|
+
def new_instances
|
246
|
+
require_deploy!
|
247
|
+
while launch_configuration.all_instances.length < auto_scaling.min_size
|
248
|
+
info "Waiting for instances to come up"
|
249
|
+
sleep 10
|
250
|
+
end
|
251
|
+
launch_configuration.all_instances
|
252
|
+
end
|
253
|
+
|
254
|
+
def with_deploy(name)
|
255
|
+
unless dep = Aerosol::Deploy[name]
|
256
|
+
raise "No deploy named '#{name}'"
|
257
|
+
end
|
258
|
+
original = @deploy
|
259
|
+
@deploy = dep
|
260
|
+
yield self
|
261
|
+
@deploy = original
|
262
|
+
end
|
263
|
+
|
264
|
+
def require_deploy!
|
265
|
+
raise "@deploy must be present" if deploy.nil?
|
266
|
+
end
|
267
|
+
|
268
|
+
def git_sha
|
269
|
+
@git_sha ||= Aerosol::Util.git_sha
|
270
|
+
end
|
271
|
+
|
272
|
+
delegate :ssh, :migration_ssh, :package, :auto_scaling, :stop_command,
|
273
|
+
:live_check, :db_config_path, :instance_live_grace_period,
|
274
|
+
:app_port, :continue_if_stop_app_fails, :stop_app_retries,
|
275
|
+
:is_alive?, :log_files, :tail_logs, :to => :deploy
|
276
|
+
delegate :launch_configuration, :to => :auto_scaling
|
277
|
+
|
278
|
+
private
|
279
|
+
|
280
|
+
def stop_one_app(instance)
|
281
|
+
debug "attempting to stop app on: #{instance.public_hostname}"
|
282
|
+
ssh.with_connection(instance) do |session|
|
283
|
+
session.exec!(stop_command)
|
284
|
+
session.loop
|
285
|
+
end
|
286
|
+
info "successfully stopped app on: #{instance.public_hostname}"
|
287
|
+
true
|
288
|
+
rescue => ex
|
289
|
+
warn "stop app failed on #{instance.public_hostname} due to: #{ex}"
|
290
|
+
false
|
291
|
+
end
|
292
|
+
|
293
|
+
# inspired by: http://stackoverflow.com/questions/3386233/how-to-get-exit-status-with-rubys-netssh-library
|
294
|
+
def ssh_exec!(ssh, command, options = {}, &block)
|
295
|
+
res = { :out => "", :err => "", :exit_status => nil }
|
296
|
+
ssh.open_channel do |channel|
|
297
|
+
if options[:tty]
|
298
|
+
channel.request_pty do |ch, success|
|
299
|
+
raise "could not start a pseudo-tty" unless success
|
300
|
+
channel = ch
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
channel.exec(command) do |ch, success|
|
305
|
+
raise "unable to run remote cmd: #{command}" unless success
|
306
|
+
|
307
|
+
channel.on_data do |_, data|
|
308
|
+
block.call(:out, data) unless block.nil?
|
309
|
+
res[:out] << data
|
310
|
+
end
|
311
|
+
channel.on_extended_data do |_, type, data|
|
312
|
+
block.call(:err, data) unless block.nil?
|
313
|
+
res[:err] << data
|
314
|
+
end
|
315
|
+
channel.on_request("exit-status") { |_, data| res[:exit_status] = data.read_long }
|
316
|
+
end
|
317
|
+
end
|
318
|
+
ssh.loop
|
319
|
+
res
|
320
|
+
end
|
321
|
+
|
322
|
+
def random_open_port
|
323
|
+
socket = Socket.new(:INET, :STREAM, 0)
|
324
|
+
socket.bind(Addrinfo.tcp("127.0.0.1", 0))
|
325
|
+
port = socket.local_address.ip_port
|
326
|
+
socket.close
|
327
|
+
port
|
328
|
+
end
|
329
|
+
end
|
data/lib/aerosol/util.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'grit'
|
2
|
+
|
3
|
+
module Aerosol::Util
|
4
|
+
extend self
|
5
|
+
|
6
|
+
def is_tar?(path)
|
7
|
+
if File.size(path) < 262
|
8
|
+
return false
|
9
|
+
end
|
10
|
+
magic = nil
|
11
|
+
File.open(path, "r") do |f|
|
12
|
+
f.read(257)
|
13
|
+
magic = f.read(5)
|
14
|
+
end
|
15
|
+
magic == "ustar"
|
16
|
+
end
|
17
|
+
|
18
|
+
def is_gzip?(path)
|
19
|
+
if File.size(path) < 2
|
20
|
+
return false
|
21
|
+
end
|
22
|
+
magic = nil
|
23
|
+
File.open(path, "r") do |f|
|
24
|
+
magic = f.read(2)
|
25
|
+
end
|
26
|
+
magic = magic.unpack('H*')[0]
|
27
|
+
magic == "1f8b"
|
28
|
+
end
|
29
|
+
|
30
|
+
def strip_heredoc(str)
|
31
|
+
str.gsub(/^#{str[/\A\s*/]}/, '')
|
32
|
+
end
|
33
|
+
|
34
|
+
def git_repo
|
35
|
+
@git_repo ||= Grit::Repo.new('.')
|
36
|
+
end
|
37
|
+
|
38
|
+
def git_sha
|
39
|
+
@git_sha ||= git_repo.git.show.lines.first.chomp.match(/^commit ([a-f0-9]+)$/)[1][0..6] rescue 'unknown'
|
40
|
+
end
|
41
|
+
end
|
data/lib/aerosol.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'fog'
|
2
|
+
require 'dockly/util'
|
3
|
+
|
4
|
+
module Aerosol
|
5
|
+
require 'aerosol/aws'
|
6
|
+
require 'aerosol/util'
|
7
|
+
require 'aerosol/aws_model'
|
8
|
+
require 'aerosol/launch_configuration'
|
9
|
+
require 'aerosol/auto_scaling'
|
10
|
+
require 'aerosol/instance'
|
11
|
+
require 'aerosol/connection'
|
12
|
+
require 'aerosol/deploy'
|
13
|
+
require 'aerosol/env'
|
14
|
+
|
15
|
+
attr_reader :deploy, :instance, :git_sha, :namespace
|
16
|
+
attr_writer :load_file
|
17
|
+
|
18
|
+
LOAD_FILE = 'aerosol.rb'
|
19
|
+
|
20
|
+
def load_file
|
21
|
+
@load_file || LOAD_FILE
|
22
|
+
end
|
23
|
+
|
24
|
+
def inst
|
25
|
+
@instance ||= load_inst
|
26
|
+
end
|
27
|
+
|
28
|
+
def load_inst
|
29
|
+
setup.tap do |state|
|
30
|
+
if File.exists?(load_file)
|
31
|
+
instance_eval(IO.read(load_file), load_file)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def namespace(value = nil)
|
37
|
+
if value.nil?
|
38
|
+
@namespace
|
39
|
+
else
|
40
|
+
@namespace = value
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def setup
|
45
|
+
{
|
46
|
+
:auto_scalings => Aerosol::AutoScaling.instances,
|
47
|
+
:deploys => Aerosol::Deploy.instances,
|
48
|
+
:launch_configurations => Aerosol::LaunchConfiguration.instances,
|
49
|
+
:sshs => Aerosol::Connection.instances,
|
50
|
+
:envs => Aerosol::Env.instances
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
{
|
55
|
+
:auto_scaling => Aerosol::AutoScaling,
|
56
|
+
:deploy => Aerosol::Deploy,
|
57
|
+
:launch_configuration => Aerosol::LaunchConfiguration,
|
58
|
+
:ssh => Aerosol::Connection,
|
59
|
+
:env => Aerosol::Env
|
60
|
+
}.each do |method, klass|
|
61
|
+
define_method(method) do |sym, &block|
|
62
|
+
if block.nil?
|
63
|
+
inst[:"#{method}s"][sym]
|
64
|
+
else
|
65
|
+
klass.new!(:name => sym, &block)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
[:auto_scalings, :deploys, :launch_configurations, :sshs, :envs].each do |method|
|
71
|
+
define_method(method) do
|
72
|
+
inst[method]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
module_function :inst, :load_inst, :setup, :load_file, :load_file=,
|
77
|
+
:auto_scaling, :launch_configuration, :deploy, :ssh, :git_sha,
|
78
|
+
:auto_scalings, :launch_configurations, :deploys, :sshs,
|
79
|
+
:namespace, :env, :envs
|
80
|
+
end
|
81
|
+
|
82
|
+
require 'aerosol/runner'
|
83
|
+
require 'aerosol/rake_task'
|