loe-super-ebs-pruner-9000 1.0.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/.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