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