automan 2.1.2
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 +17 -0
- data/.rvmrc +1 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +29 -0
- data/Rakefile +6 -0
- data/automan.gemspec +30 -0
- data/bin/baker +4 -0
- data/bin/mover +4 -0
- data/bin/scanner +4 -0
- data/bin/snapper +4 -0
- data/bin/stacker +4 -0
- data/bin/stalker +4 -0
- data/lib/automan.rb +27 -0
- data/lib/automan/base.rb +64 -0
- data/lib/automan/beanstalk/application.rb +74 -0
- data/lib/automan/beanstalk/configuration.rb +137 -0
- data/lib/automan/beanstalk/deployer.rb +193 -0
- data/lib/automan/beanstalk/errors.rb +22 -0
- data/lib/automan/beanstalk/package.rb +39 -0
- data/lib/automan/beanstalk/router.rb +102 -0
- data/lib/automan/beanstalk/terminator.rb +60 -0
- data/lib/automan/beanstalk/uploader.rb +58 -0
- data/lib/automan/beanstalk/version.rb +100 -0
- data/lib/automan/chef/uploader.rb +30 -0
- data/lib/automan/cli/baker.rb +63 -0
- data/lib/automan/cli/base.rb +14 -0
- data/lib/automan/cli/mover.rb +47 -0
- data/lib/automan/cli/scanner.rb +24 -0
- data/lib/automan/cli/snapper.rb +78 -0
- data/lib/automan/cli/stacker.rb +106 -0
- data/lib/automan/cli/stalker.rb +279 -0
- data/lib/automan/cloudformation/errors.rb +40 -0
- data/lib/automan/cloudformation/launcher.rb +196 -0
- data/lib/automan/cloudformation/replacer.rb +102 -0
- data/lib/automan/cloudformation/terminator.rb +61 -0
- data/lib/automan/cloudformation/uploader.rb +57 -0
- data/lib/automan/ec2/errors.rb +4 -0
- data/lib/automan/ec2/image.rb +137 -0
- data/lib/automan/ec2/instance.rb +83 -0
- data/lib/automan/mixins/aws_caller.rb +115 -0
- data/lib/automan/mixins/utils.rb +18 -0
- data/lib/automan/rds/errors.rb +7 -0
- data/lib/automan/rds/snapshot.rb +244 -0
- data/lib/automan/s3/downloader.rb +25 -0
- data/lib/automan/s3/uploader.rb +20 -0
- data/lib/automan/version.rb +3 -0
- data/lib/automan/wait_rescuer.rb +17 -0
- data/spec/beanstalk/application_spec.rb +49 -0
- data/spec/beanstalk/configuration_spec.rb +98 -0
- data/spec/beanstalk/deployer_spec.rb +162 -0
- data/spec/beanstalk/package_spec.rb +9 -0
- data/spec/beanstalk/router_spec.rb +65 -0
- data/spec/beanstalk/terminator_spec.rb +67 -0
- data/spec/beanstalk/uploader_spec.rb +53 -0
- data/spec/beanstalk/version_spec.rb +60 -0
- data/spec/chef/uploader_spec.rb +9 -0
- data/spec/cloudformation/launcher_spec.rb +240 -0
- data/spec/cloudformation/replacer_spec.rb +58 -0
- data/spec/cloudformation/templates/worker_role.json +337 -0
- data/spec/cloudformation/terminator_spec.rb +63 -0
- data/spec/cloudformation/uploader_spec.rb +50 -0
- data/spec/ec2/image_spec.rb +158 -0
- data/spec/ec2/instance_spec.rb +57 -0
- data/spec/mixins/aws_caller_spec.rb +39 -0
- data/spec/mixins/utils_spec.rb +44 -0
- data/spec/rds/snapshot_spec.rb +152 -0
- metadata +278 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
require 'automan'
|
|
2
|
+
require 'wait'
|
|
3
|
+
|
|
4
|
+
module Automan::Cloudformation
|
|
5
|
+
class Replacer < Automan::Base
|
|
6
|
+
add_option :name
|
|
7
|
+
|
|
8
|
+
def initialize(options=nil)
|
|
9
|
+
super
|
|
10
|
+
@wait = Wait.new({
|
|
11
|
+
delay: 60,
|
|
12
|
+
attempts: 20, # 20 x 60s == 20m
|
|
13
|
+
debug: true,
|
|
14
|
+
rescuer: WaitRescuer.new,
|
|
15
|
+
logger: @logger
|
|
16
|
+
})
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# potential states
|
|
20
|
+
# stack does not exist
|
|
21
|
+
# CREATE_IN_PROGRESS | CREATE_FAILED | CREATE_COMPLETE
|
|
22
|
+
# ROLLBACK_IN_PROGRESS | ROLLBACK_FAILED | ROLLBACK_COMPLETE
|
|
23
|
+
# DELETE_IN_PROGRESS | DELETE_FAILED | DELETE_COMPLETE
|
|
24
|
+
# UPDATE_IN_PROGRESS | UPDATE_COMPLETE_CLEANUP_IN_PROGRESS | UPDATE_COMPLETE
|
|
25
|
+
# UPDATE_ROLLBACK_IN_PROGRESS | UPDATE_ROLLBACK_FAILED | UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS | UPDATE_ROLLBACK_COMPLETE
|
|
26
|
+
|
|
27
|
+
# so we want:
|
|
28
|
+
# when CREATE_COMPLETE | UPDATE_COMPLETE then continue
|
|
29
|
+
# when CREATE_FAILED | .*ROLLBACK_.* | ^DELETE_.* then raise error
|
|
30
|
+
# else try again
|
|
31
|
+
#
|
|
32
|
+
def ok_to_replace_instances?(stack_status, last_mod_time)
|
|
33
|
+
case stack_status
|
|
34
|
+
when 'UPDATE_COMPLETE'
|
|
35
|
+
if (Time.now - last_mod_time) <= (5*60)
|
|
36
|
+
true
|
|
37
|
+
else
|
|
38
|
+
false
|
|
39
|
+
end
|
|
40
|
+
when 'CREATE_FAILED', /^.*ROLLBACK.*$/, /^DELETE.*/
|
|
41
|
+
raise StackBrokenError, "Stack is in broken state #{stack_status}"
|
|
42
|
+
else
|
|
43
|
+
false
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def stack_exists?(stack_name)
|
|
48
|
+
cfn.stacks[stack_name].exists?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def stack_asg
|
|
52
|
+
resources = cfn.stacks[name].resources
|
|
53
|
+
asgs = resources.select {|i| i.resource_type == "AWS::AutoScaling::AutoScalingGroup"}
|
|
54
|
+
if asgs.nil? || asgs.empty?
|
|
55
|
+
return nil
|
|
56
|
+
end
|
|
57
|
+
asg_id = asgs.first.physical_resource_id
|
|
58
|
+
as.groups[asg_id]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def stack_was_just_updated?(stack)
|
|
62
|
+
stack.status == 'UPDATE_COMPLETE' && (Time.now - stack.last_updated_time) <= (5*60)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# terminate all running instances in the ASG so that new
|
|
66
|
+
# machines with the latest build will be started
|
|
67
|
+
def replace_instances
|
|
68
|
+
log_options
|
|
69
|
+
|
|
70
|
+
unless stack_exists?(name)
|
|
71
|
+
raise StackDoesNotExistError, "Stack #{name} does not exist."
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
stack = cfn.stacks[name]
|
|
75
|
+
unless stack_was_just_updated?(stack)
|
|
76
|
+
logger.info "stack was not updated recently, not replacing instances."
|
|
77
|
+
return
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
ex = WaitTimedOutError.new "Timed out waiting to replace instances."
|
|
81
|
+
wait_until(ex) do
|
|
82
|
+
ok_to_replace_instances?(stack.status, stack.last_updated_time)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
logger.info "replacing all auto-scaling instances in #{name}"
|
|
86
|
+
|
|
87
|
+
if stack_asg.nil?
|
|
88
|
+
raise MissingAutoScalingGroupError, "No ASG found for stack #{name}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
stack_asg.ec2_instances.each do |i|
|
|
92
|
+
if i.status == :running
|
|
93
|
+
logger.info "terminating instance #{i.id}"
|
|
94
|
+
i.terminate
|
|
95
|
+
else
|
|
96
|
+
logger.info "Not terminating #{i.id} due to status: #{i.status}"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
require 'automan'
|
|
2
|
+
|
|
3
|
+
module Automan::Cloudformation
|
|
4
|
+
class Terminator < Automan::Base
|
|
5
|
+
add_option :name, :wait_for_completion
|
|
6
|
+
|
|
7
|
+
def initialize(options=nil)
|
|
8
|
+
@wait_for_completion = false
|
|
9
|
+
super
|
|
10
|
+
@wait = Wait.new({
|
|
11
|
+
delay: 60,
|
|
12
|
+
attempts: 20, # 20 x 60s == 20m
|
|
13
|
+
debug: true,
|
|
14
|
+
rescuer: WaitRescuer.new(),
|
|
15
|
+
logger: @logger
|
|
16
|
+
})
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def stack_exists?(stack_name)
|
|
20
|
+
cfn.stacks[stack_name].exists?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def delete_stack(stack_name)
|
|
24
|
+
cfn.stacks[stack_name].delete
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def stack_status(stack_name)
|
|
28
|
+
cfn.stacks[stack_name].status
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def stack_deleted?(stack_name)
|
|
32
|
+
return true if !stack_exists?(stack_name)
|
|
33
|
+
|
|
34
|
+
case stack_status(stack_name)
|
|
35
|
+
when 'DELETE_COMPLETE'
|
|
36
|
+
true
|
|
37
|
+
when 'DELETE_FAILED'
|
|
38
|
+
raise StackDeletionError, "#{stack_name} failed to delete"
|
|
39
|
+
else
|
|
40
|
+
false
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def terminate
|
|
45
|
+
log_options
|
|
46
|
+
|
|
47
|
+
if !stack_exists? name
|
|
48
|
+
logger.warn "Stack #{name} does not exist. Doing nothing."
|
|
49
|
+
return
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
logger.info "terminating stack #{name}"
|
|
53
|
+
delete_stack name
|
|
54
|
+
|
|
55
|
+
if wait_for_completion
|
|
56
|
+
logger.info "waiting for stack #{name} to be deleted"
|
|
57
|
+
wait_until { stack_deleted? name }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
require 'automan'
|
|
2
|
+
|
|
3
|
+
module Automan::Cloudformation
|
|
4
|
+
class Uploader < Automan::Base
|
|
5
|
+
add_option :template_files, :s3_path
|
|
6
|
+
|
|
7
|
+
def templates
|
|
8
|
+
Dir.glob(template_files)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def template_valid?(template)
|
|
12
|
+
contents = File.read template
|
|
13
|
+
response = cfn.validate_template(contents)
|
|
14
|
+
if response.has_key?(:code)
|
|
15
|
+
logger.warn "#{template} is invalid: #{response[:message]}"
|
|
16
|
+
return false
|
|
17
|
+
else
|
|
18
|
+
return true
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def all_templates_valid?
|
|
23
|
+
if templates.empty?
|
|
24
|
+
raise NoTemplatesError, "No stack templates found for #{template_files}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
valid = true
|
|
28
|
+
templates.each do |template|
|
|
29
|
+
unless template_valid?(template)
|
|
30
|
+
valid = false
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
valid
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def upload_file(file)
|
|
37
|
+
opts = {
|
|
38
|
+
localfile: file,
|
|
39
|
+
s3file: "#{s3_path}/#{File.basename(file)}"
|
|
40
|
+
}
|
|
41
|
+
s = Automan::S3::Uploader.new opts
|
|
42
|
+
s.upload
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def upload_templates
|
|
46
|
+
log_options
|
|
47
|
+
|
|
48
|
+
unless all_templates_valid?
|
|
49
|
+
raise InvalidTemplateError, "There are invalid templates. Halting upload."
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
templates.each do |template|
|
|
53
|
+
upload_file(template)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
require 'automan/base'
|
|
2
|
+
require 'automan/ec2/errors'
|
|
3
|
+
|
|
4
|
+
module Automan::Ec2
|
|
5
|
+
class Image < Automan::Base
|
|
6
|
+
add_option :instance, :name, :prune
|
|
7
|
+
|
|
8
|
+
include Automan::Mixins::Utils
|
|
9
|
+
|
|
10
|
+
def initialize(options=nil)
|
|
11
|
+
super
|
|
12
|
+
@wait = Wait.new({
|
|
13
|
+
delay: 5,
|
|
14
|
+
attempts: 24, # 24 x 5s == 2m
|
|
15
|
+
debug: true,
|
|
16
|
+
rescuer: WaitRescuer.new,
|
|
17
|
+
logger: @logger
|
|
18
|
+
})
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def find_inst
|
|
22
|
+
inst = nil
|
|
23
|
+
if !instance.nil?
|
|
24
|
+
inst = ec2.instances[instance]
|
|
25
|
+
end
|
|
26
|
+
inst
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def create
|
|
30
|
+
inst = find_inst
|
|
31
|
+
myname = default_image_name
|
|
32
|
+
logger.info "Creating image #{myname} for #{inst.id}"
|
|
33
|
+
newami = ec2.images.create(instance_id: instance, name: myname, no_reboot: true)
|
|
34
|
+
if prune == true
|
|
35
|
+
set_prunable(newami)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def is_more_than_month_old?(mytime)
|
|
40
|
+
if mytime.class == Time && mytime < Time.now.utc - (60*60*24*30)
|
|
41
|
+
true
|
|
42
|
+
else
|
|
43
|
+
false
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def default_image_name
|
|
48
|
+
stime = Time.new.iso8601.gsub(/:/, '-')
|
|
49
|
+
return name + "-" + stime
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def image_snapshot_exists?(image)
|
|
53
|
+
!image_snapshot(image).nil?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def image_snapshot(image)
|
|
57
|
+
image.block_devices.each do |device|
|
|
58
|
+
if !device.nil? &&
|
|
59
|
+
!device[:ebs].nil? &&
|
|
60
|
+
!device[:ebs][:snapshot_id].nil?
|
|
61
|
+
|
|
62
|
+
return device[:ebs][:snapshot_id]
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def set_prunable(newami)
|
|
69
|
+
logger.info "Setting prunable for AMI #{newami.image_id}"
|
|
70
|
+
newami.tags["CanPrune"] = "yes"
|
|
71
|
+
|
|
72
|
+
wait.until do
|
|
73
|
+
logger.info "Waiting for a valid snapshot so we can tag it."
|
|
74
|
+
image_snapshot_exists? newami
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
snapshot = image_snapshot(newami)
|
|
78
|
+
logger.info "Setting prunable for snapshot #{snapshot}"
|
|
79
|
+
ec2.snapshots[snapshot].tags["CanPrune"] = "yes"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def my_images
|
|
83
|
+
ec2.images.with_owner('self')
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def deregister_images(snaplist)
|
|
87
|
+
my_images.each do |image|
|
|
88
|
+
my_snapshot = image_snapshot(image)
|
|
89
|
+
|
|
90
|
+
next unless snaplist.include?(my_snapshot)
|
|
91
|
+
|
|
92
|
+
if image.state != :available
|
|
93
|
+
logger.warn "AMI #{image.id} could not be deleted because its state is #{image.state}"
|
|
94
|
+
next
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
if image.tags["CanPrune"] == "yes"
|
|
98
|
+
logger.info "Deregistering AMI #{image.id}"
|
|
99
|
+
image.delete
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def delete_snapshots(snaplist)
|
|
105
|
+
snaplist.each do |snap|
|
|
106
|
+
if snap.status != :completed
|
|
107
|
+
logger.warn "Snapshot #{snap.id} could not be deleted because its status is #{snap.status}"
|
|
108
|
+
next
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
logger.info "Deleting snapshot #{snap.id}"
|
|
112
|
+
snap.delete
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def prune_amis
|
|
118
|
+
condemned_snaps = []
|
|
119
|
+
allsnapshots = ec2.snapshots.with_owner('self')
|
|
120
|
+
allsnapshots.each do |onesnapshot|
|
|
121
|
+
next unless onesnapshot.status == :completed
|
|
122
|
+
|
|
123
|
+
mycreatetime = onesnapshot.start_time
|
|
124
|
+
if is_more_than_month_old?(mycreatetime) and onesnapshot.tags["CanPrune"] == "yes"
|
|
125
|
+
logger.info "Adding snapshot #{onesnapshot.id} to condemed list"
|
|
126
|
+
condemned_snaps.push onesnapshot
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
unless condemned_snaps.empty?
|
|
131
|
+
deregister_images condemned_snaps.map {|s| s.id }
|
|
132
|
+
delete_snapshots condemned_snaps
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
require 'automan'
|
|
2
|
+
|
|
3
|
+
module Automan::Ec2
|
|
4
|
+
class Instance < Automan::Base
|
|
5
|
+
add_option :environment, :private_key_file
|
|
6
|
+
|
|
7
|
+
def password_data(instance_id)
|
|
8
|
+
opts = { instance_id: instance_id }
|
|
9
|
+
response = ec2.client.get_password_data opts
|
|
10
|
+
|
|
11
|
+
unless response.successful?
|
|
12
|
+
raise RequestFailedError, "get_password_data failed: #{response.error}"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
response.data[:password_data]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def decrypt_password(encrypted, private_key)
|
|
19
|
+
pk = OpenSSL::PKey::RSA.new(private_key)
|
|
20
|
+
begin
|
|
21
|
+
decoded = Base64.decode64(encrypted)
|
|
22
|
+
password = pk.private_decrypt(decoded)
|
|
23
|
+
rescue OpenSSL::PKey::RSAError => e
|
|
24
|
+
logger.warn "Decrypt failed: #{e.message}"
|
|
25
|
+
return nil
|
|
26
|
+
end
|
|
27
|
+
password
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def windows_password(instance_id, private_key)
|
|
31
|
+
begin
|
|
32
|
+
encrypted = password_data(instance_id)
|
|
33
|
+
rescue RequestFailedError => e
|
|
34
|
+
logger.warn e.message
|
|
35
|
+
return nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
if encrypted.nil?
|
|
39
|
+
return nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
decrypt_password(encrypted, private_key)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def windows_name(ip_address)
|
|
46
|
+
return nil if ip_address.nil?
|
|
47
|
+
return nil if ip_address.empty?
|
|
48
|
+
|
|
49
|
+
quads = ip_address.split('.')
|
|
50
|
+
quad_str = quads.map { |q| q.to_i.to_s(16).rjust(2,'0') }.join('')
|
|
51
|
+
"ip-#{quad_str}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def show_env
|
|
55
|
+
data = []
|
|
56
|
+
ec2.instances.with_tag("Name", "*-#{environment}").each do |i|
|
|
57
|
+
next unless i.status == :running
|
|
58
|
+
|
|
59
|
+
tokens = []
|
|
60
|
+
tokens << i.tags.Name
|
|
61
|
+
tokens << i.private_ip_address
|
|
62
|
+
tokens << windows_name(i.private_ip_address)
|
|
63
|
+
if i.platform == "windows"
|
|
64
|
+
tokens << windows_password(i.id, File.read(private_key_file))
|
|
65
|
+
else
|
|
66
|
+
tokens << ""
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
data << tokens
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
return if data.empty?
|
|
73
|
+
|
|
74
|
+
# find longest name tag
|
|
75
|
+
max_name_size = data.max_by {|i| i[0].size }.first.size
|
|
76
|
+
data.map {|i| i[0] = i[0].ljust(max_name_size) }
|
|
77
|
+
|
|
78
|
+
data.each do |i|
|
|
79
|
+
puts i.join("\t")
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|