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,115 @@
1
+ require 'aws-sdk'
2
+
3
+ module Automan
4
+ module Mixins
5
+ module AwsCaller
6
+
7
+ def account
8
+ ENV['AWS_ACCOUNT_ID']
9
+ end
10
+
11
+ attr_reader :log_aws_calls
12
+ def log_aws_calls=(value)
13
+ if value == true
14
+ AWS.config(logger: @logger)
15
+ else
16
+ AWS.config(logger: nil)
17
+ end
18
+ @log_aws_calls = value
19
+ end
20
+
21
+ attr_writer :eb
22
+ def eb
23
+ if @eb.nil?
24
+ @eb = AWS::ElasticBeanstalk.new.client
25
+ end
26
+ @eb
27
+ end
28
+
29
+ attr_writer :s3
30
+ def s3
31
+ if @s3.nil?
32
+ @s3 = AWS::S3.new
33
+ end
34
+ @s3
35
+ end
36
+
37
+ S3_PROTO = 's3://'
38
+
39
+ def looks_like_s3_path?(path)
40
+ path.start_with? S3_PROTO
41
+ end
42
+
43
+ def parse_s3_path(path)
44
+ if !looks_like_s3_path? path
45
+ raise ArgumentError, "s3 path must start with '#{S3_PROTO}'"
46
+ end
47
+
48
+ rel_path = path[S3_PROTO.length..-1]
49
+ bucket = rel_path.split('/').first
50
+ key = rel_path.split('/')[1..-1].join('/')
51
+
52
+ return bucket, key
53
+ end
54
+
55
+ def s3_object_exists?(s3_path)
56
+ bucket, key = parse_s3_path s3_path
57
+ s3.buckets[bucket].objects[key].exists?
58
+ end
59
+
60
+ def s3_read(s3_path)
61
+ bucket, key = parse_s3_path s3_path
62
+ s3.buckets[bucket].objects[key].read
63
+ end
64
+
65
+ attr_writer :r53
66
+ def r53
67
+ if @r53.nil?
68
+ @r53 = AWS::Route53.new
69
+ end
70
+ @r53
71
+ end
72
+
73
+ attr_writer :elb
74
+ def elb
75
+ if @elb.nil?
76
+ @elb = AWS::ELB.new
77
+ end
78
+ @elb
79
+ end
80
+
81
+ attr_writer :cfn
82
+ def cfn
83
+ if @cfn.nil?
84
+ @cfn = AWS::CloudFormation.new
85
+ end
86
+ @cfn
87
+ end
88
+
89
+ attr_writer :as
90
+ def as
91
+ if @as.nil?
92
+ @as = AWS::AutoScaling.new
93
+ end
94
+ @as
95
+ end
96
+
97
+ attr_writer :rds
98
+ def rds
99
+ if @rds.nil?
100
+ @rds = AWS::RDS.new
101
+ end
102
+ @rds
103
+ end
104
+
105
+ attr_writer :ec2
106
+ def ec2
107
+ if @ec2.nil?
108
+ @ec2 = AWS::EC2.new
109
+ end
110
+ @ec2
111
+ end
112
+
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,18 @@
1
+ module Automan::Mixins
2
+ module Utils
3
+
4
+ def region_from_az(availability_zone)
5
+ availability_zone[0..-2]
6
+ end
7
+
8
+ class ::String
9
+ def underscore
10
+ self.gsub(/::/, '/').
11
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
12
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
13
+ tr("-", "_").
14
+ downcase
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,7 @@
1
+ module Automan::RDS
2
+ class RequestFailedError < StandardError
3
+ end
4
+
5
+ class DatabaseDoesNotExistError < StandardError
6
+ end
7
+ end
@@ -0,0 +1,244 @@
1
+ require 'automan'
2
+ require 'time'
3
+ require 'wait'
4
+
5
+ module Automan::RDS
6
+ class Snapshot < Automan::Base
7
+ add_option :database,
8
+ :name,
9
+ :environment,
10
+ :prune,
11
+ :wait_for_completion
12
+
13
+ attr_accessor :max_snapshots
14
+
15
+ def initialize(options=nil)
16
+ @prune = true
17
+ @wait_for_completion = false
18
+ super
19
+ @wait = Wait.new({
20
+ delay: 30,
21
+ attempts: 20, # 20 x 30s == 10m
22
+ debug: true,
23
+ rescuer: WaitRescuer.new(),
24
+ logger: @logger
25
+ })
26
+
27
+ if ENV['MAX_SNAPSHOTS'].nil?
28
+ @max_snapshots = 50
29
+ else
30
+ @max_snapshots = ENV['MAX_SNAPSHOTS'].to_i
31
+ end
32
+ end
33
+
34
+ include Automan::Mixins::Utils
35
+
36
+ def find_db
37
+ db = nil
38
+ if !database.nil?
39
+ db = rds.db_instances[database]
40
+ elsif !environment.nil?
41
+ db = find_db_by_environment(environment)
42
+ end
43
+ db
44
+ end
45
+
46
+ def database_available?(database)
47
+ database.db_instance_status == 'available'
48
+ end
49
+
50
+ def wait_until_database_available(database)
51
+ state_wait = Wait.new({
52
+ delay: 10,
53
+ attempts: 20,
54
+ debug: true,
55
+ rescuer: WaitRescuer.new(),
56
+ logger: @logger
57
+ })
58
+
59
+ state_wait.until do
60
+ logger.info "Waiting for database #{database.id} to be available"
61
+ database_available?(database)
62
+ end
63
+ end
64
+
65
+ def snapshot_count
66
+ rds.db_instances[database].snapshots.count
67
+ end
68
+
69
+ def create
70
+ log_options
71
+
72
+ db = find_db
73
+
74
+ if db.nil? || !db.exists?
75
+ raise DatabaseDoesNotExistError, "Database for #{environment} does not exist"
76
+ end
77
+
78
+ myname = name.nil? ? default_snapshot_name(db) : name.dup
79
+
80
+ wait_until_database_available(db)
81
+
82
+ if snapshot_count >= max_snapshots
83
+ logger.info "Too many snapshots (>= #{max_snapshots}), deleting oldest prunable."
84
+ old = nil
85
+ AWS.memoize do
86
+ old = oldest_prunable_snapshot
87
+ end
88
+ logger.info "Deleting #{old.id}"
89
+ old.delete
90
+ end
91
+
92
+ logger.info "Creating snapshot #{myname} for #{db.id}"
93
+ snap = db.create_snapshot(myname)
94
+
95
+ if prune == true
96
+ logger.info "Setting snapshot to be prunable"
97
+ set_prunable(snap)
98
+ end
99
+
100
+ if wait_for_completion == true
101
+ wait.until do
102
+ logger.info "Waiting for snapshot to complete..."
103
+ snapshot_finished?(snap)
104
+ end
105
+ logger.info "Snapshot finished (or timed out)."
106
+ end
107
+ end
108
+
109
+ def snapshot_finished?(snapshot)
110
+ unless snapshot.exists?
111
+ return false
112
+ end
113
+ snapshot.status == "available"
114
+ end
115
+
116
+ def db_environment(db)
117
+ arn = db_arn(db)
118
+ return tags(arn)['Name']
119
+ end
120
+
121
+ def default_snapshot_name(db)
122
+ env = db_environment db
123
+ stime = Time.new.iso8601.gsub(/:/,'-')
124
+
125
+ return env + "-" + stime
126
+ end
127
+
128
+ def find_db_by_environment(environment)
129
+ rds.db_instances.each do |db|
130
+ if db_environment(db) == environment
131
+ return db
132
+ end
133
+ end
134
+ return nil
135
+ end
136
+
137
+ def db_arn(database)
138
+ region = region_from_az(database.availability_zone_name)
139
+ "arn:aws:rds:#{region}:#{account}:db:#{database.id}"
140
+ end
141
+
142
+ def snapshot_arn(snapshot)
143
+ region = region_from_az(snapshot.availability_zone_name)
144
+ "arn:aws:rds:#{region}:#{account}:snapshot:#{snapshot.id}"
145
+ end
146
+
147
+ # tag with CanPrune
148
+ def set_prunable(snapshot)
149
+ opts = {
150
+ resource_name: snapshot_arn(snapshot),
151
+ tags: [ {key: 'CanPrune', value: 'yes'} ]
152
+ }
153
+ response = rds.client.add_tags_to_resource opts
154
+
155
+ unless response.successful?
156
+ raise RequestFailedError "add_tags_to_resource failed: #{response.error}"
157
+ end
158
+ end
159
+
160
+ def delete
161
+ log_options
162
+
163
+ logger.info "Deleting snapshot #{name}"
164
+ rds.db_snapshots[name].delete
165
+ end
166
+
167
+ def tags(arn)
168
+ opts = {
169
+ resource_name: arn
170
+ }
171
+ response = rds.client.list_tags_for_resource opts
172
+
173
+ unless response.successful?
174
+ raise RequestFailedError "list_tags_for_resource failed: #{response.error}"
175
+ end
176
+
177
+ result = {}
178
+ response.data[:tag_list].each do |t|
179
+ result[ t[:key] ] = t[:value]
180
+ end
181
+ result
182
+ end
183
+
184
+ def can_prune?(snapshot)
185
+ tagged_can_prune?(snapshot) && available?(snapshot) && manual?(snapshot)
186
+ end
187
+
188
+ def tagged_can_prune?(snapshot)
189
+ arn = snapshot_arn(snapshot)
190
+ tags(arn)['CanPrune'] == 'yes'
191
+ end
192
+
193
+ def available?(snapshot)
194
+ snapshot.status == 'available'
195
+ end
196
+
197
+ def manual?(snapshot)
198
+ snapshot.snapshot_type == 'manual'
199
+ end
200
+
201
+ # older than a month?
202
+ def too_old?(time)
203
+ time.utc < (Time.now.utc - 60*60*24*30)
204
+ end
205
+
206
+ def get_all_snapshots
207
+ rds.db_snapshots
208
+ end
209
+
210
+ def prunable_snapshots
211
+ snapshots = get_all_snapshots
212
+ snapshots.select { |s| can_prune?(s) }
213
+ end
214
+
215
+ def oldest_prunable_snapshot
216
+ prunable_snapshots.sort_by { |s| s.created_at }.first
217
+ end
218
+
219
+ def prune_snapshots
220
+ logger.info "Pruning old db snapshots"
221
+
222
+ AWS.memoize do
223
+ prunable_snapshots.each do |snapshot|
224
+
225
+ timestamp = snapshot.created_at
226
+ snapshot_name = snapshot.db_snapshot_identifier
227
+
228
+ if too_old?(timestamp)
229
+ logger.info "Deleting #{snapshot_name} because it is too old."
230
+ snapshot.delete
231
+ end
232
+ end
233
+ end
234
+ end
235
+
236
+ def latest
237
+ log_options
238
+ db = find_db
239
+ logger.info "Finding most recent snapshot for #{db.id}"
240
+ s = db.snapshots.sort_by {|i| i.created_at }.last
241
+ logger.info "Most recent snapshot is #{s.id}"
242
+ end
243
+ end
244
+ end
@@ -0,0 +1,25 @@
1
+ require 'automan'
2
+
3
+ module Automan::S3
4
+ class Downloader < Automan::Base
5
+ add_option :localfile, :s3file
6
+
7
+ include Automan::Mixins::Utils
8
+
9
+ def download
10
+ log_options
11
+
12
+ logger.info "uploading #{localfile} to #{s3file}"
13
+
14
+ bucket, key = parse_s3_path s3file
15
+
16
+ File.open(localfile, 'wb') do |file|
17
+ s3.buckets[bucket].objects[key].read do |chunk|
18
+ file.write(chunk)
19
+ end
20
+ end
21
+
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,20 @@
1
+ require 'automan'
2
+
3
+ module Automan::S3
4
+ class Uploader < Automan::Base
5
+ add_option :localfile, :s3file
6
+
7
+ include Automan::Mixins::Utils
8
+
9
+ def upload
10
+ log_options
11
+
12
+ logger.info "uploading #{localfile} to #{s3file}"
13
+
14
+ bucket, key = parse_s3_path s3file
15
+ s3.buckets[bucket].objects[key].write(:file => localfile)
16
+
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,3 @@
1
+ module Automan
2
+ VERSION = "2.1.2"
3
+ end
@@ -0,0 +1,17 @@
1
+ require 'wait'
2
+
3
+ class ::WaitRescuer < Wait::BaseRescuer
4
+ def initialize(exceptions=nil)
5
+ super
6
+ end
7
+ # Logs an exception.
8
+ def log(exception)
9
+ return if @logger.nil?
10
+
11
+ klass = exception.class.name
12
+ # We can omit the message if it's identical to the class name.
13
+ message = exception.message unless exception.message == klass
14
+
15
+ @logger.debug("Rescuer") { "rescued: #{klass}#{": #{message}" if message}" }
16
+ end
17
+ end