webtranslateit-safe 0.4.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.
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