BackupMan 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -1
- data/Rakefile +14 -2
- data/VERSION +1 -1
- data/features/development.feature +18 -0
- data/features/dsl_parameters.feature +33 -0
- data/features/logfile.feature +30 -0
- data/features/overview.feature +16 -0
- data/features/step_definitions/common_steps.rb +163 -0
- data/features/step_definitions/dsl_parameters_steps.rb +81 -0
- data/features/step_definitions/logfile_steps.rb +0 -0
- data/features/support/common.rb +29 -0
- data/features/support/env.rb +14 -0
- data/features/support/matchers.rb +11 -0
- data/lib/backup_man/backup.rb +39 -23
- data/lib/backup_man/backup_man.rb +2 -2
- data/lib/backup_man/cli.rb +23 -12
- data/lib/backup_man/command.rb +7 -7
- data/lib/backup_man/dsl.rb +32 -13
- data/lib/backup_man/log.rb +10 -1
- data/lib/backup_man/mysql.rb +2 -0
- data/lib/backup_man/rsync.rb +3 -0
- data/lib/backup_man/tar.rb +5 -0
- data/lib/backup_man.rb +1 -1
- data/spec/BackupMan_spec.rb +2 -2
- metadata +60 -17
data/.gitignore
CHANGED
data/Rakefile
CHANGED
@@ -12,7 +12,8 @@ begin
|
|
12
12
|
gem.authors = ["Markus Strauss"]
|
13
13
|
gem.rubyforge_project = "backupman"
|
14
14
|
gem.add_development_dependency "rspec", ">= 1.2.9"
|
15
|
-
gem.add_development_dependency "yard", ">= 0"
|
15
|
+
gem.add_development_dependency "yard", ">= 0.4.0"
|
16
|
+
gem.add_development_dependency "cucumber", ">= 0.4.4"
|
16
17
|
gem.add_dependency "log4r", ">= 1.1.2"
|
17
18
|
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
18
19
|
end
|
@@ -39,7 +40,18 @@ end
|
|
39
40
|
|
40
41
|
task :spec => :check_dependencies
|
41
42
|
|
42
|
-
|
43
|
+
begin
|
44
|
+
require 'cucumber/rake/task'
|
45
|
+
Cucumber::Rake::Task.new(:features)
|
46
|
+
|
47
|
+
task :features => :check_dependencies
|
48
|
+
rescue LoadError
|
49
|
+
task :features do
|
50
|
+
abort "Cucumber is not available. In order to run features, you must: sudo gem install cucumber"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
task :default => :features
|
43
55
|
|
44
56
|
require 'rake/rdoctask'
|
45
57
|
Rake::RDocTask.new do |rdoc|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.1.
|
1
|
+
0.1.2
|
@@ -0,0 +1,18 @@
|
|
1
|
+
Feature: Development processes of BackupMan itself (rake tasks)
|
2
|
+
|
3
|
+
As a BackupMan maintainer or contributor
|
4
|
+
I want rake tasks to maintain and release the gem
|
5
|
+
So that I can spend time on the tests and code, and not excessive time on maintenance processes
|
6
|
+
|
7
|
+
Scenario: Generate RubyGem
|
8
|
+
Given this project is active project folder
|
9
|
+
And "pkg" folder is deleted
|
10
|
+
When I invoke task "rake build"
|
11
|
+
Then folder "pkg" is created
|
12
|
+
And file with name matching "pkg/*.gem" is created
|
13
|
+
# And gem spec key "rdoc_options" contains /--mainREADME.rdoc/
|
14
|
+
|
15
|
+
Scenario: Test specs
|
16
|
+
Given this project is active project folder
|
17
|
+
When I invoke task "rake spec"
|
18
|
+
Then I should see all 2 examples pass
|
@@ -0,0 +1,33 @@
|
|
1
|
+
Feature: The DSL supports various parameters
|
2
|
+
|
3
|
+
Scenario Outline: Presence of DSL parameters
|
4
|
+
Given the task is "<task>"
|
5
|
+
And the parameters are "<parameters>"
|
6
|
+
And that goes into file "tmp/configuration_file"
|
7
|
+
And this project is active project folder
|
8
|
+
When I run project executable "./bin/backup_man" with arguments "-t tmp/configuration_file"
|
9
|
+
Then the result should be <result>
|
10
|
+
|
11
|
+
Scenarios: full set, all parameters are provided and existent (valid)
|
12
|
+
| task | parameters | result |
|
13
|
+
| Tar | onlyif, backup, to, user, host, filename, options | ok |
|
14
|
+
| Mysql | onlyif, backup, to, user, host, filename, options | ok |
|
15
|
+
| Rsync | onlyif, backup, to, user, host, options | ok |
|
16
|
+
|
17
|
+
Scenarios: full set, but invalid parameters are present
|
18
|
+
| task | parameters | result |
|
19
|
+
| Tar | onlyif, backup, to, user, host, filename, options, invalid_parameter | fatal "undefined method `invalid_parameter'" |
|
20
|
+
| Mysql | onlyif, backup, to, user, host, filename, options, invalid_parameter | fatal "undefined method `invalid_parameter'" |
|
21
|
+
| Rsync | onlyif, backup, to, user, host, options, invalid_parameter | fatal "undefined method `invalid_parameter'" |
|
22
|
+
|
23
|
+
Scenarios: minimal set, all required parameters are provided
|
24
|
+
| task | parameters | result |
|
25
|
+
| Tar | backup | ok |
|
26
|
+
| Mysql | | ok |
|
27
|
+
| Rsync | backup | ok |
|
28
|
+
|
29
|
+
Scenarios: one required parameter is missing
|
30
|
+
| task | parameters | result |
|
31
|
+
| Tar | onlyif, to, user, host, filename, options | error "A required parameter is missing: backup" |
|
32
|
+
| Rsync | onlyif, to, user, host, options | error "A required parameter is missing: backup" |
|
33
|
+
|
@@ -0,0 +1,30 @@
|
|
1
|
+
Feature: logfile
|
2
|
+
In order to have a log of the BackupMan's doings
|
3
|
+
BackupMan should write to a logfile
|
4
|
+
|
5
|
+
Scenario: the logfile is writable
|
6
|
+
Given the logfile is writeable
|
7
|
+
And the user has not used option '-h'
|
8
|
+
When I start BackupMan
|
9
|
+
Then the logfile should become updated
|
10
|
+
|
11
|
+
Scenario: the logfile is not writeable
|
12
|
+
Given the logfile is not writeable
|
13
|
+
And the user has not used option '-h'
|
14
|
+
When I start BackupMan
|
15
|
+
Then error "logfile not writable" should be printed
|
16
|
+
|
17
|
+
Scenario: logfile parameter given, and writeable
|
18
|
+
When I start BackupMan with parameter "-l /path/to/logfile"
|
19
|
+
And the file "path/to/logfile" is writeable
|
20
|
+
Then the logfile should become updated
|
21
|
+
|
22
|
+
Scenario: logfile parameter given, and not writeable
|
23
|
+
When I start BackupMan with parameter "-l /path/to/logfile"
|
24
|
+
And the file "path/to/logfile" is not writeable
|
25
|
+
And the directory "path/to" is not writeable
|
26
|
+
Then error "logfile not writable" should be printed
|
27
|
+
|
28
|
+
Scenario: no logfile parameter given
|
29
|
+
When I start BackupMan
|
30
|
+
Then "/var/log/backup_man.log" should be used as the default logfile
|
@@ -0,0 +1,16 @@
|
|
1
|
+
### Primary features
|
2
|
+
Feature: BackupMan can do tar backups from remote hosts
|
3
|
+
Feature: BackupMan can do rsync backups from remote hosts
|
4
|
+
Feature: BackupMan can do mysql dumps from remote hosts
|
5
|
+
# Feature: BackupMan can cleanup backups it has created
|
6
|
+
# Feature: BackupMan keeps a database of all backups it has created in order to make safe cleanups
|
7
|
+
Feature: BackupMan per default is completely silent when everything runs smoothly
|
8
|
+
# Feature: BackupMan can do "dry runs" in order for the user to know that the program will do
|
9
|
+
|
10
|
+
### Secondary features
|
11
|
+
Feature: BackupMan can print its usage
|
12
|
+
Feature: BackupMan uses a log file
|
13
|
+
Feature: BackupMan can be configured to use a custom logfile
|
14
|
+
Feature: BackupMan can print debug messages on screen and in the logfile when requested
|
15
|
+
Feature: BackupMan uses convention over configuration, has reasonable defaults whenever possible
|
16
|
+
# Feature: To provide reasonable defaults, BackupMan evaluates what kind of OS the remote is
|
@@ -0,0 +1,163 @@
|
|
1
|
+
Given /^this project is active project folder/ do
|
2
|
+
@active_project_folder = File.expand_path(File.dirname(__FILE__) + "/../..")
|
3
|
+
end
|
4
|
+
|
5
|
+
Given /^env variable \$([\w_]+) set to "(.*)"/ do |env_var, value|
|
6
|
+
ENV[env_var] = value
|
7
|
+
end
|
8
|
+
|
9
|
+
Given /"(.*)" folder is deleted/ do |folder|
|
10
|
+
in_project_folder { FileUtils.rm_rf folder }
|
11
|
+
end
|
12
|
+
|
13
|
+
When /^I invoke "(.*)" generator with arguments "(.*)"$/ do |generator, arguments|
|
14
|
+
@stdout = StringIO.new
|
15
|
+
in_project_folder do
|
16
|
+
if Object.const_defined?("APP_ROOT")
|
17
|
+
APP_ROOT.replace(FileUtils.pwd)
|
18
|
+
else
|
19
|
+
APP_ROOT = FileUtils.pwd
|
20
|
+
end
|
21
|
+
run_generator(generator, arguments.split(' '), SOURCES, :stdout => @stdout)
|
22
|
+
end
|
23
|
+
File.open(File.join(@tmp_root, "generator.out"), "w") do |f|
|
24
|
+
@stdout.rewind
|
25
|
+
f << @stdout.read
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
When /^I run executable "(.*)" with arguments "(.*)"/ do |executable, arguments|
|
30
|
+
@stdout = File.expand_path(File.join(@tmp_root, "executable.out"))
|
31
|
+
in_project_folder do
|
32
|
+
system "#{executable} #{arguments} > #{@stdout} 2> #{@stdout}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
When /^I run project executable "(.*)" with arguments "(.*)"/ do |executable, arguments|
|
37
|
+
@stdout = File.expand_path(File.join(@tmp_root, "executable.out"))
|
38
|
+
in_project_folder do
|
39
|
+
system "ruby #{executable} #{arguments} > #{@stdout} 2> #{@stdout}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
When /^I run local executable "(.*)" with arguments "(.*)"/ do |executable, arguments|
|
44
|
+
@stdout = File.expand_path(File.join(@tmp_root, "executable.out"))
|
45
|
+
executable = File.expand_path(File.join(File.dirname(__FILE__), "/../../bin", executable))
|
46
|
+
in_project_folder do
|
47
|
+
system "ruby #{executable} #{arguments} > #{@stdout} 2> #{@stdout}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
When /^I invoke task "rake (.*)"/ do |task|
|
52
|
+
@stdout = File.expand_path(File.join(@tmp_root, "tests.out"))
|
53
|
+
in_project_folder do
|
54
|
+
system "rake #{task} --trace > #{@stdout} 2> #{@stdout}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
Then /^folder "(.*)" (is|is not) created/ do |folder, is|
|
59
|
+
in_project_folder do
|
60
|
+
File.exists?(folder).should(is == 'is' ? be_true : be_false)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
Then /^file "(.*)" (is|is not) created/ do |file, is|
|
65
|
+
in_project_folder do
|
66
|
+
File.exists?(file).should(is == 'is' ? be_true : be_false)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
Then /^file with name matching "(.*)" is created/ do |pattern|
|
71
|
+
in_project_folder do
|
72
|
+
Dir[pattern].should_not be_empty
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
Then /^file "(.*)" contents (does|does not) match \/(.*)\// do |file, does, regex|
|
77
|
+
in_project_folder do
|
78
|
+
actual_output = File.read(file)
|
79
|
+
(does == 'does') ?
|
80
|
+
actual_output.should(match(/#{regex}/)) :
|
81
|
+
actual_output.should_not(match(/#{regex}/))
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
Then /gem file "(.*)" and generated file "(.*)" should be the same/ do |gem_file, project_file|
|
86
|
+
File.exists?(gem_file).should be_true
|
87
|
+
File.exists?(project_file).should be_true
|
88
|
+
gem_file_contents = File.read(File.dirname(__FILE__) + "/../../#{gem_file}")
|
89
|
+
project_file_contents = File.read(File.join(@active_project_folder, project_file))
|
90
|
+
project_file_contents.should == gem_file_contents
|
91
|
+
end
|
92
|
+
|
93
|
+
Then /^(does|does not) invoke generator "(.*)"$/ do |does_invoke, generator|
|
94
|
+
actual_output = File.read(@stdout)
|
95
|
+
does_invoke == "does" ?
|
96
|
+
actual_output.should(match(/dependency\s+#{generator}/)) :
|
97
|
+
actual_output.should_not(match(/dependency\s+#{generator}/))
|
98
|
+
end
|
99
|
+
|
100
|
+
Then /help options "(.*)" and "(.*)" are displayed/ do |opt1, opt2|
|
101
|
+
actual_output = File.read(@stdout)
|
102
|
+
actual_output.should match(/#{opt1}/)
|
103
|
+
actual_output.should match(/#{opt2}/)
|
104
|
+
end
|
105
|
+
|
106
|
+
Then /^I should see$/ do |text|
|
107
|
+
actual_output = File.read(@stdout)
|
108
|
+
actual_output.should contain(text)
|
109
|
+
end
|
110
|
+
|
111
|
+
Then /^I should not see$/ do |text|
|
112
|
+
actual_output = File.read(@stdout)
|
113
|
+
actual_output.should_not contain(text)
|
114
|
+
end
|
115
|
+
|
116
|
+
Then /^I should see exactly$/ do |text|
|
117
|
+
actual_output = File.read(@stdout)
|
118
|
+
actual_output.should == text
|
119
|
+
end
|
120
|
+
|
121
|
+
Then /^I should see all (\d+) tests pass/ do |expected_test_count|
|
122
|
+
expected = %r{^#{expected_test_count} tests, \d+ assertions, 0 failures, 0 errors}
|
123
|
+
actual_output = File.read(@stdout)
|
124
|
+
actual_output.should match(expected)
|
125
|
+
end
|
126
|
+
|
127
|
+
Then /^I should see all (\d+) examples pass/ do |expected_test_count|
|
128
|
+
expected = %r{^#{expected_test_count} examples?, 0 failures}
|
129
|
+
actual_output = File.read(@stdout)
|
130
|
+
actual_output.should match(expected)
|
131
|
+
end
|
132
|
+
|
133
|
+
Then /^yaml file "(.*)" contains (\{.*\})/ do |file, yaml|
|
134
|
+
in_project_folder do
|
135
|
+
yaml = eval yaml
|
136
|
+
YAML.load(File.read(file)).should == yaml
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
Then /^Rakefile can display tasks successfully/ do
|
141
|
+
@stdout = File.expand_path(File.join(@tmp_root, "rakefile.out"))
|
142
|
+
in_project_folder do
|
143
|
+
system "rake -T > #{@stdout} 2> #{@stdout}"
|
144
|
+
end
|
145
|
+
actual_output = File.read(@stdout)
|
146
|
+
actual_output.should match(/^rake\s+\w+\s+#\s.*/)
|
147
|
+
end
|
148
|
+
|
149
|
+
Then /^task "rake (.*)" is executed successfully/ do |task|
|
150
|
+
@stdout.should_not be_nil
|
151
|
+
actual_output = File.read(@stdout)
|
152
|
+
actual_output.should_not match(/^Don't know how to build task '#{task}'/)
|
153
|
+
actual_output.should_not match(/Error/i)
|
154
|
+
end
|
155
|
+
|
156
|
+
Then /^gem spec key "(.*)" contains \/(.*)\// do |key, regex|
|
157
|
+
in_project_folder do
|
158
|
+
gem_file = Dir["pkg/*.gem"].first
|
159
|
+
gem_spec = Gem::Specification.from_yaml(`gem spec #{gem_file}`)
|
160
|
+
spec_value = gem_spec.send(key.to_sym)
|
161
|
+
spec_value.to_s.should match(/#{regex}/)
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
|
2
|
+
def defaults
|
3
|
+
{
|
4
|
+
"onlyif" => "'true'",
|
5
|
+
"backup" => "['/']",
|
6
|
+
"to" => "'to'",
|
7
|
+
"user" => "'user'",
|
8
|
+
"host" => "'host'",
|
9
|
+
"filename" => "'filename'",
|
10
|
+
"options" => "'options'"
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
Given /^the task is "([^\"]*)"$/ do |task|
|
15
|
+
@subject = task
|
16
|
+
end
|
17
|
+
|
18
|
+
Given /^the parameters are "([^\"]*)"$/ do |parameters|
|
19
|
+
@parameters = parameters.split(",").map{ |p| p.strip }
|
20
|
+
end
|
21
|
+
|
22
|
+
Given /^that goes into file "([^\"]*)"$/ do |configfile|
|
23
|
+
f = File.open(configfile, "w") do |f|
|
24
|
+
f.puts "#{@subject}.new('test') do |b|"
|
25
|
+
@parameters.each { |par| f.puts " b.#{par} #{defaults[par]}"}
|
26
|
+
f.puts "end"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
Then /^the result should be ok$/ do
|
31
|
+
steps %Q{
|
32
|
+
Then I should not see any output
|
33
|
+
And the exit code should be 0
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
Then /^the result should be error$/ do
|
38
|
+
steps %Q{
|
39
|
+
Then I should see some output
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
Then /^the result should be fatal$/ do
|
44
|
+
steps %Q{
|
45
|
+
Then I should see some output
|
46
|
+
And the exit code should not be 0
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
Then /^the result should be error "([^\"]*)"$/ do |error|
|
51
|
+
steps %Q{
|
52
|
+
Then I should see
|
53
|
+
"""
|
54
|
+
#{error}
|
55
|
+
"""
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
Then /^the result should be fatal "([^\"]*)"$/ do |error|
|
60
|
+
steps %Q{
|
61
|
+
Then I should see
|
62
|
+
"""
|
63
|
+
#{error}
|
64
|
+
"""
|
65
|
+
And the exit code should not be 0
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
Then /^the exit code (should|should not) be (.+)$/ do |should, code|
|
70
|
+
$?.method(:should) == code
|
71
|
+
end
|
72
|
+
|
73
|
+
Then /^Then I should see some output$/ do
|
74
|
+
actual_output = File.read(@stdout)
|
75
|
+
actual_output.should_not == ""
|
76
|
+
end
|
77
|
+
|
78
|
+
Then /^I should not see any output$/ do
|
79
|
+
actual_output = File.read(@stdout)
|
80
|
+
actual_output.should == ""
|
81
|
+
end
|
File without changes
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module CommonHelpers
|
2
|
+
def in_tmp_folder(&block)
|
3
|
+
FileUtils.chdir(@tmp_root, &block)
|
4
|
+
end
|
5
|
+
|
6
|
+
def in_project_folder(&block)
|
7
|
+
project_folder = @active_project_folder || @tmp_root
|
8
|
+
FileUtils.chdir(project_folder, &block)
|
9
|
+
end
|
10
|
+
|
11
|
+
def in_home_folder(&block)
|
12
|
+
FileUtils.chdir(@home_path, &block)
|
13
|
+
end
|
14
|
+
|
15
|
+
def force_local_lib_override(project_name = @project_name)
|
16
|
+
rakefile = File.read(File.join(project_name, 'Rakefile'))
|
17
|
+
File.open(File.join(project_name, 'Rakefile'), "w+") do |f|
|
18
|
+
f << "$:.unshift('#{@lib_path}')\n"
|
19
|
+
f << rakefile
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def setup_active_project_folder project_name
|
24
|
+
@active_project_folder = File.join(@tmp_root, project_name)
|
25
|
+
@project_name = project_name
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
World(CommonHelpers)
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/../../lib/backup_man"
|
2
|
+
|
3
|
+
gem 'cucumber'
|
4
|
+
require 'cucumber'
|
5
|
+
gem 'rspec'
|
6
|
+
require 'spec'
|
7
|
+
|
8
|
+
Before do
|
9
|
+
@tmp_root = File.dirname(__FILE__) + "/../../tmp"
|
10
|
+
@home_path = File.expand_path(File.join(@tmp_root, "home"))
|
11
|
+
FileUtils.rm_rf @tmp_root
|
12
|
+
FileUtils.mkdir_p @home_path
|
13
|
+
ENV['HOME'] = @home_path
|
14
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Matchers
|
2
|
+
def contain(expected)
|
3
|
+
simple_matcher("contain #{expected.inspect}") do |given, matcher|
|
4
|
+
matcher.failure_message = "expected #{given.inspect} to contain #{expected.inspect}"
|
5
|
+
matcher.negative_failure_message = "expected #{given.inspect} not to contain #{expected.inspect}"
|
6
|
+
given.index expected
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
World(Matchers)
|
data/lib/backup_man/backup.rb
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
require 'backup_man/backup_man'
|
2
2
|
require 'backup_man/dsl'
|
3
|
+
require 'fileutils'
|
3
4
|
|
4
5
|
module BackupMan
|
5
6
|
|
6
7
|
def self.log_end_of_operation
|
7
|
-
Log.
|
8
|
+
Log.info( "Finished #{self}." )
|
8
9
|
end
|
9
10
|
|
10
11
|
# we wanna log when the program ends
|
@@ -13,30 +14,32 @@ module BackupMan
|
|
13
14
|
|
14
15
|
# References: _Design Patterns in Ruby_ by Russ Olsen
|
15
16
|
class Backup
|
16
|
-
|
17
|
+
|
17
18
|
# the name of our backup set; this is used for generating a default
|
18
19
|
# backup_directory; e.g. use the hostname of the machine to backup
|
19
20
|
attr_reader :name
|
20
|
-
|
21
|
+
|
21
22
|
# where shall our backup data go (directory path)
|
22
23
|
attr_reader :backup_directory
|
23
|
-
|
24
|
+
|
24
25
|
# user name of the remote machine (defaults to 'root')
|
25
26
|
attr_accessor :user
|
26
|
-
|
27
|
+
|
27
28
|
# hostname of the remote machine (defaults to job definition name)
|
28
29
|
attr_accessor :host
|
29
|
-
|
30
|
+
|
30
31
|
# DSL for conditional execution
|
31
32
|
include DSL
|
32
|
-
|
33
|
+
|
33
34
|
def_dsl :onlyif
|
35
|
+
def_dsl_required :onlyif
|
36
|
+
|
34
37
|
def_dsl :backup, :data_sources
|
35
|
-
def_dsl :to, :backup_directory
|
36
|
-
def_dsl :user
|
37
|
-
def_dsl :host
|
38
|
-
|
39
|
-
|
38
|
+
def_dsl :to, :backup_directory, true
|
39
|
+
def_dsl :user, :user, true
|
40
|
+
def_dsl :host, :user, true
|
41
|
+
|
42
|
+
|
40
43
|
# this method sets all the default values but does not overwrite existing
|
41
44
|
# settings; this method cannot be called in the initializer of Backup
|
42
45
|
# because the default values are not available at that time;
|
@@ -55,50 +58,63 @@ module BackupMan
|
|
55
58
|
yield(self) if block_given?
|
56
59
|
BackupMan.instance.register_backup( self )
|
57
60
|
end
|
58
|
-
|
61
|
+
|
59
62
|
# calling this actually runs the backup; DO NOT override this; override
|
60
63
|
# _run instead
|
61
64
|
def run
|
62
65
|
log_begin_of_run
|
63
66
|
set_defaults
|
64
67
|
debug_log_dsl_info
|
68
|
+
unless missing_required_parameters.empty?
|
69
|
+
Log.error( "#{self}: A required parameter is missing: #{missing_required_parameters.join ' '}")
|
70
|
+
return
|
71
|
+
end
|
65
72
|
onlyif = eval( @onlyif )
|
66
|
-
Log.
|
73
|
+
Log.debug( "onlyif = { #{@onlyif} } evaluates #{onlyif}" )
|
67
74
|
if onlyif
|
68
75
|
unless @backup_directory
|
69
|
-
Log.
|
76
|
+
Log.error( "#{self}: No backup directory. Don't know where to store all this stuff.")
|
70
77
|
else
|
71
78
|
FileUtils.mkdir_p @backup_directory
|
72
79
|
_run
|
73
80
|
end
|
74
81
|
else
|
75
|
-
Log.
|
82
|
+
Log.info( "#{self}: Preconditions for backup run not fulfilled.")
|
76
83
|
end
|
77
84
|
log_end_of_run
|
78
85
|
end
|
79
|
-
|
86
|
+
|
80
87
|
# @return [String]
|
81
88
|
def to_s
|
82
89
|
"#{self.class} #{self.name}"
|
83
90
|
end
|
84
91
|
|
85
92
|
|
86
|
-
|
93
|
+
|
87
94
|
private
|
88
|
-
|
95
|
+
|
89
96
|
# @abstract override this to implement the actual backup commands
|
90
97
|
def _run
|
91
98
|
throw "Hey. Cannot run just 'Backup'."
|
92
99
|
end
|
93
|
-
|
100
|
+
|
101
|
+
# @return [Array of Strings] of missing parameters
|
102
|
+
def missing_required_parameters
|
103
|
+
missing = []
|
104
|
+
self.class.dsl_methods.each do |name, var, mandatory|
|
105
|
+
missing << name if mandatory && self.instance_variable_get("@#{var}").empty?
|
106
|
+
end
|
107
|
+
missing
|
108
|
+
end
|
109
|
+
|
94
110
|
# not used acutally
|
95
111
|
def log_begin_of_run
|
96
|
-
Log.
|
112
|
+
Log.info( "Starting #{self.class} run for #{@name}." )
|
97
113
|
end
|
98
|
-
|
114
|
+
|
99
115
|
# simply logs that the program terminates
|
100
116
|
def log_end_of_run
|
101
|
-
Log.
|
117
|
+
Log.info( "Finished #{self.class} run for #{@name}." )
|
102
118
|
end
|
103
119
|
|
104
120
|
# @return [String] the ssh command string including user@host
|
@@ -7,7 +7,7 @@ module BackupMan
|
|
7
7
|
|
8
8
|
include Singleton
|
9
9
|
|
10
|
-
attr_accessor :destdir, :ssh_app, :logfile, :lockdir
|
10
|
+
attr_accessor :destdir, :ssh_app, :logfile, :lockdir, :testing
|
11
11
|
|
12
12
|
def initialize
|
13
13
|
@backups = []
|
@@ -22,7 +22,7 @@ module BackupMan
|
|
22
22
|
begin
|
23
23
|
backup.run
|
24
24
|
rescue Interrupt
|
25
|
-
Log.
|
25
|
+
Log.warn( "Interrupt: Cancelling remaining operations.")
|
26
26
|
return
|
27
27
|
end
|
28
28
|
end
|
data/lib/backup_man/cli.rb
CHANGED
@@ -16,6 +16,7 @@ module BackupMan
|
|
16
16
|
|
17
17
|
options = {
|
18
18
|
:debug => false,
|
19
|
+
:testing => false,
|
19
20
|
:logpath => '/var/log/backup_man.log'
|
20
21
|
}
|
21
22
|
mandatory_options = %w( )
|
@@ -37,6 +38,9 @@ module BackupMan
|
|
37
38
|
}
|
38
39
|
opts.on("-h", "--help",
|
39
40
|
"Show this help message.") { stdout.puts opts; exit }
|
41
|
+
opts.on("-t", "--test", "Testing mode.", "No actions will be performed. Just to test if the config parses fine." ) {
|
42
|
+
options[:testing] = true
|
43
|
+
}
|
40
44
|
opts.parse!(arguments)
|
41
45
|
|
42
46
|
if mandatory_options && mandatory_options.find { |option| options[option.to_sym].nil? }
|
@@ -45,27 +49,28 @@ module BackupMan
|
|
45
49
|
end
|
46
50
|
|
47
51
|
# doing our stuff here
|
52
|
+
BackupMan.instance.testing = options[:testing]
|
48
53
|
|
49
54
|
# first we check if our logfile is writeable; if not, we give a warning
|
50
55
|
logfile = Pathname.new( options[:logpath] )
|
51
56
|
if (logfile.exist? && logfile.writable?) || logfile.parent.writable?
|
52
57
|
BackupMan.instance.logfile = logfile.to_s
|
53
58
|
else
|
54
|
-
Log.
|
59
|
+
Log.warn( "Log file is not writeable: #{logfile}.")
|
55
60
|
end
|
56
61
|
|
57
62
|
# root-warning
|
58
|
-
Log.
|
63
|
+
Log.warn( "Please do not run this program as root.") if `id -u`.strip == '0'
|
59
64
|
|
60
65
|
# reconfigure our Logger for debugging if necessary
|
61
66
|
if options[:debug]
|
62
|
-
Log.
|
63
|
-
Log.
|
67
|
+
Log.enable_debugmode
|
68
|
+
Log.debug( "Debugging mode enabled.")
|
64
69
|
end
|
65
70
|
|
66
71
|
|
67
72
|
unless ARGV[0]
|
68
|
-
Log.
|
73
|
+
Log.fatal( "No config file given." )
|
69
74
|
stdout.puts parser
|
70
75
|
exit 1
|
71
76
|
else
|
@@ -73,24 +78,30 @@ module BackupMan
|
|
73
78
|
config_filepath = Pathname.new "/etc/backup_man/#{config_filepath}" unless config_filepath.file?
|
74
79
|
|
75
80
|
unless config_filepath.file?
|
76
|
-
Log.
|
81
|
+
Log.fatal( "Config file not found [#{config_filepath}].")
|
77
82
|
exit 2
|
78
83
|
end
|
79
84
|
|
80
85
|
# get and evaluate the config file; errors in the config file may be
|
81
86
|
# difficult to debug, so be careful
|
82
|
-
Log.
|
83
|
-
|
87
|
+
Log.debug( "Reading file '#{config_filepath}'.")
|
88
|
+
|
89
|
+
begin
|
90
|
+
eval( File.read( config_filepath ) )
|
91
|
+
rescue NoMethodError
|
92
|
+
Log.fatal( $! )
|
93
|
+
exit 3
|
94
|
+
end
|
84
95
|
|
85
96
|
# configure global defaults
|
86
97
|
BackupMan.instance.set_default( :destdir, '/var/backups/backup_man')
|
87
98
|
BackupMan.instance.set_default( :lockdir, '/var/lock/backup_man')
|
88
99
|
BackupMan.instance.set_default( :ssh_app, 'ssh')
|
89
100
|
|
90
|
-
Log.
|
91
|
-
Log.
|
92
|
-
Log.
|
93
|
-
Log.
|
101
|
+
Log.debug( "Global settings:")
|
102
|
+
Log.debug( " DESTDIR = #{BackupMan.instance.destdir}")
|
103
|
+
Log.debug( " LOCKDIR = #{BackupMan.instance.lockdir}")
|
104
|
+
Log.debug( " SSH_APP = #{BackupMan.instance.ssh_app}")
|
94
105
|
|
95
106
|
# run the whole thing
|
96
107
|
BackupMan.instance.run
|
data/lib/backup_man/command.rb
CHANGED
@@ -37,16 +37,16 @@ module BackupMan
|
|
37
37
|
unless locked?
|
38
38
|
lock
|
39
39
|
begin
|
40
|
-
Log.
|
41
|
-
print `#{cmd}` unless
|
40
|
+
Log.info( "#{self}: Running.")
|
41
|
+
print `#{cmd}` unless BackupMan.instance.testing
|
42
42
|
rescue Interrupt
|
43
|
-
Log.
|
43
|
+
Log.warn( "#{self}: Operation interrupted.")
|
44
44
|
raise
|
45
45
|
ensure
|
46
46
|
unlock
|
47
47
|
end
|
48
48
|
else
|
49
|
-
Log.
|
49
|
+
Log.warn( "Command already running: #{self}." )
|
50
50
|
end
|
51
51
|
end
|
52
52
|
|
@@ -54,19 +54,19 @@ module BackupMan
|
|
54
54
|
private
|
55
55
|
|
56
56
|
def lock
|
57
|
-
Log.
|
57
|
+
Log.debug( "Locking command " + self.cmd )
|
58
58
|
unless locked?
|
59
59
|
f = File.new( self.lockfile, "w" )
|
60
60
|
# FIXME: command output shall go into the correct logging lvl (eg ERROR)
|
61
61
|
f.write(self.cmd)
|
62
62
|
f.close
|
63
63
|
else
|
64
|
-
Log.
|
64
|
+
Log.info( "Lockfile exists: " + lockfile )
|
65
65
|
end
|
66
66
|
end
|
67
67
|
|
68
68
|
def unlock
|
69
|
-
Log.
|
69
|
+
Log.debug( "Unlocking command " + self.cmd )
|
70
70
|
FileUtils.remove_file( self.lockfile )
|
71
71
|
end
|
72
72
|
|
data/lib/backup_man/dsl.rb
CHANGED
@@ -1,43 +1,62 @@
|
|
1
|
+
class Array
|
2
|
+
# @return [Array] a deep copy of self
|
3
|
+
def dclone
|
4
|
+
Marshal.load( Marshal.dump( self ) )
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
|
1
9
|
module BackupMan
|
2
10
|
|
3
11
|
# use "include DSL" to use this module
|
4
12
|
module DSL
|
5
|
-
|
13
|
+
|
6
14
|
# extend host class with class methods when we're included
|
7
15
|
def self.included(host_class)
|
8
16
|
host_class.extend(ClassMethods)
|
9
17
|
end
|
10
18
|
|
11
19
|
def debug_log_dsl_info
|
12
|
-
Log.
|
20
|
+
Log.debug( "Job settings:")
|
13
21
|
self.class.dsl_methods.each do |method, var|
|
14
|
-
Log.
|
22
|
+
Log.debug( " #{method} = #{self.instance_variable_get("@#{var}")}" )
|
15
23
|
end
|
16
24
|
end
|
17
25
|
|
18
26
|
module ClassMethods
|
19
|
-
|
20
|
-
|
27
|
+
|
28
|
+
# @param [String] name
|
29
|
+
# @param [String] variable name, used for internal storage
|
30
|
+
# @param [Boolean] mandatory, true if this var is a required parameter
|
31
|
+
def def_dsl( name, var = name, mandatory = false )
|
21
32
|
class_eval( %Q{
|
22
33
|
def #{name}( #{var} )
|
23
34
|
@#{var} = #{var}
|
24
35
|
end
|
25
36
|
})
|
26
|
-
register_dsl( name, var )
|
37
|
+
register_dsl( name, var, mandatory )
|
38
|
+
end
|
39
|
+
|
40
|
+
# @param [Symbol] name of required parameter
|
41
|
+
def def_dsl_required( required_name )
|
42
|
+
self.dsl_methods.each_index do |i|
|
43
|
+
if self.dsl_methods[i][0] == required_name
|
44
|
+
self.dsl_methods[i][2] = true
|
45
|
+
end
|
46
|
+
end
|
27
47
|
end
|
28
48
|
|
29
|
-
def register_dsl( name, var )
|
49
|
+
def register_dsl( name, var, mandatory )
|
30
50
|
@dsl_methods = [] if @dsl_methods.nil?
|
31
|
-
@dsl_methods << [name, var]
|
51
|
+
@dsl_methods << [name, var, mandatory]
|
32
52
|
end
|
33
53
|
|
34
54
|
# returns an array of all dsl methods of this and all superclasses
|
35
55
|
def dsl_methods
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
dsl_methods | @dsl_methods
|
56
|
+
# if we dont have anything yet, we copy from superclass (copying is
|
57
|
+
# necessary beacause we might change the :required setting)
|
58
|
+
@dsl_methods ||= self.superclass.dsl_methods.dclone
|
59
|
+
@dsl_methods
|
41
60
|
end
|
42
61
|
|
43
62
|
|
data/lib/backup_man/log.rb
CHANGED
@@ -39,10 +39,19 @@ module BackupMan
|
|
39
39
|
end
|
40
40
|
end
|
41
41
|
|
42
|
+
# globally enable debugmode for all outputters
|
42
43
|
def enable_debugmode
|
43
44
|
Log4r::Outputter.each_outputter { |outputter| outputter.level = DEBUG }
|
44
45
|
end
|
45
|
-
|
46
|
+
|
47
|
+
# we send all missing methods to our single logger instance
|
48
|
+
def self.method_missing(name, *args)
|
49
|
+
unless args.empty?
|
50
|
+
self.instance.send( name, args )
|
51
|
+
else
|
52
|
+
self.instance.send( name )
|
53
|
+
end
|
54
|
+
end
|
46
55
|
end
|
47
56
|
|
48
57
|
end
|
data/lib/backup_man/mysql.rb
CHANGED
data/lib/backup_man/rsync.rb
CHANGED
@@ -5,7 +5,10 @@ module BackupMan
|
|
5
5
|
class Rsync < Backup
|
6
6
|
|
7
7
|
# options for the rsync run (DSL)
|
8
|
+
def_dsl_required :backup
|
9
|
+
|
8
10
|
def_dsl :options
|
11
|
+
def_dsl_required :options
|
9
12
|
|
10
13
|
def set_defaults
|
11
14
|
@backup_directory = "#{BackupMan.instance.destdir}/#{@name}/rsync" unless @backup_directory
|
data/lib/backup_man/tar.rb
CHANGED
data/lib/backup_man.rb
CHANGED
data/spec/BackupMan_spec.rb
CHANGED
@@ -16,10 +16,10 @@ module BackupMan
|
|
16
16
|
argv = ["-l", "/tmp/does/not/exist.log"]
|
17
17
|
out = StringIO.new
|
18
18
|
$stderr = StringIO.new
|
19
|
-
lambda { ::BackupMan::CLI.execute( out,
|
19
|
+
lambda { ::BackupMan::CLI.execute( out, argv) }.should_not raise_error SystemExit
|
20
20
|
$stderr.string.should include("Log file is not writeable")
|
21
21
|
end
|
22
|
-
|
22
|
+
|
23
23
|
end
|
24
24
|
end
|
25
25
|
end
|
metadata
CHANGED
@@ -1,7 +1,12 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: BackupMan
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 2
|
9
|
+
version: 0.1.2
|
5
10
|
platform: ruby
|
6
11
|
authors:
|
7
12
|
- Markus Strauss
|
@@ -9,39 +14,65 @@ autorequire:
|
|
9
14
|
bindir: bin
|
10
15
|
cert_chain: []
|
11
16
|
|
12
|
-
date:
|
17
|
+
date: 2010-02-26 00:00:00 +01:00
|
13
18
|
default_executable: backup_man
|
14
19
|
dependencies:
|
15
20
|
- !ruby/object:Gem::Dependency
|
16
21
|
name: rspec
|
17
|
-
|
18
|
-
|
19
|
-
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
20
24
|
requirements:
|
21
25
|
- - ">="
|
22
26
|
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 1
|
29
|
+
- 2
|
30
|
+
- 9
|
23
31
|
version: 1.2.9
|
24
|
-
|
32
|
+
type: :development
|
33
|
+
version_requirements: *id001
|
25
34
|
- !ruby/object:Gem::Dependency
|
26
35
|
name: yard
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
segments:
|
42
|
+
- 0
|
43
|
+
- 4
|
44
|
+
- 0
|
45
|
+
version: 0.4.0
|
27
46
|
type: :development
|
28
|
-
|
29
|
-
|
47
|
+
version_requirements: *id002
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: cucumber
|
50
|
+
prerelease: false
|
51
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
30
52
|
requirements:
|
31
53
|
- - ">="
|
32
54
|
- !ruby/object:Gem::Version
|
33
|
-
|
34
|
-
|
55
|
+
segments:
|
56
|
+
- 0
|
57
|
+
- 4
|
58
|
+
- 4
|
59
|
+
version: 0.4.4
|
60
|
+
type: :development
|
61
|
+
version_requirements: *id003
|
35
62
|
- !ruby/object:Gem::Dependency
|
36
63
|
name: log4r
|
37
|
-
|
38
|
-
|
39
|
-
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
prerelease: false
|
65
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
40
66
|
requirements:
|
41
67
|
- - ">="
|
42
68
|
- !ruby/object:Gem::Version
|
69
|
+
segments:
|
70
|
+
- 1
|
71
|
+
- 1
|
72
|
+
- 2
|
43
73
|
version: 1.1.2
|
44
|
-
|
74
|
+
type: :runtime
|
75
|
+
version_requirements: *id004
|
45
76
|
description: 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.
|
46
77
|
email: Markus@ITstrauss.eu
|
47
78
|
executables:
|
@@ -61,6 +92,16 @@ files:
|
|
61
92
|
- VERSION
|
62
93
|
- bin/backup_man
|
63
94
|
- examples/example-host-config
|
95
|
+
- features/development.feature
|
96
|
+
- features/dsl_parameters.feature
|
97
|
+
- features/logfile.feature
|
98
|
+
- features/overview.feature
|
99
|
+
- features/step_definitions/common_steps.rb
|
100
|
+
- features/step_definitions/dsl_parameters_steps.rb
|
101
|
+
- features/step_definitions/logfile_steps.rb
|
102
|
+
- features/support/common.rb
|
103
|
+
- features/support/env.rb
|
104
|
+
- features/support/matchers.rb
|
64
105
|
- lib/backup_man.rb
|
65
106
|
- lib/backup_man/backup.rb
|
66
107
|
- lib/backup_man/backup_man.rb
|
@@ -87,18 +128,20 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
87
128
|
requirements:
|
88
129
|
- - ">="
|
89
130
|
- !ruby/object:Gem::Version
|
131
|
+
segments:
|
132
|
+
- 0
|
90
133
|
version: "0"
|
91
|
-
version:
|
92
134
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
93
135
|
requirements:
|
94
136
|
- - ">="
|
95
137
|
- !ruby/object:Gem::Version
|
138
|
+
segments:
|
139
|
+
- 0
|
96
140
|
version: "0"
|
97
|
-
version:
|
98
141
|
requirements: []
|
99
142
|
|
100
143
|
rubyforge_project: backupman
|
101
|
-
rubygems_version: 1.3.
|
144
|
+
rubygems_version: 1.3.6
|
102
145
|
signing_key:
|
103
146
|
specification_version: 3
|
104
147
|
summary: A tool for system administrators to easily configure pull-over-SSH backups.
|