prune_cloudfiles_db_backups 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +7 -0
- data/README.md +35 -0
- data/Rakefile +5 -0
- data/bin/prune_cloudfiles_db_backups +7 -0
- data/lib/prune_cloudfiles_db_backups/backup.rb +66 -0
- data/lib/prune_cloudfiles_db_backups/cli.rb +43 -0
- data/lib/prune_cloudfiles_db_backups/pruner.rb +49 -0
- data/lib/prune_cloudfiles_db_backups/retention_calculator.rb +86 -0
- data/lib/prune_cloudfiles_db_backups/version.rb +3 -0
- data/prune_cloudfiles_db_backups.gemspec +29 -0
- data/spec/prune_cloudfiles_db_backups/backup_spec.rb +64 -0
- data/spec/prune_cloudfiles_db_backups/cli_spec.rb +7 -0
- data/spec/prune_cloudfiles_db_backups/lists/backup_pile.txt +9854 -0
- data/spec/prune_cloudfiles_db_backups/pruner_spec.rb +60 -0
- data/spec/prune_cloudfiles_db_backups/retention_calculator_spec.rb +70 -0
- data/spec/prune_cloudfiles_db_backups/version_spec.rb +10 -0
- data/spec/spec_helper.rb +17 -0
- data/test-util/populate_dummy_container +45 -0
- metadata +194 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright (C) 2013 SmartReceipt, Inc.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# PruneCfDbBackups
|
2
|
+
|
3
|
+
[![Code Climate](https://codeclimate.com/github/SmartReceipt/prune_cloudfiles_db_backups.png)](https://codeclimate.com/github/SmartReceipt/prune_cloudfiles_db_backups)
|
4
|
+
|
5
|
+
This gem helps prune useless database backups from Cloudfiles.
|
6
|
+
|
7
|
+
The current backup retention defaults enforced by this tool are:
|
8
|
+
|
9
|
+
* Keep hourly files for two weeks.
|
10
|
+
* Keep weekly backups for 8 weeks on the Sundays.
|
11
|
+
* Keep monthy backups for 12 months on the Sundays.
|
12
|
+
|
13
|
+
## Usage
|
14
|
+
|
15
|
+
To see the usage prompt, run:
|
16
|
+
|
17
|
+
prune_cloudfiles_db_backups --help
|
18
|
+
|
19
|
+
The first deletion run, if not run for a year, will take a very long time.
|
20
|
+
|
21
|
+
## Installation
|
22
|
+
|
23
|
+
$ gem install prune_cloudfiles_db_backups
|
24
|
+
|
25
|
+
Or stick it in bundler, a chef recipe, or whatever!
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
|
29
|
+
To see the usage prompt, run:
|
30
|
+
|
31
|
+
prune_cloudfiles_db_backups --help
|
32
|
+
|
33
|
+
## Development Tools
|
34
|
+
|
35
|
+
There is a `populate_dummy_container` ruby script inside the `test-util` folder. Also run it with `-h` to see the help banner. This tool is to aid in copying the object names in a container to a test container for manual integration testing. It does not copy the content and only copies the object names.
|
data/Rakefile
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
module PruneCloudfilesDbBackups
|
4
|
+
class Backup
|
5
|
+
attr_reader :daily, :weekly, :monthly
|
6
|
+
attr_reader :datetime
|
7
|
+
attr_reader :objects
|
8
|
+
attr_reader :name
|
9
|
+
|
10
|
+
# Creates a new instance of backup with a grouped set of related files
|
11
|
+
# @param [Hash] options in the form of a hash
|
12
|
+
# @option options [Set] :objects
|
13
|
+
# @option options [String] :name is optional and will be gleaned from objects if not specified
|
14
|
+
# @option options [DateTime] :date is optional and will be gleaned from objects if not specified
|
15
|
+
# @option options [Boolean] :monthly
|
16
|
+
# @option options [Boolean] :weekly
|
17
|
+
# @option options [Boolean] :daily
|
18
|
+
# @return [Backup]
|
19
|
+
def initialize(options = {})
|
20
|
+
@objects = options[:objects] || (raise ArgumentError, 'Objects not specified')
|
21
|
+
@name = options[:name] || self.class.parse_for_name(@objects)
|
22
|
+
@datetime = options[:datetime] || self.class.parse_for_datetime(@objects)
|
23
|
+
@monthly = options[:monthly]
|
24
|
+
@weekly = options[:weekly]
|
25
|
+
@daily = options[:daily]
|
26
|
+
end
|
27
|
+
|
28
|
+
# @param [Enumerable] objects
|
29
|
+
def self.parse_for_name(objects)
|
30
|
+
objects.first[/(^.+\.pgdump).*/, 1]
|
31
|
+
end
|
32
|
+
|
33
|
+
# @param [Enumerable] objects
|
34
|
+
def self.parse_for_datetime(objects)
|
35
|
+
str_date = objects.first[/\d{14}/]
|
36
|
+
DateTime.strptime(str_date, '%Y%m%d%H%M%S')
|
37
|
+
end
|
38
|
+
|
39
|
+
def dbname
|
40
|
+
@name[/(.+)-\d{14}/, 1]
|
41
|
+
end
|
42
|
+
|
43
|
+
def deletable?
|
44
|
+
!(@daily|@weekly|@monthly)
|
45
|
+
end
|
46
|
+
|
47
|
+
# @param [Backup] other_backup
|
48
|
+
def ==(other_backup)
|
49
|
+
self.objects == other_backup.objects
|
50
|
+
end
|
51
|
+
|
52
|
+
#noinspection RubyResolve
|
53
|
+
def to_s
|
54
|
+
mwd = '['
|
55
|
+
mwd << 'd' if @daily
|
56
|
+
mwd << '-' unless @daily
|
57
|
+
mwd << 'w' if @weekly
|
58
|
+
mwd << '-' unless @weekly
|
59
|
+
mwd << 'm' if @monthly
|
60
|
+
mwd << '-' unless @monthly
|
61
|
+
mwd << ']'
|
62
|
+
|
63
|
+
"#{mwd} #{dbname} #{@datetime.rfc822} (#{objects.size} item#{'s' if @objects.size > 1})"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'slop'
|
2
|
+
require 'prune_cloudfiles_db_backups/pruner'
|
3
|
+
require 'ruby-progressbar'
|
4
|
+
|
5
|
+
|
6
|
+
module PruneCloudfilesDbBackups
|
7
|
+
class CLI
|
8
|
+
def self.start
|
9
|
+
opts = Slop.parse(strict: true, help: true) do
|
10
|
+
banner 'Usage: prune_cloudfiles_db_backups [options]'
|
11
|
+
on :u, :user=, 'Rackspace user to use. Default: ENV["N_RACKSPACE_API_USERNAME"]', default: ENV['N_RACKSPACE_API_USERNAME']
|
12
|
+
on :k, :key=, 'Rackspace key to use. Default: ENV["N_RACKSPACE_API_KEY"]', as: String, default: ENV['N_RACKSPACE_API_KEY']
|
13
|
+
on :authurl=, 'Auth URL Default: https://identity.api.rackspacecloud.com/v1.0', default: 'https://identity.api.rackspacecloud.com/v1.0'
|
14
|
+
on :c, :container=, 'Rackspace container to prune. Default: dummy_container', default: 'dummy_container'
|
15
|
+
on :allow_deletion, 'WARNING: Actually delete files from Rackspace Cloud. Without this option, only a listing of files to be deleted are given.'
|
16
|
+
on :d, :daily, 'Set daily retention amount. Default: 14', default: 14, as: Integer
|
17
|
+
on :w, :weekly, 'Set weekly retention amount. Default: 12', default: 12, as: Integer
|
18
|
+
on :m, :monthly, 'Set monthly retention amount. Default: 8', default: 8, as: Integer
|
19
|
+
end
|
20
|
+
|
21
|
+
pruner = PruneCloudfilesDbBackups::Pruner.new(opts)
|
22
|
+
|
23
|
+
puts pruner.date_sorted_delete_list.map { |backup| "Delete: #{backup.to_s}" }
|
24
|
+
puts pruner.date_sorted_keep_list.map { |backup| "Keep: #{backup.to_s}" }
|
25
|
+
|
26
|
+
if opts[:allow_deletion]
|
27
|
+
puts "Commencing deletion in #{opts[:container]}!"
|
28
|
+
progress = ProgressBar.create(title: 'Objects for deletion deleted',
|
29
|
+
total: pruner.list_to_delete.size,
|
30
|
+
format: '%e %a |%b>%i| %p%% %t'
|
31
|
+
)
|
32
|
+
pruner.delete! do |_|
|
33
|
+
progress.increment
|
34
|
+
end
|
35
|
+
progress.finish
|
36
|
+
puts 'Deletion completed.'
|
37
|
+
else
|
38
|
+
puts "Dry run on #{opts[:container]}. No files deleted. Run with --allow_deletion to commence deletion."
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'openstack'
|
2
|
+
require 'logger'
|
3
|
+
require 'prune_cloudfiles_db_backups/retention_calculator'
|
4
|
+
|
5
|
+
module PruneCloudfilesDbBackups
|
6
|
+
class Pruner
|
7
|
+
attr_reader :list_to_keep, :list_to_delete
|
8
|
+
|
9
|
+
def initialize(opts = {})
|
10
|
+
cf = OpenStack::Connection.create(username: opts[:user],
|
11
|
+
api_key: opts[:key],
|
12
|
+
auth_url: 'https://identity.api.rackspacecloud.com/v1.0',
|
13
|
+
service_type:'object-store')
|
14
|
+
@container = cf.container(opts[:container])
|
15
|
+
|
16
|
+
calc = RetentionCalculator.new(@container.objects,
|
17
|
+
opts[:daily],
|
18
|
+
opts[:weekly],
|
19
|
+
opts[:monthly]
|
20
|
+
)
|
21
|
+
@list_to_delete = calc.list_to_delete
|
22
|
+
@list_to_keep = calc.list_to_keep
|
23
|
+
end
|
24
|
+
|
25
|
+
def delete!(&block)
|
26
|
+
@list_to_delete.map do |backup|
|
27
|
+
if block
|
28
|
+
block.call(backup)
|
29
|
+
end
|
30
|
+
backup.objects.map do |object|
|
31
|
+
begin
|
32
|
+
@container.delete_object(object)
|
33
|
+
rescue OpenStack::Exception::ItemNotFound
|
34
|
+
# ignored
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def date_sorted_keep_list
|
41
|
+
@list_to_keep.sort_by {|a| a.datetime}
|
42
|
+
end
|
43
|
+
|
44
|
+
def date_sorted_delete_list
|
45
|
+
@list_to_delete.sort_by {|a| a.datetime}
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'active_support/time'
|
2
|
+
require 'prune_cloudfiles_db_backups/backup'
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
module PruneCloudfilesDbBackups
|
6
|
+
class RetentionCalculator
|
7
|
+
|
8
|
+
# Initialize the Retention Calculator with a list of cloudfile
|
9
|
+
# container objects
|
10
|
+
# @param [Array[String]] objects from cloudfiles
|
11
|
+
# @param [Integer] day_retention
|
12
|
+
# @param [Integer] week_retention
|
13
|
+
# @param [Integer] month_retention
|
14
|
+
def initialize(objects,
|
15
|
+
day_retention = 14,
|
16
|
+
week_retention = 8,
|
17
|
+
month_retention = 12)
|
18
|
+
@day_retention = day_retention || 14
|
19
|
+
@week_retention = week_retention || 8
|
20
|
+
@month_retention = month_retention || 12
|
21
|
+
|
22
|
+
@now = DateTime.now
|
23
|
+
|
24
|
+
sets = objects.group_by do |o|
|
25
|
+
/(^.+\.pgdump).*/.match(o)[1]
|
26
|
+
end
|
27
|
+
|
28
|
+
@backup_sets = sets.map do |_, v|
|
29
|
+
objects = Set.new(v)
|
30
|
+
datetime = Backup.parse_for_datetime(objects)
|
31
|
+
|
32
|
+
daily = keep_daily_dates.include?(datetime.to_date)
|
33
|
+
weekly = keep_weekly_dates.include?(datetime.to_date)
|
34
|
+
monthly = keep_monthly_dates.include?(datetime.to_date)
|
35
|
+
daily = true if datetime.to_date > @now
|
36
|
+
|
37
|
+
Backup.new(objects: objects,
|
38
|
+
datetime: datetime,
|
39
|
+
daily: daily,
|
40
|
+
weekly: weekly,
|
41
|
+
monthly: monthly)
|
42
|
+
end.to_set
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
# Based off of http://www.infi.nl/blog/view/id/23/Backup_retention_script
|
47
|
+
|
48
|
+
# @return [Set[DateTime]] a calculated array of days to be kept
|
49
|
+
def keep_daily_dates
|
50
|
+
@daily_dates ||= (0...@day_retention).map do |i|
|
51
|
+
@now.at_midnight.advance(days: -i).to_date
|
52
|
+
end.to_set
|
53
|
+
end
|
54
|
+
|
55
|
+
# @return [Set[DateTime]] a calculated array of days to be kept
|
56
|
+
def keep_weekly_dates
|
57
|
+
@weekly_dates ||= (0...@week_retention).map do |i|
|
58
|
+
# Sunday on the last couple of weeks
|
59
|
+
@now.at_beginning_of_week(:sunday).advance(weeks: -i).to_date
|
60
|
+
end.to_set
|
61
|
+
end
|
62
|
+
|
63
|
+
# @return [Set[DateTime]] a calculated array of days to be kept
|
64
|
+
def keep_monthly_dates
|
65
|
+
@monthly_dates ||= (0...@month_retention).map do |i|
|
66
|
+
# First Sunday of every month
|
67
|
+
@now.at_beginning_of_month.advance(months: -i).advance(days: 6).beginning_of_week(:sunday).to_date
|
68
|
+
end.to_set
|
69
|
+
end
|
70
|
+
|
71
|
+
# @return [Set[Backup]] a set of files for deletion
|
72
|
+
def list_to_delete
|
73
|
+
@backup_sets.select do |set|
|
74
|
+
set.deletable?
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# @return [Set[Backup]] a set of files that will be kept
|
79
|
+
def list_to_keep
|
80
|
+
@backup_sets.select do |set|
|
81
|
+
!set.deletable?
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'prune_cloudfiles_db_backups/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'prune_cloudfiles_db_backups'
|
8
|
+
spec.version = PruneCloudfilesDbBackups::VERSION
|
9
|
+
spec.authors = ['Nelson Chen']
|
10
|
+
spec.email = %w(nelson.chen@receipt.com)
|
11
|
+
spec.description = 'Prunes SmartReceipts DB Backups from Cloudfiles'
|
12
|
+
spec.summary = spec.description
|
13
|
+
spec.homepage = 'https://github.com/SmartReceipt/prune_cloudfiles_db_backups'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = %w(lib)
|
20
|
+
|
21
|
+
spec.add_development_dependency 'bundler', '~> 1.3'
|
22
|
+
spec.add_development_dependency 'rake'
|
23
|
+
spec.add_development_dependency 'rspec'
|
24
|
+
|
25
|
+
spec.add_dependency 'openstack', '~> 1.0.9'
|
26
|
+
spec.add_dependency 'slop', '~> 3.4.4'
|
27
|
+
spec.add_dependency 'activesupport', '~> 3.2.13'
|
28
|
+
spec.add_dependency 'ruby-progressbar', '~> 1.0.2'
|
29
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require 'prune_cloudfiles_db_backups/backup'
|
3
|
+
|
4
|
+
module PruneCloudfilesDbBackups
|
5
|
+
describe Backup do
|
6
|
+
before(:each) do
|
7
|
+
@object_pile = %w{
|
8
|
+
reports_production-20120819000003.pgdump
|
9
|
+
reports_production-20120819000003.pgdump.000
|
10
|
+
reports_production-20120819000003.pgdump.001
|
11
|
+
}.to_set
|
12
|
+
@datetime = DateTime.strptime('20120819000003', '%Y%m%d%H%M%S')
|
13
|
+
@name = 'reports_production-20120819000003'
|
14
|
+
end
|
15
|
+
|
16
|
+
describe '#initialize' do
|
17
|
+
it 'is created with a hash' do
|
18
|
+
Backup.new({ objects: @object_pile,
|
19
|
+
name: @name,
|
20
|
+
datetime: @datetime,
|
21
|
+
monthly: false,
|
22
|
+
weekly: false,
|
23
|
+
daily: false
|
24
|
+
})
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe '#deletable?' do
|
29
|
+
it 'returns true if none of the daily, weekly, or monthly flags are
|
30
|
+
false' do
|
31
|
+
backup = Backup.new({ objects: @object_pile,
|
32
|
+
name: @name,
|
33
|
+
datetime: @datetime,
|
34
|
+
monthly: false,
|
35
|
+
weekly: false,
|
36
|
+
daily: false
|
37
|
+
})
|
38
|
+
|
39
|
+
backup.deletable?.should be_true
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'returns false if a certain flag (say, monthly) is set' do
|
43
|
+
backup = Backup.new({ objects: @object_pile,
|
44
|
+
name: @name,
|
45
|
+
datetime: @datetime,
|
46
|
+
monthly: true,
|
47
|
+
weekly: false,
|
48
|
+
daily: false
|
49
|
+
})
|
50
|
+
|
51
|
+
backup.deletable?.should be_false
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe '#==' do
|
56
|
+
it 'allows an equality comparison of backups that checks each field' do
|
57
|
+
one = Backup.new(objects: @object_pile, name: @name, datetime: @datetime)
|
58
|
+
two= Backup.new(objects: @object_pile, name: @name, datetime: @datetime)
|
59
|
+
one.should == two
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|