BackupMan 0.1.1 → 0.1.2
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/.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.
|