webtranslateit-safe 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/.autotest +3 -0
  3. data/.document +5 -0
  4. data/.github/dependabot.yml +26 -0
  5. data/.github/release-drafter.yml +36 -0
  6. data/.github/workflows/ci.yml +51 -0
  7. data/.github/workflows/release-drafter.yml +29 -0
  8. data/.gitignore +18 -0
  9. data/.rspec +3 -0
  10. data/.rubocop.yml +8 -0
  11. data/.rubocop_todo.yml +552 -0
  12. data/CHANGELOG +42 -0
  13. data/Gemfile +11 -0
  14. data/Gemfile.lock +89 -0
  15. data/LICENSE.txt +22 -0
  16. data/README.markdown +237 -0
  17. data/Rakefile +8 -0
  18. data/TODO +31 -0
  19. data/bin/webtranslateit-safe +64 -0
  20. data/lib/extensions/mktmpdir.rb +45 -0
  21. data/lib/webtranslateit/safe/archive.rb +29 -0
  22. data/lib/webtranslateit/safe/backup.rb +27 -0
  23. data/lib/webtranslateit/safe/cloudfiles.rb +77 -0
  24. data/lib/webtranslateit/safe/config/builder.rb +100 -0
  25. data/lib/webtranslateit/safe/config/node.rb +79 -0
  26. data/lib/webtranslateit/safe/ftp.rb +85 -0
  27. data/lib/webtranslateit/safe/gpg.rb +52 -0
  28. data/lib/webtranslateit/safe/gzip.rb +29 -0
  29. data/lib/webtranslateit/safe/local.rb +55 -0
  30. data/lib/webtranslateit/safe/mongodump.rb +30 -0
  31. data/lib/webtranslateit/safe/mysqldump.rb +36 -0
  32. data/lib/webtranslateit/safe/pgdump.rb +36 -0
  33. data/lib/webtranslateit/safe/pipe.rb +23 -0
  34. data/lib/webtranslateit/safe/s3.rb +80 -0
  35. data/lib/webtranslateit/safe/sftp.rb +96 -0
  36. data/lib/webtranslateit/safe/sink.rb +40 -0
  37. data/lib/webtranslateit/safe/source.rb +51 -0
  38. data/lib/webtranslateit/safe/stream.rb +40 -0
  39. data/lib/webtranslateit/safe/svndump.rb +17 -0
  40. data/lib/webtranslateit/safe/tmp_file.rb +53 -0
  41. data/lib/webtranslateit/safe/version.rb +9 -0
  42. data/lib/webtranslateit/safe.rb +70 -0
  43. data/spec/integration/archive_integration_spec.rb +89 -0
  44. data/spec/integration/cleanup_spec.rb +62 -0
  45. data/spec/spec_helper.rb +7 -0
  46. data/spec/webtranslateit/safe/archive_spec.rb +67 -0
  47. data/spec/webtranslateit/safe/cloudfiles_spec.rb +175 -0
  48. data/spec/webtranslateit/safe/config_spec.rb +307 -0
  49. data/spec/webtranslateit/safe/gpg_spec.rb +148 -0
  50. data/spec/webtranslateit/safe/gzip_spec.rb +64 -0
  51. data/spec/webtranslateit/safe/local_spec.rb +109 -0
  52. data/spec/webtranslateit/safe/mongodump_spec.rb +54 -0
  53. data/spec/webtranslateit/safe/mysqldump_spec.rb +83 -0
  54. data/spec/webtranslateit/safe/pgdump_spec.rb +45 -0
  55. data/spec/webtranslateit/safe/s3_spec.rb +168 -0
  56. data/spec/webtranslateit/safe/svndump_spec.rb +39 -0
  57. data/templates/script.rb +183 -0
  58. data/webtranslateit-safe.gemspec +32 -0
  59. metadata +149 -0
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2010-2013 Astrails Ltd.
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,237 @@
1
+ # webtranslateit-safe
2
+
3
+ Simple database and filesystem backups with S3 and Rackspace Cloud Files support (with optional encryption)
4
+
5
+ * Code: [http://github.com/webtranslateit/safe](http://github.com/webtranslateit/safe)
6
+
7
+ ## Motivation
8
+
9
+ We needed a backup solution that will satisfy the following requirements:
10
+
11
+ * opensource
12
+ * simple to install and configure
13
+ * support for simple ‘tar’ backups of directories (with includes/excludes)
14
+ * support for simple mysqldump of mysql databases
15
+ * support for symmetric or public key encryption
16
+ * support for local filesystem, Amazon S3, and Rackspace Cloud Files for storage
17
+ * support for backup rotation. we don’t want backups filling all the diskspace or cost a fortune on S3 or Cloud Files
18
+
19
+ And since we didn't find any, we wrote our own :)
20
+
21
+ ## Contributions
22
+
23
+ The following functionality was contributed by webtranslateit-safe users:
24
+
25
+ * PostgreSQL dump using `pg_dump` (by Mark Mansour <mark@stateofflux.com>)
26
+ * Subversion dump using svndump (by Richard Luther <richard.luther@gmail.com>)
27
+ * SFTP remote storage (by Adam <adam@mediadrive.ca>)
28
+ * benchmarking output (By Neer)
29
+ * README fixes (by Bobby Wilson)
30
+ * improved config file parsing (by Fedor Kocherga <fkocherga@gmail.com>)
31
+ * mysql password file quoting (by Jonathan Sutherland <jonathan.sutherland@gmail.com>)
32
+ * Rackspace Cloud Files support (by H. Wade Minter <minter@lunenburg.org>)
33
+ * Plan FTP support (by seroy <seroy@bk.ru>)
34
+ * mongodump support (by Matt Berther <matt@mattberther.com>)
35
+
36
+ Thanks to all :)
37
+
38
+ ## Installation
39
+
40
+ gem install webtranslateit-safe
41
+
42
+ ## Reporting problems
43
+
44
+ Please report problems at the [Issues tracker](http://github.com/webtranslateit/safe/issues)
45
+
46
+ ## Usage
47
+
48
+ Usage:
49
+ webtranslateit-safe [OPTIONS] CONFIG_FILE
50
+ Options:
51
+ -h, --help This help screen
52
+ -v, --verbose be verbose, duh!
53
+ -n, --dry-run just pretend, don't do anything.
54
+ -L, --local skip remote storage, only do local backups
55
+
56
+ Note: CONFIG\_FILE will be created from template if missing
57
+
58
+ ## Encryption
59
+
60
+ If you want to encrypt your backups you have 2 options:
61
+ * use simple password encryption
62
+ * use GPG public key encryption
63
+
64
+ > IMPORTANT: some gpg installations automatically set 'use-agent' option in the default
65
+ > configuration file that is created when you run gpg for the first time. This will cause
66
+ > gpg to fail on the 2nd run if you don't have the agent running. The result is that
67
+ > 'webtranslateit-safe' will work ONCE when you manually test it and then fail on any subsequent run.
68
+ > The solution is to remove the 'use-agent' from the config file (usually /root/.gnupg/gpg.conf)
69
+ > To mitigate this problem for the gpg 1.x series '--no-use-agent' option is added by defaults
70
+ > to the autogenerated config file, but for gpg2 is doesn't work. as the manpage says it:
71
+ > "This is dummy option. gpg2 always requires the agent." :(
72
+
73
+ For simple password, just add password entry in gpg section.
74
+ For public key encryption you will need to create a public/secret keypair.
75
+
76
+ We recommend to create your GPG keys only on your local machine and then
77
+ transfer your public key to the server that will do the backups.
78
+
79
+ This way the server will only know how to encrypt the backups but only you
80
+ will be able to decrypt them using the secret key you have locally. Of course
81
+ you MUST backup your backup encryption key :)
82
+ We recommend also pringing the hard paper copy of your GPG key 'just in case'.
83
+
84
+ The procedure to create and transfer the key is as follows:
85
+
86
+ 1. run 'gpg --gen-key' on your local machine and follow onscreen instructions to create the key
87
+ (you can accept all the defaults).
88
+
89
+ 2. extract your public key into a file (assuming you used test@example.com as your key email):
90
+ `gpg -a --export test@example.com > test@example.com.pub`
91
+
92
+ 3. transfer public key to the server
93
+ `scp test@example.com.pub root@example.com:`
94
+
95
+ 4. import public key on the remote system:
96
+
97
+ $ gpg --import test@example.com.pub
98
+ gpg: key 45CA9403: public key "Test Backup <test@example.com>" imported
99
+ gpg: Total number processed: 1
100
+ gpg: imported: 1
101
+
102
+ 5. since we don't keep the secret part of the key on the remote server, gpg has
103
+ no way to know its yours and can be trusted.
104
+ To fix that we can sign it with other trusted key, or just directly modify its
105
+ trust level in gpg (use level 5):
106
+
107
+ $ gpg --edit-key test@example.com
108
+ ...
109
+ Command> trust
110
+ ...
111
+ 1 = I don't know or won't say
112
+ 2 = I do NOT trust
113
+ 3 = I trust marginally
114
+ 4 = I trust fully
115
+ 5 = I trust ultimately
116
+ m = back to the main menu
117
+
118
+ Your decision? 5
119
+ ...
120
+ Command> quit
121
+
122
+ 6. export your secret key for backup
123
+ (we recommend to print it on paper and burn to a CD/DVD and store in a safe place):
124
+
125
+ $ gpg -a --export-secret-key test@example.com > test@example.com.key
126
+
127
+
128
+
129
+ ## Example configuration
130
+
131
+ safe do
132
+ verbose true
133
+
134
+ local :path => "/backup/:kind/:id"
135
+
136
+ s3 do
137
+ key "...................."
138
+ secret "........................................"
139
+ bucket "backup.astrails.com"
140
+ path "servers/alpha/:kind/:id"
141
+ end
142
+
143
+ cloudfiles do
144
+ user "..........."
145
+ api_key "................................."
146
+ container "safe_backup"
147
+ path ":kind/" # this is default
148
+ service_net false
149
+ end
150
+
151
+ sftp do
152
+ host "sftp.astrails.com"
153
+ user "astrails"
154
+ # port 8023
155
+ password "ssh password for sftp"
156
+ end
157
+
158
+ gpg do
159
+ command "/usr/local/bin/gpg"
160
+ options "--no-use-agent"
161
+ # symmetric encryption key
162
+ # password "qwe"
163
+
164
+ # public GPG key (must be known to GPG, i.e. be on the keyring)
165
+ key "backup@astrails.com"
166
+ end
167
+
168
+ keep do
169
+ local 20
170
+ s3 100
171
+ cloudfiles 100
172
+ sftp 100
173
+ end
174
+
175
+ mysqldump do
176
+ options "-ceKq --single-transaction --create-options"
177
+
178
+ user "root"
179
+ password "............"
180
+ socket "/var/run/mysqld/mysqld.sock"
181
+
182
+ database :blog
183
+ database :servershape
184
+ database :astrails_com
185
+ database :secret_project_com do
186
+ skip_tables "foo"
187
+ skip_tables ["bar", "baz"]
188
+ end
189
+
190
+ end
191
+
192
+ svndump do
193
+ repo :my_repo do
194
+ repo_path "/home/svn/my_repo"
195
+ end
196
+ end
197
+
198
+ pgdump do
199
+ options "-i -x -O" # -i => ignore version, -x => do not dump privileges (grant/revoke), -O => skip restoration of object ownership in plain text format
200
+
201
+ user "username"
202
+ password "............" # shouldn't be used, instead setup ident. Current functionality exports a password env to the shell which pg_dump uses - untested!
203
+
204
+ database :blog
205
+ database :stateofflux_com
206
+ end
207
+
208
+ tar do
209
+ options "-h" # dereference symlinks
210
+ archive "git-repositories", :files => "/home/git/repositories"
211
+ archive "dot-configs", :files => "/home/*/.[^.]*"
212
+ archive "etc", :files => "/etc", :exclude => "/etc/puppet/other"
213
+
214
+ archive "blog-astrails-com" do
215
+ files "/var/www/blog.astrails.com/"
216
+ exclude "/var/www/blog.astrails.com/log"
217
+ exclude "/var/www/blog.astrails.com/tmp"
218
+ end
219
+
220
+ archive "astrails-com" do
221
+ files "/var/www/astrails.com/"
222
+ exclude ["/var/www/astrails.com/log", "/var/www/astrails.com/tmp"]
223
+ end
224
+ end
225
+ end
226
+
227
+ ## Contributing
228
+
229
+ 1. Fork it
230
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
231
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
232
+ 4. Push to the branch (`git push origin my-new-feature`)
233
+ 5. Create new Pull Request
234
+
235
+ ## Copyright
236
+
237
+ Copyright (c) 2010-2023 WebTranslateIt Software SL. See LICENSE.txt for details.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ require 'rspec/core/rake_task'
4
+
5
+ desc 'run specs'
6
+ RSpec::Core::RakeTask.new
7
+
8
+ task default: :spec
data/TODO ADDED
@@ -0,0 +1,31 @@
1
+ - refactor
2
+ - refactor out global variables. pass a config object around instead
3
+ - common logging
4
+ - remove 1.8.6 support
5
+ - module registry
6
+ - base => prefix ?
7
+ - move requires into specific modules
8
+ - config.foo instead of config[:foo]
9
+
10
+ - features
11
+ - remote-only s3 support
12
+ - generic notifier support
13
+ - email notifier
14
+ - hipchat
15
+ - generic error notifier support
16
+ - email
17
+ - hipchat
18
+
19
+
20
+
21
+ - add 'silent'
22
+ - handle errors from mysqldump
23
+ - check that gpg is installed
24
+ - support percona XtraBackup as an option instead of mysqldump [patches anyone :) ?]
25
+ - backup validation:
26
+ - support for 'minsize' opition in backup that will check that produced backup is at least the expected size
27
+ this should catch many backup failure scenarious (like broken mysql connection, insufficient disk space etc.
28
+ - support differencial backups
29
+ - it should be fairly easy for filesystem backups using tar's built in incremental functionality.
30
+ - for mysql need to use XtraBackup
31
+ - or we can keep the previous dump locally and store only diff with the latest dump
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+
5
+ require 'webtranslateit/safe'
6
+
7
+ include WebTranslateIt::Safe
8
+
9
+ def die(msg)
10
+ puts "ERROR: #{msg}"
11
+ exit 1
12
+ end
13
+
14
+ def usage
15
+ puts <<~END
16
+ Usage: webtranslateit-safe [OPTIONS] CONFIG_FILE
17
+ Options:
18
+ -h, --help This help screen
19
+ -v, --verbose be verbose, duh!
20
+ -n, --dry-run just pretend, don't do anything.
21
+ -L, --local skip S3 and Cloud Files
22
+
23
+ Note: config file will be created from template if missing
24
+ END
25
+ exit 1
26
+ end
27
+
28
+ OPTS = [
29
+ '-h', '--help',
30
+ '-v', '--verbose', '--not-verbose',
31
+ '-n', '--dry-run', '--not-dry-run',
32
+ '-L', '--local', '--not-local'
33
+ ].freeze
34
+ def main
35
+ opts = ARGV & OPTS
36
+ args = ARGV - OPTS
37
+
38
+ usage unless args.first
39
+ usage if opts.delete('-h') || opts.delete('--help')
40
+
41
+ config_file = File.expand_path(args.first)
42
+
43
+ is_dry = (opts.delete('-n') || opts.delete('--dry-run')) && !opts.delete('--not-dry-run')
44
+ is_verbose = (opts.delete('-v') || opts.delete('--verbose')) && !opts.delete('--not-verbose')
45
+ is_local_only = (opts.delete('-L') || opts.delete('--local')) && !opts.delete('--not-local')
46
+
47
+ unless File.exist?(config_file)
48
+ die 'Missing configuration file. NOT CREATED! Rerun w/o the -n argument to create a template configuration file.' if is_dry
49
+
50
+ FileUtils.cp File.join(WebTranslateIt::Safe::ROOT, 'templates', 'script.rb'), config_file
51
+
52
+ die "Created default #{config_file}. Please edit and run again."
53
+ end
54
+
55
+ config = eval(File.read(config_file))
56
+
57
+ config[:verbose] = is_verbose
58
+ config[:dry_run] = is_dry
59
+ config[:local_only] = is_local_only
60
+
61
+ process config
62
+ end
63
+
64
+ main
@@ -0,0 +1,45 @@
1
+ require 'tmpdir'
2
+
3
+ unless Dir.respond_to?(:mktmpdir)
4
+ # backward compat for 1.8.6
5
+ class Dir
6
+ def Dir.mktmpdir(prefix_suffix=nil, tmpdir=nil)
7
+ case prefix_suffix
8
+ when nil
9
+ prefix = 'd'
10
+ suffix = ''
11
+ when String
12
+ prefix = prefix_suffix
13
+ suffix = ''
14
+ when Array
15
+ prefix = prefix_suffix[0]
16
+ suffix = prefix_suffix[1]
17
+ else
18
+ raise ArgumentError, "unexpected prefix_suffix: #{prefix_suffix.inspect}"
19
+ end
20
+ tmpdir ||= Dir.tmpdir
21
+ t = Time.now.strftime('%Y%m%d')
22
+ n = nil
23
+ begin
24
+ path = "#{tmpdir}/#{prefix}#{t}-#{$$}-#{rand(0x100000000).to_s(36)}"
25
+ path << "-#{n}" if n
26
+ path << suffix
27
+ Dir.mkdir(path, 0700)
28
+ rescue Errno::EEXIST
29
+ n ||= 0
30
+ n += 1
31
+ retry
32
+ end
33
+
34
+ if block_given?
35
+ begin
36
+ yield path
37
+ ensure
38
+ FileUtils.remove_entry_secure path
39
+ end
40
+ else
41
+ path
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,29 @@
1
+ module WebTranslateIt
2
+
3
+ module Safe
4
+
5
+ class Archive < Source
6
+
7
+ def command
8
+ "tar -cf - #{config[:options]} #{tar_exclude_files} #{tar_files}"
9
+ end
10
+
11
+ def extension = '.tar'
12
+
13
+ protected
14
+
15
+ def tar_exclude_files
16
+ [*config[:exclude]].compact.map { |x| "--exclude=#{x}" }.join(' ')
17
+ end
18
+
19
+ def tar_files
20
+ raise 'missing files for tar' unless config[:files]
21
+
22
+ [*config[:files]].map(&:strip).join(' ')
23
+ end
24
+
25
+ end
26
+
27
+ end
28
+
29
+ end
@@ -0,0 +1,27 @@
1
+ module WebTranslateIt
2
+
3
+ module Safe
4
+
5
+ class Backup
6
+
7
+ attr_accessor :id, :kind, :filename, :extension, :command, :compressed, :timestamp, :path
8
+
9
+ def initialize(opts = {})
10
+ opts.each do |k, v|
11
+ send("#{k}=", v)
12
+ end
13
+ end
14
+
15
+ def run(config, *mods)
16
+ mods.each do |mod|
17
+ mod = mod.to_s
18
+ mod[0] = mod[0..0].upcase
19
+ WebTranslateIt::Safe.const_get(mod).new(config, self).process
20
+ end
21
+ end
22
+
23
+ end
24
+
25
+ end
26
+
27
+ end
@@ -0,0 +1,77 @@
1
+ module WebTranslateIt
2
+ module Safe
3
+ class Cloudfiles < Sink
4
+ MAX_CLOUDFILES_FILE_SIZE = 5368709120
5
+
6
+ def active?
7
+ container && user && api_key
8
+ end
9
+
10
+ protected
11
+
12
+ def path
13
+ @path ||= expand(config[:cloudfiles, :path] || config[:local, :path] || ':kind/:id')
14
+ end
15
+
16
+ # UGLY: we need this function for the reason that
17
+ # we can't double mock on ruby 1.9.2, duh!
18
+ # so we created this func to mock it all together
19
+ def get_file_size(path)
20
+ File.stat(path).size
21
+ end
22
+
23
+ def save
24
+ raise RuntimeError, 'pipe-streaming not supported for S3.' unless @backup.path
25
+
26
+ # needed in cleanup even on dry run
27
+ cf = CloudFiles::Connection.new(user, api_key, true, service_net) unless local_only?
28
+ puts "Uploading #{container}:#{full_path} from #{@backup.path}" if verbose? || dry_run?
29
+ unless dry_run? || local_only?
30
+ if get_file_size(@backup.path) > MAX_CLOUDFILES_FILE_SIZE
31
+ STDERR.puts "ERROR: File size exceeds maximum allowed for upload to Cloud Files (#{MAX_CLOUDFILES_FILE_SIZE}): #{@backup.path}"
32
+ return
33
+ end
34
+ benchmark = Benchmark.realtime do
35
+ cf_container = cf.create_container(container)
36
+ o = cf_container.create_object(full_path,true)
37
+ o.write(File.open(@backup.path))
38
+ end
39
+ puts '...done' if verbose?
40
+ puts('Upload took ' + sprintf('%.2f', benchmark) + ' second(s).') if verbose?
41
+ end
42
+ end
43
+
44
+ def cleanup
45
+ return if local_only?
46
+
47
+ return unless keep = config[:keep, :cloudfiles]
48
+
49
+ puts "listing files: #{container}:#{base}*" if verbose?
50
+ cf = CloudFiles::Connection.new(user, api_key, true, service_net) unless local_only?
51
+ cf_container = cf.container(container)
52
+ files = cf_container.objects(:prefix => base).sort
53
+
54
+ cleanup_with_limit(files, keep) do |f|
55
+ puts "removing Cloud File #{container}:#{f}" if dry_run? || verbose?
56
+ cf_container.delete_object(f) unless dry_run? || local_only?
57
+ end
58
+ end
59
+
60
+ def container
61
+ config[:cloudfiles, :container]
62
+ end
63
+
64
+ def user
65
+ config[:cloudfiles, :user]
66
+ end
67
+
68
+ def api_key
69
+ config[:cloudfiles, :api_key]
70
+ end
71
+
72
+ def service_net
73
+ config[:cloudfiles, :service_net] || false
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,100 @@
1
+ module WebTranslateIt
2
+
3
+ module Safe
4
+
5
+ module Config
6
+
7
+ class Builder
8
+
9
+ def initialize(node, data = {})
10
+ @node = node
11
+ data.each { |k, v| send k, v }
12
+ end
13
+
14
+
15
+ class << self
16
+
17
+ def simple_value(*names)
18
+ names.each do |m|
19
+ define_method(m) do |value|
20
+ ensure_uniq(m)
21
+ @node.set m, value
22
+ end
23
+ end
24
+ end
25
+
26
+ def multi_value(*names)
27
+ names.each do |m|
28
+ define_method(m) do |value|
29
+ value = value.map(&:to_s) if value.is_a?(Array)
30
+ @node.set_multi m, value
31
+ end
32
+ end
33
+ end
34
+
35
+ def hash_value(*names)
36
+ names.each do |m|
37
+ define_method(m) do |data = {}, &block|
38
+ ensure_uniq(m)
39
+ ensure_hash(m, data)
40
+ @node.set m, Node.new(@node, data || {}, &block)
41
+ end
42
+ end
43
+ end
44
+
45
+ def mixed_value(*names)
46
+ names.each do |m|
47
+ define_method(m) do |data = {}, &block|
48
+ ensure_uniq(m)
49
+ if data.is_a?(Hash) || block
50
+ ensure_hash(m, data) if block
51
+ @node.set m, Node.new(@node, data, &block)
52
+ else
53
+ @node.set m, data
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ def collection(*names)
60
+ names.each do |m|
61
+ define_method(m) do |id, data = {}, &block|
62
+ raise "bad collection id: #{id.inspect}" unless id
63
+
64
+ ensure_hash(m, data)
65
+
66
+ name = "#{m}s"
67
+ collection = @node.get(name) || @node.set(name, Node.new(@node, {}))
68
+ collection.set id, Node.new(collection, data, &block)
69
+ end
70
+ end
71
+ end
72
+
73
+ end
74
+
75
+ simple_value :verbose, :dry_run, :local_only, :path, :command,
76
+ :options, :user, :host, :port, :password, :key, :secret, :bucket,
77
+ :api_key, :container, :socket, :service_net, :repo_path
78
+ multi_value :skip_tables, :exclude, :files
79
+ hash_value :mysqldump, :tar, :gpg, :keep, :pgdump, :tar, :svndump,
80
+ :sftp, :ftp, :mongodump
81
+ mixed_value :s3, :local, :cloudfiles
82
+ collection :database, :archive, :repo
83
+
84
+ private
85
+
86
+ def ensure_uniq(m)
87
+ raise(ArgumentError, "duplicate value for '#{m}'") if @node.get(m)
88
+ end
89
+
90
+ def ensure_hash(k, v)
91
+ raise "#{k}: hash expected: #{v.inspect}" unless v.is_a?(Hash)
92
+ end
93
+
94
+ end
95
+
96
+ end
97
+
98
+ end
99
+
100
+ end