BackupMan 0.1.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/.document +5 -0
- data/.gitignore +23 -0
- data/.yardopts +1 -0
- data/LICENSE +674 -0
- data/README.rdoc +138 -0
- data/Rakefile +49 -0
- data/VERSION +1 -0
- data/bin/backup_man +10 -0
- data/examples/example-host-config +60 -0
- data/lib/backup_man/backup.rb +110 -0
- data/lib/backup_man/backup_man.rb +45 -0
- data/lib/backup_man/cli.rb +101 -0
- data/lib/backup_man/command.rb +74 -0
- data/lib/backup_man/dsl.rb +47 -0
- data/lib/backup_man/log.rb +48 -0
- data/lib/backup_man/mysql.rb +30 -0
- data/lib/backup_man/rsync.rb +24 -0
- data/lib/backup_man/tar.rb +29 -0
- data/lib/backup_man.rb +18 -0
- data/spec/BackupMan_spec.rb +25 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +17 -0
- metadata +107 -0
data/README.rdoc
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
= BackupMan
|
|
2
|
+
|
|
3
|
+
* Homepage: http://github.com/oowoo/backupman
|
|
4
|
+
|
|
5
|
+
== DESCRIPTION:
|
|
6
|
+
|
|
7
|
+
A tool for system administrators to easily configure pull-over-SSH backups.
|
|
8
|
+
Install it on your backup server and pull your backups over SSH from there.
|
|
9
|
+
|
|
10
|
+
== BACKGROUND
|
|
11
|
+
|
|
12
|
+
When you have servers on the Internet or in a DMZ they are kind of "untrusted"
|
|
13
|
+
because they are vulnerable to become hacked/cracked. It is therefore a best
|
|
14
|
+
practice to <b>pull</b> backups from there instead of letting these servers
|
|
15
|
+
push their data to the backup servers. Advantage: When doing SSH-pull you do
|
|
16
|
+
not have to store any kind of authentication information on your "untrusted"
|
|
17
|
+
remote servers.
|
|
18
|
+
|
|
19
|
+
== FEATURES/PROBLEMS:
|
|
20
|
+
|
|
21
|
+
Currently these types of backups are configurable:
|
|
22
|
+
|
|
23
|
+
* tar - the tar becomes compressed on the source and is <b>streamed</b> over SSH to
|
|
24
|
+
the backup server; no tar files are created on the source side.
|
|
25
|
+
|
|
26
|
+
* rsync - changes on the source side become synchronized in a very efficient
|
|
27
|
+
way to a copy on the backup server; you get a 1:1 copy of your data on the
|
|
28
|
+
backup server
|
|
29
|
+
|
|
30
|
+
* mysqldump - this simply does a full dump of all your databases, compresses
|
|
31
|
+
it and streams it over to the backup server
|
|
32
|
+
|
|
33
|
+
== SYNOPSIS:
|
|
34
|
+
|
|
35
|
+
To run a fully configured backup configured in <tt>/etc/backup_man/somehost</tt> run
|
|
36
|
+
|
|
37
|
+
backup_man somehost
|
|
38
|
+
|
|
39
|
+
To run a backup with custom log destination (default is
|
|
40
|
+
<tt>/var/log/backup_man.log</tt>) and with a custom configuration file path
|
|
41
|
+
use
|
|
42
|
+
|
|
43
|
+
backup_man -l /full/path/to/logfile /full/path/to/configfile
|
|
44
|
+
|
|
45
|
+
Full synopsis:
|
|
46
|
+
|
|
47
|
+
Usage: backup_man [options] {configname | configpath}
|
|
48
|
+
|
|
49
|
+
Options are:
|
|
50
|
+
-l, --logpath=PATH Path to the log file.
|
|
51
|
+
This can NOT be configured in the config file.
|
|
52
|
+
Default: /var/log/backup_man.log
|
|
53
|
+
-d, --debug Debug mode.
|
|
54
|
+
Much output on screen and in the log file.
|
|
55
|
+
-h, --help Show this help message.
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
== REQUIREMENTS:
|
|
59
|
+
|
|
60
|
+
* gems: log4r
|
|
61
|
+
|
|
62
|
+
== INSTALL / CONFIGURATION ON THE BACKUP SERVER:
|
|
63
|
+
|
|
64
|
+
* <tt>sudo gem install backup_man</tt>
|
|
65
|
+
* remove the password from your ssh private key, e.g. do +ssh-keygen -p+
|
|
66
|
+
* create an configuration file in <tt>/etc/backup_man</tt>, copy this example and adapt to your needs:
|
|
67
|
+
|
|
68
|
+
############################################################################
|
|
69
|
+
|
|
70
|
+
# = Global settings
|
|
71
|
+
# These settings are valid for all latter backup definitions
|
|
72
|
+
|
|
73
|
+
# the default destination directory; if you do not set this it defaults to
|
|
74
|
+
# /var/backups/backup_man
|
|
75
|
+
DESTDIR = "/Users/srm/Documents/Backups"
|
|
76
|
+
|
|
77
|
+
# where the lockfiles go (defaults to /var/lock/backup_man/_backupname_);
|
|
78
|
+
# backup_man ensures, that each backup task can only run once at a time and
|
|
79
|
+
# uses lockfiles for this
|
|
80
|
+
LOCKDIR = "#{DESTDIR}"
|
|
81
|
+
|
|
82
|
+
# ssh_app defines the call to ssh with command line parameters, e.g. if you
|
|
83
|
+
# want to use a special identity file; SSH_APP defaults to 'ssh'
|
|
84
|
+
SSH_APP='ssh -i /Users/srm/.ssh/id_rsa'
|
|
85
|
+
|
|
86
|
+
############################################################################
|
|
87
|
+
|
|
88
|
+
# = Helper variables
|
|
89
|
+
# These are just used in this file to ease the backup definitions
|
|
90
|
+
|
|
91
|
+
dirs_to_backup = [ "/etc", "/home", "/root", "/srv", "/var/backups",
|
|
92
|
+
"/var/cache/git", "/var/log", "/var/mail", "/var/rails", "/var/svn",
|
|
93
|
+
"/var/www" ]
|
|
94
|
+
|
|
95
|
+
############################################################################
|
|
96
|
+
|
|
97
|
+
# = Backup defintions
|
|
98
|
+
# Currently there are these types of backups:
|
|
99
|
+
# * Tar - as the name says; simple tar of a bunch of directories into a tgz
|
|
100
|
+
# * Rsync - rsync of a bunch of directories
|
|
101
|
+
# * Mysql - does a simple mysqldump of all databases
|
|
102
|
+
#
|
|
103
|
+
# == Common settings
|
|
104
|
+
# * backup: an array of directories to backup
|
|
105
|
+
# * to: the destination directory on the local machine (defaults to DESTDIR)
|
|
106
|
+
# * onlyif: specify a condition (as Ruby code) which is checked before the
|
|
107
|
+
# actual backup run; it must evaluate true to have the backup run - see the
|
|
108
|
+
# homepage for details on how to write conditions
|
|
109
|
+
#
|
|
110
|
+
Tar.new( 'akeso' ) do |b|
|
|
111
|
+
b.backup dirs_to_backup
|
|
112
|
+
# in this example, this job only runs on saturdays and only if the backup
|
|
113
|
+
# does not exist already
|
|
114
|
+
b.onlyif 'Date.today.cwday == 6 && !exists?'
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
Rsync.new( 'akeso' ) do |b|
|
|
118
|
+
b.backup dirs_to_backup
|
|
119
|
+
# rsync backups go to _DESTDIR_/rsync per default; to override use the "to"
|
|
120
|
+
# setting:
|
|
121
|
+
## b.to '/var/backup/custom-rsync-destination'
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
Mysql.new( 'akeso' ) do |b|
|
|
125
|
+
# in this case, we simply do not run the backup if we already have one
|
|
126
|
+
b.onlyif '!exists?'
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
== CONFIGURATION TASKS ON THE REMOTES
|
|
131
|
+
|
|
132
|
+
* configure passwordless ssh login via certificates, e.g. do a +ssh-copy-id user@remotehostname+
|
|
133
|
+
|
|
134
|
+
== LICENSE:
|
|
135
|
+
|
|
136
|
+
Author:: Markus Strauss (mailto:markus@itstrauss.eu)
|
|
137
|
+
Copyright:: Copyright (c)2009 Markus Strauss
|
|
138
|
+
License:: GPLv3 (http://www.gnu.org/licenses/gpl-3.0.txt)
|
data/Rakefile
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'rake'
|
|
3
|
+
|
|
4
|
+
begin
|
|
5
|
+
require 'jeweler'
|
|
6
|
+
Jeweler::Tasks.new do |gem|
|
|
7
|
+
gem.name = "BackupMan"
|
|
8
|
+
gem.summary = %Q{A tool for system administrators to easily configure pull-over-SSH backups.}
|
|
9
|
+
gem.description = %Q{A tool for system administrators to easily configure pull-over-SSH backups. Install this gem on your backup server. Configure your backups definitions in /etc/backup_man and run backup_man from cron to securely pull your data over SSH.}
|
|
10
|
+
gem.email = "Markus@ITstrauss.eu"
|
|
11
|
+
gem.homepage = "http://github.com/oowoo/BackupMan"
|
|
12
|
+
gem.authors = ["Markus Strauss"]
|
|
13
|
+
gem.rubyforge_project = "backupman"
|
|
14
|
+
gem.add_development_dependency "rspec", ">= 1.2.9"
|
|
15
|
+
gem.add_development_dependency "yard", ">= 0"
|
|
16
|
+
gem.add_dependency "log4r", ">= 1.1.2"
|
|
17
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
|
18
|
+
end
|
|
19
|
+
Jeweler::RubyforgeTasks.new do |rubyforge|
|
|
20
|
+
rubyforge.doc_task = "yardoc"
|
|
21
|
+
end
|
|
22
|
+
rescue LoadError
|
|
23
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
require 'spec/rake/spectask'
|
|
27
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
|
28
|
+
spec.libs << 'lib' << 'spec'
|
|
29
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
|
33
|
+
spec.libs << 'lib' << 'spec'
|
|
34
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
|
35
|
+
spec.rcov = true
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
task :spec => :check_dependencies
|
|
39
|
+
|
|
40
|
+
task :default => :spec
|
|
41
|
+
|
|
42
|
+
begin
|
|
43
|
+
require 'yard'
|
|
44
|
+
YARD::Rake::YardocTask.new
|
|
45
|
+
rescue LoadError
|
|
46
|
+
task :yardoc do
|
|
47
|
+
abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard"
|
|
48
|
+
end
|
|
49
|
+
end
|
data/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.1.0
|
data/bin/backup_man
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
#
|
|
3
|
+
# Created on 2009-11-15.
|
|
4
|
+
# Copyright (c) 2009. All rights reserved.
|
|
5
|
+
|
|
6
|
+
require 'rubygems'
|
|
7
|
+
require File.expand_path(File.dirname(__FILE__) + "/../lib/backup_man")
|
|
8
|
+
require "backup_man/cli"
|
|
9
|
+
|
|
10
|
+
BackupMan::CLI.execute(STDOUT, ARGV)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
############################################################################
|
|
2
|
+
|
|
3
|
+
# = Global settings
|
|
4
|
+
# These settings are valid for all latter backup definitions
|
|
5
|
+
|
|
6
|
+
# the default destination directory; if you do not set this it defaults to
|
|
7
|
+
# /var/backups/backup_man
|
|
8
|
+
DESTDIR = "/Users/srm/Documents/Backups"
|
|
9
|
+
|
|
10
|
+
# where the lockfiles go (defaults to /var/lock/backup_man/_backupname_);
|
|
11
|
+
# backup_man ensures, that each backup task can only run once at a time and
|
|
12
|
+
# uses lockfiles for this
|
|
13
|
+
LOCKDIR = "#{DESTDIR}"
|
|
14
|
+
|
|
15
|
+
# ssh_app defines the call to ssh with command line parameters, e.g. if you
|
|
16
|
+
# want to use a special identity file; SSH_APP defaults to 'ssh'
|
|
17
|
+
SSH_APP='ssh -i /Users/srm/.ssh/id_rsa'
|
|
18
|
+
|
|
19
|
+
############################################################################
|
|
20
|
+
|
|
21
|
+
# = Helper variables
|
|
22
|
+
# These are just used in this file to ease the backup definitions
|
|
23
|
+
|
|
24
|
+
dirs_to_backup = [ "/etc", "/home", "/root", "/srv", "/var/backups",
|
|
25
|
+
"/var/cache/git", "/var/log", "/var/mail", "/var/rails", "/var/svn",
|
|
26
|
+
"/var/www" ]
|
|
27
|
+
|
|
28
|
+
############################################################################
|
|
29
|
+
|
|
30
|
+
# = Backup defintions
|
|
31
|
+
# Currently there are these types of backups:
|
|
32
|
+
# * Tar - as the name says; simple tar of a bunch of directories into a tgz
|
|
33
|
+
# * Rsync - rsync of a bunch of directories
|
|
34
|
+
# * Mysql - does a simple mysqldump of all databases
|
|
35
|
+
#
|
|
36
|
+
# == Common settings
|
|
37
|
+
# * backup: an array of directories to backup
|
|
38
|
+
# * to: the destination directory on the local machine (defaults to DESTDIR)
|
|
39
|
+
# * onlyif: specify a condition (as Ruby code) which is checked before the
|
|
40
|
+
# actual backup run; it must evaluate true to have the backup run - see the
|
|
41
|
+
# homepage for details on how to write conditions
|
|
42
|
+
#
|
|
43
|
+
Tar.new( 'akeso' ) do |b|
|
|
44
|
+
b.backup dirs_to_backup
|
|
45
|
+
# in this example, this job only runs on saturdays and only if the backup
|
|
46
|
+
# does not exist already
|
|
47
|
+
b.onlyif 'Date.today.cwday == 6 && !exists?'
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
Rsync.new( 'akeso' ) do |b|
|
|
51
|
+
b.backup dirs_to_backup
|
|
52
|
+
# rsync backups go to _DESTDIR_/rsync per default; to override use the "to"
|
|
53
|
+
# setting:
|
|
54
|
+
## b.to '/var/backup/custom-rsync-destination'
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
Mysql.new( 'akeso' ) do |b|
|
|
58
|
+
# in this case, we simply do not run the backup if we already have one
|
|
59
|
+
b.onlyif '!exists?'
|
|
60
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
require 'backup_man/backup_man'
|
|
2
|
+
require 'backup_man/dsl'
|
|
3
|
+
|
|
4
|
+
module BackupMan
|
|
5
|
+
|
|
6
|
+
def self.log_end_of_operation
|
|
7
|
+
Log.instance.info( "Finished #{self}." )
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# we wanna log when the program ends
|
|
11
|
+
at_exit { self.log_end_of_operation }
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# References: _Design Patterns in Ruby_ by Russ Olsen
|
|
15
|
+
class Backup
|
|
16
|
+
|
|
17
|
+
# the name of our backup set; this is used for generating a default
|
|
18
|
+
# backup_directory; e.g. use the hostname of the machine to backup
|
|
19
|
+
attr_reader :name
|
|
20
|
+
|
|
21
|
+
# where shall our backup data go (directory path)
|
|
22
|
+
attr_reader :backup_directory
|
|
23
|
+
|
|
24
|
+
# user name of the remote machine (defaults to 'root')
|
|
25
|
+
attr_accessor :user
|
|
26
|
+
|
|
27
|
+
# hostname of the remote machine (defaults to job definition name)
|
|
28
|
+
attr_accessor :host
|
|
29
|
+
|
|
30
|
+
# DSL for conditional execution
|
|
31
|
+
include DSL
|
|
32
|
+
|
|
33
|
+
def_dsl :onlyif
|
|
34
|
+
def_dsl :backup, :data_sources
|
|
35
|
+
def_dsl :to, :backup_directory
|
|
36
|
+
def_dsl :user
|
|
37
|
+
def_dsl :host
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# this method sets all the default values but does not overwrite existing
|
|
41
|
+
# settings; this method cannot be called in the initializer of Backup
|
|
42
|
+
# because the default values are not available at that time;
|
|
43
|
+
def set_defaults
|
|
44
|
+
@data_sources = [] unless @data_sources
|
|
45
|
+
@backup_directory = "#{BackupMan.instance.destdir}/#{@name}" unless @backup_directory
|
|
46
|
+
@onlyif = "true" if @onlyif.nil?
|
|
47
|
+
@user = 'root' unless @user
|
|
48
|
+
@host = @name unless @host
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# yields the block which comes from the DSL configuration file; also
|
|
52
|
+
# registers the new backup configuration with {BackupMan}
|
|
53
|
+
def initialize( name )
|
|
54
|
+
@name = name
|
|
55
|
+
yield(self) if block_given?
|
|
56
|
+
BackupMan.instance.register_backup( self )
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# calling this actually runs the backup; DO NOT override this; override
|
|
60
|
+
# _run instead
|
|
61
|
+
def run
|
|
62
|
+
log_begin_of_run
|
|
63
|
+
set_defaults
|
|
64
|
+
debug_log_dsl_info
|
|
65
|
+
onlyif = eval( @onlyif )
|
|
66
|
+
Log.instance.debug( "onlyif = { #{@onlyif} } evaluates #{onlyif}" )
|
|
67
|
+
if onlyif
|
|
68
|
+
unless @backup_directory
|
|
69
|
+
Log.instance.error( "#{self}: No backup directory. Don't know where to store all this stuff.")
|
|
70
|
+
else
|
|
71
|
+
FileUtils.mkdir_p @backup_directory
|
|
72
|
+
_run
|
|
73
|
+
end
|
|
74
|
+
else
|
|
75
|
+
Log.instance.info( "#{self}: Preconditions for backup run not fulfilled.")
|
|
76
|
+
end
|
|
77
|
+
log_end_of_run
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# @return [String]
|
|
81
|
+
def to_s
|
|
82
|
+
"#{self.class} #{self.name}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
# @abstract override this to implement the actual backup commands
|
|
90
|
+
def _run
|
|
91
|
+
throw "Hey. Cannot run just 'Backup'."
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# not used acutally
|
|
95
|
+
def log_begin_of_run
|
|
96
|
+
Log.instance.info( "Starting #{self.class} run for #{@name}." )
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# simply logs that the program terminates
|
|
100
|
+
def log_end_of_run
|
|
101
|
+
Log.instance.info( "Finished #{self.class} run for #{@name}." )
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# @return [String] the ssh command string including user@host
|
|
105
|
+
def ssh_connect_cmd
|
|
106
|
+
"#{BackupMan.instance.ssh_app} #{@user}@#{@host}"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
require 'singleton'
|
|
2
|
+
|
|
3
|
+
module BackupMan
|
|
4
|
+
# this is a singleton class managing the application; it holds some
|
|
5
|
+
# important global settings, like the global destination directory;
|
|
6
|
+
class BackupMan
|
|
7
|
+
|
|
8
|
+
include Singleton
|
|
9
|
+
|
|
10
|
+
attr_accessor :destdir, :ssh_app, :logfile, :lockdir
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@backups = []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def register_backup( backup )
|
|
17
|
+
@backups << backup
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def run
|
|
21
|
+
@backups.each do |backup|
|
|
22
|
+
begin
|
|
23
|
+
backup.run
|
|
24
|
+
rescue Interrupt
|
|
25
|
+
Log.instance.warn( "Interrupt: Cancelling remaining operations.")
|
|
26
|
+
return
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def build_lockfile_path( filename )
|
|
32
|
+
"#{@lockdir}/#{filename}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def set_default( symbol, value )
|
|
36
|
+
const_s = symbol.to_s.upcase
|
|
37
|
+
if CLI.const_defined?( const_s )
|
|
38
|
+
self.instance_variable_set( "@#{symbol.to_s}", CLI.const_get( const_s ) )
|
|
39
|
+
else
|
|
40
|
+
self.instance_variable_set( "@#{symbol.to_s}", value )
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
require 'optparse'
|
|
2
|
+
require 'date'
|
|
3
|
+
require 'pathname'
|
|
4
|
+
|
|
5
|
+
require_relative 'log'
|
|
6
|
+
require_relative 'tar'
|
|
7
|
+
require_relative 'rsync'
|
|
8
|
+
require_relative 'mysql'
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
module BackupMan
|
|
12
|
+
class CLI
|
|
13
|
+
def self.execute(stdout, arguments=[])
|
|
14
|
+
|
|
15
|
+
# NOTE: the option -p/--path= is given as an example, and should be replaced in your application.
|
|
16
|
+
|
|
17
|
+
options = {
|
|
18
|
+
:debug => false,
|
|
19
|
+
:logpath => '/var/log/backup_man.log'
|
|
20
|
+
}
|
|
21
|
+
mandatory_options = %w( )
|
|
22
|
+
|
|
23
|
+
parser = OptionParser.new do |opts|
|
|
24
|
+
opts.banner = <<-BANNER.gsub(/^ /,'')
|
|
25
|
+
BackupMan handles your SSH-pull-style backups. Call it via cron or any other scheduler.
|
|
26
|
+
|
|
27
|
+
Usage: #{File.basename($0)} [options] {configname | configpath}
|
|
28
|
+
|
|
29
|
+
Options are:
|
|
30
|
+
BANNER
|
|
31
|
+
opts.separator ""
|
|
32
|
+
opts.on("-l", "--logpath=PATH", String,
|
|
33
|
+
"Path to the log file. This can NOT be configured in the config file.",
|
|
34
|
+
"Default: /var/log/backup_man.log") { |arg| options[:logpath] = arg }
|
|
35
|
+
opts.on("-d", "--debug", "Debug mode.", "Much output on screen and in the log file." ) {
|
|
36
|
+
options[:debug] = true
|
|
37
|
+
}
|
|
38
|
+
opts.on("-h", "--help",
|
|
39
|
+
"Show this help message.") { stdout.puts opts; exit }
|
|
40
|
+
opts.parse!(arguments)
|
|
41
|
+
|
|
42
|
+
if mandatory_options && mandatory_options.find { |option| options[option.to_sym].nil? }
|
|
43
|
+
stdout.puts opts; exit
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# doing our stuff here
|
|
48
|
+
|
|
49
|
+
# first we check if our logfile is writeable; if not, we give a warning
|
|
50
|
+
logfile = Pathname.new( options[:logpath] )
|
|
51
|
+
if (logfile.exist? && logfile.writable?) || logfile.parent.writable?
|
|
52
|
+
BackupMan.instance.logfile = logfile.to_s
|
|
53
|
+
else
|
|
54
|
+
Log.instance.warn( "Log file is not writeable: #{logfile}.")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# root-warning
|
|
58
|
+
Log.instance.warn( "Please do not run this program as root.") if `id -u`.strip == '0'
|
|
59
|
+
|
|
60
|
+
# reconfigure our Logger for debugging if necessary
|
|
61
|
+
if options[:debug]
|
|
62
|
+
Log.instance.enable_debugmode
|
|
63
|
+
Log.instance.debug( "Debugging mode enabled.")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
unless ARGV[0]
|
|
68
|
+
Log.instance.fatal( "No config file given." )
|
|
69
|
+
stdout.puts parser
|
|
70
|
+
exit 1
|
|
71
|
+
else
|
|
72
|
+
config_filepath = Pathname.new( ARGV[0] )
|
|
73
|
+
config_filepath = Pathname.new "/etc/backup_man/#{config_filepath}" unless config_filepath.file?
|
|
74
|
+
|
|
75
|
+
unless config_filepath.file?
|
|
76
|
+
Log.instance.fatal( "Config file not found [#{config_filepath}].")
|
|
77
|
+
exit 2
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# get and evaluate the config file; errors in the config file may be
|
|
81
|
+
# difficult to debug, so be careful
|
|
82
|
+
Log.instance.debug( "Reading file '#{config_filepath}'.")
|
|
83
|
+
eval( File.read( config_filepath ) )
|
|
84
|
+
|
|
85
|
+
# configure global defaults
|
|
86
|
+
BackupMan.instance.set_default( :destdir, '/var/backups/backup_man')
|
|
87
|
+
BackupMan.instance.set_default( :lockdir, '/var/lock/backup_man')
|
|
88
|
+
BackupMan.instance.set_default( :ssh_app, 'ssh')
|
|
89
|
+
|
|
90
|
+
Log.instance.debug( "Global settings:")
|
|
91
|
+
Log.instance.debug( " DESTDIR = #{BackupMan.instance.destdir}")
|
|
92
|
+
Log.instance.debug( " LOCKDIR = #{BackupMan.instance.lockdir}")
|
|
93
|
+
Log.instance.debug( " SSH_APP = #{BackupMan.instance.ssh_app}")
|
|
94
|
+
|
|
95
|
+
# run the whole thing
|
|
96
|
+
BackupMan.instance.run
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
|
|
2
|
+
require 'digest/md5'
|
|
3
|
+
|
|
4
|
+
module BackupMan
|
|
5
|
+
# = Command class
|
|
6
|
+
# This class allows to run commands and makes sure the same command cannot
|
|
7
|
+
# run two times at the same time.
|
|
8
|
+
#
|
|
9
|
+
# This ensured by creating lockfiles in the working directory. To override
|
|
10
|
+
# define LOCKDIR.
|
|
11
|
+
|
|
12
|
+
class Command
|
|
13
|
+
|
|
14
|
+
attr_reader :cmd
|
|
15
|
+
|
|
16
|
+
def initialize( cmd )
|
|
17
|
+
@cmd = cmd
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# returns true if the command is locked (= currently running)
|
|
21
|
+
def locked?
|
|
22
|
+
File.exists?( self.lockfile )
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# returns the path to the lockfile for this command
|
|
26
|
+
def lockfile
|
|
27
|
+
hash = Digest::MD5.hexdigest(cmd.to_s)
|
|
28
|
+
BackupMan.instance.build_lockfile_path( "#{hash}.lock.txt" )
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# returns a nice string representation of the command
|
|
32
|
+
def to_s
|
|
33
|
+
self.cmd.to_s
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def run
|
|
37
|
+
unless locked?
|
|
38
|
+
lock
|
|
39
|
+
begin
|
|
40
|
+
Log.instance.info( "#{self}: Running.")
|
|
41
|
+
print `#{cmd}` unless TESTING
|
|
42
|
+
rescue Interrupt
|
|
43
|
+
Log.instance.warn( "#{self}: Operation interrupted.")
|
|
44
|
+
raise
|
|
45
|
+
ensure
|
|
46
|
+
unlock
|
|
47
|
+
end
|
|
48
|
+
else
|
|
49
|
+
Log.instance.warn( "Command already running: #{self}." )
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def lock
|
|
57
|
+
Log.instance.debug( "Locking command " + self.cmd )
|
|
58
|
+
unless locked?
|
|
59
|
+
f = File.new( self.lockfile, "w" )
|
|
60
|
+
# FIXME: command output shall go into the correct logging lvl (eg ERROR)
|
|
61
|
+
f.write(self.cmd)
|
|
62
|
+
f.close
|
|
63
|
+
else
|
|
64
|
+
Log.instance.info( "Lockfile exists: " + lockfile )
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def unlock
|
|
69
|
+
Log.instance.debug( "Unlocking command " + self.cmd )
|
|
70
|
+
FileUtils.remove_file( self.lockfile )
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module BackupMan
|
|
2
|
+
|
|
3
|
+
# use "include DSL" to use this module
|
|
4
|
+
module DSL
|
|
5
|
+
|
|
6
|
+
# extend host class with class methods when we're included
|
|
7
|
+
def self.included(host_class)
|
|
8
|
+
host_class.extend(ClassMethods)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def debug_log_dsl_info
|
|
12
|
+
Log.instance.debug( "Job settings:")
|
|
13
|
+
self.class.dsl_methods.each do |method, var|
|
|
14
|
+
Log.instance.debug( " #{method} = #{self.instance_variable_get("@#{var}")}" )
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
module ClassMethods
|
|
19
|
+
|
|
20
|
+
def def_dsl( name, var = name )
|
|
21
|
+
class_eval( %Q{
|
|
22
|
+
def #{name}( #{var} )
|
|
23
|
+
@#{var} = #{var}
|
|
24
|
+
end
|
|
25
|
+
})
|
|
26
|
+
register_dsl( name, var )
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def register_dsl( name, var )
|
|
30
|
+
@dsl_methods = [] if @dsl_methods.nil?
|
|
31
|
+
@dsl_methods << [name, var]
|
|
32
|
+
puts "#{self} #{self.dsl_methods.join ","}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# returns an array of all dsl methods of this and all superclasses
|
|
36
|
+
def dsl_methods
|
|
37
|
+
dsl_methods = []
|
|
38
|
+
if self.superclass != Object
|
|
39
|
+
dsl_methods = dsl_methods | self.superclass.dsl_methods
|
|
40
|
+
end
|
|
41
|
+
dsl_methods | @dsl_methods
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
end # ClassMethods
|
|
46
|
+
end #DSL
|
|
47
|
+
end #BackupMan
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require 'singleton'
|
|
2
|
+
require 'delegate'
|
|
3
|
+
require 'rubygems'
|
|
4
|
+
require 'log4r'
|
|
5
|
+
|
|
6
|
+
module BackupMan
|
|
7
|
+
|
|
8
|
+
# = Singleton logger
|
|
9
|
+
#
|
|
10
|
+
# This one logs INFO and higher to a file and WARN and higher to STDERR. It
|
|
11
|
+
# does this by creating a new Log4r::Logger object and configuring it.
|
|
12
|
+
# SimpleDelegator wrapps this.
|
|
13
|
+
#
|
|
14
|
+
# By default, the logfile goes into workdir/backup_man.log. To override use
|
|
15
|
+
# LOGFILE in the backup case definition.
|
|
16
|
+
#
|
|
17
|
+
# == References
|
|
18
|
+
# * http://www.5dollarwhitebox.org/drupal/ruby_dual_output_logging
|
|
19
|
+
# * http://rubyforge.org/snippet/detail.php?type=snippet&id=146
|
|
20
|
+
#
|
|
21
|
+
class Log < SimpleDelegator
|
|
22
|
+
include Singleton
|
|
23
|
+
include Log4r
|
|
24
|
+
|
|
25
|
+
def initialize
|
|
26
|
+
@logger = Logger.new( "BackupMan" )
|
|
27
|
+
super( @logger )
|
|
28
|
+
|
|
29
|
+
console_format = PatternFormatter.new(:pattern => "%l:\t %m")
|
|
30
|
+
stderr_outputter = Log4r::StderrOutputter.new( 'console', :formatter => console_format )
|
|
31
|
+
stderr_outputter.level = WARN
|
|
32
|
+
@logger.add stderr_outputter
|
|
33
|
+
|
|
34
|
+
if filename = BackupMan.instance.logfile
|
|
35
|
+
file_format = PatternFormatter.new(:pattern => "[ %d ] %l\t %m")
|
|
36
|
+
file_outputter = FileOutputter.new('fileOutputter', :filename => filename, :trunc => false, :formatter => file_format )
|
|
37
|
+
file_outputter.level = DEBUG
|
|
38
|
+
@logger.add file_outputter
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def enable_debugmode
|
|
43
|
+
Log4r::Outputter.each_outputter { |outputter| outputter.level = DEBUG }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
end
|