loe-super-ebs-pruner-9000 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ .DS_Store
2
+ config.yml
data/Rakefile ADDED
@@ -0,0 +1,26 @@
1
+ require 'rake/testtask'
2
+
3
+ begin
4
+ require 'jeweler'
5
+ Jeweler::Tasks.new do |gemspec|
6
+ gemspec.name = "super-ebs-pruner-9000"
7
+ gemspec.summary = "Thins EBS volume snapshots."
8
+ gemspec.description = "Thins EBS volume snapshots."
9
+ gemspec.email = "andrew@andrewloe.com"
10
+ gemspec.homepage = "http://github.com/loe/super-ebs-pruner-9000"
11
+ gemspec.authors = ["W. Andrew Loe III"]
12
+ gemspec.add_dependency('right_aws', '>=1.10.0')
13
+ end
14
+ rescue LoadError
15
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
16
+ end
17
+
18
+ task :default => :test
19
+
20
+ Rake::TestTask.new(:test) do |test|
21
+ test.test_files = FileList.new('test/**/test_*.rb') do |list|
22
+ list.exclude 'test/test_helper.rb'
23
+ end
24
+ test.libs << 'test'
25
+ test.verbose = true
26
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
data/bin/pruner ADDED
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env ruby
2
+ require 'optparse'
3
+ require File.expand_path(File.dirname(__FILE__) + '/../lib/pruner')
4
+
5
+ options = {}
6
+
7
+ OptionParser.new do |opts|
8
+ opts.banner = 'Usage: pruner [options] vol-1 vol-2... providing no volumes will default to all volumes associated with the account.'
9
+
10
+ opts.on('-i', '--aws-id ID', 'AWS ID') do |id|
11
+ options[:aws_id] = id
12
+ end
13
+
14
+ opts.on('-k', '--aws-key KEY', 'AWS Secret Key') do |key|
15
+ options[:aws_key] = key
16
+ end
17
+
18
+ options[:verbose] = true
19
+ opts.on('-v', '--[no-]verbose', 'Run verbosely') do |v|
20
+ options[:verbose] = v
21
+ end
22
+
23
+ options[:live] = false
24
+ opts.on('-l', '--[no-]live', 'Run live') do |l|
25
+ options[:live] = l
26
+ end
27
+
28
+ opts.on_tail('-h', '--help', 'Show this message') do
29
+ puts "Super Pruner 9000 - #{Pruner::VERSION}"
30
+ puts opts
31
+ exit
32
+ end
33
+
34
+ begin
35
+ opts.parse!
36
+
37
+ raise 'You must provide AWS credentials.' unless options[:aws_id] && options[:aws_key]
38
+ rescue Exception => e
39
+ print "ERROR: #{e}\n\n"
40
+ puts opts
41
+ exit
42
+ end
43
+ end
44
+
45
+ options[:volumes] = ARGV
46
+
47
+ Pruner.new(options).prune!
data/lib/pruner.rb ADDED
@@ -0,0 +1,102 @@
1
+ require 'right_aws'
2
+ require 'activesupport'
3
+
4
+ require File.expand_path(File.dirname(__FILE__) + '/pruner/version')
5
+ require File.expand_path(File.dirname(__FILE__) + '/pruner/silence_ssl_warning')
6
+
7
+ class Pruner
8
+ attr_reader :options
9
+ attr_accessor :ec2, :volumes, :all_snapshots, :old_snapshots
10
+
11
+ NOW = Time.now
12
+
13
+ HOURLY_AFTER_A_DAY = {:after => NOW - 1.week,
14
+ :before => NOW - 1.day,
15
+ :interval => 1.hour,
16
+ :name => 'Hourly After A Day'}
17
+ DAILY_AFTER_A_WEEK = {:after => NOW - 1.month,
18
+ :before => NOW - 1.week,
19
+ :interval => 24.hours,
20
+ :name => 'Daily After A Week'}
21
+ EVERY_OTHER_DAY_AFTER_A_MONTH = {:after => NOW - 3.month,
22
+ :before => NOW - 1.month,
23
+ :interval => 48.hours,
24
+ :name => 'Every Other Day After A Month'}
25
+ WEEKLY_AFTER_A_QUARTER = {:after => NOW - 2.years,
26
+ :before => NOW - 3.months,
27
+ :interval => 1.week,
28
+ :name => 'Weekly After A Quarter'}
29
+ EVERY_THREE_WEEKS_AFTER_TWO_YEARS = {:after => NOW - 10.years,
30
+ :before => NOW - 2.years,
31
+ :interval => 3.weeks,
32
+ :name => 'Every Three Weeks After Two Years'}
33
+
34
+ RULES = [HOURLY_AFTER_A_DAY, DAILY_AFTER_A_WEEK, EVERY_OTHER_DAY_AFTER_A_MONTH, WEEKLY_AFTER_A_QUARTER, EVERY_THREE_WEEKS_AFTER_TWO_YEARS]
35
+
36
+ def initialize(options ={})
37
+ @options = options
38
+ @old_snapshots = []
39
+ end
40
+
41
+ def prune!
42
+ apply_rules
43
+ remove_snapshots
44
+ end
45
+
46
+ def apply_rules
47
+ RULES.each do |rule|
48
+ cached_size = old_snapshots.size if options[:verbose]
49
+ apply_rule(rule)
50
+ puts "#{rule[:name]}: #{old_snapshots.size - cached_size}" if options[:verbose]
51
+ end
52
+ end
53
+
54
+ def apply_rule(rule)
55
+ volumes.each do |volume|
56
+
57
+ # Gather the snapshots that might fall in the rule's time window.
58
+ vulnerable_snaps = snapshots(volume).select { |snap| snap[:aws_started_at] > rule[:after] && snap[:aws_started_at] < rule[:before]}
59
+
60
+ # Step across the rule's time window one interval at a time, keeping the last snapshot in that window.
61
+ window_start = rule[:before] - rule[:interval]
62
+ while window_start > rule[:after] do
63
+ # Gather snaps in the window
64
+ snaps_in_window = vulnerable_snaps.select { |snap| snap[:aws_started_at] >= window_start && snap[:aws_started_at] < window_start + rule[:interval]}
65
+ # The first one in the selection survives
66
+ keeper = snaps_in_window.pop
67
+ # Send the rest to die.
68
+ @old_snapshots += snaps_in_window
69
+ # Shrink the window
70
+ window_start -= rule[:interval]
71
+ end
72
+ end
73
+
74
+ old_snapshots
75
+ end
76
+
77
+ def remove_snapshots
78
+ puts "Removing #{old_snapshots.size} Snapshots:" if options[:verbose]
79
+ old_snapshots.each do |snap|
80
+ puts " #{snap[:aws_id]} - #{snap[:aws_started_at]} (#{snap[:aws_volume_id]})" if options[:verbose]
81
+ ec2.delete_snapshot(snap[:aws_id]) if options[:live]
82
+ end
83
+
84
+ old_snapshots
85
+ end
86
+
87
+ def ec2
88
+ @ec2 ||= RightAws::Ec2.new(options[:aws_id], options[:aws_key])
89
+ end
90
+
91
+ def volumes
92
+ @volumes ||= options[:volumes].empty? ? ec2.describe_volumes.map {|vol| vol[:aws_id] } : options[:volumes]
93
+ end
94
+
95
+ def all_snapshots
96
+ @all_snapshots ||= ec2.describe_snapshots
97
+ end
98
+
99
+ def snapshots(volume)
100
+ all_snapshots.select { |snap| volume == snap[:aws_volume_id] }
101
+ end
102
+ end
@@ -0,0 +1,8 @@
1
+ class Net::HTTP
2
+ alias_method :old_initialize, :initialize
3
+ def initialize(*args)
4
+ old_initialize(*args)
5
+ @ssl_context = OpenSSL::SSL::SSLContext.new
6
+ @ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
7
+ end
8
+ end
@@ -0,0 +1,3 @@
1
+ class Pruner
2
+ VERSION = '1.0.0'
3
+ end
@@ -0,0 +1,46 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{super-ebs-pruner-9000}
5
+ s.version = "1.0.0"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["W. Andrew Loe III"]
9
+ s.date = %q{2009-07-24}
10
+ s.default_executable = %q{pruner}
11
+ s.description = %q{Thins EBS volume snapshots.}
12
+ s.email = %q{andrew@andrewloe.com}
13
+ s.executables = ["pruner"]
14
+ s.files = [
15
+ ".gitignore",
16
+ "Rakefile",
17
+ "VERSION",
18
+ "bin/pruner",
19
+ "lib/pruner.rb",
20
+ "lib/pruner/silence_ssl_warning.rb",
21
+ "lib/pruner/version.rb",
22
+ "super-ebs-pruner-9000.gemspec",
23
+ "test/test_pruner.rb"
24
+ ]
25
+ s.homepage = %q{http://github.com/loe/super-ebs-pruner-9000}
26
+ s.rdoc_options = ["--charset=UTF-8"]
27
+ s.require_paths = ["lib"]
28
+ s.rubygems_version = %q{1.3.4}
29
+ s.summary = %q{Thins EBS volume snapshots.}
30
+ s.test_files = [
31
+ "test/test_pruner.rb"
32
+ ]
33
+
34
+ if s.respond_to? :specification_version then
35
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
36
+ s.specification_version = 3
37
+
38
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
39
+ s.add_runtime_dependency(%q<right_aws>, ["= 1.10.0"])
40
+ else
41
+ s.add_dependency(%q<right_aws>, ["= 1.10.0"])
42
+ end
43
+ else
44
+ s.add_dependency(%q<right_aws>, ["= 1.10.0"])
45
+ end
46
+ end
@@ -0,0 +1,205 @@
1
+ require 'test/unit'
2
+ require 'rubygems'
3
+ require 'mocha'
4
+ require 'pruner'
5
+
6
+ class TestPruner < Test::Unit::TestCase
7
+
8
+ NOW = Time.now
9
+
10
+ # From RightAws::Ec2#describe_instances documentation.
11
+ def volumes_array
12
+ [{:aws_id => "vol-foo"},
13
+ {:aws_id => "vol-bar"},
14
+ {:aws_id => "vol-baz"}]
15
+ end
16
+
17
+ # Generate twice hourly for yesterday
18
+ # Should result in 24 deleted snapshots.
19
+ def twice_hourly_yesterday(vol)
20
+ yesterday = NOW - 1.day
21
+ (1..48).map do |i|
22
+ {:aws_progress => "100%",
23
+ :aws_status => "completed",
24
+ :aws_id => "snap-yesterday-#{i}-#{vol}",
25
+ :aws_volume_id => vol,
26
+ :aws_started_at => yesterday - i * 30.minutes}
27
+ end
28
+ end
29
+
30
+ # Generate 2 times daily for last week.
31
+ # Should result in 7 deleted snapshots.
32
+ def twice_daily_last_week(vol)
33
+ last_week = NOW - 1.week
34
+ (1..14).map do |i|
35
+ {:aws_progress => "100%",
36
+ :aws_status => "completed",
37
+ :aws_id => "snap-last-week-#{i}-#{vol}",
38
+ :aws_volume_id => vol,
39
+ :aws_started_at => last_week - i * 12.hours}
40
+ end
41
+ end
42
+
43
+ # Generate daily for last month.
44
+ # Should result in 15 deleted snapshots
45
+ def daily_last_month(vol)
46
+ last_month = NOW - 1.month
47
+ (1..30).map do |i|
48
+ {:aws_progress => "100%",
49
+ :aws_status => "completed",
50
+ :aws_id => "snap-last-month-#{i}-#{vol}",
51
+ :aws_volume_id => vol,
52
+ :aws_started_at => last_month - i * 1.day}
53
+ end
54
+ end
55
+
56
+ # Generate 2 weekly for last quarter.
57
+ # Should result in 6 deleted snapshots.
58
+ def twice_weekly_last_quarter(vol)
59
+ last_quarter = NOW - 3.months
60
+ (1..12).map do |i|
61
+ {:aws_progress => "100%",
62
+ :aws_status => "completed",
63
+ :aws_id => "snap-last-quarter-#{i}-#{vol}",
64
+ :aws_volume_id => vol,
65
+ :aws_started_at => last_quarter - i * 3.days}
66
+ end
67
+ end
68
+
69
+ # Generate weekly for last two years.
70
+ # Should result in 16 deleted snapshots.
71
+ def weekly_two_years_ago(vol)
72
+ two_years_ago = NOW - 2.years
73
+ (1..48).map do |i|
74
+ {:aws_progress => "100%",
75
+ :aws_status => "completed",
76
+ :aws_id => "snap-two-years-ago-#{i}-#{vol}",
77
+ :aws_volume_id => vol,
78
+ :aws_started_at => two_years_ago - i * 1.week}
79
+ end
80
+ end
81
+
82
+ # Generate a set that we can prune from with known results.
83
+ # RULES = [HOURLY_AFTER_A_DAY, DAILY_AFTER_A_WEEK, EVERY_OTHER_DAY_AFTER_A_MONTH, WEEKLY_AFTER_A_QUARTER, EVERY_THREE_WEEKS_AFTER_TWO_YEARS]
84
+ def snapshots_array_foo
85
+ twice_hourly_yesterday('vol-foo') + twice_daily_last_week('vol-foo') + daily_last_month('vol-foo') + twice_weekly_last_quarter('vol-foo') + weekly_two_years_ago('vol-foo')
86
+ end
87
+
88
+ def snapshots_array_bar
89
+ twice_hourly_yesterday('vol-bar') + twice_daily_last_week('vol-bar') + daily_last_month('vol-bar') + twice_weekly_last_quarter('vol-bar') + weekly_two_years_ago('vol-bar')
90
+ end
91
+
92
+ def snapshots_array_baz
93
+ twice_hourly_yesterday('vol-baz') + twice_daily_last_week('vol-baz') + daily_last_month('vol-baz') + twice_weekly_last_quarter('vol-baz') + weekly_two_years_ago('vol-baz')
94
+ end
95
+
96
+ def mock_aws
97
+ @mock_ec2 = mock()
98
+ RightAws::Ec2.stubs(:new).returns(@mock_ec2)
99
+ end
100
+
101
+ def new_pruner
102
+ @pruner = Pruner.new({:volumes => [], :verbose => true, :live => true})
103
+ end
104
+
105
+ def setup
106
+ mock_aws
107
+ new_pruner
108
+ end
109
+
110
+ def teardown
111
+ @pruner = nil
112
+ end
113
+
114
+ def test_no_window_overlap
115
+ windows = [twice_hourly_yesterday('vol-foo'), twice_daily_last_week('vol-foo'), daily_last_month('vol-foo'), twice_weekly_last_quarter('vol-foo'), weekly_two_years_ago('vol-foo')]
116
+ windows.each do |window|
117
+ assert_equal window & (windows.flatten - window), []
118
+ end
119
+ end
120
+
121
+ def test_version
122
+ assert_not_nil Pruner::VERSION
123
+ end
124
+
125
+ def test_ec2
126
+ assert_not_nil @pruner.ec2
127
+ end
128
+
129
+ def test_volumes
130
+ @mock_ec2.expects(:describe_volumes).returns(volumes_array).once
131
+ assert_equal @pruner.volumes, ['vol-foo', 'vol-bar', 'vol-baz']
132
+ end
133
+
134
+ def test_snapshots
135
+ @mock_ec2.expects(:describe_snapshots).returns(snapshots_array_foo).once
136
+ assert_equal @pruner.snapshots('vol-foo'), snapshots_array_foo
137
+ end
138
+
139
+ def test_apply_rule_HOURLY_AFTER_A_DAY
140
+ @pruner.volumes = ['vol-foo']
141
+ @mock_ec2.expects(:describe_snapshots).returns(twice_hourly_yesterday('vol-foo'))
142
+ @pruner.apply_rule(Pruner::HOURLY_AFTER_A_DAY)
143
+ assert_equal @pruner.old_snapshots.size, 24
144
+ end
145
+
146
+ def test_apply_rule_DAILY_AFTER_A_WEEK
147
+ @pruner.volumes = ['vol-foo']
148
+ @mock_ec2.expects(:describe_snapshots).returns(twice_daily_last_week('vol-foo'))
149
+ @pruner.apply_rule(Pruner::DAILY_AFTER_A_WEEK)
150
+ assert_equal @pruner.old_snapshots.size, 7
151
+ end
152
+
153
+ def test_apply_rule_EVERY_OTHER_DAY_AFTER_A_MONTH
154
+ @pruner.volumes = ['vol-foo']
155
+ @mock_ec2.expects(:describe_snapshots).returns(daily_last_month('vol-foo'))
156
+ @pruner.apply_rule(Pruner::EVERY_OTHER_DAY_AFTER_A_MONTH)
157
+ assert_equal @pruner.old_snapshots.size, 15
158
+ end
159
+
160
+ def test_apply_rule_WEEKLY_AFTER_A_QUARTER
161
+ @pruner.volumes = ['vol-foo']
162
+ @mock_ec2.expects(:describe_snapshots).returns(twice_weekly_last_quarter('vol-foo'))
163
+ @pruner.apply_rule(Pruner::WEEKLY_AFTER_A_QUARTER)
164
+ assert_equal @pruner.old_snapshots.size, 6
165
+ end
166
+
167
+ def test_apply_rule_EVERY_THREE_WEEKS_AFTER_TWO_YEARS
168
+ @pruner.volumes = ['vol-foo']
169
+ @mock_ec2.expects(:describe_snapshots).returns(weekly_two_years_ago('vol-foo'))
170
+ @pruner.apply_rule(Pruner::EVERY_THREE_WEEKS_AFTER_TWO_YEARS)
171
+ assert_equal @pruner.old_snapshots.size, 32
172
+ end
173
+
174
+ def test_apply_rules
175
+ @pruner.volumes = ['vol-foo']
176
+ @mock_ec2.expects(:describe_snapshots).returns(snapshots_array_foo)
177
+ @pruner.apply_rules
178
+ assert_equal @pruner.old_snapshots.size, 24 + 7 + 15 + 6 + 32
179
+ end
180
+
181
+ def test_remove_snapshots
182
+ dead_snaps = 24 + 7 + 15 + 6 + 32
183
+ @pruner.volumes = ['vol-foo']
184
+ @mock_ec2.expects(:describe_snapshots).returns(snapshots_array_foo)
185
+ @mock_ec2.expects(:delete_snapshot).times(dead_snaps)
186
+ @pruner.apply_rules
187
+ @pruner.remove_snapshots
188
+ end
189
+
190
+ def test_one_volume_prune!
191
+ dead_snaps = 24 + 7 + 15 + 6 + 32
192
+ @mock_ec2.expects(:describe_volumes).returns([{:aws_id => "vol-foo"}]).once
193
+ @mock_ec2.expects(:describe_snapshots).returns(snapshots_array_foo).once
194
+ @mock_ec2.expects(:delete_snapshot).times(dead_snaps)
195
+ @pruner.prune!
196
+ end
197
+
198
+ def test_multiple_volumes_prune!
199
+ dead_snaps = (24 + 7 + 15 + 6 + 32) * 3 # one for each array!
200
+ @mock_ec2.expects(:describe_volumes).returns(volumes_array).once
201
+ @mock_ec2.expects(:describe_snapshots).returns(snapshots_array_foo + snapshots_array_bar + snapshots_array_baz).once
202
+ @mock_ec2.expects(:delete_snapshot).times(dead_snaps)
203
+ @pruner.prune!
204
+ end
205
+ end
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: loe-super-ebs-pruner-9000
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - W. Andrew Loe III
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-07-24 00:00:00 -07:00
13
+ default_executable: pruner
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: right_aws
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - "="
22
+ - !ruby/object:Gem::Version
23
+ version: 1.10.0
24
+ version:
25
+ description: Thins EBS volume snapshots.
26
+ email: andrew@andrewloe.com
27
+ executables:
28
+ - pruner
29
+ extensions: []
30
+
31
+ extra_rdoc_files: []
32
+
33
+ files:
34
+ - .gitignore
35
+ - Rakefile
36
+ - VERSION
37
+ - bin/pruner
38
+ - lib/pruner.rb
39
+ - lib/pruner/silence_ssl_warning.rb
40
+ - lib/pruner/version.rb
41
+ - super-ebs-pruner-9000.gemspec
42
+ - test/test_pruner.rb
43
+ has_rdoc: false
44
+ homepage: http://github.com/loe/super-ebs-pruner-9000
45
+ licenses:
46
+ post_install_message:
47
+ rdoc_options:
48
+ - --charset=UTF-8
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: "0"
56
+ version:
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: "0"
62
+ version:
63
+ requirements: []
64
+
65
+ rubyforge_project:
66
+ rubygems_version: 1.3.5
67
+ signing_key:
68
+ specification_version: 3
69
+ summary: Thins EBS volume snapshots.
70
+ test_files:
71
+ - test/test_pruner.rb