backupgem 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,89 @@
1
+ #------------------------------------------------------------------------------
2
+ # Backup Global Settings
3
+ # @author: Nate Murray <nate@natemurray.com>
4
+ # @date: Mon Aug 28 07:28:22 PDT 2006
5
+ #
6
+ # The settings contained in this file will be global for all tasks and can be
7
+ # overridden locally.
8
+ #------------------------------------------------------------------------------
9
+
10
+ # Sepcify sever settings
11
+ set :servers, %w{ localhost }
12
+ set :action_order, %w{ content compress encrypt deliver rotate cleanup }
13
+
14
+ # Name of the SSH user
15
+ set :ssh_user, ENV['USER']
16
+
17
+ # Path to your SSH key
18
+ set :identity_key, ENV['HOME'] + "/.ssh/id_rsa"
19
+
20
+ # Set global actions
21
+ action :compress, :method => :tar_bz2
22
+ action :deliver, :method => :mv # action :deliver, :method => :scp
23
+ action :rotate, :method => :via_mv # action :rotate, :method => :via_ssh
24
+ # action :encrypt, :method => :gpg
25
+
26
+ # Specify a directory that backup can use as a temporary directory
27
+ set :tmp_dir, "/tmp"
28
+
29
+ # Options to be passed to gpg when encrypting
30
+ set :encrypt, false
31
+ set :gpg_encrypt_options, ""
32
+
33
+ # These settings specify the rotation variables
34
+ # Rotation method. Currently the only method is gfs, grandfather-father-son.
35
+ # Read more about that below
36
+ set :rotation_method, :gfs
37
+
38
+ # :mon-sun
39
+ # :last_day_of_the_month # whatever son_promoted on son was, but the last of the month
40
+ # everything else you can define with a Runt object
41
+ # set :son_created_on, :every_day - if you dont want a son created dont run the program
42
+ # a backup is created every time the program is run
43
+
44
+ set :son_promoted_on, :fri
45
+ set :father_promoted_on, :last_fri_of_the_month
46
+
47
+ # more complex
48
+ # mon_wed_fri = Runt::DIWeek.new(Runt::Mon) |
49
+ # Runt::DIWeek.new(Runt::Wed) |
50
+ # Runt::DIWeek.new(Runt::Fri)
51
+ # set :son_promoted_on, mon_wed_fri
52
+
53
+ set :sons_to_keep, 14
54
+ set :fathers_to_keep, 6
55
+ set :grandfathers_to_keep, 6 # 6 months, by default
56
+
57
+
58
+ # -------------------------
59
+ # Standard Actions
60
+ # -------------------------
61
+ action(:tar_bz2) do
62
+ name = c[:tmp_dir] + "/" + File.basename(last_result) + ".tar.bz2"
63
+ sh "tar -cvjf #{name} #{last_result}"
64
+ name
65
+ end
66
+
67
+ action(:scp) do
68
+ # what should the default scp task be?
69
+ # scp the local file to the foreign directory. same name.
70
+ c[:servers].each do |server|
71
+ host = server =~ /localhost/ ? "" : "#{server}:"
72
+ sh "scp #{last_result} #{host}#{c[:backup_path]}/"
73
+ end
74
+ c[:backup_path] + "/" + File.basename(last_result)
75
+ end
76
+
77
+ action(:mv) do
78
+ sh "mv #{last_result} #{c[:backup_path]}/"
79
+ c[:backup_path] + "/" + File.basename(last_result)
80
+ end
81
+
82
+ action(:encrypt) do
83
+ result = last_result
84
+ if c[:encrypt]
85
+ sh "gpg #{c[:gpg_encrypt_options]} --encrypt #{last_result}"
86
+ result = last_result + ".gpg" # ?
87
+ end
88
+ result
89
+ end
@@ -0,0 +1,155 @@
1
+ require "rake"
2
+
3
+ module Backup
4
+ class Rotator
5
+ include FileUtils
6
+
7
+ attr_reader :actor
8
+ attr_reader :configuration
9
+ alias_method :c, :configuration
10
+
11
+ def initialize(actor)
12
+ @actor = actor
13
+ @configuration = actor.configuration
14
+ end
15
+
16
+ # Take the last result and rotate it via mv on the local machine
17
+ def rotate_via_mv(last_result)
18
+ # verify that each of the directories exist, grandfathers, fathers, sons
19
+ hierarchy.each { |m| verify_local_backup_directory_exists(m) }
20
+
21
+ # place todays backup into the specified directory with a timestamp.
22
+ newname = timestamped_prefix(last_result)
23
+ sh "mv #{last_result} #{place_in}/#{newname}"
24
+
25
+ cleanup_via_mv(place_in, how_many_to_keep_today)
26
+ end
27
+
28
+ def rotate_via_ssh(last_result)
29
+ ssh = Backup::SshActor.new(c)
30
+ ssh.connect
31
+ ssh.run "echo \"#{last_result}\""
32
+
33
+ hierarchy.each do |m|
34
+ dir = c[:backup_path] + "/" + m
35
+ ssh.verify_directory_exists(dir)
36
+ end
37
+
38
+ newname = timestamped_prefix(last_result)
39
+ ssh.run "mv #{last_result} #{place_in}/#{newname}"
40
+
41
+ ssh.cleanup_directory(place_in, how_many_to_keep_today)
42
+ ssh.close
43
+ end
44
+
45
+ # TODO
46
+ def rotate_via_ftp(last_result)
47
+ # ftp = Backup::FtpActor.new(c)
48
+ # ftp.connect
49
+ #
50
+ # hierarchy.each do |m|
51
+ # dir = c[:backup_path] + "/" + m
52
+ # ftp.verify_directory_exists(dir)
53
+ # end
54
+ #
55
+ # newname = timestamped_prefix(last_result)
56
+ # ftp.run "mv #{last_result} #{place_in}/#{newname}"
57
+ #
58
+ # ftp.cleanup_directory(place_in, how_many_to_keep_today)
59
+ # ftp.close
60
+ end
61
+
62
+ def create_sons_today?; is_today_a? :son_created_on; end
63
+ def promote_sons_today?; is_today_a? :son_promoted_on; end
64
+ def promote_fathers_today?; is_today_a? :father_promoted_on; end
65
+
66
+ # old ( offset_days % c[:son_promoted_on] ) == 0 ? true : false
67
+ # old ( offset_days % (c[:son_promoted_on] * c[:father_promoted_on]) ) == 0 ? true : false
68
+
69
+ private
70
+ def is_today_a?(symbol)
71
+ t = $test_time || Date.today
72
+ day = DateParser.date_from( c[symbol] )
73
+ day.include?( t )
74
+ end
75
+
76
+ def verify_local_backup_directory_exists(dir)
77
+ path = c[:backup_path]
78
+ full = path + "/" + dir
79
+ unless File.exists?(full)
80
+ sh "mkdir -p #{full}"
81
+ end
82
+ end
83
+
84
+ #def offset_days
85
+ # t = $test_time || Time.now # todo, write a test to use this
86
+ # num_from_day = Time.num_from_day( c[:promote_on] )
87
+ # offset = ( t.days_since_epoch + num_from_day + 0)
88
+ # offset
89
+ #end
90
+
91
+ def cleanup_via_mv(where, num_keep)
92
+
93
+ files = Dir[where + "/*"].sort
94
+ diff = files.size - num_keep
95
+
96
+ 1.upto( diff ) do
97
+ extra = files.shift
98
+ sh "rm #{extra}"
99
+ end
100
+ end
101
+
102
+ def hierarchy
103
+ %w{grandfathers fathers sons}
104
+ end
105
+
106
+ # figure out which generation today's backup is
107
+ public
108
+ def todays_generation
109
+ goes_in = promote_fathers_today? ? "grandfathers" : \
110
+ promote_sons_today? ? "fathers" : "sons"
111
+ end
112
+
113
+ private
114
+ def place_in
115
+ goes_in = todays_generation
116
+ place_in = c[:backup_path] + "/" + goes_in
117
+ end
118
+
119
+
120
+ # Given +name+ returns a timestamped version of name.
121
+ def timestamped_prefix(name)
122
+ newname = Time.now.strftime("%Y-%m-%d-%H-%M-%S_") + File.basename(name)
123
+ end
124
+
125
+ # Returns the number of sons to keep. Looks for config values +:sons_to_keep+,
126
+ # +:son_promoted_on+. Default +14+.
127
+ def sons_to_keep
128
+ c[:sons_to_keep] || c[:son_promoted_on] || 14
129
+ end
130
+
131
+ # Returns the number of fathers to keep. Looks for config values +:fathers_to_keep+,
132
+ # +:fathers_promoted_on+. Default +6+.
133
+ def fathers_to_keep
134
+ c[:fathers_to_keep] || c[:father_promoted_on] || 6
135
+ end
136
+
137
+ # Returns the number of grandfathers to keep. Looks for config values
138
+ # +:grandfathers_to_keep+. Default +6+.
139
+ def gfathers_to_keep
140
+ c[:grandfathers_to_keep] || 6
141
+ end
142
+
143
+ # This method returns the number of how many to keep in whatever today
144
+ # goes in. Example: if today is a day to create a +son+ then this
145
+ # function returns the value of +sons_to_keep+. If today is a +father+
146
+ # then +fathers_to_keep+ etc.
147
+ def how_many_to_keep_today
148
+ goes_in = todays_generation
149
+ keep = goes_in =~ /^sons$/ ? sons_to_keep :
150
+ goes_in =~ /^fathers$/ ? fathers_to_keep :
151
+ goes_in =~ /^grandfathers$/ ? gfathers_to_keep : 14
152
+ end
153
+
154
+ end
155
+ end
@@ -0,0 +1,138 @@
1
+ # large portions borrowed from capistrano
2
+ require 'rubygems'
3
+ require 'net/ssh'
4
+
5
+ # TODO - add in a way to extend the belay script to work with this. thats a
6
+ # good idea for the belay script over all. write the kernal extensions, and the
7
+ # rake extensions. form there write an example third party extension.
8
+ module Backup
9
+ class Command
10
+ attr_reader :command, :options
11
+ attr_reader :actor
12
+
13
+ def initialize(server_name, command, callback, options, actor) #:nodoc:
14
+ @command = command.strip.gsub(/\r?\n/, "\\\n")
15
+ @callback = callback
16
+ @options = options
17
+ @actor = actor
18
+ @channels = open_channels
19
+ end
20
+
21
+ def process!
22
+ since = Time.now
23
+ loop do
24
+ active = 0
25
+ @channels.each do |ch|
26
+ next if ch[:closed]
27
+ active += 1
28
+ ch.connection.process(true)
29
+ end
30
+
31
+ break if active == 0
32
+ if Time.now - since >= 1
33
+ since = Time.now
34
+ @channels.each { |ch| ch.connection.ping! }
35
+ end
36
+ sleep 0.01 # a brief respite, to keep the CPU from going crazy
37
+ end
38
+
39
+ #logger.trace "command finished"
40
+
41
+ if failed = @channels.detect { |ch| ch[:status] != 0 }
42
+ raise "command #{@command.inspect} failed"
43
+ end
44
+
45
+ self
46
+ end
47
+
48
+ def open_channels
49
+ channel = actor.session.open_channel do |channel|
50
+ channel.request_pty( :want_reply => true )
51
+ channel[:actor] = @actor
52
+
53
+ channel.on_success do |ch|
54
+ #logger.trace "executing command", ch[:host]
55
+ ch.exec command
56
+ ch.send_data options[:data] if options[:data]
57
+ end
58
+
59
+ channel.on_data do |ch, data|
60
+ puts data
61
+ @callback[ch, :out, data] if @callback
62
+ end
63
+
64
+ channel.on_failure do |ch|
65
+ #logger.important "could not open channel", ch[:host]
66
+ # puts "we got a faulure"
67
+ ch.close
68
+ end
69
+
70
+ channel.on_request do |ch, request, reply, data|
71
+ ch[:status] = data.read_long if request == "exit-status"
72
+ end
73
+
74
+ channel.on_close do |ch|
75
+ ch[:closed] = true
76
+ end
77
+ end
78
+ [channel]
79
+ end
80
+ end # end Class Command
81
+
82
+ class SshActor
83
+
84
+ #def self.new_for_ssh(server_name)
85
+ # a = new(server_name)
86
+ #end
87
+
88
+ attr_reader :session
89
+ attr_reader :config
90
+ alias_method :c, :config
91
+
92
+ def initialize(config)
93
+ @config = config
94
+ end
95
+
96
+ def connect
97
+ c[:servers].each do |server| # todo, make this actually work
98
+ @session = Net::SSH.start(
99
+ server,
100
+ c[:ssh_user],
101
+ :host_key => "ssh-rsa",
102
+ :keys => [ c[:identity_key] ],
103
+ :auth_methods => %w{ publickey } )
104
+ end
105
+ end
106
+
107
+ def run(cmd, options={}, &block)
108
+ #logger.debug "executing #{cmd.strip.inspect}"
109
+ puts "executing #{cmd.strip.inspect}"
110
+ command = Command.new(@server_name, cmd, block, options, self)
111
+ command.process! # raises an exception if command fails on any server
112
+ end
113
+
114
+ def on_remote(&block)
115
+ connect
116
+ self.instance_eval(&block)
117
+ close
118
+ end
119
+
120
+ def close
121
+ @session.close
122
+ end
123
+
124
+ def verify_directory_exists(dir)
125
+ run "if [ -d '#{dir}' ]; then true; else mkdir -p '#{dir}'; fi"
126
+ end
127
+
128
+ def cleanup_directory(dir, keep)
129
+ puts "Cleaning up"
130
+ cleanup = <<-END
131
+ LOOK_IN="#{dir}"; COUNT=`ls -1 $LOOK_IN | wc -l`; MAX=#{keep}; if (( $COUNT > $MAX )); then let "OFFSET=$COUNT-$MAX"; i=1; for f in `ls -1 $LOOK_IN | sort`; do if (( $i <= $OFFSET )); then CMD="rm $LOOK_IN/$f"; echo $CMD; $CMD; fi; let "i=$i + 1"; done; else true; fi
132
+ END
133
+ run cleanup # todo make it so that even this can be overridden
134
+ end
135
+
136
+ end # end class SshActor
137
+
138
+ end # end Module Backup
data/lib/backup.rb ADDED
@@ -0,0 +1,7 @@
1
+ require 'rubygems'
2
+ require 'runt'
3
+ require 'date'
4
+ require 'backup/configuration'
5
+ require 'backup/extensions'
6
+ require 'backup/ssh_helpers'
7
+ require 'backup/date_parser'
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/ruby
2
+ require File.dirname(__FILE__) + "/tests_helper"
3
+
4
+ class ActorTest < Test::Unit::TestCase
5
+ def setup
6
+ @config = Backup::Configuration.new
7
+ @config.load "standard"
8
+ @actor = @config.actor
9
+ setup_tmp_backup_dir
10
+ end
11
+
12
+ def dont_test_exists
13
+ assert @actor
14
+ end
15
+
16
+ def dont_test_is_file
17
+ dir = create_tmp_files
18
+ config = <<-END
19
+ action :content, :is_file => "#{dir}/1"
20
+ END
21
+ @config.load :string => config
22
+ assert result = @actor.content
23
+ assert File.exists?(result)
24
+ puts "content result is: #{result}"
25
+ @actor.start_process!
26
+ end
27
+
28
+ def dont_test_is_folder
29
+ dir = create_tmp_files
30
+ config = <<-END
31
+ action :content, :is_folder => "#{dir}"
32
+ END
33
+ @config.load :string => config
34
+ assert result = @actor.content
35
+ assert File.exists?(result)
36
+ assert File.directory?(result)
37
+ puts "content result is: #{result}"
38
+ @actor.start_process!
39
+ end
40
+
41
+ def test_is_contents_of
42
+ dir = create_tmp_files
43
+ config = <<-END
44
+ action :content, :is_contents_of => "#{dir}", :copy => true
45
+ END
46
+ @config.load :string => config
47
+ #@actor.content
48
+ #@actor.cleanup
49
+ @actor.start_process!
50
+ end
51
+
52
+ private
53
+ def setup_tmp_backup_dir
54
+ newtmp = @config[:tmp_dir] + "/backup_#{rand}_" + Time.now.strftime("%Y%m%d%H%M%S")
55
+ sh "mkdir #{newtmp}"
56
+ config = <<-END
57
+ set :backup_path, "#{newtmp}"
58
+ END
59
+ @config.load :string => config
60
+ end
61
+
62
+ def create_tmp_files
63
+ newtmp = @config[:tmp_dir] + "/test_#{rand}_" + Time.now.strftime("%Y%m%d%H%M%S")
64
+ sh "mkdir #{newtmp}"
65
+ 0.upto(5) { |i| sh "touch #{newtmp}/#{i}" }
66
+ newtmp
67
+ end
68
+
69
+ end
70
+
data/tests/cleanup.sh ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/bash
2
+ LOOK_IN="/var/local/backups/mediawiki/sons"; COUNT=`ls -1 $LOOK_IN | wc -l`; MAX=14; if (( $COUNT > $MAX )); then let "OFFSET=$COUNT-$MAX"; i=1; for f in `ls -1 $LOOK_IN | sort`; do if (( $i <= $OFFSET )); then CMD="rm $LOOK_IN/$f"; echo $CMD; $CMD; fi; echo "$i $f"; let "i=$i + 1"; done; else true; fi
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/ruby
2
+ require File.dirname(__FILE__) + "/tests_helper"
3
+ require 'runt'
4
+ require 'date'
5
+
6
+ class RotationTest < Test::Unit::TestCase
7
+ def setup
8
+ # setup our config to test the objects
9
+ @config = Backup::Configuration.new
10
+ @config.load "standard"
11
+ @rotator = Backup::Rotator.new(@config.actor)
12
+ end
13
+
14
+ def test_rotator
15
+ t = Date.today
16
+ r = @rotator
17
+ 0.upto(14) do |i|
18
+ $test_time = t
19
+ print t.to_s + t.strftime(" #{i} %a ").to_s
20
+ puts r.todays_generation
21
+ t += 1
22
+ end
23
+ end
24
+
25
+ def test_date_parsing
26
+ dp = Backup::DateParser.new
27
+ assert fri = dp.date_from(:fri)
28
+ assert daily = dp.date_from(:daily)
29
+ assert last = dp.date_from(:last_mon_of_the_month)
30
+ assert_raise(RuntimeError) { dp.date_from(:asdfasdf) }
31
+ end
32
+ end
data/tests/ssh_test.rb ADDED
@@ -0,0 +1,21 @@
1
+ require File.dirname(__FILE__) + "/tests_helper"
2
+
3
+ class SSHTest < Test::Unit::TestCase
4
+ def setup
5
+ @config = Backup::Configuration.new
6
+ @config.load "standard"
7
+ @config.action :deliver, :method => :via_ssh
8
+ @actor = Backup::SshActor.new(@config)
9
+ end
10
+
11
+ def test_exists
12
+ assert @config
13
+ assert @actor
14
+ end
15
+
16
+ def test_on_remote
17
+ @actor.on_remote do
18
+ run "echo \"hello $HOSTNAME\""
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,5 @@
1
+ $:.unshift File.dirname(__FILE__) + "/../lib"
2
+
3
+ require 'test/unit'
4
+ require 'backup'
5
+ require 'backup/extensions'
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.9.0
3
+ specification_version: 1
4
+ name: backupgem
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.0.2
7
+ date: 2006-10-06 00:00:00 -07:00
8
+ summary: Beginning-to-end solution for backups and rotation.
9
+ require_paths:
10
+ - lib
11
+ email: nate@natemurray.com
12
+ homepage: http://tech.natemurray.com/backup
13
+ rubyforge_project:
14
+ description:
15
+ autorequire: backupgem
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ post_install_message:
29
+ authors:
30
+ - Nate Murray
31
+ files:
32
+ - bin/backup
33
+ - bin/commands.sh
34
+ - lib/backup
35
+ - lib/backup.rb
36
+ - lib/backup/actor.rb
37
+ - lib/backup/cli.rb
38
+ - lib/backup/configuration.rb
39
+ - lib/backup/date_parser.rb
40
+ - lib/backup/extensions.rb
41
+ - lib/backup/recipes
42
+ - lib/backup/rotator.rb
43
+ - lib/backup/ssh_helpers.rb
44
+ - lib/backup/recipes/standard.rb
45
+ - tests/actor_test.rb
46
+ - tests/cleanup.sh
47
+ - tests/rotation_test.rb
48
+ - tests/ssh_test.rb
49
+ - tests/tests_helper.rb
50
+ - examples/global.rb
51
+ - examples/mediawiki.rb
52
+ - doc/index.html
53
+ - doc/LICENSE-GPL
54
+ - doc/styles.css
55
+ - README
56
+ - CHANGELOG
57
+ test_files:
58
+ - tests/actor_test.rb
59
+ - tests/rotation_test.rb
60
+ - tests/ssh_test.rb
61
+ rdoc_options: []
62
+
63
+ extra_rdoc_files:
64
+ - README
65
+ - CHANGELOG
66
+ executables: []
67
+
68
+ extensions: []
69
+
70
+ requirements: []
71
+
72
+ dependencies:
73
+ - !ruby/object:Gem::Dependency
74
+ name: rake
75
+ version_requirement:
76
+ version_requirements: !ruby/object:Gem::Version::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: 0.7.1
81
+ version:
82
+ - !ruby/object:Gem::Dependency
83
+ name: runt
84
+ version_requirement:
85
+ version_requirements: !ruby/object:Gem::Version::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: 0.3.0
90
+ version:
91
+ - !ruby/object:Gem::Dependency
92
+ name: net-ssh
93
+ version_requirement:
94
+ version_requirements: !ruby/object:Gem::Version::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: 1.0.9
99
+ version: