ext_backup 5.0.0.beta.2.1

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 (137) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +19 -0
  3. data/README.md +33 -0
  4. data/bin/backup +5 -0
  5. data/bin/docker_test +24 -0
  6. data/lib/backup.rb +140 -0
  7. data/lib/backup/archive.rb +169 -0
  8. data/lib/backup/binder.rb +18 -0
  9. data/lib/backup/cleaner.rb +112 -0
  10. data/lib/backup/cli.rb +370 -0
  11. data/lib/backup/cloud_io/base.rb +38 -0
  12. data/lib/backup/cloud_io/cloud_files.rb +296 -0
  13. data/lib/backup/cloud_io/s3.rb +253 -0
  14. data/lib/backup/compressor/base.rb +32 -0
  15. data/lib/backup/compressor/bzip2.rb +35 -0
  16. data/lib/backup/compressor/custom.rb +49 -0
  17. data/lib/backup/compressor/gzip.rb +73 -0
  18. data/lib/backup/config.rb +128 -0
  19. data/lib/backup/config/dsl.rb +102 -0
  20. data/lib/backup/config/helpers.rb +137 -0
  21. data/lib/backup/database/base.rb +86 -0
  22. data/lib/backup/database/mongodb.rb +186 -0
  23. data/lib/backup/database/mysql.rb +191 -0
  24. data/lib/backup/database/openldap.rb +93 -0
  25. data/lib/backup/database/postgresql.rb +132 -0
  26. data/lib/backup/database/redis.rb +176 -0
  27. data/lib/backup/database/riak.rb +79 -0
  28. data/lib/backup/database/sqlite.rb +55 -0
  29. data/lib/backup/encryptor/base.rb +27 -0
  30. data/lib/backup/encryptor/gpg.rb +737 -0
  31. data/lib/backup/encryptor/open_ssl.rb +74 -0
  32. data/lib/backup/errors.rb +53 -0
  33. data/lib/backup/logger.rb +197 -0
  34. data/lib/backup/logger/console.rb +48 -0
  35. data/lib/backup/logger/fog_adapter.rb +25 -0
  36. data/lib/backup/logger/logfile.rb +131 -0
  37. data/lib/backup/logger/syslog.rb +114 -0
  38. data/lib/backup/model.rb +472 -0
  39. data/lib/backup/notifier/base.rb +126 -0
  40. data/lib/backup/notifier/campfire.rb +61 -0
  41. data/lib/backup/notifier/command.rb +99 -0
  42. data/lib/backup/notifier/datadog.rb +104 -0
  43. data/lib/backup/notifier/flowdock.rb +99 -0
  44. data/lib/backup/notifier/hipchat.rb +116 -0
  45. data/lib/backup/notifier/http_post.rb +114 -0
  46. data/lib/backup/notifier/mail.rb +232 -0
  47. data/lib/backup/notifier/nagios.rb +65 -0
  48. data/lib/backup/notifier/pagerduty.rb +79 -0
  49. data/lib/backup/notifier/prowl.rb +68 -0
  50. data/lib/backup/notifier/pushover.rb +71 -0
  51. data/lib/backup/notifier/ses.rb +123 -0
  52. data/lib/backup/notifier/slack.rb +147 -0
  53. data/lib/backup/notifier/twitter.rb +55 -0
  54. data/lib/backup/notifier/zabbix.rb +60 -0
  55. data/lib/backup/package.rb +51 -0
  56. data/lib/backup/packager.rb +106 -0
  57. data/lib/backup/pipeline.rb +120 -0
  58. data/lib/backup/splitter.rb +73 -0
  59. data/lib/backup/storage/base.rb +66 -0
  60. data/lib/backup/storage/cloud_files.rb +156 -0
  61. data/lib/backup/storage/cycler.rb +70 -0
  62. data/lib/backup/storage/dropbox.rb +206 -0
  63. data/lib/backup/storage/ftp.rb +116 -0
  64. data/lib/backup/storage/local.rb +61 -0
  65. data/lib/backup/storage/qiniu.rb +65 -0
  66. data/lib/backup/storage/rsync.rb +246 -0
  67. data/lib/backup/storage/s3.rb +155 -0
  68. data/lib/backup/storage/scp.rb +65 -0
  69. data/lib/backup/storage/sftp.rb +80 -0
  70. data/lib/backup/syncer/base.rb +67 -0
  71. data/lib/backup/syncer/cloud/base.rb +176 -0
  72. data/lib/backup/syncer/cloud/cloud_files.rb +81 -0
  73. data/lib/backup/syncer/cloud/local_file.rb +97 -0
  74. data/lib/backup/syncer/cloud/s3.rb +109 -0
  75. data/lib/backup/syncer/rsync/base.rb +50 -0
  76. data/lib/backup/syncer/rsync/local.rb +27 -0
  77. data/lib/backup/syncer/rsync/pull.rb +47 -0
  78. data/lib/backup/syncer/rsync/push.rb +201 -0
  79. data/lib/backup/template.rb +41 -0
  80. data/lib/backup/utilities.rb +233 -0
  81. data/lib/backup/version.rb +3 -0
  82. data/lib/ext_backup.rb +5 -0
  83. data/lib/ext_backup/version.rb +5 -0
  84. data/templates/cli/archive +28 -0
  85. data/templates/cli/compressor/bzip2 +4 -0
  86. data/templates/cli/compressor/custom +7 -0
  87. data/templates/cli/compressor/gzip +4 -0
  88. data/templates/cli/config +123 -0
  89. data/templates/cli/databases/mongodb +15 -0
  90. data/templates/cli/databases/mysql +18 -0
  91. data/templates/cli/databases/openldap +24 -0
  92. data/templates/cli/databases/postgresql +16 -0
  93. data/templates/cli/databases/redis +16 -0
  94. data/templates/cli/databases/riak +17 -0
  95. data/templates/cli/databases/sqlite +11 -0
  96. data/templates/cli/encryptor/gpg +27 -0
  97. data/templates/cli/encryptor/openssl +9 -0
  98. data/templates/cli/model +26 -0
  99. data/templates/cli/notifier/zabbix +15 -0
  100. data/templates/cli/notifiers/campfire +12 -0
  101. data/templates/cli/notifiers/command +32 -0
  102. data/templates/cli/notifiers/datadog +57 -0
  103. data/templates/cli/notifiers/flowdock +16 -0
  104. data/templates/cli/notifiers/hipchat +16 -0
  105. data/templates/cli/notifiers/http_post +32 -0
  106. data/templates/cli/notifiers/mail +24 -0
  107. data/templates/cli/notifiers/nagios +13 -0
  108. data/templates/cli/notifiers/pagerduty +12 -0
  109. data/templates/cli/notifiers/prowl +11 -0
  110. data/templates/cli/notifiers/pushover +11 -0
  111. data/templates/cli/notifiers/ses +15 -0
  112. data/templates/cli/notifiers/slack +22 -0
  113. data/templates/cli/notifiers/twitter +13 -0
  114. data/templates/cli/splitter +7 -0
  115. data/templates/cli/storages/cloud_files +11 -0
  116. data/templates/cli/storages/dropbox +20 -0
  117. data/templates/cli/storages/ftp +13 -0
  118. data/templates/cli/storages/local +8 -0
  119. data/templates/cli/storages/qiniu +12 -0
  120. data/templates/cli/storages/rsync +17 -0
  121. data/templates/cli/storages/s3 +16 -0
  122. data/templates/cli/storages/scp +15 -0
  123. data/templates/cli/storages/sftp +15 -0
  124. data/templates/cli/syncers/cloud_files +22 -0
  125. data/templates/cli/syncers/rsync_local +20 -0
  126. data/templates/cli/syncers/rsync_pull +28 -0
  127. data/templates/cli/syncers/rsync_push +28 -0
  128. data/templates/cli/syncers/s3 +27 -0
  129. data/templates/general/links +3 -0
  130. data/templates/general/version.erb +2 -0
  131. data/templates/notifier/mail/failure.erb +16 -0
  132. data/templates/notifier/mail/success.erb +16 -0
  133. data/templates/notifier/mail/warning.erb +16 -0
  134. data/templates/storage/dropbox/authorization_url.erb +6 -0
  135. data/templates/storage/dropbox/authorized.erb +4 -0
  136. data/templates/storage/dropbox/cache_file_written.erb +10 -0
  137. metadata +506 -0
@@ -0,0 +1,79 @@
1
+ module Backup
2
+ module Database
3
+ class Riak < Base
4
+ ##
5
+ # Node is the node from which to perform the backup.
6
+ # Default: riak@127.0.0.1
7
+ attr_accessor :node
8
+
9
+ ##
10
+ # Cookie is the Erlang cookie/shared secret used to connect to the node.
11
+ # Default: riak
12
+ attr_accessor :cookie
13
+
14
+ ##
15
+ # Username for the riak instance
16
+ # Default: riak
17
+ attr_accessor :user
18
+
19
+ def initialize(model, database_id = nil, &block)
20
+ super
21
+ instance_eval(&block) if block_given?
22
+
23
+ @node ||= "riak@127.0.0.1"
24
+ @cookie ||= "riak"
25
+ @user ||= "riak"
26
+ end
27
+
28
+ ##
29
+ # Performs the dump using `riak-admin backup`.
30
+ #
31
+ # This will be stored in the final backup package as
32
+ # <trigger>/databases/<dump_filename>-<node>[.gz]
33
+ def perform!
34
+ super
35
+
36
+ dump_file = File.join(dump_path, dump_filename)
37
+ with_riak_owned_dump_path do
38
+ run("#{riakadmin} backup #{node} #{cookie} '#{dump_file}' node")
39
+ end
40
+
41
+ if model.compressor
42
+ model.compressor.compress_with do |command, ext|
43
+ dump_file << "-#{node}" # `riak-admin` appends `node` to the filename.
44
+ run("#{command} -c '#{dump_file}' > '#{dump_file + ext}'")
45
+ FileUtils.rm_f(dump_file)
46
+ end
47
+ end
48
+
49
+ log!(:finished)
50
+ end
51
+
52
+ private
53
+
54
+ ##
55
+ # The `riak-admin backup` command is run as the riak +user+,
56
+ # so +user+ must have write priviledges to the +dump_path+.
57
+ #
58
+ # Note that the riak +user+ must also have access to +dump_path+.
59
+ # This means Backup's +tmp_path+ can not be under the home directory of
60
+ # the user running Backup, since the absence of the execute bit on their
61
+ # home directory would deny +user+ access.
62
+ def with_riak_owned_dump_path
63
+ run "#{utility(:sudo)} -n #{utility(:chown)} #{user} '#{dump_path}'"
64
+ yield
65
+ ensure
66
+ # reclaim ownership
67
+ run "#{utility(:sudo)} -n #{utility(:chown)} -R " \
68
+ "#{Config.user} '#{dump_path}'"
69
+ end
70
+
71
+ ##
72
+ # `riak-admin` must be run as the riak +user+.
73
+ # It will do this itself, but without `-n` and emits a message on STDERR.
74
+ def riakadmin
75
+ "#{utility(:sudo)} -n -u #{user} #{utility("riak-admin")}"
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,55 @@
1
+ module Backup
2
+ module Database
3
+ class SQLite < Base
4
+ class Error < Backup::Error; end
5
+
6
+ ##
7
+ # Path to the sqlite3 file
8
+ attr_accessor :path
9
+
10
+ ##
11
+ # Path to sqlite utility (optional)
12
+ attr_accessor :sqlitedump_utility
13
+
14
+ ##
15
+ # Creates a new instance of the SQLite adapter object
16
+ def initialize(model, database_id = nil, &block)
17
+ super
18
+ instance_eval(&block) if block_given?
19
+
20
+ @sqlitedump_utility ||= utility(:sqlitedump)
21
+ end
22
+
23
+ ##
24
+ # Performs the sqlitedump command and outputs the
25
+ # data to the specified path based on the 'trigger'
26
+ def perform!
27
+ super
28
+
29
+ dump = "echo '.dump' | #{sqlitedump_utility} #{path}"
30
+
31
+ pipeline = Pipeline.new
32
+ dump_ext = "sql"
33
+
34
+ pipeline << dump
35
+ if model.compressor
36
+ model.compressor.compress_with do |command, ext|
37
+ pipeline << command
38
+ dump_ext << ext
39
+ end
40
+ end
41
+
42
+ pipeline << "cat > '#{File.join(dump_path, dump_filename)}.#{dump_ext}'"
43
+
44
+ pipeline.run
45
+
46
+ if pipeline.success?
47
+ log!(:finished)
48
+ else
49
+ raise Error,
50
+ "#{database_name} Dump Failed!\n" + pipeline.error_messages
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,27 @@
1
+ module Backup
2
+ module Encryptor
3
+ class Base
4
+ include Utilities::Helpers
5
+ include Config::Helpers
6
+
7
+ def initialize
8
+ load_defaults!
9
+ end
10
+
11
+ private
12
+
13
+ ##
14
+ # Return the encryptor name, with Backup namespace removed
15
+ def encryptor_name
16
+ self.class.to_s.sub("Backup::", "")
17
+ end
18
+
19
+ ##
20
+ # Logs a message to the console and log file to inform
21
+ # the client that Backup is encrypting the archive
22
+ def log!
23
+ Logger.info "Using #{encryptor_name} to encrypt the archive."
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,737 @@
1
+ module Backup
2
+ module Encryptor
3
+ ##
4
+ # The GPG Encryptor allows you to encrypt your final archive using GnuPG,
5
+ # using one of three {#mode modes} of operation.
6
+ #
7
+ # == First, setup defaults in your +config.rb+ file
8
+ #
9
+ # Configure the {#keys} Hash using {.defaults} in your +config.rb+
10
+ # to specify all valid {#recipients} and their Public Key.
11
+ #
12
+ # Backup::Encryptor::GPG.defaults do |encryptor|
13
+ # # setup all GnuPG public keys
14
+ # encryptor.keys = {}
15
+ # encryptor.keys['joe@example.com'] = <<-EOS
16
+ # # ...public key here...
17
+ # EOS
18
+ # encryptor.keys['mary@example.com'] = <<-EOS
19
+ # # ...public key here...
20
+ # EOS
21
+ # end
22
+ #
23
+ # The optional {#gpg_config} and {#gpg_homedir} options would also
24
+ # typically be set using {.defaults} in +config.rb+ as well.
25
+ #
26
+ # == Then, setup each of your Models
27
+ #
28
+ # Set the desired {#recipients} and/or {#passphrase} (or {#passphrase_file})
29
+ # for each {Model}, depending on the {#mode} used.
30
+ #
31
+ # === my_backup_01
32
+ #
33
+ # This archive can only be decrypted using the private key for joe@example.com
34
+ #
35
+ # Model.new(:my_backup_01, 'Backup Job #1') do
36
+ # # ... archives, databases, compressor and storage options, etc...
37
+ # encrypt_with GPG do |encryptor|
38
+ # encryptor.mode = :asymmetric
39
+ # encryptor.recipients = 'joe@example.com'
40
+ # end
41
+ # end
42
+ #
43
+ # === my_backup_02
44
+ #
45
+ # This archive can only be decrypted using the passphrase "a secret".
46
+ #
47
+ # Model.new(:my_backup_02, 'Backup Job #2') do
48
+ # # ... archives, databases, compressor and storage options, etc...
49
+ # encrypt_with GPG do |encryptor|
50
+ # encryptor.mode = :symmetric
51
+ # encryptor.passphrase = 'a secret'
52
+ # end
53
+ # end
54
+ #
55
+ # === my_backup_03
56
+ #
57
+ # This archive may be decrypted using either the private key for joe@example.com
58
+ # *or* mary@example.com, *and* may also be decrypted using the passphrase.
59
+ #
60
+ # Model.new(:my_backup_03, 'Backup Job #3') do
61
+ # # ... archives, databases, compressor and storage options, etc...
62
+ # encrypt_with GPG do |encryptor|
63
+ # encryptor.mode = :both
64
+ # encryptor.passphrase = 'a secret'
65
+ # encryptor.recipients = ['joe@example.com', 'mary@example.com']
66
+ # end
67
+ # end
68
+ #
69
+ class GPG < Base
70
+ class Error < Backup::Error; end
71
+
72
+ MODES = [:asymmetric, :symmetric, :both]
73
+
74
+ ##
75
+ # Sets the mode of operation.
76
+ #
77
+ # [:asymmetric]
78
+ # In this mode, the final backup archive will be encrypted using the
79
+ # public key(s) specified by the key identifiers in {#recipients}.
80
+ # The archive may then be decrypted by anyone with a private key that
81
+ # corresponds to one of the public keys used. See {#recipients} and
82
+ # {#keys} for more information.
83
+ #
84
+ # [:symmetric]
85
+ # In this mode, the final backup archive will be encrypted using the
86
+ # passphrase specified by {#passphrase} or {#passphrase_file}.
87
+ # The archive will be encrypted using the encryption algorithm
88
+ # specified in your GnuPG configuration. See {#gpg_config} for more
89
+ # information. Anyone with the passphrase may decrypt the archive.
90
+ #
91
+ # [:both]
92
+ # In this mode, both +:asymmetric+ and +:symmetric+ options are used.
93
+ # Meaning that the archive may be decrypted by anyone with a valid
94
+ # private key or by using the proper passphrase.
95
+ #
96
+ # @param mode [String, Symbol] Sets the mode of operation.
97
+ # (Defaults to +:asymmetric+)
98
+ # @return [Symbol] mode that was set.
99
+ # @raise [Backup::Errors::Encryptor::GPG::InvalidModeError]
100
+ # if mode given is invalid.
101
+ #
102
+ attr_reader :mode
103
+ def mode=(mode)
104
+ @mode = mode.to_sym
105
+ raise Error, "'#{@mode}' is not a valid mode." unless MODES.include?(@mode)
106
+ end
107
+
108
+ ##
109
+ # Specifies the GnuPG configuration to be used.
110
+ #
111
+ # This should be given as the text of a +gpg.conf+ file. It will be
112
+ # written to a temporary file, which will be passed to the +gpg+ command
113
+ # to use instead of the +gpg.conf+ found in the GnuPG home directory.
114
+ # This allows you to be certain your preferences are used.
115
+ #
116
+ # This is especially useful if you've also set {#gpg_homedir} and plan
117
+ # on allowing Backup to automatically create that directory and import
118
+ # all your public keys specified in {#keys}. In this situation, that
119
+ # folder would not contain any +gpg.conf+ file, so GnuPG would simply
120
+ # use it's defaults.
121
+ #
122
+ # While this may be specified on a per-Model basis, you would generally
123
+ # just specify this in the defaults. Leading tabs/spaces are stripped
124
+ # before writing the given string to the temporary configuration file.
125
+ #
126
+ # Backup::Encryptor::GPG.defaults do |enc|
127
+ # enc.gpg_config = <<-EOF
128
+ # # safely override preferences set in the receiver's public key(s)
129
+ # personal-cipher-preferences TWOFISH AES256 BLOWFISH AES192 CAST5 AES
130
+ # personal-digest-preferences SHA512 SHA256 SHA1 MD5
131
+ # personal-compress-preferences BZIP2 ZLIB ZIP Uncompressed
132
+ # # cipher algorithm for symmetric encryption
133
+ # # (if personal-cipher-preferences are not specified)
134
+ # s2k-cipher-algo TWOFISH
135
+ # # digest algorithm for mangling the symmetric encryption passphrase
136
+ # s2k-digest-algo SHA512
137
+ # EOF
138
+ # end
139
+ #
140
+ # @see #gpg_homedir
141
+ # @return [String]
142
+ attr_accessor :gpg_config
143
+
144
+ ##
145
+ # Set the GnuPG home directory to be used.
146
+ #
147
+ # This allows you to specify the GnuPG home directory on the system
148
+ # where Backup will be run, keeping the keyrings used by Backup separate
149
+ # from the default keyrings of the user running Backup.
150
+ # By default, this would be +`~/.gnupg`+.
151
+ #
152
+ # If a directory is specified here, Backup will create it if needed
153
+ # and ensure the correct permissions are set. All public keys Backup
154
+ # imports would be added to the +pubring.gpg+ file within this directory,
155
+ # and +gpg+ would be given this directory using it's +--homedir+ option.
156
+ #
157
+ # Any +gpg.conf+ file located in this directory would also be used by
158
+ # +gpg+, unless {#gpg_config} is specified.
159
+ #
160
+ # The given path will be expanded before use.
161
+ #
162
+ # @return [String]
163
+ attr_accessor :gpg_homedir
164
+
165
+ ##
166
+ # Specifies a Hash of public key identifiers and their public keys.
167
+ #
168
+ # While not _required_, it is recommended that all public keys you intend
169
+ # to use be setup in {#keys}. The best place to do this is in your defaults
170
+ # in +config.rb+.
171
+ #
172
+ # Backup::Encryptor::GPG.defaults do |enc|
173
+ # enc.keys = {}
174
+ #
175
+ # enc.keys['joe@example.com'] = <<-EOS
176
+ # -----BEGIN PGP PUBLIC KEY BLOCK-----
177
+ # Version: GnuPG v1.4.12 (GNU/Linux)
178
+ #
179
+ # mQMqBEd5F8MRCACfArHCJFR6nkmxNiW+UE4PAW3bQla9JWFqCwu4VqLkPI/lHb5p
180
+ # xHff8Fzy2O89BxD/6hXSDx2SlVmAGHOCJhShx1vfNGVYNsJn2oNK50in9kGvD0+m
181
+ # [...]
182
+ # SkQEHOxhMiFjAN9q4LuirSOu65uR1bnTmF+Z92++qMIuEkH4/LnN
183
+ # =8gNa
184
+ # -----END PGP PUBLIC KEY BLOCK-----
185
+ # EOS
186
+ #
187
+ # enc.keys['mary@example.com'] = <<-EOS
188
+ # -----BEGIN PGP PUBLIC KEY BLOCK-----
189
+ # Version: GnuPG v1.4.12 (GNU/Linux)
190
+ #
191
+ # 2SlVmAGHOCJhShx1vfNGVYNxHff8Fzy2O89BxD/6in9kGvD0+mhXSDxsJn2oNK50
192
+ # kmxNiW+UmQMqBEd5F8MRCACfArHCJFR6qCwu4VqLkPI/lHb5pnE4PAW3bQla9JWF
193
+ # [...]
194
+ # AN9q4LSkQEHOxhMiFjuirSOu65u++qMIuEkH4/LnNR1bnTmF+Z92
195
+ # =8gNa
196
+ # -----END PGP PUBLIC KEY BLOCK-----
197
+ #
198
+ # EOS
199
+ # end
200
+ #
201
+ # All leading spaces/tabs will be stripped from the key, so the above
202
+ # form may be used to set each identifier's key.
203
+ #
204
+ # When a public key can not be found for an identifier specified in
205
+ # {#recipients}, the corresponding public key from this Hash will be
206
+ # imported into +pubring.gpg+ in the GnuPG home directory ({#gpg_homedir}).
207
+ # Therefore, each key *must* be the same identifier used in {#recipients}.
208
+ #
209
+ # To obtain the public key in ASCII format, use:
210
+ #
211
+ # $ gpg -a --export joe@example.com
212
+ #
213
+ # See {#recipients} for information on what may be used as valid identifiers.
214
+ #
215
+ # @return [Hash]
216
+ attr_accessor :keys
217
+
218
+ ##
219
+ # Specifies the recipients to use when encrypting the backup archive.
220
+ #
221
+ # When {#mode} is set to +:asymmetric+ or +:both+, the public key for
222
+ # each recipient given here will be used to encrypt the archive. Each
223
+ # recipient will be able to decrypt the archive using their private key.
224
+ #
225
+ # If there is only one recipient, this may be specified as a String.
226
+ # Otherwise, this should be an Array of Strings. Each String must be a
227
+ # valid public key identifier, and *must* be the same identifier used to
228
+ # specify the recipient's public key in {#keys}. This is so that if a
229
+ # public key is not found for the given identifier, it may be imported
230
+ # from {#keys}.
231
+ #
232
+ # Valid identifiers which may be used are as follows:
233
+ #
234
+ # [Key Fingerprint]
235
+ # The key fingerprint is a 40-character hex string, which uniquely
236
+ # identifies a public key. This may be obtained using the following:
237
+ #
238
+ # $ gpg --fingerprint john.smith@example.com
239
+ # pub 1024R/4E5E8D8A 2012-07-20
240
+ # Key fingerprint = FFEA D1DB 201F B214 873E 7399 4A83 569F 4E5E 8D8A
241
+ # uid John Smith <john.smith@example.com>
242
+ # sub 1024R/92C8DFD8 2012-07-20
243
+ #
244
+ # [Long Key ID]
245
+ # The long Key ID is the last 16-characters of the key's fingerprint.
246
+ #
247
+ # The Long Key ID in this example is: 4A83569F4E5E8D8A
248
+ #
249
+ # $ gpg --keyid-format long -k john.smith@example.com
250
+ # pub 1024R/4A83569F4E5E8D8A 2012-07-20
251
+ # uid John Smith <john.smith@example.com>
252
+ # sub 1024R/662F18DB92C8DFD8 2012-07-20
253
+ #
254
+ # [Short Key ID]
255
+ # The short Key ID is the last 8-characters of the key's fingerprint.
256
+ # This is the default key format seen when listing keys.
257
+ #
258
+ # The Short Key ID in this example is: 4E5E8D8A
259
+ #
260
+ # $ gpg -k john.smith@example.com
261
+ # pub 1024R/4E5E8D8A 2012-07-20
262
+ # uid John Smith <john.smith@example.com>
263
+ # sub 1024R/92C8DFD8 2012-07-20
264
+ #
265
+ # [Email Address]
266
+ # This must exactly match an email address for one of the UID records
267
+ # associated with the recipient's public key.
268
+ #
269
+ # Recipient identifier forms may be mixed, as long as the identifier used
270
+ # here is the same as that used in {#keys}. Also, all spaces will be stripped
271
+ # from the identifier when used, so the following would be valid.
272
+ #
273
+ # Backup::Model.new(:my_backup, 'My Backup') do
274
+ # encrypt_with GPG do |enc|
275
+ # enc.recipients = [
276
+ # # John Smith
277
+ # '4A83 569F 4E5E 8D8A',
278
+ # # Mary Smith
279
+ # 'mary.smith@example.com'
280
+ # ]
281
+ # end
282
+ # end
283
+ #
284
+ # @return [String, Array]
285
+ attr_accessor :recipients
286
+
287
+ ##
288
+ # Specifies the passphrase to use symmetric encryption.
289
+ #
290
+ # When {#mode} is +:symmetric+ or +:both+, this passphrase will be used
291
+ # to symmetrically encrypt the archive.
292
+ #
293
+ # Use of this option will override the use of {#passphrase_file}.
294
+ #
295
+ # @return [String]
296
+ attr_accessor :passphrase
297
+
298
+ ##
299
+ # Specifies the passphrase file to use symmetric encryption.
300
+ #
301
+ # When {#mode} is +:symmetric+ or +:both+, this file will be passed
302
+ # to the +gpg+ command line, where +gpg+ will read the first line from
303
+ # this file and use it for the passphrase.
304
+ #
305
+ # The file path given here will be expanded to a full path.
306
+ #
307
+ # If {#passphrase} is specified, {#passphrase_file} will be ignored.
308
+ # Therefore, if you have set {#passphrase} in your global defaults,
309
+ # but wish to use {#passphrase_file} with a specific {Model}, be sure
310
+ # to clear {#passphrase} within that model's configuration.
311
+ #
312
+ # Backup::Encryptor::GPG.defaults do |enc|
313
+ # enc.passphrase = 'secret phrase'
314
+ # end
315
+ #
316
+ # Backup::Model.new(:my_backup, 'My Backup') do
317
+ # # other directives...
318
+ # encrypt_with GPG do |enc|
319
+ # enc.mode = :symmetric
320
+ # enc.passphrase = nil
321
+ # enc.passphrase_file = '/path/to/passphrase.file'
322
+ # end
323
+ # end
324
+ #
325
+ # @return [String]
326
+ attr_accessor :passphrase_file
327
+
328
+ ##
329
+ # Configures default accessor values for new class instances.
330
+ #
331
+ # If all required options are set, then no further configuration
332
+ # would be needed within a Model's definition when an Encryptor is added.
333
+ # Therefore, the following example is sufficient to encrypt +:my_backup+:
334
+ #
335
+ # # Defaults set in config.rb
336
+ # Backup::Encryptor::GPG.defaults do |encryptor|
337
+ # encryptor.keys = {}
338
+ # encryptor.keys['joe@example.com'] = <<-EOS
339
+ # -----BEGIN PGP PUBLIC KEY BLOCK-----
340
+ # Version: GnuPG v1.4.12 (GNU/Linux)
341
+ #
342
+ # mI0EUBR6CwEEAMVSlFtAXO4jXYnVFAWy6chyaMw+gXOFKlWojNXOOKmE3SujdLKh
343
+ # kWqnafx7VNrb8cjqxz6VZbumN9UgerFpusM3uLCYHnwyv/rGMf4cdiuX7gGltwGb
344
+ # (...etc...)
345
+ # mLekS3xntUhhgHKc4lhf4IVBqG4cFmwSZ0tZEJJUSESb3TqkkdnNLjE=
346
+ # =KEW+
347
+ # -----END PGP PUBLIC KEY BLOCK-----
348
+ # EOS
349
+ #
350
+ # encryptor.recipients = 'joe@example.com'
351
+ # end
352
+ #
353
+ # # Encryptor set in the model
354
+ # Backup::Model.new(:my_backup, 'My Backup') do
355
+ # # archives, storage options, etc...
356
+ # encrypt_with GPG
357
+ # end
358
+ #
359
+ # @!scope class
360
+ # @see Config::Helpers::ClassMethods#defaults
361
+ # @yield [config] OpenStruct object
362
+ # @!method defaults
363
+
364
+ ##
365
+ # Creates a new instance of Backup::Encryptor::GPG.
366
+ #
367
+ # This constructor is not used directly when configuring Backup.
368
+ # Use {Model#encrypt_with}.
369
+ #
370
+ # Model.new(:backup_trigger, 'Backup Label') do
371
+ # archive :my_archive do |archive|
372
+ # archive.add '/some/directory'
373
+ # end
374
+ #
375
+ # compress_with Gzip
376
+ #
377
+ # encrypt_with GPG do |encryptor|
378
+ # encryptor.mode = :both
379
+ # encryptor.passphrase = 'a secret'
380
+ # encryptor.recipients = ['joe@example.com', 'mary@example.com']
381
+ # end
382
+ #
383
+ # store_with SFTP
384
+ #
385
+ # notify_by Mail
386
+ # end
387
+ #
388
+ # @api private
389
+ def initialize(&block)
390
+ super
391
+
392
+ instance_eval(&block) if block_given?
393
+
394
+ @mode ||= :asymmetric
395
+ end
396
+
397
+ ##
398
+ # This is called as part of the procedure run by the Packager.
399
+ # It sets up the needed options to pass to the gpg command,
400
+ # then yields the command to use as part of the packaging procedure.
401
+ # Once the packaging procedure is complete, it will return
402
+ # so that any clean-up may be performed after the yield.
403
+ # Cleanup is also ensured, as temporary files may hold sensitive data.
404
+ # If no options can be built, the packaging process will be aborted.
405
+ #
406
+ # @api private
407
+ def encrypt_with
408
+ log!
409
+ prepare
410
+
411
+ if mode_options.empty?
412
+ raise Error, "Encryption could not be performed for mode '#{mode}'"
413
+ end
414
+
415
+ yield "#{utility(:gpg)} #{base_options} #{mode_options}", ".gpg"
416
+ ensure
417
+ cleanup
418
+ end
419
+
420
+ private
421
+
422
+ ##
423
+ # Remove any temporary directories and reset all instance variables.
424
+ #
425
+ def prepare
426
+ FileUtils.rm_rf(@tempdirs, secure: true) if @tempdirs
427
+ @tempdirs = []
428
+ @base_options = nil
429
+ @mode_options = nil
430
+ @user_recipients = nil
431
+ @user_keys = nil
432
+ @system_identifiers = nil
433
+ end
434
+ alias :cleanup :prepare
435
+
436
+ ##
437
+ # Returns the options needed for the gpg command line which are
438
+ # not dependant on the #mode. --no-tty supresses output of certain
439
+ # messages, like the "Reading passphrase from file descriptor..."
440
+ # messages during symmetric encryption
441
+ #
442
+ def base_options
443
+ @base_options ||= begin
444
+ opts = ["--no-tty"]
445
+ path = setup_gpg_homedir
446
+ opts << "--homedir '#{path}'" if path
447
+ path = setup_gpg_config
448
+ opts << "--options '#{path}'" if path
449
+ opts.join(" ")
450
+ end
451
+ end
452
+
453
+ ##
454
+ # Setup the given :gpg_homedir if needed, ensure the proper permissions
455
+ # are set, and return the directory's path. Otherwise, return false.
456
+ #
457
+ # If the GnuPG files do not exist, trigger their creation by requesting
458
+ # --list-secret-keys. Some commands, like for symmetric encryption, will
459
+ # issue messages about their creation on STDERR, which generates unwanted
460
+ # warnings in the log. This way, if any of these files are created here,
461
+ # we will get those messages on STDOUT for the log, without the actual
462
+ # secret key listing which we don't care about.
463
+ #
464
+ def setup_gpg_homedir
465
+ return false unless gpg_homedir
466
+
467
+ path = File.expand_path(gpg_homedir)
468
+ FileUtils.mkdir_p(path)
469
+ FileUtils.chown(Config.user, nil, path)
470
+ FileUtils.chmod(0o700, path)
471
+
472
+ unless %w[pubring.gpg secring.gpg trustdb.gpg]
473
+ .all? { |name| File.exist? File.join(path, name) }
474
+ run("#{utility(:gpg)} --homedir '#{path}' -K 2>&1 >/dev/null")
475
+ end
476
+
477
+ path
478
+ rescue => err
479
+ raise Error.wrap \
480
+ err, "Failed to create or set permissions for #gpg_homedir"
481
+ end
482
+
483
+ ##
484
+ # Write the given #gpg_config to a tempfile, within a tempdir, and
485
+ # return the file's path to be given to the gpg --options argument.
486
+ # If no #gpg_config is set, return false.
487
+ #
488
+ # This is required in order to set the proper permissions on the
489
+ # directory containing the tempfile. The tempdir will be removed
490
+ # after the packaging procedure is completed.
491
+ #
492
+ # Once written, we'll call check_gpg_config to make sure there are
493
+ # no problems that would prevent gpg from running with this config.
494
+ # If any errors occur during this process, we can not proceed.
495
+ # We'll cleanup to remove the tempdir (if created) and raise an error.
496
+ #
497
+ def setup_gpg_config
498
+ return false unless gpg_config
499
+
500
+ dir = Dir.mktmpdir("backup-gpg_config", Config.tmp_path)
501
+ @tempdirs << dir
502
+ file = Tempfile.open("backup-gpg_config", dir)
503
+ file.write gpg_config.gsub(/^[[:blank:]]+/, "")
504
+ file.close
505
+
506
+ check_gpg_config(file.path)
507
+
508
+ file.path
509
+ rescue => err
510
+ cleanup
511
+ raise Error.wrap(err, "Error creating temporary file for #gpg_config.")
512
+ end
513
+
514
+ ##
515
+ # Make sure the temporary GnuPG config file created from #gpg_config
516
+ # does not have any syntax errors that would prevent gpg from running.
517
+ # If so, raise the returned error message.
518
+ # Note that Cli::Helpers#run may also raise an error here.
519
+ #
520
+ def check_gpg_config(path)
521
+ ret = run(
522
+ "#{utility(:gpg)} --options '#{path}' --gpgconf-test 2>&1"
523
+ ).chomp
524
+ raise ret unless ret.empty?
525
+ end
526
+
527
+ ##
528
+ # Returns the options needed for the gpg command line to perform
529
+ # the encryption based on the #mode.
530
+ #
531
+ def mode_options
532
+ @mode_options ||= begin
533
+ s_opts = symmetric_options if mode != :asymmetric
534
+ a_opts = asymmetric_options if mode != :symmetric
535
+ [s_opts, a_opts].compact.join(" ")
536
+ end
537
+ end
538
+
539
+ ##
540
+ # Process :passphrase or :passphrase_file and return the command line
541
+ # options to perform symmetric encryption. If no :passphrase is
542
+ # specified, or an error occurs creating a temporary file for it, then
543
+ # try to use :passphrase_file if it's set.
544
+ # If the option can not be set, log a warning and return nil.
545
+ #
546
+ def symmetric_options
547
+ path = setup_passphrase_file
548
+ unless path || passphrase_file.to_s.empty?
549
+ path = File.expand_path(passphrase_file.to_s)
550
+ end
551
+
552
+ if path && File.exist?(path)
553
+ "-c --passphrase-file '#{path}'"
554
+ else
555
+ Logger.warn("Symmetric encryption options could not be set.")
556
+ nil
557
+ end
558
+ end
559
+
560
+ ##
561
+ # Create a temporary file, within a tempdir, to hold the :passphrase and
562
+ # return the file's path. If an error occurs, log a warning.
563
+ # Return false if no :passphrase is set or an error occurs.
564
+ #
565
+ def setup_passphrase_file
566
+ return false if passphrase.to_s.empty?
567
+
568
+ dir = Dir.mktmpdir("backup-gpg_passphrase", Config.tmp_path)
569
+ @tempdirs << dir
570
+ file = Tempfile.open("backup-gpg_passphrase", dir)
571
+ file.write passphrase.to_s
572
+ file.close
573
+
574
+ file.path
575
+ rescue => err
576
+ Logger.warn Error.wrap(err, "Error creating temporary passphrase file.")
577
+ false
578
+ end
579
+
580
+ ##
581
+ # Process :recipients, importing their public key from :keys if needed,
582
+ # and return the command line options to perform asymmetric encryption.
583
+ # Log a warning and return nil if no valid recipients are found.
584
+ #
585
+ def asymmetric_options
586
+ if user_recipients.empty?
587
+ Logger.warn "No recipients available for asymmetric encryption."
588
+ nil
589
+ else
590
+ # skip trust database checks
591
+ "-e --trust-model always " +
592
+ user_recipients.map { |r| "-r '#{r}'" }.join(" ")
593
+ end
594
+ end
595
+
596
+ ##
597
+ # Returns an Array of the public key identifiers the user specified
598
+ # in :recipients. Each identifier is 'cleaned' so that exact matches
599
+ # can be performed. Then each is checked to ensure it will find a
600
+ # public key that exists in the system's public keyring.
601
+ # If the identifier does not match an existing key, the public key
602
+ # associated with the identifier in :keys will be imported for use.
603
+ # If no key can be found in the system or in :keys for the identifier,
604
+ # a warning will be issued; as we will attempt to encrypt the backup
605
+ # and proceed if at all possible.
606
+ #
607
+ def user_recipients
608
+ @user_recipients ||= begin
609
+ [recipients].flatten.compact.map do |identifier|
610
+ identifier = clean_identifier(identifier)
611
+ if system_identifiers.include?(identifier)
612
+ identifier
613
+ else
614
+ key = user_keys[identifier]
615
+ if key
616
+ # will log a warning and return nil if the import fails
617
+ import_key(identifier, key)
618
+ else
619
+ Logger.warn \
620
+ "No public key was found in #keys for '#{identifier}'"
621
+ nil
622
+ end
623
+ end
624
+ end.compact
625
+ end
626
+ end
627
+
628
+ ##
629
+ # Returns the #keys hash set by the user with all identifiers
630
+ # (Hash keys) 'cleaned' for exact matching. If the cleaning process
631
+ # creates duplicate keys, the user will be warned.
632
+ #
633
+ def user_keys
634
+ @user_keys ||= begin
635
+ _keys = keys || {}
636
+ ret = Hash[_keys.map { |k, v| [clean_identifier(k), v] }]
637
+ if ret.keys.count != _keys.keys.count
638
+ Logger.warn \
639
+ "Duplicate public key identifiers were detected in #keys."
640
+ end
641
+ ret
642
+ end
643
+ end
644
+
645
+ ##
646
+ # Cleans a public key identifier.
647
+ # Strip out all spaces, upcase non-email identifiers,
648
+ # and wrap email addresses in <> to perform exact matching.
649
+ #
650
+ def clean_identifier(str)
651
+ str = str.to_s.gsub(/[[:blank:]]+/, "")
652
+ str =~ /@/ ? "<#{str.gsub(/(<|>)/, "")}>" : str.upcase
653
+ end
654
+
655
+ ##
656
+ # Import the given public key and return the 16 character Key ID.
657
+ # If the import fails, return nil.
658
+ # Note that errors raised by Cli::Helpers#run may also be rescued here.
659
+ #
660
+ def import_key(identifier, key)
661
+ file = Tempfile.open("backup-gpg_import", Config.tmp_path)
662
+ file.write(key.gsub(/^[[:blank:]]+/, ""))
663
+ file.close
664
+ ret = run "#{utility(:gpg)} #{base_options} " \
665
+ "--keyid-format 0xlong --import '#{file.path}' 2>&1"
666
+ file.delete
667
+
668
+ keyid = ret.match(/ 0x(\w{16})/).to_a[1]
669
+ raise "GPG Returned:\n#{ret.gsub(/^\s*/, " ")}" unless keyid
670
+ keyid
671
+ rescue => err
672
+ Logger.warn Error.wrap(
673
+ err, "Public key import failed for '#{identifier}'"
674
+ )
675
+ nil
676
+ end
677
+
678
+ ##
679
+ # Parse the information for all the public keys found in the public
680
+ # keyring (based on #gpg_homedir setting) and return an Array of all
681
+ # identifiers which could be used to specify a valid key.
682
+ #
683
+ def system_identifiers
684
+ @system_identifiers ||= begin
685
+ skip_key = false
686
+ data = run "#{utility(:gpg)} #{base_options} " \
687
+ "--with-colons --fixed-list-mode --fingerprint"
688
+ data.lines.map do |line|
689
+ line.strip!
690
+
691
+ # process public key record
692
+ if line =~ /^pub:/
693
+ validity, keyid, capabilities = line.split(":").values_at(1, 4, 11)
694
+ # skip keys marked as revoked ('r'), expired ('e'),
695
+ # invalid ('i') or disabled ('D')
696
+ if validity[0, 1] =~ /(r|e|i)/ || capabilities =~ /D/
697
+ skip_key = true
698
+ next nil
699
+ else
700
+ skip_key = false
701
+ # return both the long and short id
702
+ next [keyid[-8..-1], keyid]
703
+ end
704
+ else
705
+ # wait for the next valid public key record
706
+ next if skip_key
707
+
708
+ # process UID records for the current public key
709
+ if line =~ /^uid:/
710
+ validity, userid = line.split(":").values_at(1, 9)
711
+ # skip records marked as revoked ('r'), expired ('e')
712
+ # or invalid ('i')
713
+ if validity !~ /(r|e|i)/
714
+ # return the last email found in user id string,
715
+ # since this includes user supplied comments.
716
+ # return nil if no email found.
717
+ email = nil
718
+ str = userid
719
+ while match = str.match(/<.+?@.+?>/)
720
+ email = match[0]
721
+ str = match.post_match
722
+ end
723
+ next email
724
+ end
725
+ # return public key's fingerprint
726
+ elsif line =~ /^fpr:/
727
+ next line.split(":")[9]
728
+ end
729
+
730
+ nil # ignore any other lines
731
+ end
732
+ end.flatten.compact
733
+ end
734
+ end
735
+ end
736
+ end
737
+ end