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.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rvmrc +1 -0
  4. data/.travis.yml +5 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +29 -0
  8. data/Rakefile +6 -0
  9. data/automan.gemspec +30 -0
  10. data/bin/baker +4 -0
  11. data/bin/mover +4 -0
  12. data/bin/scanner +4 -0
  13. data/bin/snapper +4 -0
  14. data/bin/stacker +4 -0
  15. data/bin/stalker +4 -0
  16. data/lib/automan.rb +27 -0
  17. data/lib/automan/base.rb +64 -0
  18. data/lib/automan/beanstalk/application.rb +74 -0
  19. data/lib/automan/beanstalk/configuration.rb +137 -0
  20. data/lib/automan/beanstalk/deployer.rb +193 -0
  21. data/lib/automan/beanstalk/errors.rb +22 -0
  22. data/lib/automan/beanstalk/package.rb +39 -0
  23. data/lib/automan/beanstalk/router.rb +102 -0
  24. data/lib/automan/beanstalk/terminator.rb +60 -0
  25. data/lib/automan/beanstalk/uploader.rb +58 -0
  26. data/lib/automan/beanstalk/version.rb +100 -0
  27. data/lib/automan/chef/uploader.rb +30 -0
  28. data/lib/automan/cli/baker.rb +63 -0
  29. data/lib/automan/cli/base.rb +14 -0
  30. data/lib/automan/cli/mover.rb +47 -0
  31. data/lib/automan/cli/scanner.rb +24 -0
  32. data/lib/automan/cli/snapper.rb +78 -0
  33. data/lib/automan/cli/stacker.rb +106 -0
  34. data/lib/automan/cli/stalker.rb +279 -0
  35. data/lib/automan/cloudformation/errors.rb +40 -0
  36. data/lib/automan/cloudformation/launcher.rb +196 -0
  37. data/lib/automan/cloudformation/replacer.rb +102 -0
  38. data/lib/automan/cloudformation/terminator.rb +61 -0
  39. data/lib/automan/cloudformation/uploader.rb +57 -0
  40. data/lib/automan/ec2/errors.rb +4 -0
  41. data/lib/automan/ec2/image.rb +137 -0
  42. data/lib/automan/ec2/instance.rb +83 -0
  43. data/lib/automan/mixins/aws_caller.rb +115 -0
  44. data/lib/automan/mixins/utils.rb +18 -0
  45. data/lib/automan/rds/errors.rb +7 -0
  46. data/lib/automan/rds/snapshot.rb +244 -0
  47. data/lib/automan/s3/downloader.rb +25 -0
  48. data/lib/automan/s3/uploader.rb +20 -0
  49. data/lib/automan/version.rb +3 -0
  50. data/lib/automan/wait_rescuer.rb +17 -0
  51. data/spec/beanstalk/application_spec.rb +49 -0
  52. data/spec/beanstalk/configuration_spec.rb +98 -0
  53. data/spec/beanstalk/deployer_spec.rb +162 -0
  54. data/spec/beanstalk/package_spec.rb +9 -0
  55. data/spec/beanstalk/router_spec.rb +65 -0
  56. data/spec/beanstalk/terminator_spec.rb +67 -0
  57. data/spec/beanstalk/uploader_spec.rb +53 -0
  58. data/spec/beanstalk/version_spec.rb +60 -0
  59. data/spec/chef/uploader_spec.rb +9 -0
  60. data/spec/cloudformation/launcher_spec.rb +240 -0
  61. data/spec/cloudformation/replacer_spec.rb +58 -0
  62. data/spec/cloudformation/templates/worker_role.json +337 -0
  63. data/spec/cloudformation/terminator_spec.rb +63 -0
  64. data/spec/cloudformation/uploader_spec.rb +50 -0
  65. data/spec/ec2/image_spec.rb +158 -0
  66. data/spec/ec2/instance_spec.rb +57 -0
  67. data/spec/mixins/aws_caller_spec.rb +39 -0
  68. data/spec/mixins/utils_spec.rb +44 -0
  69. data/spec/rds/snapshot_spec.rb +152 -0
  70. 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,4 @@
1
+ module Automan::Ec2
2
+ class RequestFailedError < StandardError
3
+ end
4
+ 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