ebs_prune_snapshot 0.0.1
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/.gitignore +18 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.txt +60 -0
- data/Rakefile +7 -0
- data/bin/ebs_prune_snapshot +109 -0
- data/ebs_prune_snapshot.gemspec +19 -0
- data/lib/ebs_prune_snapshot.rb +249 -0
- data/lib/ebs_prune_snapshot/version.rb +3 -0
- data/test/test_ebs_prune_snapshot.rb +164 -0
- metadata +73 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Dusty Doris
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.txt
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
# EbsPruneSnapshot
|
2
|
+
|
3
|
+
Manage your EBS snapshots on a daily, weekly, and monthly rotation schedule.
|
4
|
+
|
5
|
+
Features
|
6
|
+
- Keep at least one snapshot of each volume, regardless of the rotation schedule.
|
7
|
+
- Saves the youngest snapshot of a particular day as a daily snapshot.
|
8
|
+
- Saves the oldest snapshot of any week as a weekly snapshot (Sundays).
|
9
|
+
- Saves the oldest snapshot of any 4 week period as a monthly snapshot (Sunday).
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
|
13
|
+
Add this line to your application's Gemfile:
|
14
|
+
|
15
|
+
gem 'ebs_prune_snapshot'
|
16
|
+
|
17
|
+
And then execute:
|
18
|
+
|
19
|
+
$ bundle
|
20
|
+
|
21
|
+
Or install it yourself as:
|
22
|
+
|
23
|
+
$ gem install ebs_prune_snapshot
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
AWS Access Key and AWS Secret Access Key are required. These options can be
|
28
|
+
provided through the command line or they may be stored in the environmental
|
29
|
+
variables of AMAZON_ACCESS_KEY_ID and AMAZON_SECRET_ACCESS_KEY. Any options
|
30
|
+
provided through the command line will override the environmental variables.
|
31
|
+
|
32
|
+
A daily, weekly, and monthly rotation are also required. The monthly rotation
|
33
|
+
is every 4 weeks, so provide 13 to go back a full year.
|
34
|
+
|
35
|
+
If you only want to see what would happen if you ran the schedule, then pass
|
36
|
+
the --dry-run (-n) argument.
|
37
|
+
|
38
|
+
Usage: ebs_prune_snapshot [options]
|
39
|
+
eg: ebs_snapshot -d 7 -w 12 -m 14
|
40
|
+
|
41
|
+
Required rotation configuration
|
42
|
+
-d, --daily DAILY Number of daily snapshots to keep
|
43
|
+
-w, --weekly WEEKLY Number of weekly snapshots to keep
|
44
|
+
-m, --monthly MONTHLY Number of monthly snapshots to keep
|
45
|
+
|
46
|
+
AWS Identifications, required unless environmental variables set
|
47
|
+
-k, --key KEY AWS Access Key (AMAZON_ACCESS_KEY_ID)
|
48
|
+
-s, --secret SECRET AWS Secret Access Key (AMAZON_SECRET_ACCESS_KEY)
|
49
|
+
|
50
|
+
Informational arguments
|
51
|
+
-n, --dry-run Describe what would be done (SAFE)
|
52
|
+
-h, --help Show Command Help
|
53
|
+
|
54
|
+
## Contributing
|
55
|
+
|
56
|
+
1. Fork it
|
57
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
58
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
59
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
60
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'ebs_prune_snapshot'
|
3
|
+
require 'optparse'
|
4
|
+
|
5
|
+
def send_error(message)
|
6
|
+
puts @opts
|
7
|
+
puts ""
|
8
|
+
puts "ERROR: #{message}"
|
9
|
+
puts ""
|
10
|
+
exit(1)
|
11
|
+
end
|
12
|
+
|
13
|
+
@options = {}
|
14
|
+
@opts = OptionParser.new
|
15
|
+
|
16
|
+
@opts.banner = <<-EOD
|
17
|
+
EbsPruneSnapshot
|
18
|
+
|
19
|
+
Manage your EBS snapshots on a daily, weekly, and monthly rotation schedule.
|
20
|
+
|
21
|
+
AWS Access Key and AWS Secret Access Key are required. These options can be
|
22
|
+
provided through the command line or they may be stored in the environmental
|
23
|
+
variables of AMAZON_ACCESS_KEY_ID and AMAZON_SECRET_ACCESS_KEY. Any options
|
24
|
+
provided through the command line will override the environmental variables.
|
25
|
+
|
26
|
+
A daily, weekly, and monthly rotation are also required. The monthly rotation
|
27
|
+
is every 4 weeks, so provide 13 to go back a full year.
|
28
|
+
|
29
|
+
If you only want to see what would happen if you ran the schedule, then pass
|
30
|
+
the --dry-run (-n) argument.
|
31
|
+
|
32
|
+
Usage: ebs_prune_snapshot [options]
|
33
|
+
eg: ebs_snapshot -d 7 -w 12 -m 13"
|
34
|
+
EOD
|
35
|
+
|
36
|
+
@opts.separator ""
|
37
|
+
@opts.separator "Required rotation configuration"
|
38
|
+
|
39
|
+
@opts.on(
|
40
|
+
"-d", "--daily DAILY", "Number of daily snapshots to keep"
|
41
|
+
) { |value| @options[:daily]= value }
|
42
|
+
|
43
|
+
@opts.on(
|
44
|
+
"-w", "--weekly WEEKLY", "Number of weekly snapshots to keep"
|
45
|
+
) { |value| @options[:weekly] = value }
|
46
|
+
|
47
|
+
@opts.on(
|
48
|
+
'-m', "--monthly MONTHLY", "Number of monthly snapshots to keep"
|
49
|
+
) { |value| @options[:monthly] = value }
|
50
|
+
|
51
|
+
@opts.separator ""
|
52
|
+
@opts.separator "AWS Identifications, required unless environmental variables set"
|
53
|
+
|
54
|
+
@opts.on(
|
55
|
+
"-k", "--key KEY", "AWS Access Key (AMAZON_ACCESS_KEY_ID)"
|
56
|
+
) { |value| @options[:access_key] = value }
|
57
|
+
|
58
|
+
@opts.on(
|
59
|
+
"-s", "--secret SECRET", "AWS Secret Access Key (AMAZON_SECRET_ACCESS_KEY)"
|
60
|
+
) { |value| @options[:secret_key] = value }
|
61
|
+
|
62
|
+
@opts.separator ""
|
63
|
+
@opts.separator "Informational arguments"
|
64
|
+
|
65
|
+
@opts.on(
|
66
|
+
"-n", "--dry-run", "Describe what would be done (DOES NOT delete snapshots)"
|
67
|
+
) { |value| @options[:verbose] = true }
|
68
|
+
|
69
|
+
@opts.on_tail("-h", "--help", "Show Command Help") do
|
70
|
+
puts @opts
|
71
|
+
puts ""
|
72
|
+
exit(1)
|
73
|
+
end
|
74
|
+
|
75
|
+
begin
|
76
|
+
@opts.parse!
|
77
|
+
rescue StandardError => e
|
78
|
+
send_error e.message
|
79
|
+
end
|
80
|
+
|
81
|
+
@options[:access_key] ||= ENV['AMAZON_ACCESS_KEY_ID']
|
82
|
+
@options[:secret_key] ||= ENV['AMAZON_SECRET_ACCESS_KEY']
|
83
|
+
|
84
|
+
unless @options[:access_key] && @options[:secret_key]
|
85
|
+
send_error "Missing Arguments: --key and --secret are required
|
86
|
+
NOTE: AMAZON_ACCESS_KEY_ID and AMAZON_SECRET_ACCESS_KEY env variables are accepted"
|
87
|
+
end
|
88
|
+
|
89
|
+
unless @options[:daily] && @options[:weekly] && @options[:monthly]
|
90
|
+
send_error "Missing Arguments: --daily, --weekly and --monthly are required"
|
91
|
+
end
|
92
|
+
|
93
|
+
ebs = EbsPruneSnapshot::Base.new(
|
94
|
+
:aws => {
|
95
|
+
:access_key => @options[:access_key],
|
96
|
+
:secret_key => @options[:secret_key]
|
97
|
+
},
|
98
|
+
:rotation => {
|
99
|
+
:daily => @options[:daily].to_i,
|
100
|
+
:weekly => @options[:weekly].to_i,
|
101
|
+
:monthly => @options[:monthly].to_i
|
102
|
+
}
|
103
|
+
)
|
104
|
+
|
105
|
+
if @options[:verbose]
|
106
|
+
ebs.describe
|
107
|
+
else
|
108
|
+
ebs.prune
|
109
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/ebs_prune_snapshot/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Dusty Doris"]
|
6
|
+
gem.email = ["dusty@doris.name"]
|
7
|
+
gem.description = %q{Prune EBS snapshots}
|
8
|
+
gem.summary = %q{Prune EBS snapshots on a daily, weekly, and monthly rotation}
|
9
|
+
gem.homepage = "https://github.com/dusty/ebs_prune_snapshot"
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "ebs_prune_snapshot"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = EbsPruneSnapshot::VERSION
|
17
|
+
|
18
|
+
gem.add_runtime_dependency('amazon-ec2')
|
19
|
+
end
|
@@ -0,0 +1,249 @@
|
|
1
|
+
require "ebs_prune_snapshot/version"
|
2
|
+
require 'AWS'
|
3
|
+
|
4
|
+
module EbsPruneSnapshot
|
5
|
+
class Base
|
6
|
+
|
7
|
+
attr_reader :rotation, :aws
|
8
|
+
|
9
|
+
def initialize(args={})
|
10
|
+
@aws = Aws.new(args[:aws])
|
11
|
+
@rotation = Rotation.new(args[:rotation])
|
12
|
+
end
|
13
|
+
|
14
|
+
def volumes
|
15
|
+
@volumes ||= begin
|
16
|
+
volumes = {}
|
17
|
+
aws.snapshots.each do |snapshot|
|
18
|
+
_snapshot = Snapshot.new(snapshot)
|
19
|
+
volumes[_snapshot.volume_id] ||= []
|
20
|
+
volumes[_snapshot.volume_id].push(_snapshot)
|
21
|
+
end
|
22
|
+
volumes
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def volume(volume_id)
|
27
|
+
volumes[volume_id]
|
28
|
+
end
|
29
|
+
|
30
|
+
def describe
|
31
|
+
puts rotation
|
32
|
+
volumes.keys.each { |volume_id| describe_volume(volume_id) }
|
33
|
+
end
|
34
|
+
|
35
|
+
def prune
|
36
|
+
volumes.keys.each { |volume_id| prune_volume(volume_id) }
|
37
|
+
end
|
38
|
+
|
39
|
+
def describe_volume(volume_id)
|
40
|
+
analyzer = SnapshotAnalyzer.new(volume(volume_id), rotation)
|
41
|
+
puts "VOLUME: #{volume_id}"
|
42
|
+
analyzer.to_skip.each {|snapshot| puts " SKIP: #{snapshot.snapshot_id} : #{snapshot.date}"}
|
43
|
+
analyzer.to_save.each {|snapshot| puts " KEEP: #{snapshot.snapshot_id} : #{snapshot.date}"}
|
44
|
+
analyzer.to_delete.each {|snapshot| puts " DELE: #{snapshot.snapshot_id} : #{snapshot.date}"}
|
45
|
+
end
|
46
|
+
|
47
|
+
def prune_volume(volume_id)
|
48
|
+
analyzer = SnapshotAnalyzer.new(volume(volume_id), rotation)
|
49
|
+
analyzer.to_delete.each { |snapshot| aws.delete(snapshot) }
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
class Aws
|
56
|
+
|
57
|
+
attr_reader :access_key, :secret_key
|
58
|
+
|
59
|
+
def initialize(args={})
|
60
|
+
args ||= {}
|
61
|
+
@access_key = args[:access_key] || ENV['AMAZON_ACCESS_KEY_ID']
|
62
|
+
@secret_key = args[:secret_key] || ENV['AMAZON_SECRET_ACCESS_KEY']
|
63
|
+
end
|
64
|
+
|
65
|
+
def connection
|
66
|
+
@aws ||= ::AWS::EC2::Base.new(
|
67
|
+
:access_key_id => access_key, :secret_access_key => secret_key
|
68
|
+
)
|
69
|
+
end
|
70
|
+
|
71
|
+
def snapshots
|
72
|
+
snapshots = connection.describe_snapshots(:owner => 'self')['snapshotSet']
|
73
|
+
snapshots ? snapshots["item"] : []
|
74
|
+
end
|
75
|
+
|
76
|
+
def delete(snapshot)
|
77
|
+
connection.delete_snapshot(:snapshot_id => snapshot.snapshot_id)
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
class SnapshotAnalyzer
|
83
|
+
|
84
|
+
attr_reader :snapshots, :rotation, :to_skip
|
85
|
+
|
86
|
+
def initialize(snapshots, rotation)
|
87
|
+
@snapshots = snapshots.sort
|
88
|
+
@rotation = rotation
|
89
|
+
@to_skip = []
|
90
|
+
@to_delete = []
|
91
|
+
@to_save = {}
|
92
|
+
analyze_snapshots
|
93
|
+
end
|
94
|
+
|
95
|
+
def to_save
|
96
|
+
@to_save.empty? ? [snapshots[-1]] : @to_save.values
|
97
|
+
end
|
98
|
+
|
99
|
+
def to_delete
|
100
|
+
snapshots - to_save - to_skip
|
101
|
+
end
|
102
|
+
|
103
|
+
def now
|
104
|
+
@now ||= Time.now.utc
|
105
|
+
end
|
106
|
+
|
107
|
+
protected
|
108
|
+
|
109
|
+
def save_newest_snapshot(snapshot, day)
|
110
|
+
@to_save[day_key(day)] = snapshot
|
111
|
+
end
|
112
|
+
|
113
|
+
def save_oldest_snapshot(snapshot, day)
|
114
|
+
@to_save[day_key(day)] ||= snapshot
|
115
|
+
end
|
116
|
+
|
117
|
+
def skip_snapshot(snapshot)
|
118
|
+
@to_skip.push(snapshot) unless @to_skip.include?(snapshot)
|
119
|
+
end
|
120
|
+
|
121
|
+
def analyze_snapshots
|
122
|
+
snapshots.each do |snapshot|
|
123
|
+
if snapshot.completed?
|
124
|
+
prune_daily(snapshot) || prune_weekly(snapshot) || prune_monthly(snapshot)
|
125
|
+
else
|
126
|
+
skip_snapshot(snapshot)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
to_save.sort!
|
130
|
+
to_skip.sort!
|
131
|
+
to_delete.sort!
|
132
|
+
end
|
133
|
+
|
134
|
+
## Save newest snapshot on a day
|
135
|
+
def prune_daily(snapshot)
|
136
|
+
catch :found do
|
137
|
+
(0..rotation.daily - 1).each do |count|
|
138
|
+
day = in_days(count)
|
139
|
+
if day_key(snapshot.created_at) == day_key(day)
|
140
|
+
save_newest_snapshot(snapshot, day)
|
141
|
+
throw :found, true
|
142
|
+
end
|
143
|
+
end
|
144
|
+
false
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
## Save oldest snapshot on a week
|
149
|
+
def prune_weekly(snapshot)
|
150
|
+
catch :found do
|
151
|
+
(0..rotation.weekly - 1).each do |count|
|
152
|
+
week_beg = beginning_of_week(in_weeks(count))
|
153
|
+
week_end = end_of_week(in_weeks(count))
|
154
|
+
if snapshot.created_at >= week_beg && snapshot.created_at <= week_end
|
155
|
+
save_oldest_snapshot(snapshot, week_beg)
|
156
|
+
throw :found, true
|
157
|
+
end
|
158
|
+
end
|
159
|
+
false
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
## Save oldest snapshot of every 4 weeks
|
164
|
+
def prune_monthly(snapshot)
|
165
|
+
catch :found do
|
166
|
+
(0..rotation.monthly - 1).each do |count|
|
167
|
+
month_beg = beginning_of_week(in_months(count))
|
168
|
+
month_end = end_of_week(in_months(count))
|
169
|
+
if snapshot.created_at >= month_beg && snapshot.created_at <= month_end
|
170
|
+
save_oldest_snapshot(snapshot, month_beg)
|
171
|
+
throw :found, true
|
172
|
+
end
|
173
|
+
end
|
174
|
+
false
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def in_months(months)
|
179
|
+
in_weeks(months * 4)
|
180
|
+
end
|
181
|
+
|
182
|
+
def in_weeks(weeks)
|
183
|
+
in_days(weeks * 7)
|
184
|
+
end
|
185
|
+
|
186
|
+
def in_days(days)
|
187
|
+
now - (86400 * days)
|
188
|
+
end
|
189
|
+
|
190
|
+
def beginning_of_week(date)
|
191
|
+
days_from_sunday = date.wday
|
192
|
+
bow = (date - (86400 * days_from_sunday))
|
193
|
+
Time.utc(bow.year, bow.month, bow.day, 0, 0, 0)
|
194
|
+
end
|
195
|
+
|
196
|
+
def end_of_week(date)
|
197
|
+
days_till_saturday = (6 - date.wday)
|
198
|
+
eow = (date + (86400 * days_till_saturday))
|
199
|
+
Time.utc(eow.year, eow.month, eow.day, 23, 59, 59)
|
200
|
+
end
|
201
|
+
|
202
|
+
def day_key(day)
|
203
|
+
day.strftime("%Y%m%d")
|
204
|
+
end
|
205
|
+
|
206
|
+
end
|
207
|
+
|
208
|
+
class Snapshot
|
209
|
+
|
210
|
+
attr_reader :snapshot_id, :volume_id, :status, :created_at
|
211
|
+
|
212
|
+
def initialize(params={})
|
213
|
+
@snapshot_id = params['snapshotId']
|
214
|
+
@volume_id = params['volumeId']
|
215
|
+
@status = params['status']
|
216
|
+
@created_at = Time.parse(params['startTime']).utc
|
217
|
+
end
|
218
|
+
|
219
|
+
def completed?
|
220
|
+
status == 'completed'
|
221
|
+
end
|
222
|
+
|
223
|
+
def <=>(other)
|
224
|
+
created_at <=> other.created_at
|
225
|
+
end
|
226
|
+
|
227
|
+
def date
|
228
|
+
created_at.strftime("%Y%m%d")
|
229
|
+
end
|
230
|
+
|
231
|
+
end
|
232
|
+
|
233
|
+
class Rotation
|
234
|
+
|
235
|
+
attr_reader :daily, :weekly, :monthly
|
236
|
+
|
237
|
+
def initialize(args={})
|
238
|
+
@daily = args[:daily] ? args[:daily].to_i : 7
|
239
|
+
@weekly = args[:weekly] ? args[:weekly].to_i : 12
|
240
|
+
@monthly = args[:monthly] ? args[:monthly].to_i : 14
|
241
|
+
end
|
242
|
+
|
243
|
+
def to_s
|
244
|
+
"Rotation: Daily #{daily}, Weekly #{weekly}, Monthly #{monthly}"
|
245
|
+
end
|
246
|
+
|
247
|
+
end
|
248
|
+
|
249
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
#require 'time'
|
4
|
+
require 'ebs_prune_snapshot'
|
5
|
+
require 'minitest/autorun'
|
6
|
+
|
7
|
+
##
|
8
|
+
# Generates snapshots that simulate amazon-ec describe snapshots
|
9
|
+
# - 400 daily completed snapshots for 2 different volumes
|
10
|
+
# - 1 daily pending snapshot for 2 volumes
|
11
|
+
# - 1 really old single snapshot for a 3rd volume
|
12
|
+
module EbsPruneSnapshot
|
13
|
+
class Tester
|
14
|
+
|
15
|
+
def self.snapshots
|
16
|
+
@snapshots ||= new.snapshots
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.now
|
20
|
+
Time.utc(2012,5,1)
|
21
|
+
end
|
22
|
+
|
23
|
+
def generate_snapshot(snapshot_id, volume_id, start_time)
|
24
|
+
{
|
25
|
+
"snapshotId" => snapshot_id,
|
26
|
+
"volumeId" => volume_id,
|
27
|
+
"status" => 'completed',
|
28
|
+
"startTime" => start_time.xmlschema,
|
29
|
+
"progress" => '100%',
|
30
|
+
"ownerId" => '00000000',
|
31
|
+
"volumeSize" => '1',
|
32
|
+
"description" => "test #{snapshot_id}"
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
def initialize
|
37
|
+
@volumes = ['vol-000000', 'vol-000001']
|
38
|
+
@now = Tester.now
|
39
|
+
@days = 400
|
40
|
+
end
|
41
|
+
|
42
|
+
def snapshots
|
43
|
+
@items ||= begin
|
44
|
+
items = []
|
45
|
+
count = 0
|
46
|
+
# generate snapshots for each day for the previous 400 days for each volume
|
47
|
+
@volumes[0..1].each do |volume_id|
|
48
|
+
(0..@days - 1).each do |day|
|
49
|
+
snapshot_id = "%06d" % count
|
50
|
+
start_time = @now - (86400 * day)
|
51
|
+
items << generate_snapshot(snapshot_id, volume_id, start_time)
|
52
|
+
count += 1
|
53
|
+
end
|
54
|
+
end
|
55
|
+
# add pending snapshots
|
56
|
+
items << generate_snapshot("111111", @volumes[0], @now - 86400).update("status" => "pending")
|
57
|
+
items << generate_snapshot("222222", @volumes[1], @now - 86400).update("status" => "pending")
|
58
|
+
|
59
|
+
# add 2 really old snapshots under another volume
|
60
|
+
items << generate_snapshot("333333", @volumes[3], @now - 86400 * 7 * 60)
|
61
|
+
items << generate_snapshot("444444", @volumes[3], @now - 86400 * 7 * 90)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Starting on 5/1/12, assuming a snapshot taken every day, with a 7/12/13 rotation
|
66
|
+
def self.expectations
|
67
|
+
day = [
|
68
|
+
Time.utc(2012,5,1), Time.utc(2012,4,30), Time.utc(2012,4,29), Time.utc(2012,4,28), Time.utc(2012,4,27),
|
69
|
+
Time.utc(2012,4,26), Time.utc(2012,4,25)
|
70
|
+
]
|
71
|
+
week = [
|
72
|
+
Time.utc(2012,4,29), Time.utc(2012,4,22), Time.utc(2012,4,15), Time.utc(2012,4,8), Time.utc(2012,4,1),
|
73
|
+
Time.utc(2012,3,25), Time.utc(2012,3,18), Time.utc(2012,3,11), Time.utc(2012,3,4), Time.utc(2012,2,26),
|
74
|
+
Time.utc(2012,2,19), Time.utc(2012,2,12)
|
75
|
+
]
|
76
|
+
month = [
|
77
|
+
Time.utc(2012,4,29), Time.utc(2012,4,1), Time.utc(2012,3,4), Time.utc(2012,2,5), Time.utc(2012,1,8),
|
78
|
+
Time.utc(2011,12,11), Time.utc(2011,11,13), Time.utc(2011,10,16), Time.utc(2011,9,18), Time.utc(2011,8,21),
|
79
|
+
Time.utc(2011,7,24), Time.utc(2011,6,26), Time.utc(2011,5,29), Time.utc(2011,5,1)
|
80
|
+
]
|
81
|
+
(day + week + month).uniq.sort
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
##
|
87
|
+
# Stub snapshots
|
88
|
+
class EbsPruneSnapshot::Aws
|
89
|
+
def snapshots
|
90
|
+
EbsPruneSnapshot::Tester.snapshots
|
91
|
+
end
|
92
|
+
def delete(snapshot)
|
93
|
+
puts "running delete on #{snapshot.snapshot_id}"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
class EbsPruneSnapshot::SnapshotAnalyzer
|
97
|
+
def now
|
98
|
+
EbsPruneSnapshot::Tester.now
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
|
103
|
+
class TestEbsPruneSnapshot < MiniTest::Unit::TestCase
|
104
|
+
def self.ebs
|
105
|
+
@ebs ||= EbsPruneSnapshot::Base.new(:rotation => {:daily => 7, :weekly => 12, :monthly => 14})
|
106
|
+
end
|
107
|
+
|
108
|
+
def setup
|
109
|
+
@ebs = self.class.ebs
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.now
|
113
|
+
Time.utc(2012,5,1)
|
114
|
+
end
|
115
|
+
|
116
|
+
def in_days(days)
|
117
|
+
self.class.now - (86400 * days)
|
118
|
+
end
|
119
|
+
|
120
|
+
def test_sort_by_volume
|
121
|
+
assert_equal self.class.ebs.volumes.count, 3
|
122
|
+
end
|
123
|
+
|
124
|
+
def test_volume_analyzer
|
125
|
+
vol1 = @ebs.volumes[@ebs.volumes.keys[0]]
|
126
|
+
vol2 = @ebs.volumes[@ebs.volumes.keys[1]]
|
127
|
+
[vol1, vol2].each do |snapshots|
|
128
|
+
analyzer = EbsPruneSnapshot::SnapshotAnalyzer.new(snapshots, @ebs.rotation)
|
129
|
+
assert_equal analyzer.to_save.count, 29
|
130
|
+
assert_equal analyzer.to_skip.count, 1
|
131
|
+
assert_equal analyzer.to_delete.count, 371
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def test_safety_mechanism
|
136
|
+
snapshots = @ebs.volumes[@ebs.volumes.keys[2]]
|
137
|
+
analyzer = EbsPruneSnapshot::SnapshotAnalyzer.new(snapshots, @ebs.rotation)
|
138
|
+
assert_equal analyzer.to_save.count, 1
|
139
|
+
assert_equal analyzer.to_skip.count, 0
|
140
|
+
assert_equal analyzer.to_delete.count, 1
|
141
|
+
assert_equal analyzer.to_save.first.snapshot_id, "333333"
|
142
|
+
end
|
143
|
+
|
144
|
+
def test_actual_expected_dates
|
145
|
+
vol1 = @ebs.volumes[@ebs.volumes.keys[0]]
|
146
|
+
vol2 = @ebs.volumes[@ebs.volumes.keys[1]]
|
147
|
+
[vol1, vol2].each do |snapshots|
|
148
|
+
analyzer = EbsPruneSnapshot::SnapshotAnalyzer.new(snapshots, @ebs.rotation)
|
149
|
+
assert_equal analyzer.to_save.map {|s| s.created_at} , EbsPruneSnapshot::Tester.expectations
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def test_describe
|
154
|
+
skip
|
155
|
+
@ebs.describe
|
156
|
+
end
|
157
|
+
|
158
|
+
def test_prune
|
159
|
+
skip
|
160
|
+
@ebs.prune
|
161
|
+
end
|
162
|
+
|
163
|
+
end
|
164
|
+
|
metadata
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ebs_prune_snapshot
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Dusty Doris
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-05-02 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: amazon-ec2
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
description: Prune EBS snapshots
|
31
|
+
email:
|
32
|
+
- dusty@doris.name
|
33
|
+
executables:
|
34
|
+
- ebs_prune_snapshot
|
35
|
+
extensions: []
|
36
|
+
extra_rdoc_files: []
|
37
|
+
files:
|
38
|
+
- .gitignore
|
39
|
+
- Gemfile
|
40
|
+
- LICENSE
|
41
|
+
- README.txt
|
42
|
+
- Rakefile
|
43
|
+
- bin/ebs_prune_snapshot
|
44
|
+
- ebs_prune_snapshot.gemspec
|
45
|
+
- lib/ebs_prune_snapshot.rb
|
46
|
+
- lib/ebs_prune_snapshot/version.rb
|
47
|
+
- test/test_ebs_prune_snapshot.rb
|
48
|
+
homepage: https://github.com/dusty/ebs_prune_snapshot
|
49
|
+
licenses: []
|
50
|
+
post_install_message:
|
51
|
+
rdoc_options: []
|
52
|
+
require_paths:
|
53
|
+
- lib
|
54
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
55
|
+
none: false
|
56
|
+
requirements:
|
57
|
+
- - ! '>='
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
requirements: []
|
67
|
+
rubyforge_project:
|
68
|
+
rubygems_version: 1.8.22
|
69
|
+
signing_key:
|
70
|
+
specification_version: 3
|
71
|
+
summary: Prune EBS snapshots on a daily, weekly, and monthly rotation
|
72
|
+
test_files:
|
73
|
+
- test/test_ebs_prune_snapshot.rb
|