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 ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ out/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - '2.0.0'
4
+ - 1.9.3
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in prune_cloudfiles_db_backups.gemspec
4
+ gemspec
5
+
6
+ group :development do
7
+ gem 'parallel'
8
+ end
9
+
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,5 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+ task default: :spec
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
4
+
5
+ require 'prune_cloudfiles_db_backups/cli'
6
+
7
+ PruneCloudfilesDbBackups::CLI.start
@@ -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,3 @@
1
+ module PruneCloudfilesDbBackups
2
+ VERSION = '0.0.1'
3
+ 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