ec2-rotate-volume-snapshots 0.5.0 → 0.6.0
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.
- 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
|