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 CHANGED
@@ -1 +1 @@
1
- 0.5.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
- OptionParser.new do |o|
25
- o.on("--aws-access-key ACCESS_KEY", "AWS Access Key") do |v|
26
- opts[:aws_access_key] = v
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
- if ARGV.empty?
66
- puts "You must provide at least one volume id with snapshots to rotate"
67
- exit 1
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
- if opts[:backoff_limit] < 0
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
- backoffed = backoffed + 1
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.5.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-03}
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: 11
4
+ hash: 7
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
- - 5
8
+ - 6
9
9
  - 0
10
- version: 0.5.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-03 00:00:00 -06:00
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