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