ec2-rotate-volume-snapshots 0.5.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/VERSION +1 -1
- data/bin/ec2-rotate-volume-snapshots +204 -81
- data/ec2-rotate-volume-snapshots.gemspec +2 -2
- metadata +4 -4
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.6.0
|
@@ -4,94 +4,39 @@ require 'rubygems'
|
|
4
4
|
require 'right_aws'
|
5
5
|
require 'optparse'
|
6
6
|
|
7
|
-
opts = {
|
7
|
+
$opts = {
|
8
8
|
:aws_access_key => ENV["AWS_ACCESS_KEY_ID"],
|
9
9
|
:aws_secret_access_key => ENV["AWS_SECRET_ACCESS_KEY"],
|
10
10
|
:aws_region => 'us-east-1',
|
11
11
|
:pattern => nil,
|
12
|
+
:by_tags => nil,
|
12
13
|
:dry_run => false,
|
13
14
|
:backoff_limit => 0
|
14
15
|
}
|
15
16
|
|
16
|
-
time_periods = {
|
17
|
+
$time_periods = {
|
17
18
|
:hourly => { :seconds => 60 * 60, :format => '%Y-%m-%d-%H', :keep => 0, :keeping => {} },
|
18
19
|
:daily => { :seconds => 24 * 60 * 60, :format => '%Y-%m-%d', :keep => 0, :keeping => {} },
|
19
20
|
:weekly => { :seconds => 7 * 24 * 60 * 60, :format => '%Y-%W', :keep => 0, :keeping => {} },
|
20
21
|
:monthly => { :seconds => 30 * 24 * 60 * 60, :format => '%Y-%m', :keep => 0, :keeping => {} },
|
21
22
|
:yearly => { :seconds => 12 * 30 * 24 * 60 * 60, :format => '%Y', :keep => 0, :keeping => {} },
|
22
23
|
}
|
24
|
+
def backoff()
|
25
|
+
$backoffed = $backoffed + 1
|
23
26
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
end
|
28
|
-
|
29
|
-
o.on("--aws-secret-access-key SECRET_KEY", "AWS Secret Access Key") do |v|
|
30
|
-
opts[:aws_secret_access_key] = v
|
31
|
-
end
|
32
|
-
|
33
|
-
o.on("--aws-region REGION", "AWS Region") do |v|
|
34
|
-
opts[:aws_region] = v
|
35
|
-
end
|
36
|
-
|
37
|
-
o.on("--pattern STRING", "Snapshots without this string in the description will be ignored") do |v|
|
38
|
-
opts[:pattern] = v
|
39
|
-
end
|
40
|
-
|
41
|
-
o.on("--backoff-limit LIMIT", "Backoff and retry when hitting EC2 Request Limit exceptions no more than this many times. Default is 0 (no limit)") do |v|
|
42
|
-
opts[:backoff_limit] = v
|
43
|
-
end
|
44
|
-
|
45
|
-
time_periods.keys.sort { |a, b| time_periods[a][:seconds] <=> time_periods[b][:seconds] }.each do |period|
|
46
|
-
o.on("--keep-#{period} NUMBER", Integer, "Number of #{period} snapshots to keep") do |v|
|
47
|
-
time_periods[period][:keep] = v
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
o.on("--keep-last", "Keep the most recent snapshot, regardless of time-based policy") do |v|
|
52
|
-
opts[:keep_last] = true
|
53
|
-
end
|
54
|
-
|
55
|
-
o.on("--dry-run", "Shows what would happen without doing anything") do |v|
|
56
|
-
opts[:dry_run] = true
|
27
|
+
if $opts[:backoff_limit] > 0 && $opts[backoff_limit] < $backoffed
|
28
|
+
puts "Too many backoff attempts. Sorry it didn't work out."
|
29
|
+
exit 2
|
57
30
|
end
|
58
|
-
end.parse!
|
59
|
-
|
60
|
-
if opts[:aws_access_key].nil? || opts[:aws_secret_access_key].nil?
|
61
|
-
puts "You must specify your Amazon credentials via --aws-access-key and --aws-secret_access-key"
|
62
|
-
exit 1
|
63
|
-
end
|
64
31
|
|
65
|
-
|
66
|
-
puts "
|
67
|
-
|
32
|
+
naptime = rand(60) * $backoffed
|
33
|
+
puts "Backing off for #{naptime} seconds..."
|
34
|
+
sleep naptime
|
68
35
|
end
|
69
|
-
volume_ids = ARGV
|
70
36
|
|
71
|
-
|
72
|
-
puts "A negative backoff limit doesn't make much sense."
|
73
|
-
exit 1
|
74
|
-
end
|
75
|
-
|
76
|
-
backoffed = 0
|
77
|
-
ec2 = RightAws::Ec2.new(opts[:aws_access_key], opts[:aws_secret_access_key], :region => opts[:aws_region])
|
78
|
-
all_snapshots = ec2.describe_snapshots(:filters => { 'volume-id' => volume_ids })
|
79
|
-
|
80
|
-
volume_ids.each do |volume_id|
|
81
|
-
if volume_id !~ /^vol-/
|
82
|
-
# sanity check
|
83
|
-
puts "Invalid volume id: #{volume_id}"
|
84
|
-
exit 1
|
85
|
-
end
|
86
|
-
|
87
|
-
# keep track of how many deletes we've done per throttle
|
88
|
-
deletes = 0
|
89
|
-
|
37
|
+
def rotate_em(snapshots)
|
90
38
|
# poor man's way to get a deep copy of our time_periods definition hash
|
91
|
-
periods = Marshal.load(Marshal.dump(time_periods))
|
92
|
-
|
93
|
-
snapshots_to_keep = {}
|
94
|
-
snapshots = all_snapshots.select {|ss| ss[:aws_volume_id] == volume_id }.sort {|a,b| a[:aws_started_at] <=> b[:aws_started_at] }
|
39
|
+
periods = Marshal.load(Marshal.dump($time_periods))
|
95
40
|
|
96
41
|
snapshots.each do |snapshot|
|
97
42
|
time = Time.parse(snapshot[:aws_started_at])
|
@@ -99,7 +44,7 @@ volume_ids.each do |volume_id|
|
|
99
44
|
description = snapshot[:aws_description]
|
100
45
|
keep_reason = nil
|
101
46
|
|
102
|
-
if opts[:pattern] && description !~ /#{opts[:pattern]}/
|
47
|
+
if $opts[:pattern] && description !~ /#{$opts[:pattern]}/
|
103
48
|
puts " #{time.strftime '%Y-%m-%d %H:%M:%S'} #{snapshot_id} Skipping snapshot with description #{description}"
|
104
49
|
next
|
105
50
|
end
|
@@ -119,7 +64,7 @@ volume_ids.each do |volume_id|
|
|
119
64
|
end
|
120
65
|
end
|
121
66
|
|
122
|
-
if keep_reason.nil? && snapshot == snapshots.last && opts[:keep_last]
|
67
|
+
if keep_reason.nil? && snapshot == snapshots.last && $opts[:keep_last]
|
123
68
|
keep_reason = 'last snapshot'
|
124
69
|
end
|
125
70
|
|
@@ -128,22 +73,13 @@ volume_ids.each do |volume_id|
|
|
128
73
|
else
|
129
74
|
puts " #{time.strftime '%Y-%m-%d %H:%M:%S'} #{snapshot_id} Deleting"
|
130
75
|
begin
|
131
|
-
ec2.delete_snapshot(snapshot_id) unless opts[:dry_run]
|
76
|
+
ec2.delete_snapshot(snapshot_id) unless $opts[:dry_run]
|
132
77
|
rescue RightAws::AwsError => e
|
133
78
|
# If we have been sending too many EC2 requests, then let's backoff a random
|
134
79
|
# amount of time and try again later.
|
135
80
|
if e.errors.kind_of? Array
|
136
81
|
if e.errors[0][0] == 'RequestLimitExceeded'
|
137
|
-
|
138
|
-
|
139
|
-
if opts[:backoff_limit] > 0 && opts[backoff_limit] < backoffed
|
140
|
-
puts "Too many backoff attempts. Sorry it didn't work out."
|
141
|
-
exit 2
|
142
|
-
end
|
143
|
-
|
144
|
-
naptime = rand(60) * backoffed
|
145
|
-
puts "Backing off for #{naptime} seconds..."
|
146
|
-
sleep naptime
|
82
|
+
backoff()
|
147
83
|
retry
|
148
84
|
else
|
149
85
|
raise e
|
@@ -155,3 +91,190 @@ volume_ids.each do |volume_id|
|
|
155
91
|
end
|
156
92
|
end
|
157
93
|
end
|
94
|
+
|
95
|
+
|
96
|
+
def split_tag(hash,v)
|
97
|
+
v.split(',').each do |pair|
|
98
|
+
tag, value = pair.split('=',2)
|
99
|
+
if value.nil?
|
100
|
+
puts "invalid tag=value format"
|
101
|
+
exit 1
|
102
|
+
end
|
103
|
+
hash[tag] = value
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
OptionParser.new do |o|
|
108
|
+
script_name = File.basename($0)
|
109
|
+
o.banner = "Usage: #{script_name} [options] <volume_ids>\nUsage: #{script_name} --by-tags <tag=value,...> [other options]"
|
110
|
+
o.separator ""
|
111
|
+
|
112
|
+
o.on("--aws-access-key ACCESS_KEY", "AWS Access Key") do |v|
|
113
|
+
$opts[:aws_access_key] = v
|
114
|
+
end
|
115
|
+
|
116
|
+
o.on("--aws-secret-access-key SECRET_KEY", "AWS Secret Access Key") do |v|
|
117
|
+
$opts[:aws_secret_access_key] = v
|
118
|
+
end
|
119
|
+
|
120
|
+
o.on("--aws-region REGION", "AWS Region") do |v|
|
121
|
+
$opts[:aws_region] = v
|
122
|
+
end
|
123
|
+
|
124
|
+
o.on("--pattern STRING", "Snapshots without this string in the description will be ignored") do |v|
|
125
|
+
$opts[:pattern] = v
|
126
|
+
end
|
127
|
+
|
128
|
+
o.on("--by-tags TAG=VALUE,TAG=VALUE", "Instead of rotating specific volumes, rotate over all the snapshots having the intersection of all given TAG=VALUE pairs.") do |v|
|
129
|
+
$opts[:by_tags] = {}
|
130
|
+
split_tag($opts[:by_tags],v)
|
131
|
+
end
|
132
|
+
|
133
|
+
o.on("--backoff-limit LIMIT", "Backoff and retry when hitting EC2 Request Limit exceptions no more than this many times. Default is 0 (no limit)") do |v|
|
134
|
+
$opts[:backoff_limit] = v
|
135
|
+
end
|
136
|
+
|
137
|
+
$time_periods.keys.sort { |a, b| $time_periods[a][:seconds] <=> $time_periods[b][:seconds] }.each do |period|
|
138
|
+
o.on("--keep-#{period} NUMBER", Integer, "Number of #{period} snapshots to keep") do |v|
|
139
|
+
$time_periods[period][:keep] = v
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
o.on("--keep-last", "Keep the most recent snapshot, regardless of time-based policy") do |v|
|
144
|
+
$opts[:keep_last] = true
|
145
|
+
end
|
146
|
+
|
147
|
+
o.on("--dry-run", "Shows what would happen without doing anything") do |v|
|
148
|
+
$opts[:dry_run] = true
|
149
|
+
end
|
150
|
+
end.parse!
|
151
|
+
|
152
|
+
if $opts[:aws_access_key].nil? || $opts[:aws_secret_access_key].nil?
|
153
|
+
puts "You must specify your Amazon credentials via --aws-access-key and --aws-secret_access-key"
|
154
|
+
exit 1
|
155
|
+
end
|
156
|
+
|
157
|
+
if ARGV.empty? and $opts[:by_tags].nil?
|
158
|
+
puts "You must provide at least one volume id when not rotating by tags"
|
159
|
+
exit 1
|
160
|
+
end
|
161
|
+
|
162
|
+
if $opts[:by_tags].nil?
|
163
|
+
volume_ids = ARGV
|
164
|
+
|
165
|
+
volume_ids.each do |volume_id|
|
166
|
+
if volume_id !~ /^vol-/
|
167
|
+
# sanity check
|
168
|
+
puts "Invalid volume id: #{volume_id}"
|
169
|
+
exit 1
|
170
|
+
end
|
171
|
+
end
|
172
|
+
else
|
173
|
+
if !ARGV.empty?
|
174
|
+
puts "Ignoring supplied volume_ids because we're rotating by tags."
|
175
|
+
end
|
176
|
+
if $opts[:by_tags].length == 0
|
177
|
+
puts "Rotating by tags but no tags specified? Refusing to rotate all snapshots!"
|
178
|
+
exit 1
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
if $opts[:backoff_limit] < 0
|
183
|
+
puts "A negative backoff limit doesn't make much sense."
|
184
|
+
exit 1
|
185
|
+
end
|
186
|
+
|
187
|
+
$backoffed = 0
|
188
|
+
begin
|
189
|
+
ec2 = RightAws::Ec2.new($opts[:aws_access_key], $opts[:aws_secret_access_key], :region => $opts[:aws_region])
|
190
|
+
rescue RightAws::AwsError => e
|
191
|
+
# If we have been sending too many EC2 requests, then let's backoff a random
|
192
|
+
# amount of time and try again later.
|
193
|
+
if e.errors.kind_of? Array
|
194
|
+
if e.errors[0][0] == 'RequestLimitExceeded'
|
195
|
+
backoff()
|
196
|
+
retry
|
197
|
+
else
|
198
|
+
raise e
|
199
|
+
end
|
200
|
+
else
|
201
|
+
raise e
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
|
206
|
+
all_snapshots = []
|
207
|
+
if $opts[:by_tags]
|
208
|
+
$opts[:by_tags].each do |tag, value|
|
209
|
+
begin
|
210
|
+
these_snapshots = ec2.describe_tags(:filters => {'resource-type'=>"snapshot", 'key'=>tag, 'value'=>value}).map {|t| t[:resource_id]}
|
211
|
+
rescue RightAws::AwsError => e
|
212
|
+
# If we have been sending too many EC2 requests, then let's backoff a random
|
213
|
+
# amount of time and try again later.
|
214
|
+
if e.errors.kind_of? Array
|
215
|
+
if e.errors[0][0] == 'RequestLimitExceeded'
|
216
|
+
backoff()
|
217
|
+
retry
|
218
|
+
else
|
219
|
+
raise e
|
220
|
+
end
|
221
|
+
else
|
222
|
+
raise e
|
223
|
+
end
|
224
|
+
end
|
225
|
+
if these_snapshots.length == 0
|
226
|
+
puts "(tag,value)=(#{tag},#{value}) found no snapshots; nothing to rotate!"
|
227
|
+
exit 0
|
228
|
+
end
|
229
|
+
if all_snapshots.length == 0
|
230
|
+
remaining_snapshots = these_snapshots
|
231
|
+
else
|
232
|
+
remaining_snapshots = all_snapshots & these_snapshots
|
233
|
+
end
|
234
|
+
if remaining_snapshots.length == 0
|
235
|
+
puts "No remaining snapshots after applying (tag,value)=(#{tag},#{value}) filter; nothing to rotate!"
|
236
|
+
exit 0
|
237
|
+
end
|
238
|
+
all_snapshots = remaining_snapshots
|
239
|
+
end
|
240
|
+
|
241
|
+
begin
|
242
|
+
rotate_these = ec2.describe_snapshots(all_snapshots).sort {|a,b| a[:aws_started_at] <=> b[:aws_started_at] }
|
243
|
+
rescue RightAws::AwsError => e
|
244
|
+
# If we have been sending too many EC2 requests, then let's backoff a random
|
245
|
+
# amount of time and try again later.
|
246
|
+
if e.errors.kind_of? Array
|
247
|
+
if e.errors[0][0] == 'RequestLimitExceeded'
|
248
|
+
backoff()
|
249
|
+
retry
|
250
|
+
else
|
251
|
+
raise e
|
252
|
+
end
|
253
|
+
else
|
254
|
+
raise e
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
rotate_em(rotate_these)
|
259
|
+
else
|
260
|
+
begin
|
261
|
+
all_snapshots = ec2.describe_snapshots(:filters => { 'volume-id' => volume_ids })
|
262
|
+
rescue RightAws::AwsError => e
|
263
|
+
# If we have been sending too many EC2 requests, then let's backoff a random
|
264
|
+
# amount of time and try again later.
|
265
|
+
if e.errors.kind_of? Array
|
266
|
+
if e.errors[0][0] == 'RequestLimitExceeded'
|
267
|
+
backoff()
|
268
|
+
retry
|
269
|
+
else
|
270
|
+
raise e
|
271
|
+
end
|
272
|
+
else
|
273
|
+
raise e
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
volume_ids.each do |volume_id|
|
278
|
+
rotate_em(all_snapshots.select {|ss| ss[:aws_volume_id] == volume_id }.sort {|a,b| a[:aws_started_at] <=> b[:aws_started_at] })
|
279
|
+
end
|
280
|
+
end
|
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{ec2-rotate-volume-snapshots}
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.6.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Zach Wily"]
|
12
|
-
s.date = %q{2012-10-
|
12
|
+
s.date = %q{2012-10-30}
|
13
13
|
s.default_executable = %q{ec2-rotate-volume-snapshots}
|
14
14
|
s.description = %q{Provides a simple way to rotate EC2 snapshots with configurable retention periods.}
|
15
15
|
s.email = %q{zach@zwily.com}
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ec2-rotate-volume-snapshots
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 7
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
8
|
+
- 6
|
9
9
|
- 0
|
10
|
-
version: 0.
|
10
|
+
version: 0.6.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Zach Wily
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2012-10-
|
18
|
+
date: 2012-10-30 00:00:00 -06:00
|
19
19
|
default_executable: ec2-rotate-volume-snapshots
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|