Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge branch 'develop'

  • Loading branch information...
commit bcb78d8a5237d25fead57ef82b64b26e75830c8d 2 parents 8765bbe + 35315fc
@meskyanichi meskyanichi authored
Showing with 2,717 additions and 728 deletions.
  1. +1 −3 README.md
  2. +2 −0  lib/backup/cli/helpers.rb
  3. +4 −4 lib/backup/cli/utility.rb
  4. +8 −4 lib/backup/compressor/bzip2.rb
  5. +8 −4 lib/backup/compressor/gzip.rb
  6. +30 −38 lib/backup/configuration/helpers.rb
  7. +2 −1  lib/backup/database/mongodb.rb
  8. +2 −1  lib/backup/database/mysql.rb
  9. +2 −1  lib/backup/database/postgresql.rb
  10. +2 −1  lib/backup/database/redis.rb
  11. +2 −1  lib/backup/database/riak.rb
  12. +717 −37 lib/backup/encryptor/gpg.rb
  13. +2 −2 lib/backup/syncer/cloud/base.rb
  14. +14 −84 spec-live/backups/config.rb
  15. +3 −0  spec-live/backups/config.yml.template
  16. +184 −0 spec-live/backups/models.rb
  17. +239 −0 spec-live/encryptor/gpg_keys.rb
  18. +287 −0 spec-live/encryptor/gpg_spec.rb
  19. +58 −22 spec-live/notifier/mail_spec.rb
  20. +69 −3 spec-live/spec_helper.rb
  21. +25 −25 spec/cli/helpers_spec.rb
  22. +6 −3 spec/cli/utility_spec.rb
  23. +20 −41 spec/compressor/bzip2_spec.rb
  24. +20 −41 spec/compressor/gzip_spec.rb
  25. +94 −253 spec/configuration/helpers_spec.rb
  26. +9 −10 spec/database/mongodb_spec.rb
  27. +9 −10 spec/database/mysql_spec.rb
  28. +9 −10 spec/database/postgresql_spec.rb
  29. +9 −10 spec/database/redis_spec.rb
  30. +9 −10 spec/database/riak_spec.rb
  31. +830 −71 spec/encryptor/gpg_spec.rb
  32. +24 −36 spec/storage/dropbox_spec.rb
  33. +1 −1  spec/syncer/cloud/base_spec.rb
  34. +16 −1 templates/cli/utility/encryptor/gpg
View
4 README.md
@@ -167,9 +167,7 @@ Backup::Model.new(:sample_backup, 'A sample backup configuration') do
encryption.password = 'my_secret_password'
end
- compress_with Gzip do |compression|
- compression.best = true
- end
+ compress_with Gzip
store_with SFTP, "Server A" do |server|
server.username = 'my_username'
View
2  lib/backup/cli/helpers.rb
@@ -5,6 +5,8 @@ module CLI
module Helpers
UTILITY = {}
+ private
+
##
# Runs a system command
#
View
8 lib/backup/cli/utility.rb
@@ -38,10 +38,6 @@ def perform
Config.update(options)
##
- # Load the configuration file
- Config.load_config!
-
- ##
# Ensure the :log_path, :cache_path and :tmp_path are created
# if they do not yet exist
[Config.log_path, Config.cache_path, Config.tmp_path].each do |path|
@@ -49,6 +45,10 @@ def perform
end
##
+ # Load the configuration file
+ Config.load_config!
+
+ ##
# Truncate log file if needed
Logger.truncate!
View
12 lib/backup/compressor/bzip2.rb
@@ -16,11 +16,15 @@ class Bzip2 < Base
attr_accessor :level
attr_deprecate :fast, :version => '3.0.24',
- :replacement => :level,
- :value => lambda {|val| val ? 1 : nil }
+ :message => 'Use Bzip2#level instead.',
+ :action => lambda {|klass, val|
+ klass.level = 1 if val
+ }
attr_deprecate :best, :version => '3.0.24',
- :replacement => :level,
- :value => lambda {|val| val ? 9 : nil }
+ :message => 'Use Bzip2#level instead.',
+ :action => lambda {|klass, val|
+ klass.level = 9 if val
+ }
##
# Creates a new instance of Backup::Compressor::Bzip2
View
12 lib/backup/compressor/gzip.rb
@@ -16,11 +16,15 @@ class Gzip < Base
attr_accessor :level
attr_deprecate :fast, :version => '3.0.24',
- :replacement => :level,
- :value => lambda {|val| val ? 1 : nil }
+ :message => 'Use Gzip#level instead.',
+ :action => lambda {|klass, val|
+ klass.level = 1 if val
+ }
attr_deprecate :best, :version => '3.0.24',
- :replacement => :level,
- :value => lambda {|val| val ? 9 : nil }
+ :message => 'Use Gzip#level instead.',
+ :action => lambda {|klass, val|
+ klass.level = 9 if val
+ }
##
# Creates a new instance of Backup::Compressor::Gzip
View
68 lib/backup/configuration/helpers.rb
@@ -34,12 +34,9 @@ def deprecations
end
def log_deprecation_warning(name, deprecation)
- msg = "#{ self }.#{ name } has been deprecated as of " +
+ msg = "#{ self }##{ name } has been deprecated as of " +
"backup v.#{ deprecation[:version] }"
- if replacement = deprecation[:replacement]
- msg << "\nThis setting has been replaced with:\n" +
- "#{ self }.#{ replacement }"
- end
+ msg << "\n#{ deprecation[:message] }" if deprecation[:message]
Logger.warn Backup::Errors::ConfigurationError.new <<-EOS
[DEPRECATION WARNING]
#{ msg }
@@ -51,25 +48,29 @@ def log_deprecation_warning(name, deprecation)
##
# Method to deprecate an attribute.
#
- # :version should be set to the backup version which will first
+ # :version
+ # Must be set to the backup version which will first
# introduce the deprecation.
- # :replacement may be set to another attr_accessor name to set
- # the value for instead of the deprecated accessor
- # :value may be used to specify the value set on :replacement.
- # If :value is nil, the value set on the deprecated accessor
- # will be used to set the value for the :replacement.
- # If :value is a lambda, it will be passed the value the user
- # set on the deprecated accessor, and should return the value
- # to be set on the :replacement.
- # Therefore, to cause the replacement accessor not to be set,
- # use the lambda form to return nil. This is only way to specify
- # a :replacement without transferring a value.
- # e.g. :replacement => :new_attr, :value => Proc.new {}
+ #
+ # :action
+ # If set, this Proc will be called with a reference to the
+ # class instance and the value set on the deprecated accessor.
+ # e.g. deprecation[:action].call(klass, value)
+ # This should perform whatever action is neccessary, such as
+ # transferring the value to a new accessor.
+ #
+ # :message
+ # If set, this will be appended to #log_deprecation_warning
+ #
+ # Note that this replaces the `attr_accessor` method, or other
+ # method previously used to set the accessor being deprecated.
+ # #method_missing will handle any calls to `name=`.
+ #
def attr_deprecate(name, args = {})
deprecations[name] = {
:version => nil,
- :replacement => nil,
- :value => nil
+ :message => nil,
+ :action => nil
}.merge(args)
end
@@ -89,7 +90,13 @@ def load_defaults!
end
##
- # Check missing methods for deprecations
+ # Check missing methods for deprecated attribute accessors.
+ #
+ # If a value is set on an accessor that has been deprecated
+ # using #attr_deprecate, a warning will be issued and any
+ # :action (Proc) specified will be called with a reference to
+ # the class instance and the value set on the deprecated accessor.
+ # See #attr_deprecate and #log_deprecation_warning
#
# Note that OpenStruct (used for setting defaults) does not allow
# multiple arguments when assigning values for members.
@@ -99,6 +106,7 @@ def load_defaults!
# directly on the class' accessor, should not be supported.
# i.e. if an option will accept being set as an Array, then it
# should be explicitly set as such. e.g. option = [val1, val2]
+ #
def method_missing(name, *args)
deprecation = nil
if method = name.to_s.chomp!('=')
@@ -111,23 +119,7 @@ def method_missing(name, *args)
if deprecation
self.class.log_deprecation_warning(method, deprecation)
- if replacement = deprecation[:replacement]
- value =
- case deprecation[:value]
- when nil
- args[0]
- when Proc
- deprecation[:value].call(args[0])
- else
- deprecation[:value]
- end
- unless value.nil?
- Logger.warn(
- "#{ self.class }.#{ replacement } is being set to '#{ value }'"
- )
- send(:"#{ replacement }=", value)
- end
- end
+ deprecation[:action].call(self, args[0]) if deprecation[:action]
else
super
end
View
3  lib/backup/database/mongodb.rb
@@ -33,7 +33,8 @@ class MongoDB < Base
attr_accessor :mongodump_utility
attr_deprecate :utility_path, :version => '3.0.21',
- :replacement => :mongodump_utility
+ :message => 'Use MongoDB#mongodump_utility instead.',
+ :action => lambda {|klass, val| klass.mongodump_utility = val }
##
# Path to the mongo utility (optional)
View
3  lib/backup/database/mysql.rb
@@ -34,7 +34,8 @@ class MySQL < Base
attr_accessor :mysqldump_utility
attr_deprecate :utility_path, :version => '3.0.21',
- :replacement => :mysqldump_utility
+ :message => 'Use MySQL#mysqldump_utility instead.',
+ :action => lambda {|klass, val| klass.mysqldump_utility = val }
##
# Creates a new instance of the MySQL adapter object
View
3  lib/backup/database/postgresql.rb
@@ -33,7 +33,8 @@ class PostgreSQL < Base
attr_accessor :pg_dump_utility
attr_deprecate :utility_path, :version => '3.0.21',
- :replacement => :pg_dump_utility
+ :message => 'Use PostgreSQL#pg_dump_utility instead.',
+ :action => lambda {|klass, val| klass.pg_dump_utility = val }
##
# Creates a new instance of the PostgreSQL adapter object
View
3  lib/backup/database/redis.rb
@@ -31,7 +31,8 @@ class Redis < Base
attr_accessor :redis_cli_utility
attr_deprecate :utility_path, :version => '3.0.21',
- :replacement => :redis_cli_utility
+ :message => 'Use Redis#redis_cli_utility instead.',
+ :action => lambda {|klass, val| klass.redis_cli_utility = val }
##
# Creates a new instance of the Redis database object
View
3  lib/backup/database/riak.rb
@@ -21,7 +21,8 @@ class Riak < Base
attr_accessor :riak_admin_utility
attr_deprecate :utility_path, :version => '3.0.21',
- :replacement => :riak_admin_utility
+ :message => 'Use Riak#riak_admin_utility instead.',
+ :action => lambda {|klass, val| klass.riak_admin_utility = val }
##
# Creates a new instance of the Riak adapter object
View
754 lib/backup/encryptor/gpg.rb
@@ -2,77 +2,757 @@
module Backup
module Encryptor
+ ##
+ # The GPG Encryptor allows you to encrypt your final archive using GnuPG,
+ # using one of three {#mode modes} of operation.
+ #
+ # == First, setup defaults in your +config.rb+ file
+ #
+ # Configure the {#keys} Hash using {.defaults} in your +config.rb+
+ # to specify all valid {#recipients} and their Public Key.
+ #
+ # Backup::Encryptor::GPG.defaults do |encryptor|
+ # # setup all GnuPG public keys
+ # encryptor.keys = {}
+ # encryptor.keys['joe@example.com'] = <<-EOS
+ # # ...public key here...
+ # EOS
+ # encryptor.keys['mary@example.com'] = <<-EOS
+ # # ...public key here...
+ # EOS
+ # end
+ #
+ # The optional {#gpg_config} and {#gpg_homedir} options would also
+ # typically be set using {.defaults} in +config.rb+ as well.
+ #
+ # == Then, setup each of your Models
+ #
+ # Set the desired {#recipients} and/or {#passphrase} (or {#passphrase_file})
+ # for each {Model}, depending on the {#mode} used.
+ #
+ # === my_backup_01
+ #
+ # This archive can only be decrypted using the private key for joe@example.com
+ #
+ # Model.new(:my_backup_01, 'Backup Job #1') do
+ # # ... archives, databases, compressor and storage options, etc...
+ # encrypt_with GPG do |encryptor|
+ # encryptor.mode = :asymmetric
+ # encryptor.recipients = 'joe@example.com'
+ # end
+ # end
+ #
+ # === my_backup_02
+ #
+ # This archive can only be decrypted using the passphrase "a secret".
+ #
+ # Model.new(:my_backup_02, 'Backup Job #2') do
+ # # ... archives, databases, compressor and storage options, etc...
+ # encrypt_with GPG do |encryptor|
+ # encryptor.mode = :symmetric
+ # encryptor.passphrase = 'a secret'
+ # end
+ # end
+ #
+ # === my_backup_03
+ #
+ # This archive may be decrypted using either the private key for joe@example.com
+ # *or* mary@example.com, *and* may also be decrypted using the passphrase.
+ #
+ # Model.new(:my_backup_03, 'Backup Job #3') do
+ # # ... archives, databases, compressor and storage options, etc...
+ # encrypt_with GPG do |encryptor|
+ # encryptor.mode = :both
+ # encryptor.passphrase = 'a secret'
+ # encryptor.recipients = ['joe@example.com', 'mary@example.com']
+ # end
+ # end
+ #
class GPG < Base
+ MODES = [:asymmetric, :symmetric, :both]
##
- # The GPG Public key that'll be used to encrypt the backup
- attr_accessor :key
+ # Sets the mode of operation.
+ #
+ # [:asymmetric]
+ # In this mode, the final backup archive will be encrypted using the
+ # public key(s) specified by the key identifiers in {#recipients}.
+ # The archive may then be decrypted by anyone with a private key that
+ # corresponds to one of the public keys used. See {#recipients} and
+ # {#keys} for more information.
+ #
+ # [:symmetric]
+ # In this mode, the final backup archive will be encrypted using the
+ # passphrase specified by {#passphrase} or {#passphrase_file}.
+ # The archive will be encrypted using the encryption algorithm
+ # specified in your GnuPG configuration. See {#gpg_config} for more
+ # information. Anyone with the passphrase may decrypt the archive.
+ #
+ # [:both]
+ # In this mode, both +:asymmetric+ and +:symmetric+ options are used.
+ # Meaning that the archive may be decrypted by anyone with a valid
+ # private key or by using the proper passphrase.
+ #
+ # @param mode [String, Symbol] Sets the mode of operation.
+ # (Defaults to +:asymmetric+)
+ # @return [Symbol] mode that was set.
+ # @raise [Backup::Errors::Encryptor::GPG::InvalidModeError]
+ # if mode given is invalid.
+ #
+ attr_reader :mode
+ def mode=(mode)
+ @mode = mode.to_sym
+ raise Errors::Encryptor::GPG::InvalidModeError,
+ "'#{ @mode }' is not a valid mode." unless MODES.include?(@mode)
+ end
+
+ ##
+ # Specifies the GnuPG configuration to be used.
+ #
+ # This should be given as the text of a +gpg.conf+ file. It will be
+ # written to a temporary file, which will be passed to the +gpg+ command
+ # to use instead of the +gpg.conf+ found in the GnuPG home directory.
+ # This allows you to be certain your preferences are used.
+ #
+ # This is especially useful if you've also set {#gpg_homedir} and plan
+ # on allowing Backup to automatically create that directory and import
+ # all your public keys specified in {#keys}. In this situation, that
+ # folder would not contain any +gpg.conf+ file, so GnuPG would simply
+ # use it's defaults.
+ #
+ # While this may be specified on a per-Model basis, you would generally
+ # just specify this in the defaults. Leading tabs/spaces are stripped
+ # before writing the given string to the temporary configuration file.
+ #
+ # Backup::Encryptor::GPG.defaults do |enc|
+ # enc.gpg_config = <<-EOF
+ # # safely override preferences set in the receiver's public key(s)
+ # personal-cipher-preferences TWOFISH AES256 BLOWFISH AES192 CAST5 AES
+ # personal-digest-preferences SHA512 SHA256 SHA1 MD5
+ # personal-compress-preferences BZIP2 ZLIB ZIP Uncompressed
+ # # cipher algorithm for symmetric encryption
+ # # (if personal-cipher-preferences are not specified)
+ # s2k-cipher-algo TWOFISH
+ # # digest algorithm for mangling the symmetric encryption passphrase
+ # s2k-digest-algo SHA512
+ # EOF
+ # end
+ #
+ # @see #gpg_homedir
+ # @return [String]
+ attr_accessor :gpg_config
+
+ ##
+ # Set the GnuPG home directory to be used.
+ #
+ # This allows you to specify the GnuPG home directory on the system
+ # where Backup will be run, keeping the keyrings used by Backup separate
+ # from the default keyrings of the user running Backup.
+ # By default, this would be +`~/.gnupg`+.
+ #
+ # If a directory is specified here, Backup will create it if needed
+ # and ensure the correct permissions are set. All public keys Backup
+ # imports would be added to the +pubring.gpg+ file within this directory,
+ # and +gpg+ would be given this directory using it's +--homedir+ option.
+ #
+ # Any +gpg.conf+ file located in this directory would also be used by
+ # +gpg+, unless {#gpg_config} is specified.
+ #
+ # The given path will be expanded before use.
+ #
+ # @return [String]
+ attr_accessor :gpg_homedir
##
- # Creates a new instance of Backup::Encryptor::GPG and
- # sets the key to the provided GPG key. To enhance the DSL
- # the user may use tabs and spaces to indent the multi-line key string
- # since we gsub() every preceding 'space' and 'tab' on each line
+ # Specifies a Hash of public key identifiers and their public keys.
+ #
+ # While not _required_, it is recommended that all public keys you intend
+ # to use be setup in {#keys}. The best place to do this is in your defaults
+ # in +config.rb+.
+ #
+ # Backup::Encryptor::GPG.defaults do |enc|
+ # enc.keys = {}
+ #
+ # enc.keys['joe@example.com'] = <<-EOS
+ # -----BEGIN PGP PUBLIC KEY BLOCK-----
+ # Version: GnuPG v1.4.12 (GNU/Linux)
+ #
+ # mQMqBEd5F8MRCACfArHCJFR6nkmxNiW+UE4PAW3bQla9JWFqCwu4VqLkPI/lHb5p
+ # xHff8Fzy2O89BxD/6hXSDx2SlVmAGHOCJhShx1vfNGVYNsJn2oNK50in9kGvD0+m
+ # [...]
+ # SkQEHOxhMiFjAN9q4LuirSOu65uR1bnTmF+Z92++qMIuEkH4/LnN
+ # =8gNa
+ # -----END PGP PUBLIC KEY BLOCK-----
+ # EOS
+ #
+ # enc.keys['mary@example.com'] = <<-EOS
+ # -----BEGIN PGP PUBLIC KEY BLOCK-----
+ # Version: GnuPG v1.4.12 (GNU/Linux)
+ #
+ # 2SlVmAGHOCJhShx1vfNGVYNxHff8Fzy2O89BxD/6in9kGvD0+mhXSDxsJn2oNK50
+ # kmxNiW+UmQMqBEd5F8MRCACfArHCJFR6qCwu4VqLkPI/lHb5pnE4PAW3bQla9JWF
+ # [...]
+ # AN9q4LSkQEHOxhMiFjuirSOu65u++qMIuEkH4/LnNR1bnTmF+Z92
+ # =8gNa
+ # -----END PGP PUBLIC KEY BLOCK-----
+ #
+ # EOS
+ # end
+ #
+ # All leading spaces/tabs will be stripped from the key, so the above
+ # form may be used to set each identifier's key.
+ #
+ # When a public key can not be found for an identifier specified in
+ # {#recipients}, the corresponding public key from this Hash will be
+ # imported into +pubring.gpg+ in the GnuPG home directory ({#gpg_homedir}).
+ # Therefore, each key *must* be the same identifier used in {#recipients}.
+ #
+ # To obtain the public key in ASCII format, use:
+ #
+ # $ gpg -a --export joe@example.com
+ #
+ # See {#recipients} for information on what may be used as valid identifiers.
+ #
+ # @return [Hash]
+ attr_accessor :keys
+
+ ##
+ # @deprecated Use {#keys} and {#recipients}.
+ # @!attribute key
+ attr_deprecate :key,
+ :version => '3.0.26',
+ :message => "This has been replaced with #keys and #recipients",
+ :action => lambda {|klass, val|
+ identifier = klass.send(:import_key, 'deprecated :key', val)
+ klass.recipients = identifier
+ }
+
+ ##
+ # Specifies the recipients to use when encrypting the backup archive.
+ #
+ # When {#mode} is set to +:asymmetric+ or +:both+, the public key for
+ # each recipient given here will be used to encrypt the archive. Each
+ # recipient will be able to decrypt the archive using their private key.
+ #
+ # If there is only one recipient, this may be specified as a String.
+ # Otherwise, this should be an Array of Strings. Each String must be a
+ # valid public key identifier, and *must* be the same identifier used to
+ # specify the recipient's public key in {#keys}. This is so that if a
+ # public key is not found for the given identifier, it may be imported
+ # from {#keys}.
+ #
+ # Valid identifiers which may be used are as follows:
+ #
+ # [Key Fingerprint]
+ # The key fingerprint is a 40-character hex string, which uniquely
+ # identifies a public key. This may be obtained using the following:
+ #
+ # $ gpg --fingerprint john.smith@example.com
+ # pub 1024R/4E5E8D8A 2012-07-20
+ # Key fingerprint = FFEA D1DB 201F B214 873E 7399 4A83 569F 4E5E 8D8A
+ # uid John Smith <john.smith@example.com>
+ # sub 1024R/92C8DFD8 2012-07-20
+ #
+ # [Long Key ID]
+ # The long Key ID is the last 16-characters of the key's fingerprint.
+ #
+ # The Long Key ID in this example is: 4A83569F4E5E8D8A
+ #
+ # $ gpg --keyid-format long -k john.smith@example.com
+ # pub 1024R/4A83569F4E5E8D8A 2012-07-20
+ # uid John Smith <john.smith@example.com>
+ # sub 1024R/662F18DB92C8DFD8 2012-07-20
+ #
+ # [Short Key ID]
+ # The short Key ID is the last 8-characters of the key's fingerprint.
+ # This is the default key format seen when listing keys.
+ #
+ # The Short Key ID in this example is: 4E5E8D8A
+ #
+ # $ gpg -k john.smith@example.com
+ # pub 1024R/4E5E8D8A 2012-07-20
+ # uid John Smith <john.smith@example.com>
+ # sub 1024R/92C8DFD8 2012-07-20
+ #
+ # [Email Address]
+ # This must exactly match an email address for one of the UID records
+ # associated with the recipient's public key.
+ #
+ # Recipient identifier forms may be mixed, as long as the identifier used
+ # here is the same as that used in {#keys}. Also, all spaces will be stripped
+ # from the identifier when used, so the following would be valid.
+ #
+ # Backup::Model.new(:my_backup, 'My Backup') do
+ # encrypt_with GPG do |enc|
+ # enc.recipients = [
+ # # John Smith
+ # '4A83 569F 4E5E 8D8A',
+ # # Mary Smith
+ # 'mary.smith@example.com'
+ # ]
+ # end
+ # end
+ #
+ # @return [String, Array]
+ attr_accessor :recipients
+
+ ##
+ # Specifies the passphrase to use symmetric encryption.
+ #
+ # When {#mode} is +:symmetric+ or +:both+, this passphrase will be used
+ # to symmetrically encrypt the archive.
+ #
+ # Use of this option will override the use of {#passphrase_file}.
+ #
+ # @return [String]
+ attr_accessor :passphrase
+
+ ##
+ # Specifies the passphrase file to use symmetric encryption.
+ #
+ # When {#mode} is +:symmetric+ or +:both+, this file will be passed
+ # to the +gpg+ command line, where +gpg+ will read the first line from
+ # this file and use it for the passphrase.
+ #
+ # The file path given here will be expanded to a full path.
+ #
+ # If {#passphrase} is specified, {#passphrase_file} will be ignored.
+ # Therefore, if you have set {#passphrase} in your global defaults,
+ # but wish to use {#passphrase_file} with a specific {Model}, be sure
+ # to clear {#passphrase} within that model's configuration.
+ #
+ # Backup::Encryptor::GPG.defaults do |enc|
+ # enc.passphrase = 'secret phrase'
+ # end
+ #
+ # Backup::Model.new(:my_backup, 'My Backup') do
+ # # other directives...
+ # encrypt_with GPG do |enc|
+ # enc.mode = :symmetric
+ # enc.passphrase = nil
+ # enc.passphrase_file = '/path/to/passphrase.file'
+ # end
+ # end
+ #
+ # @return [String]
+ attr_accessor :passphrase_file
+
+ ##
+ # Configures default accessor values for new class instances.
+ #
+ # If all required options are set, then no further configuration
+ # would be needed within a Model's definition when an Encryptor is added.
+ # Therefore, the following example is sufficient to encrypt +:my_backup+:
+ #
+ # # Defaults set in config.rb
+ # Backup::Encryptor::GPG.defaults do |encryptor|
+ # encryptor.keys = {}
+ # encryptor.keys['joe@example.com'] = <<-EOS
+ # -----BEGIN PGP PUBLIC KEY BLOCK-----
+ # Version: GnuPG v1.4.12 (GNU/Linux)
+ #
+ # mI0EUBR6CwEEAMVSlFtAXO4jXYnVFAWy6chyaMw+gXOFKlWojNXOOKmE3SujdLKh
+ # kWqnafx7VNrb8cjqxz6VZbumN9UgerFpusM3uLCYHnwyv/rGMf4cdiuX7gGltwGb
+ # (...etc...)
+ # mLekS3xntUhhgHKc4lhf4IVBqG4cFmwSZ0tZEJJUSESb3TqkkdnNLjE=
+ # =KEW+
+ # -----END PGP PUBLIC KEY BLOCK-----
+ # EOS
+ #
+ # encryptor.recipients = 'joe@example.com'
+ # end
+ #
+ # # Encryptor set in the model
+ # Backup::Model.new(:my_backup, 'My Backup') do
+ # # archives, storage options, etc...
+ # encrypt_with GPG
+ # end
+ #
+ # @!scope class
+ # @see Configuration::Helpers::ClassMethods#defaults
+ # @yield [config] OpenStruct object
+ # @!method defaults
+
+ ##
+ # Creates a new instance of Backup::Encryptor::GPG.
+ #
+ # This constructor is not used directly when configuring Backup.
+ # Use {Model#encrypt_with}.
+ #
+ # Model.new(:backup_trigger, 'Backup Label') do
+ # archive :my_archive do |archive|
+ # archive.add '/some/directory'
+ # end
+ #
+ # compress_with Gzip
+ #
+ # encrypt_with GPG do |encryptor|
+ # encryptor.mode = :both
+ # encryptor.passphrase = 'a secret'
+ # encryptor.recipients = ['joe@example.com', 'mary@example.com']
+ # end
+ #
+ # store_with SFTP
+ #
+ # notify_by Mail
+ # end
+ #
+ # @api private
def initialize(&block)
super
instance_eval(&block) if block_given?
+
+ @mode ||= :asymmetric
end
##
# This is called as part of the procedure run by the Packager.
- # It sets up the needed encryption_key_email to pass to the gpg command,
+ # It sets up the needed options to pass to the gpg command,
# then yields the command to use as part of the packaging procedure.
# Once the packaging procedure is complete, it will return
# so that any clean-up may be performed after the yield.
+ # Cleanup is also ensured, as temporary files may hold sensitive data.
+ # If no options can be built, the packaging process will be aborted.
+ #
+ # @api private
def encrypt_with
log!
- extract_encryption_key_email!
+ prepare
+
+ if mode_options.empty?
+ raise Errors::Encryptor::GPG::EncryptionError,
+ "Encryption could not be performed for mode '#{ mode }'"
+ end
+
+ yield "#{ utility(:gpg) } #{ base_options } #{ mode_options }", '.gpg'
- yield "#{ utility(:gpg) } #{ options }", '.gpg'
+ ensure
+ cleanup
end
private
##
- # Imports the given encryption key to ensure it's available for use,
- # and extracts the email address used to create the key.
- # This is stored in '@encryption_key_email', to be used to specify
- # the --recipient when performing encryption so this key is used.
- def extract_encryption_key_email!
- if @encryption_key_email.to_s.empty?
- with_tmp_key_file do |tmp_file|
- @encryption_key_email = run(
- "#{ utility(:gpg) } --import '#{tmp_file}' 2>&1"
- ).match(/<(.+)>/)[1]
- end
+ # Remove any temporary directories and reset all instance variables.
+ #
+ def prepare
+ FileUtils.rm_rf(@tempdirs, :secure => true) if @tempdirs
+ @tempdirs = []
+ @base_options = nil
+ @mode_options = nil
+ @user_recipients = nil
+ @user_keys = nil
+ @system_identifiers = nil
+ end
+ alias :cleanup :prepare
+
+ ##
+ # Returns the options needed for the gpg command line which are
+ # not dependant on the #mode. --no-tty supresses output of certain
+ # messages, like the "Reading passphrase from file descriptor..."
+ # messages during symmetric encryption
+ #
+ def base_options
+ @base_options ||= begin
+ opts = ['--no-tty']
+ path = setup_gpg_homedir
+ opts << "--homedir '#{ path }'" if path
+ path = setup_gpg_config
+ opts << "--options '#{ path }'" if path
+ opts.join(' ')
+ end
+ end
+
+ ##
+ # Setup the given :gpg_homedir if needed, ensure the proper permissions
+ # are set, and return the directory's path. Otherwise, return false.
+ #
+ # If the GnuPG files do not exist, trigger their creation by requesting
+ # --list-secret-keys. Some commands, like for symmetric encryption, will
+ # issue messages about their creation on STDERR, which generates unwanted
+ # warnings in the log. This way, if any of these files are created here,
+ # we will get those messages on STDOUT for the log, without the actual
+ # secret key listing which we don't care about.
+ #
+ def setup_gpg_homedir
+ return false unless gpg_homedir
+
+ path = File.expand_path(gpg_homedir)
+ FileUtils.mkdir_p(path)
+ FileUtils.chown(Config.user, nil, path)
+ FileUtils.chmod(0700, path)
+
+ unless %w{ pubring.gpg secring.gpg trustdb.gpg }.
+ all? {|name| File.exist? File.join(path, name) }
+ run("#{ utility(:gpg) } --homedir '#{ path }' -K 2>&1 >/dev/null")
+ end
+
+ path
+
+ rescue => err
+ raise Errors::Encryptor::GPG::HomedirError.wrap(
+ err, "Failed to create or set permissions for #gpg_homedir")
+ end
+
+ ##
+ # Write the given #gpg_config to a tempfile, within a tempdir, and
+ # return the file's path to be given to the gpg --options argument.
+ # If no #gpg_config is set, return false.
+ #
+ # This is required in order to set the proper permissions on the
+ # directory containing the tempfile. The tempdir will be removed
+ # after the packaging procedure is completed.
+ #
+ # Once written, we'll call check_gpg_config to make sure there are
+ # no problems that would prevent gpg from running with this config.
+ # If any errors occur during this process, we can not proceed.
+ # We'll cleanup to remove the tempdir (if created) and raise an error.
+ #
+ def setup_gpg_config
+ return false unless gpg_config
+
+ dir = Dir.mktmpdir('backup-gpg_config', Config.tmp_path)
+ @tempdirs << dir
+ file = Tempfile.open('backup-gpg_config', dir)
+ file.write gpg_config.gsub(/^[[:blank:]]+/, '')
+ file.close
+
+ check_gpg_config(file.path)
+
+ file.path
+
+ rescue => err
+ cleanup
+ raise Errors::Encryptor::GPG::GPGConfigError.wrap(
+ err, "Error creating temporary file for #gpg_config.")
+ end
+
+ ##
+ # Make sure the temporary GnuPG config file created from #gpg_config
+ # does not have any syntax errors that would prevent gpg from running.
+ # If so, raise the returned error message.
+ # Note that Cli::Helpers#run may also raise an error here.
+ #
+ def check_gpg_config(path)
+ ret = run(
+ "#{ utility(:gpg) } --options '#{ path }' --gpgconf-test 2>&1"
+ ).chomp
+ raise ret unless ret.empty?
+ end
+
+ ##
+ # Returns the options needed for the gpg command line to perform
+ # the encryption based on the #mode.
+ #
+ def mode_options
+ @mode_options ||= begin
+ s_opts = symmetric_options if mode != :asymmetric
+ a_opts = asymmetric_options if mode != :symmetric
+ [s_opts, a_opts].compact.join(' ')
+ end
+ end
+
+ ##
+ # Process :passphrase or :passphrase_file and return the command line
+ # options to perform symmetric encryption. If no :passphrase is
+ # specified, or an error occurs creating a temporary file for it, then
+ # try to use :passphrase_file if it's set.
+ # If the option can not be set, log a warning and return nil.
+ #
+ def symmetric_options
+ path = setup_passphrase_file
+ unless path || passphrase_file.to_s.empty?
+ path = File.expand_path(passphrase_file.to_s)
+ end
+
+ if path && File.exist?(path)
+ "-c --passphrase-file '#{ path }'"
+ else
+ Logger.warn("Symmetric encryption options could not be set.")
+ nil
+ end
+ end
+
+ ##
+ # Create a temporary file, within a tempdir, to hold the :passphrase and
+ # return the file's path. If an error occurs, log a warning.
+ # Return false if no :passphrase is set or an error occurs.
+ #
+ def setup_passphrase_file
+ return false if passphrase.to_s.empty?
+
+ dir = Dir.mktmpdir('backup-gpg_passphrase', Config.tmp_path)
+ @tempdirs << dir
+ file = Tempfile.open('backup-gpg_passphrase', dir)
+ file.write passphrase.to_s
+ file.close
+
+ file.path
+
+ rescue => err
+ Logger.warn Errors::Encryptor::GPG::PassphraseError.wrap(
+ err, "Error creating temporary passphrase file.")
+ false
+ end
+
+ ##
+ # Process :recipients, importing their public key from :keys if needed,
+ # and return the command line options to perform asymmetric encryption.
+ # Log a warning and return nil if no valid recipients are found.
+ #
+ def asymmetric_options
+ if user_recipients.empty?
+ Logger.warn "No recipients available for asymmetric encryption."
+ nil
+ else
+ # skip trust database checks
+ "-e --trust-model always " +
+ user_recipients.map {|r| "-r '#{ r }'" }.join(' ')
end
end
##
- # GPG options
- # Sets the gpg mode to 'encrypt' and passes in the encryption_key_email
- def options
- "-e --trust-model always -r '#{ @encryption_key_email }'"
+ # Returns an Array of the public key identifiers the user specified
+ # in :recipients. Each identifier is 'cleaned' so that exact matches
+ # can be performed. Then each is checked to ensure it will find a
+ # public key that exists in the system's public keyring.
+ # If the identifier does not match an existing key, the public key
+ # associated with the identifier in :keys will be imported for use.
+ # If no key can be found in the system or in :keys for the identifier,
+ # a warning will be issued; as we will attempt to encrypt the backup
+ # and proceed if at all possible.
+ #
+ def user_recipients
+ @user_recipients ||= begin
+ [recipients].flatten.compact.map do |identifier|
+ identifier = clean_identifier(identifier)
+ if system_identifiers.include?(identifier)
+ identifier
+ else
+ key = user_keys[identifier]
+ if key
+ # will log a warning and return nil if the import fails
+ import_key(identifier, key)
+ else
+ Logger.warn(
+ "No public key was found in #keys for '#{ identifier }'"
+ )
+ nil
+ end
+ end
+ end.compact
+ end
+ end
+
+ ##
+ # Returns the #keys hash set by the user with all identifiers
+ # (Hash keys) 'cleaned' for exact matching. If the cleaning process
+ # creates duplicate keys, the user will be warned.
+ #
+ def user_keys
+ @user_keys ||= begin
+ _keys = keys || {}
+ ret = Hash[_keys.map {|k,v| [clean_identifier(k), v] }]
+ Logger.warn(
+ "Duplicate public key identifiers were detected in #keys."
+ ) if ret.keys.count != _keys.keys.count
+ ret
+ end
end
##
- # Writes the provided public gpg key to a temp file,
- # yields the path, then deletes the file when the block returns.
- def with_tmp_key_file
- tmp_file = Tempfile.new('backup.pub')
- FileUtils.chown(Config.user, nil, tmp_file.path)
- FileUtils.chmod(0600, tmp_file.path)
- tmp_file.write(encryption_key)
- tmp_file.close
- yield tmp_file.path
- tmp_file.delete
+ # Cleans a public key identifier.
+ # Strip out all spaces, upcase non-email identifiers,
+ # and wrap email addresses in <> to perform exact matching.
+ #
+ def clean_identifier(str)
+ str = str.to_s.gsub(/[[:blank:]]+/, '')
+ str =~ /@/ ? "<#{ str.gsub(/(<|>)/,'') }>" : str.upcase
end
##
- # Returns the encryption key with preceding spaces and tabs removed
- def encryption_key
- key.gsub(/^[[:blank:]]+/, '')
+ # Import the given public key and return the 16 character Key ID.
+ # If the import fails, return nil.
+ # Note that errors raised by Cli::Helpers#run may also be rescued here.
+ #
+ def import_key(identifier, key)
+ file = Tempfile.open('backup-gpg_import', Config.tmp_path)
+ file.write(key.gsub(/^[[:blank:]]+/, ''))
+ file.close
+ ret = run(
+ "#{ utility(:gpg) } #{ base_options } " +
+ "--keyid-format 0xlong --import '#{ file.path }' 2>&1"
+ )
+ file.delete
+
+ keyid = ret.match(/ 0x(\w{16})/).to_a[1]
+ raise "GPG Returned:\n#{ ret.gsub(/^\s*/, ' ') }" unless keyid
+ keyid
+
+ rescue => err
+ Logger.warn Errors::Encryptor::GPG::KeyImportError.wrap(
+ err, "Public key import failed for '#{ identifier }'")
+ nil
+ end
+
+ ##
+ # Parse the information for all the public keys found in the public
+ # keyring (based on #gpg_homedir setting) and return an Array of all
+ # identifiers which could be used to specify a valid key.
+ #
+ def system_identifiers
+ @system_identifiers ||= begin
+ skip_key = false
+ data = run(
+ "#{ utility(:gpg) } #{ base_options } " +
+ "--with-colons --fixed-list-mode --fingerprint"
+ )
+ data.lines.map do |line|
+ line.strip!
+
+ # process public key record
+ if line =~ /^pub:/
+ validity, keyid, capabilities =
+ line.split(':').values_at(1, 4, 11)
+ # skip keys marked as revoked ('r'), expired ('e'),
+ # invalid ('i') or disabled ('D')
+ if validity[0,1] =~ /(r|e|i)/ || capabilities =~ /D/
+ skip_key = true
+ next nil
+ else
+ skip_key = false
+ # return both the long and short id
+ next [keyid[-8..-1], keyid]
+ end
+ else
+ # wait for the next valid public key record
+ next nil if skip_key
+
+ # process UID records for the current public key
+ if line =~ /^uid:/
+ validity, userid = line.split(':').values_at(1, 9)
+ # skip records marked as revoked ('r'), expired ('e')
+ # or invalid ('i')
+ if validity !~ /(r|e|i)/
+ # return the last email found in user id string,
+ # since this includes user supplied comments.
+ # return nil if no email found.
+ email, str = nil, userid
+ while match = str.match(/<.+?@.+?>/)
+ email, str = match[0], match.post_match
+ end
+ next email
+ end
+ # return public key's fingerprint
+ elsif line =~ /^fpr:/
+ next line.split(':')[9]
+ end
+
+ nil # ignore any other lines
+ end
+ end.flatten.compact
+ end
end
end
View
4 lib/backup/syncer/cloud/base.rb
@@ -84,7 +84,7 @@ def sync!(mirror = false, concurrency_type = false, concurrency_level = 2)
case concurrency_type
when FalseClass
- all_file_names.each &block
+ all_file_names.each(&block)
when :threads
Parallel.each all_file_names,
:in_threads => concurrency_level, &block
@@ -128,7 +128,7 @@ def local_files
# Returns a String of file paths and their md5 hashes.
def local_hashes
Logger.message("\s\sGenerating checksums for '#{ @directory }'")
- `find #{ @directory } -print0 | xargs -0 openssl md5 2> /dev/null`
+ `find '#{ @directory }' -print0 | xargs -0 openssl md5 2> /dev/null`
end
##
View
98 spec-live/backups/config.rb
@@ -1,16 +1,13 @@
-
-##
-# Archive Job
-archive_job = lambda do |archive|
- archive.add File.expand_path('../../../lib/backup', __FILE__)
- archive.exclude File.expand_path('../../../lib/backup/storage', __FILE__)
-end
-
##
-# Configuration
+# Configuration Defaults
+#
+# Note that once config.rb has been loaded once and class defaults are set,
+# there's nothing clearing these between each test. Keep this in mind, as this
+# file will be loaded for every call to h_set_trigger() or h_set_single_model().
+# While not a problem, this wouldn't happen for a normal `backup perform ...`.
Backup::Storage::Local.defaults do |storage|
- storage.path = Backup::SpecLive::TMP_PATH
+ storage.path = SpecLive::TMP_PATH
storage.keep = 2
end
@@ -62,7 +59,7 @@
notifier.enable_starttls_auto = opts['enable_starttls_auto'] || true
notifier.sendmail = opts['sendmail']
notifier.sendmail_args = opts['sendmail_args']
- notifier.mail_folder = Backup::SpecLive::TMP_PATH
+ notifier.mail_folder = SpecLive::TMP_PATH
end
Backup::Syncer::Cloud::S3.defaults do |s3|
@@ -75,79 +72,12 @@
s3.mirror = true
end
-##
-# Models
-
-Backup::Model.new(:archive_local, 'test_label') do
- archive :test_archive, &archive_job
- store_with Local
-end
-
-Backup::Model.new(:archive_scp, 'test_label') do
- archive :test_archive, &archive_job
- store_with SCP
-end
-
-# To initialize the Dropbox session cache, run manually first using:
-# VERBOSE=1 rspec spec-live/storage/dropbox_spec.rb --tag init
-Backup::Model.new(:archive_dropbox, 'test_label') do
- archive :test_archive, &archive_job
- store_with Dropbox
-end
-
-Backup::Model.new(:compressor_gzip_archive_local, 'test_label') do
- archive :test_archive, &archive_job
- compress_with Gzip
- store_with Local
+Backup::Encryptor::GPG.defaults do |enc|
+ enc.gpg_homedir = File.join(SpecLive::TMP_PATH, 'gpg_home_tmp')
end
-Backup::Model.new(:compressor_custom_archive_local, 'test_label') do
- archive :test_archive, &archive_job
- compress_with Custom do |c|
- c.command = 'gzip -1'
- c.extension = '.foo'
- end
- store_with Local
-end
-
-Backup::Model.new(:notifier_mail, 'test_label') do
- notify_by Mail
-end
-
-Backup::Model.new(:notifier_mail_file, 'test_label') do
- notify_by Mail do |mail|
- mail.to = 'test@backup'
- mail.delivery_method = :file
- end
-end
-
-Backup::Model.new(:syncer_cloud_s3, 'test_label') do
- sync_with Cloud::S3 do |s3|
- s3.directories do
- add File.join(Backup::SpecLive::SYNC_PATH, 'dir_a')
- add File.join(Backup::SpecLive::SYNC_PATH, 'dir_b')
- end
- end
-end
-
-Backup::Model.new(:syncer_cloud_processes_s3, 'test_label') do
- sync_with Cloud::S3 do |s3|
- s3.concurrency_type = :processes
- s3.concurrency_level = 2
- s3.directories do
- add File.join(Backup::SpecLive::SYNC_PATH, 'dir_a')
- add File.join(Backup::SpecLive::SYNC_PATH, 'dir_b')
- end
- end
-end
-
-Backup::Model.new(:syncer_cloud_threads_s3, 'test_label') do
- sync_with Cloud::S3 do |s3|
- s3.concurrency_type = :threads
- s3.concurrency_level = 2
- s3.directories do
- add File.join(Backup::SpecLive::SYNC_PATH, 'dir_a')
- add File.join(Backup::SpecLive::SYNC_PATH, 'dir_b')
- end
- end
+##
+# Load the Models, unless h_set_single_model() is being used.
+if SpecLive.load_models
+ instance_eval File.read(File.join(File.dirname(__FILE__), 'models.rb'))
end
View
3  spec-live/backups/config.yml.template
@@ -41,3 +41,6 @@ syncer:
secret_access_key:
bucket:
region:
+encryptor:
+ gpg:
+ specs_enabled: false
View
184 spec-live/backups/models.rb
@@ -0,0 +1,184 @@
+##
+# Models
+
+Backup::Model.new(:archive_local, 'test_label') do
+ archive :test_archive, &SpecLive::ARCHIVE_JOB
+ store_with Local
+end
+
+Backup::Model.new(:archive_scp, 'test_label') do
+ archive :test_archive, &SpecLive::ARCHIVE_JOB
+ store_with SCP
+end
+
+# To initialize the Dropbox session cache, run manually first using:
+# VERBOSE=1 rspec spec-live/storage/dropbox_spec.rb --tag init
+Backup::Model.new(:archive_dropbox, 'test_label') do
+ archive :test_archive, &SpecLive::ARCHIVE_JOB
+ store_with Dropbox
+end
+
+Backup::Model.new(:compressor_gzip_archive_local, 'test_label') do
+ archive :test_archive, &SpecLive::ARCHIVE_JOB
+ compress_with Gzip
+ store_with Local
+end
+
+Backup::Model.new(:compressor_custom_archive_local, 'test_label') do
+ archive :test_archive, &SpecLive::ARCHIVE_JOB
+ compress_with Custom do |c|
+ c.command = 'gzip -1'
+ c.extension = '.foo'
+ end
+ store_with Local
+end
+
+Backup::Model.new(:notifier_mail, 'test_label') do
+ notify_by Mail
+end
+
+Backup::Model.new(:notifier_mail_file, 'test_label') do
+ notify_by Mail do |mail|
+ mail.to = 'test@backup'
+ mail.delivery_method = :file
+ end
+end
+
+Backup::Model.new(:syncer_cloud_s3, 'test_label') do
+ sync_with Cloud::S3 do |s3|
+ s3.directories do
+ add File.join(SpecLive::SYNC_PATH, 'dir_a')
+ add File.join(SpecLive::SYNC_PATH, 'dir_b')
+ end
+ end
+end
+
+Backup::Model.new(:encryptor_gpg_asymmetric, 'test_label') do
+ archive :test_archive, &SpecLive::ARCHIVE_JOB
+ encrypt_with GPG do |e|
+ # this is the default mode
+ # e.mode = :asymmetric
+
+ e.keys = {
+ # backup03 public key (email as identifier)
+ 'backup03@foo.com' => SpecLive::GPGKeys[:backup03][:public],
+ # backup04 public key (long key id as identifier)
+ '0F45932D3F24426D' => SpecLive::GPGKeys[:backup04][:public]
+ }
+
+ # The public keys for backup01 and backup02 will be in the system keyring
+ # when this job is run. The public keys for backup03 and backup04 will be
+ # imported from :keys above when they are not found in the system keyring.
+ e.recipients = [
+ # backup01 (short keyid)
+ '16325C61',
+ # backup02 (key fingerprint)
+ 'F9A9 9BD8 A570 182F F190 037C 7118 9938 6A6A 175A',
+ # backup03 (email)
+ 'backup03@foo.com',
+ # backup04 (long keyid)
+ '0F45932D3F24426D'
+ ]
+ end
+ store_with Local
+end
+
+Backup::Model.new(:encryptor_gpg_asymmetric_missing, 'test_label') do
+ archive :test_archive, &SpecLive::ARCHIVE_JOB
+ encrypt_with GPG do |e|
+ e.mode = :asymmetric
+
+ e.keys = {
+ 'backup02@foo.com' => SpecLive::GPGKeys[:backup02][:public]
+ }
+
+ e.recipients = [
+ 'backup01@foo.com', # in the system
+ 'backup02@foo.com', # imported from #keys
+ 'backupfoo@foo.com' # a recipient with no public key
+ ]
+ end
+ store_with Local
+end
+
+Backup::Model.new(:encryptor_gpg_asymmetric_fail, 'test_label') do
+ archive :test_archive, &SpecLive::ARCHIVE_JOB
+ encrypt_with GPG do |e|
+ # this is the default mode
+ # e.mode = :asymmetric
+
+ # no recipients have public keys available
+ e.recipients = [
+ 'backupfoo@foo.com',
+ 'backupfoo2@foo.com'
+ ]
+ end
+ store_with Local
+end
+
+Backup::Model.new(:encryptor_gpg_symmetric, 'test_label') do
+ archive :test_archive, &SpecLive::ARCHIVE_JOB
+ encrypt_with GPG do |e|
+ e.mode = :symmetric
+ e.passphrase = 'a secret'
+ end
+ store_with Local
+end
+
+Backup::Model.new(:encryptor_gpg_symmetric_with_config, 'test_label') do
+ archive :test_archive, &SpecLive::ARCHIVE_JOB
+ encrypt_with GPG do |e|
+ e.mode = :symmetric
+ e.passphrase = 'a secret'
+ e.gpg_config = <<-EOS
+ personal-cipher-preferences AES256 CAST5
+ EOS
+ end
+ store_with Local
+end
+
+Backup::Model.new(:encryptor_gpg_both, 'test_label') do
+ archive :test_archive, &SpecLive::ARCHIVE_JOB
+ encrypt_with GPG do |e|
+ e.mode = :both
+ e.passphrase = 'a secret'
+
+ e.keys = {
+ # backup03 public key (email as identifier)
+ 'backup03@foo.com' => SpecLive::GPGKeys[:backup03][:public]
+ }
+
+ # The public key for backup01 will be in the system keyring.
+ # The public key for backup03 will be imported from :keys.
+ e.recipients = [
+ # backup01 (short keyid)
+ '16325C61',
+ # backup03 (email)
+ 'backup03@foo.com'
+ ]
+ end
+ store_with Local
+end
+
+Backup::Model.new(:encryptor_gpg_both_no_asymmetric, 'test_label') do
+ archive :test_archive, &SpecLive::ARCHIVE_JOB
+ encrypt_with GPG do |e|
+ e.mode = :both
+ e.passphrase = 'a secret'
+
+ # valid entry for backup04, but this is not one of the recipients
+ e.keys = {
+ 'backup04@foo.com' => SpecLive::GPGKeys[:backup04][:public]
+ }
+
+ # no keys will be found for these,
+ # so only symmetric encryption will be possible for this backup
+ e.recipients = [
+ # backup01 (short keyid)
+ '16325C61',
+ # backup03 (email)
+ 'backup03@foo.com'
+ ]
+ end
+ store_with Local
+end
View
239 spec-live/encryptor/gpg_keys.rb
@@ -0,0 +1,239 @@
+# encoding: utf-8
+
+module Backup
+ module SpecLive
+ GPGKeys = Hash.new {|h,k| h[k] = {} }
+
+ GPGKeys[:backup01][:long_id] = '8F5D150616325C61'
+ GPGKeys[:backup01][:public] = <<-EOS
+ -----BEGIN PGP PUBLIC KEY BLOCK-----
+ Version: GnuPG v1.4.12 (GNU/Linux)
+
+ mI0EUBR6CwEEAMVSlFtAXO4jXYnVFAWy6chyaMw+gXOFKlWojNXOOKmE3SujdLKh
+ kWqnafx7VNrb8cjqxz6VZbumN9UgerFpusM3uLCYHnwyv/rGMf4cdiuX7gGltwGb
+ dwP18gzdDanXYvO4G7qtk8DH1lRI/TnLH8wAoY/DthSR//wcP33GxRnrABEBAAG0
+ HkJhY2t1cCBUZXN0IDxiYWNrdXAwMUBmb28uY29tPoi4BBMBAgAiBQJQFHoLAhsD
+ BgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRCPXRUGFjJcYZc5A/91HQ4+BXUw
+ KUHmpGyD+6b42xl3eBdmBVimCYTgfRUyRQEDWzHV4a3cYWwe30J/LzfFn1E6uVwz
+ 6SHtzwZddWwONRgC/Q7V0TrnLeuv46MKoB24EFqfbr0kosuZgHPuK4h+rLmaPVCM
+ kv9DcKCupGnFzoaZ7tdgQAXzGepZQZU0sbiNBFAUegsBBACXX6cGjaENoOMyibXh
+ HLlEOKWwlxDgo3bSu6U8kISUmwxH/MO4I/u7BAQBczitNYSWovlbvCcxCy4h12qj
+ gruN5INIDGsEfDSWcN3lxBZ+9J+FhngBjETeADFaPxoele9wVGdIYdLIP6RntK/D
+ 6F2ER8sb57YYtK50iyyuYQqSeQARAQABiJ8EGAECAAkFAlAUegsCGwwACgkQj10V
+ BhYyXGEq7AP/Y/k1A6nrCC6bfkHFMJmEJDe0Wb6q8P3gcBqNuf8iOwk53UdoXU0Q
+ JcPAQd/sJy6914huN2dzbVCSchSkLRXgejI0Xf5go1sDzspVKEflu0CWZ3A976QH
+ mLekS3xntUhhgHKc4lhf4IVBqG4cFmwSZ0tZEJJUSESb3TqkkdnNLjE=
+ =KEW+
+ -----END PGP PUBLIC KEY BLOCK-----
+ EOS
+ GPGKeys[:backup01][:private] = <<-EOS
+ -----BEGIN PGP PRIVATE KEY BLOCK-----
+ Version: GnuPG v1.4.12 (GNU/Linux)
+
+ lQHYBFAUegsBBADFUpRbQFzuI12J1RQFsunIcmjMPoFzhSpVqIzVzjiphN0ro3Sy
+ oZFqp2n8e1Ta2/HI6sc+lWW7pjfVIHqxabrDN7iwmB58Mr/6xjH+HHYrl+4BpbcB
+ m3cD9fIM3Q2p12LzuBu6rZPAx9ZUSP05yx/MAKGPw7YUkf/8HD99xsUZ6wARAQAB
+ AAP/VJDiogUAjtK7SNH4BcU6qjxWK4pyQkcE8LcOvKbn48bcXtJrtg7GWpYrNxjI
+ Mg/nHHt6Lpkqg3RmI0ILMzOj5TukhmJnB/ieogFyuiVzymcMdkcx8PRNXIoF90Au
+ 8yp5ZgXdStmIrlxh4ofJsas8YWsVynb+r6FT2UrAjYT3vAECANoCF26P14ZtGQJ4
+ eT4a19wzlcIDMuKFaXlB5WYCnQi43ngKn/mjdQrNfpfSPNF+oCHlWGisz0fg/51+
+ NXg46+sCAOe1pm8K6qO20SzkAW0b9Hzk0b0JWJiHk1QiB1gR9KZffC/8Y7rqYaiG
+ Sbtyc3ujjayFdc90bwZeSkzrCvO/CgEB/3jiuZiAOov2JFMMPzp7S9SS3CN8nopp
+ xupeE6uH3Kxp24XYgLxfRO9iqZK/BRlkb5fUKw8u08J5439X9o9mBUOf4bQeQmFj
+ a3VwIFRlc3QgPGJhY2t1cDAxQGZvby5jb20+iLgEEwECACIFAlAUegsCGwMGCwkI
+ BwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEI9dFQYWMlxhlzkD/3UdDj4FdTApQeak
+ bIP7pvjbGXd4F2YFWKYJhOB9FTJFAQNbMdXhrdxhbB7fQn8vN8WfUTq5XDPpIe3P
+ Bl11bA41GAL9DtXROuct66/jowqgHbgQWp9uvSSiy5mAc+4riH6suZo9UIyS/0Nw
+ oK6kacXOhpnu12BABfMZ6llBlTSxnQHYBFAUegsBBACXX6cGjaENoOMyibXhHLlE
+ OKWwlxDgo3bSu6U8kISUmwxH/MO4I/u7BAQBczitNYSWovlbvCcxCy4h12qjgruN
+ 5INIDGsEfDSWcN3lxBZ+9J+FhngBjETeADFaPxoele9wVGdIYdLIP6RntK/D6F2E
+ R8sb57YYtK50iyyuYQqSeQARAQABAAP6Akx2AvtBheKqnJIl3cn5FWxWS3Q/Jygi
+ +2rVJNJYKbu2hVJ/xDMLWoZNC3AXsof95X3f/e4sJVpN/FPS/IdqqBmZpREOzash
+ fWAjs6j7Z7lQEuxKvdSdcy4olzcYGOegYaZpL0B9eeOtX3Hb4JXHwp0i9NwFlXgg
+ MR10rIy48qECAMBEW64UAaZ31/Q1P0NXfMYTm+vogrAi8lsVdSAjDBAKtAD3+mGD
+ JGymg0R6uzw9xDijN9HgBi55TfCAiVoVhiECAMmNHHuypUMVsPstBtUEELwDHH7K
+ acWFNhi6x4Ccl1C7xncKK6BedjjwP8K06hBjWAkBzNUwX79N0Lm+ob9O0VkB+wdE
+ ykurS0qEob/PiLBA5ksWRA1EKLQ/PjEmSxYJAldVLyKtUPtfyXVKTpZ2xCwuLi/m
+ sspaiPeTJp7Gtr1l/smYf4ifBBgBAgAJBQJQFHoLAhsMAAoJEI9dFQYWMlxhKuwD
+ /2P5NQOp6wgum35BxTCZhCQ3tFm+qvD94HAajbn/IjsJOd1HaF1NECXDwEHf7Ccu
+ vdeIbjdnc21QknIUpC0V4HoyNF3+YKNbA87KVShH5btAlmdwPe+kB5i3pEt8Z7VI
+ YYBynOJYX+CFQahuHBZsEmdLWRCSVEhEm906pJHZzS4x
+ =ls9W
+ -----END PGP PRIVATE KEY BLOCK-----
+ EOS
+
+ GPGKeys[:backup02][:long_id] = '711899386A6A175A'
+ GPGKeys[:backup02][:public] = <<-EOS
+ -----BEGIN PGP PUBLIC KEY BLOCK-----
+ Version: GnuPG v1.4.12 (GNU/Linux)
+
+ mI0EUBR6JQEEAMPuY4qoKR7MpZA7D+KRrhiJRCT8t5F4IZ+gq7FBCfwoopcStlin
+ nTGRL1F/UEngu5xHgLsERzPdKNoTpoy1aCcgB7i9vyi2bA3iNGs1YzWqf1vGJxEp
+ +CXXtlBU+QYod6wRMK4cHKVjPaD+Ga+saU1Pz9tBGYAbADYVmhJd7R21ABEBAAG0
+ HkJhY2t1cCBUZXN0IDxiYWNrdXAwMkBmb28uY29tPoi4BBMBAgAiBQJQFHolAhsD
+ BgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRBxGJk4amoXWjJxA/sG60qZMn40
+ IP0pjuY+3kc5199xcavxgHZdQTBq2vrJHzr3K8Zk8xxSCADRJf4++t4+8yG7g9rk
+ bmEAeGoFBIbdx7zGcxYIe5g9UG/+eDCJ44HCb0v8vHUT7iqt5Jk6cryYtcYvqO6I
+ 84HXdXz4rCMCUeRP4ceMCZB6GZKDXR8zoriNBFAUeiUBBADEEOUdfCmwJ5BEPWzm
+ QTGcp2dSY8SgpbZtk23LvWIlEziYflIqOGeL623AOBb9S2FChR+zPyOA9D3LEZrA
+ PF8TS3YY+qjBdiXLKchRr5y2Kwylh/vVdcCcgTjfPkga4pGq+cHBOevPiqAvoCtO
+ ntN7+Cqk3B7nWTPQRFDQJVThwwARAQABiJ8EGAECAAkFAlAUeiUCGwwACgkQcRiZ
+ OGpqF1oNNgP/adkSZZVaYy/JEHjjrtC9UwirSluAdHQZGPGv/FHaFou+4mlLi0+R
+ gQLieU+3AHFSWVYMNjIsSjA4hhMLchteFlJnbkGAFUaA2QUoyf7cSSmdpNKQdDdZ
+ oe3OXEQM2pUTEwE9AqKXthrSMQuJ5bozm8zHm/CohZPHGTeQH75Kq/4=
+ =AgbQ
+ -----END PGP PUBLIC KEY BLOCK-----
+ EOS
+ GPGKeys[:backup02][:private] = <<-EOS
+ -----BEGIN PGP PRIVATE KEY BLOCK-----
+ Version: GnuPG v1.4.12 (GNU/Linux)
+
+ lQHYBFAUeiUBBADD7mOKqCkezKWQOw/ika4YiUQk/LeReCGfoKuxQQn8KKKXErZY
+ p50xkS9Rf1BJ4LucR4C7BEcz3SjaE6aMtWgnIAe4vb8otmwN4jRrNWM1qn9bxicR
+ Kfgl17ZQVPkGKHesETCuHBylYz2g/hmvrGlNT8/bQRmAGwA2FZoSXe0dtQARAQAB
+ AAP9Ek+8U+Alf7BmpNUwRd+Ros9pY/+OdHUCx3VvtnA6q6tsjqv8CMsZgOFtx7Mb
+ YNw1DIUOPexHb0xzHfaKMUpfAmc4PRYnEiggloT+UmB/Hs2tHfeE3hONiDDD9PMU
+ PiFmm2d+P0pRVTZUrioBaACWpu5YS03I6SC0zDdJeJapk10CANvaaRUZwBqpccfR
+ MFuV78NYPDkgwKuIJzMRFvp2PnEYPKmS7Ivz5ZimwltDukEOdmTCUAjW6iibKav5
+ EFmENSMCAOQlGzr3kQRNbcTWh4MYqQ0mDS0Gv9IWZR1DJBPziWnaMqvMyRrFK031
+ WXzHFDkhbph/HjZ0M33e7bsaLfBsq0cB/Ag+/9rJyTAOyCgbx4VNeipuseiLW3rn
+ gE5UdJa2FHpZcJqHk81IssCTVLnU3NADH0Eg6rVB4/31L1CtlkxgxZiXWbQeQmFj
+ a3VwIFRlc3QgPGJhY2t1cDAyQGZvby5jb20+iLgEEwECACIFAlAUeiUCGwMGCwkI
+ BwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEHEYmThqahdaMnED+wbrSpkyfjQg/SmO
+ 5j7eRznX33Fxq/GAdl1BMGra+skfOvcrxmTzHFIIANEl/j763j7zIbuD2uRuYQB4
+ agUEht3HvMZzFgh7mD1Qb/54MInjgcJvS/y8dRPuKq3kmTpyvJi1xi+o7ojzgdd1
+ fPisIwJR5E/hx4wJkHoZkoNdHzOinQHXBFAUeiUBBADEEOUdfCmwJ5BEPWzmQTGc
+ p2dSY8SgpbZtk23LvWIlEziYflIqOGeL623AOBb9S2FChR+zPyOA9D3LEZrAPF8T
+ S3YY+qjBdiXLKchRr5y2Kwylh/vVdcCcgTjfPkga4pGq+cHBOevPiqAvoCtOntN7
+ +Cqk3B7nWTPQRFDQJVThwwARAQABAAP0CszwvLeNuTt80e53OXmOmYJZ6zhzRz2k
+ kiX+LwzoRKjYycFdFz3gCqiDj9w1Pq6Pcx030Wmh+tI07znhkXGG6WgEdg3WMBa0
+ ephoNZKsmU2Xhbpf/au8uNwXLtaCEgZoC04JGlGzbX4gJXyUooW+/paxZMQFWFlM
+ ztf04oIHFQIA2+wOHvJc3HxP0I1Gtfx9bf27UKp1O/4te3m8yOZuIb8gAhlvqkJr
+ q+HcJ6604c8vKBHA08F+E6EJ8Sdt/eMlTwIA5Dr4AmGEKqkk92FuXm6R0ruo9Bm3
+ Ep7eFCjRul5lKV/lTF88+KLrzMzho1E/9BgiOKvm7Z1yQTZQUAHEwcqHTQIAusG5
+ gr2CASWpXbCxX6DWl8n7hdWAhGR0wHWk3ValNl0hLzxMx2Icf2ZWZ3rZw2/JsLiQ
+ oujz1T1DWgxsNyZaraPsiJ8EGAECAAkFAlAUeiUCGwwACgkQcRiZOGpqF1oNNgP/
+ adkSZZVaYy/JEHjjrtC9UwirSluAdHQZGPGv/FHaFou+4mlLi0+RgQLieU+3AHFS
+ WVYMNjIsSjA4hhMLchteFlJnbkGAFUaA2QUoyf7cSSmdpNKQdDdZoe3OXEQM2pUT
+ EwE9AqKXthrSMQuJ5bozm8zHm/CohZPHGTeQH75Kq/4=
+ =+a/7
+ -----END PGP PRIVATE KEY BLOCK-----
+ EOS
+
+ GPGKeys[:backup03][:long_id] = '5847D49DD791E4FA'
+ GPGKeys[:backup03][:public] = <<-EOS
+ -----BEGIN PGP PUBLIC KEY BLOCK-----
+ Version: GnuPG v1.4.12 (GNU/Linux)
+
+ mI0EUBR4LgEEAL6TxhYYNEirawnp/fmc0TwKYVSiTk60qGJyspEj0QKJ3MmunC/o
+ YX+5k8HN68BKP7xkJyHYFdXCJn2HqSbj0T7oV4scQGLJSO5adNa/6uMDZIA6Ptt4
+ UFOwdc8bnV9sDQTdrp2Gqsxi3f34WX0DV7/KrMD/jQJ9hiWphXSmvaV9ABEBAAG0
+ HkJhY2t1cCBUZXN0IDxiYWNrdXAwM0Bmb28uY29tPoi4BBMBAgAiBQJQFHguAhsD
+ BgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRBYR9Sd15Hk+shkA/4yev3zyK+/
+ jPZQgFLq10biRDYMdWk4ysHCkSbBi4GMnjIanBFk6ggbH/jQIb79oZRv2MB/N8Ag
+ 4s1oZdC5W1gSCg4m4pUB06NkKD4NvKAHHzsfr93farAct1zBQJ19e2ErFeo2Bb5G
+ uAvf2VWziMQD9aB0lF6tf/CToeWp+2xK77iNBFAUeC4BBADGwKc66SFldjN9dIxk
+ NYWLwCSvEgsOUdQpYckWLeUFnvKMpOW9+0Q5fc7QZuTs4TnT6cHjKk7F4ZQpMlqy
+ vc5H99vfp+/fus4LyAYIZKXmuc44SWwxU0y2MsGguFZbWKctQLquKD3uk3S8LPvY
+ wYn5weLVOhoUgbYGi6HeK8hkuQARAQABiJ8EGAECAAkFAlAUeC4CGwwACgkQWEfU
+ ndeR5PqQIQP8DMrzFSpyOEsxWRC8x4dhBtpptjO2MIS9FsX9pmycC52V9mCQX5pb
+ 5ZN6YPHOArYNUQfrXGRrOQFAPuNaucet/w39KIOmMZGPRGzOkTmp4AEhO7fKtuw8
+ /oWOO6hlX/rG4JNZUu3DQEt8WKCV9dHGxNWkxEptRA/CRYW4aj0MkaE=
+ =dxCg
+ -----END PGP PUBLIC KEY BLOCK-----
+ EOS
+ GPGKeys[:backup03][:private] = <<-EOS
+ -----BEGIN PGP PRIVATE KEY BLOCK-----
+ Version: GnuPG v1.4.12 (GNU/Linux)
+
+ lQHYBFAUeC4BBAC+k8YWGDRIq2sJ6f35nNE8CmFUok5OtKhicrKRI9ECidzJrpwv
+ 6GF/uZPBzevASj+8ZCch2BXVwiZ9h6km49E+6FeLHEBiyUjuWnTWv+rjA2SAOj7b
+ eFBTsHXPG51fbA0E3a6dhqrMYt39+Fl9A1e/yqzA/40CfYYlqYV0pr2lfQARAQAB
+ AAP9HVrYQHd+eDIVRPvqr7vwu7mSl+1/N97ab/2gVTxp2aUAIf24F6YI/JpKcOgF
+ 2ALn0d4wa+VjqZ8j/CJ9Et01EeIkEl9rDBVHLFKLpEABSRCCPpYPBhNqKgRQnLBl
+ M9kb6OxlBF0Y2L7oonq19Txer8FNB724m7yvBlWQko5id6ECAND9Cgn/YVuj83Es
+ DEBMjyAysgs3L69CoKQRL02PULbfVgabKqKqFJKDZr4DVyVo6v0L/c9pvnrz5GZG
+ McGH390CAOlyfn9fWtNjMci+DZ2ZXjut8FXnU+ChyGoc62brcQ0YE1767pyIpmxe
+ sG06jTMPpmBSA1xbMpScfLQehnLJUiEB/3f3U/8UiUTmB8bWe2JpHefJAn3Y8VLr
+ tLXujEmYqrn6kUNo1UgIodLtlKY/HfYm6gvcBl5W4+z7FnDBWwDIlX6e/7QeQmFj
+ a3VwIFRlc3QgPGJhY2t1cDAzQGZvby5jb20+iLgEEwECACIFAlAUeC4CGwMGCwkI
+ BwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEFhH1J3XkeT6yGQD/jJ6/fPIr7+M9lCA
+ UurXRuJENgx1aTjKwcKRJsGLgYyeMhqcEWTqCBsf+NAhvv2hlG/YwH83wCDizWhl
+ 0LlbWBIKDibilQHTo2QoPg28oAcfOx+v3d9qsBy3XMFAnX17YSsV6jYFvka4C9/Z
+ VbOIxAP1oHSUXq1/8JOh5an7bErvnQHYBFAUeC4BBADGwKc66SFldjN9dIxkNYWL
+ wCSvEgsOUdQpYckWLeUFnvKMpOW9+0Q5fc7QZuTs4TnT6cHjKk7F4ZQpMlqyvc5H
+ 99vfp+/fus4LyAYIZKXmuc44SWwxU0y2MsGguFZbWKctQLquKD3uk3S8LPvYwYn5
+ weLVOhoUgbYGi6HeK8hkuQARAQABAAP9G+yT4lFAXbC8fb8a/XZOl8qsbMN4da/W
+ AuVn+vuCPqatFckSNT3BAWHVZY7bUZO4S/d/A/NdA2zU4+/c8dl8inzv5tAmVFQY
+ w9XYZoA8d4GS+IflmVAzu8aWJlQLfMI8HKtwrf4gpSjZFsLIe9E/xQSgGWt1WXJ5
+ fbgbBKBLRB0CANF5C0VHOg/zIc7AODCi9GYqYWxQE0MiMhh1SzuG/NVHke+ldhqb
+ ogV+TMBAR8PCRNf64cKtXyJ2u5ZnXlSBWdUCAPLmCZBCFW3hm/26fdAovNHA53HZ
+ ucxhwrLP0Ks1+vr779JMypl6QVYbbHXaI7oyRFKg5KOdz4S2ij213gVOzVUCAKy1
+ MftwhFXCrQwfuA8d2rnro4WqreNyzRj/7QL11N8oQ1XasHCuuOVpDVyuBxtwGLG3
+ KUeriIODL6Bo/RevX0an8YifBBgBAgAJBQJQFHguAhsMAAoJEFhH1J3XkeT6kCED
+ /AzK8xUqcjhLMVkQvMeHYQbaabYztjCEvRbF/aZsnAudlfZgkF+aW+WTemDxzgK2
+ DVEH61xkazkBQD7jWrnHrf8N/SiDpjGRj0RszpE5qeABITu3yrbsPP6FjjuoZV/6
+ xuCTWVLtw0BLfFiglfXRxsTVpMRKbUQPwkWFuGo9DJGh
+ =6CYb
+ -----END PGP PRIVATE KEY BLOCK-----
+ EOS
+
+ GPGKeys[:backup04][:long_id] = '0F45932D3F24426D'
+ GPGKeys[:backup04][:public] = <<-EOS
+ -----BEGIN PGP PUBLIC KEY BLOCK-----
+ Version: GnuPG v1.4.12 (GNU/Linux)
+
+ mI0EUBR40wEEAMLAMVwpXCeEz1QZPJc8RFwAUBZRc/FYZ+CY4RKfQKZPuBMyzz3y
+ mk3gFB8raw2nycH00zwrP/qm/gIBzpCza+87uqGrB8bezgHoU1CaFnO0ZNUzyB8j
+ FsaQ/7BScjFNQp4RvbryZJUMXJ0V8fV9mQMe+O83CskabLCS4UY6EhWXABEBAAG0
+ HkJhY2t1cCBUZXN0IDxiYWNrdXAwNEBmb28uY29tPoi4BBMBAgAiBQJQFHjTAhsD
+ BgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRAPRZMtPyRCbU30A/0YU8U7EOKd
+ 5jSKhkAN50n4Cvwr0uOfC3UoJHInglRc+hDLB5ZF5GMqSfLYIb6/saY99Xb76l7i
+ k/0Q+rpk0AJ/dbY2ARrPqetleTPBsjrQbj9pzU4yvpYohpippjUbGAo74ssRGJoX
+ 28O98TXn8+q3bcz7bbLBmYj1Fy4QrSpBj7iNBFAUeNMBBADNBWFF0UFiPOCh9g3P
+ qOAYOXRsmEpLwGBGUB5ZdWzkIGrvs3KjTxIgy+uvx5Q8nX7+OLePaR9BM0qyryzz
+ LLjEKuaeDk1KelZ6/aV4ErgBUfVDs8pniLrSUqD65o18PL+T6nQ4aUYpgcqdb7t/
+ /by/yzt0+eEaAoArm0B8IdzB/QARAQABiJ8EGAECAAkFAlAUeNMCGwwACgkQD0WT
+ LT8kQm2LDgP/cUpWPR/GVdwLlbUIWBh37lLHEEuppZFg2P15Pv0UK9pNxCFhGouS
+ eBXW8xC/mvOrf7mj/tEV3CxGvIFdebtraEwLUQW0109vWHNclK6/SvmSNcaPo1t0
+ FtsIP4HI4ymvvTKUObfQliRdk1u1wY7sCWGarQgN9NtHuuyYP5+wmYg=
+ =LfdH
+ -----END PGP PUBLIC KEY BLOCK-----
+ EOS
+ GPGKeys[:backup04][:private] = <<-EOS
+ -----BEGIN PGP PRIVATE KEY BLOCK-----
+ Version: GnuPG v1.4.12 (GNU/Linux)
+
+ lQHYBFAUeNMBBADCwDFcKVwnhM9UGTyXPERcAFAWUXPxWGfgmOESn0CmT7gTMs89
+ 8ppN4BQfK2sNp8nB9NM8Kz/6pv4CAc6Qs2vvO7qhqwfG3s4B6FNQmhZztGTVM8gf
+ IxbGkP+wUnIxTUKeEb268mSVDFydFfH1fZkDHvjvNwrJGmywkuFGOhIVlwARAQAB
+ AAP8C0NOjNFu3Rw5r1fsS092y/P6rb0VMv0quMWup+3iMRwa0wVqZd9CHESGyrdZ
+ 8BwRjNrfXTNKdnEHmfnHxqeRvit+WRPCuhGcRTNJ8UTBPw4XMY/UYXHIXQGXwqk6
+ lzd+UQvqyZPv3ukuChDrXuCTG+443WFfHxl4S7PAn3aTwjUCANQjjqSweQZAHvn0
+ XXcnTNRDV8Bf+Sl7rxhMcBUJhFu3ALBWosZDI/zYTDQ/FZqbM8A9Poh6cnB2TS/S
+ 47kNwDMCAOsESXIs/RBg2RDactjqxj5TVn/T0PMZ1AhV2BPl2RmP/bnwyBWy0O9N
+ V3hKY0ArEEpcNf8HuRz7/nOduCtEYQ0CAN2AODbYyG/sL+wH0k392fpnsWmdYs4N
+ 21AgfPY8U3Oyrodgjz1hU3C3dxyE3co4sU42d3sLI2tpxa0xy6mHetKdbLQeQmFj
+ a3VwIFRlc3QgPGJhY2t1cDA0QGZvby5jb20+iLgEEwECACIFAlAUeNMCGwMGCwkI
+ BwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEA9Fky0/JEJtTfQD/RhTxTsQ4p3mNIqG
+ QA3nSfgK/CvS458LdSgkcieCVFz6EMsHlkXkYypJ8tghvr+xpj31dvvqXuKT/RD6
+ umTQAn91tjYBGs+p62V5M8GyOtBuP2nNTjK+liiGmKmmNRsYCjviyxEYmhfbw73x
+ Nefz6rdtzPttssGZiPUXLhCtKkGPnQHYBFAUeNMBBADNBWFF0UFiPOCh9g3PqOAY
+ OXRsmEpLwGBGUB5ZdWzkIGrvs3KjTxIgy+uvx5Q8nX7+OLePaR9BM0qyryzzLLjE
+ KuaeDk1KelZ6/aV4ErgBUfVDs8pniLrSUqD65o18PL+T6nQ4aUYpgcqdb7t//by/
+ yzt0+eEaAoArm0B8IdzB/QARAQABAAP9H1IfFidtsbBTMOsCGSNXeNvuKVjqoL/2
+ 9UbwHAKQbBl3vL7RWJmPz2rXyrbWspvs9rF7eXE50SAg3UNdvpiqcSdBwC4VSDNe
+ VSbKaZ5MmqapC7ioLH/FEcvZgaZfTsTsLC10CIZgisW+z+zKq2fafwSI5o2XMuoE
+ /iuai9z6ZoECANEKpjytZB93+O2fUWPG7BjIzVUZGIeSxGKPze9jgQloHAou5sH8
+ WygPVlQzy9Xxt7Z9/QrZlGdalyeDQaoC6dkCAPsThN0K0s2pabEfMtYOneJzR5Xs
+ ze6aqs6+FpHikAISMswd5EHnfPtKA51bGxWKAxgeCqNMpf8tJMAFCmj2fsUB/3Z5
+ IZJE+OmdiMDm81yDNpK801RZAiySpbZ5CEaWB/BuuTIyG7k/dtSnWtodEOEcEjFs
+ yBaY+gUfFbKHBatQN7qf2oifBBgBAgAJBQJQFHjTAhsMAAoJEA9Fky0/JEJtiw4D
+ /3FKVj0fxlXcC5W1CFgYd+5SxxBLqaWRYNj9eT79FCvaTcQhYRqLkngV1vMQv5rz
+ q3+5o/7RFdwsRryBXXm7a2hMC1EFtNdPb1hzXJSuv0r5kjXGj6NbdBbbCD+ByOMp
+ r70ylDm30JYkXZNbtcGO7Alhmq0IDfTbR7rsmD+fsJmI
+ =I+GJ
+ -----END PGP PRIVATE KEY BLOCK-----
+ EOS
+ end
+end
View
287 spec-live/encryptor/gpg_spec.rb
@@ -0,0 +1,287 @@
+# encoding: utf-8
+
+require File.expand_path('../../spec_helper.rb', __FILE__)
+
+describe 'Encryptor::GPG',
+ :if => Backup::SpecLive::CONFIG['encryptor']['gpg']['specs_enabled'] do
+
+ def archive_file_for(model)
+ File.join(
+ Backup::SpecLive::TMP_PATH,
+ "#{model.trigger}", model.time, "#{model.trigger}.tar.gpg"
+ )
+ end
+
+ # clear out, then load the encryptor.gpg_homedir with the keys for the
+ # given key_type (:public/:private) for the given identifiers.
+ #
+ def load_gpg_homedir(encryptor, key_type, identifiers)
+ # Clear out any files if the directory exists
+ dir = File.expand_path(encryptor.gpg_homedir)
+ FileUtils.rm(Dir[File.join(dir, '*')]) if File.exists?(dir)
+
+ # Make sure the directory exists with proper permissions.
+ # This will also initialize the keyring files, so this method can be
+ # called with no identifiers to simply reset the directory without
+ # importing any keys.
+ encryptor.send(:setup_gpg_homedir)
+
+ # Import the keys, making sure each import is successful.
+ # #import_key will log a warning for the identifier if the
+ # import fails, so we'll just abort if we get a failure here.
+ [identifiers].flatten.compact.each do |identifier|
+ ret_id = encryptor.send(:import_key,
+ identifier, Backup::SpecLive::GPGKeys[identifier][key_type]
+ )
+ unless ret_id == Backup::SpecLive::GPGKeys[identifier][:long_id]
+ abort("load_gpg_homedir failed")
+ end
+ end
+ end
+
+ # make sure the archive can be decrypted
+ def can_decrypt?(model, passphrase = nil)
+ enc = model.encryptor
+ archive = archive_file_for(model)
+ outfile = File.join(File.dirname(archive), 'outfile')
+
+ pass_opt = "--passphrase '#{ passphrase }'" if passphrase
+ enc.send(:run,
+ "#{ enc.send(:utility, :gpg) } #{ enc.send(:base_options) } " +
+ "#{ pass_opt } -o '#{ outfile }' -d '#{ archive }' 2>&1"
+ )
+
+ if File.exist?(outfile)
+ File.delete(outfile)
+ true
+ else
+ false
+ end
+ end
+
+ context 'using :asymmetric mode with some existing keys' do
+ let(:model) { h_set_trigger('encryptor_gpg_asymmetric') }
+
+ it 'should encrypt the archive' do
+ recipients = [:backup01, :backup02, :backup03, :backup04]
+ # Preload keys for backup01 and backup02.
+ # Keys for backup03 and backup04 are configured in :keys in the model
+ # and will be imported when the model is performed.
+ # The Model specifies all 4 as :recipients.
+ load_gpg_homedir(model.encryptor, :public, recipients[0..1])
+
+ model.perform!
+
+ Backup::Logger.has_warnings?.should be_false
+
+ File.exist?(archive_file_for(model)).should be_true
+
+ # make sure all 4 recipients can decrypt the archive
+ recipients.each do |recipient|
+ load_gpg_homedir(model.encryptor, :private, recipient)
+ can_decrypt?(model).should be_true
+ end
+ end
+ end # context 'using :asymmetric mode with some existing keys'
+
+ context 'using :asymmetric mode with a missing public key' do
+ let(:model) { h_set_trigger('encryptor_gpg_asymmetric_missing') }
+
+ # backup01 will be preloaded.
+ # backup02 will be imported from :keys
+ # backupfoo will be a missing recipient
+ it 'should encrypt the archive' do
+ load_gpg_homedir(model.encryptor, :public, :backup01)
+
+ model.perform!
+
+ Backup::Logger.has_warnings?.should be_true
+ Backup::Logger.messages.any? {|msg|
+ msg =~ /No public key was found in #keys for '<backupfoo@foo.com>'/
+ }.should be_true
+
+ File.exist?(archive_file_for(model)).should be_true
+
+ [:backup01, :backup02].each do |recipient|
+ load_gpg_homedir(model.encryptor, :private, recipient)
+ can_decrypt?(model).should be_true
+ end
+ end
+ end # context 'using :asymmetric mode with a missing public key'
+
+ context 'using :asymmetric mode with no valid public keys' do
+ let(:model) { h_set_trigger('encryptor_gpg_asymmetric_fail') }
+
+ it 'should abort the backup' do
+ model.perform!
+
+ # issues warnings about the missing keys
+ Backup::Logger.has_warnings?.should be_true
+ Backup::Logger.messages.any? {|msg|
+ msg =~ /No public key was found in #keys for '<backupfoo@foo.com>'/
+ }.should be_true
+ Backup::Logger.messages.any? {|msg|
+ msg =~ /No public key was found in #keys for '<backupfoo2@foo.com>'/
+ }.should be_true
+
+ # issues warning about not being able to perform asymmetric encryption
+ Backup::Logger.messages.any? {|msg|
+ msg =~ /No recipients available for asymmetric encryption/
+ }.should be_true
+
+ # Since there are no other options for encryption,
+ # the backup failes with an error.
+ Backup::Logger.messages.any? {|msg|
+ msg =~ /\[error\]\s+ModelError/
+ }.should be_true
+ Backup::Logger.messages.any? {|msg|
+ msg =~ /\[error\]\s+Reason: Encryptor::GPG::EncryptionError/
+ }.should be_true
+ Backup::Logger.messages.any? {|msg|
+ msg =~ /\[error\]\s+Encryption could not be performed for mode 'asymmetric'/
+ }.should be_true
+
+ # Although, any further backup models would be run, as this error
+ # is rescued in Backup::Model#perform
+ Backup::Logger.messages.any? {|msg|
+ msg =~ /Backup will now attempt to continue/
+ }.should be_true
+
+ File.exist?(archive_file_for(model)).should be_false
+ end
+ end # context 'using :asymmetric mode with no valid public keys'
+
+ context 'using :symmetric mode' do
+ let(:model) { h_set_trigger('encryptor_gpg_symmetric') }
+
+ it 'should encrypt the archive' do
+ model.perform!
+
+ Backup::Logger.has_warnings?.should be_false
+
+ File.exist?(archive_file_for(model)).should be_true
+
+ can_decrypt?(model, 'a secret').should be_true
+
+ # note that without specifying any preferences, the default
+ # algorithm used is CAST5
+ Backup::Logger.messages.any? {|msg|
+ msg =~ /gpg: CAST5 encrypted data/
+ }.should be_true
+ end
+ end # context 'using :symmetric mode'
+
+ # The #gpg_config preferences should also be able to override the algorithm
+ # preferences in the recipients' public keys, but the gpg output doesn't
+ # give us an easy way to check this. You'd have to inspect the leading bytes
+ # of the encrypted file per RFC4880, and I'm not going that far :)
+ context 'using :symmetric mode with given gpg_config' do
+ let(:model) { h_set_trigger('encryptor_gpg_symmetric_with_config') }
+
+ it 'should encrypt the archive using the proper algorithm preference' do
+ model.perform!
+
+ Backup::Logger.has_warnings?.should be_false
+
+ File.exist?(archive_file_for(model)).should be_true
+
+ can_decrypt?(model, 'a secret').should be_true
+
+ # preferences set in #gpg_config specified using AES256 before CAST5
+ Backup::Logger.messages.any? {|msg|
+ msg =~ /gpg: AES256 encrypted data/
+ }.should be_true
+ end
+ end # context 'using :symmetric mode with given gpg_config'
+
+ context 'using :both mode' do
+ let(:model) { h_set_trigger('encryptor_gpg_both') }
+
+ it 'should encrypt the archive' do
+ # Preload key for backup01.
+ # Key for backup03 will be imported when the model is performed.
+ load_gpg_homedir(model.encryptor, :public, :backup01)
+
+ model.perform!
+
+ Backup::Logger.has_warnings?.should be_false
+
+ File.exist?(archive_file_for(model)).should be_true
+
+ # make sure both recipients can decrypt the archive
+ [:backup01, :backup03].each do |recipient|
+ load_gpg_homedir(model.encryptor, :private, recipient)
+ can_decrypt?(model).should be_true
+ end
+
+ # with no private keys in the keyring,
+ # archive can be decrypted using the passphrase.
+ load_gpg_homedir(model.encryptor, :private, nil)
+ can_decrypt?(model, 'a secret').should be_true
+ end
+ end # context 'using :both mode'
+
+ context 'using :both mode with no valid asymmetric recipients' do
+ let(:model) { h_set_trigger('encryptor_gpg_both_no_asymmetric') }
+
+ it 'should encrypt the archive using only symmetric encryption' do
+ # we'll load backup02, but this isn't one of the :recipients
+ load_gpg_homedir(model.encryptor, :public, :backup02)
+
+ model.perform!
+
+ # issues warnings about the missing keys
+ Backup::Logger.has_warnings?.should be_true
+ Backup::Logger.messages.any? {|msg|
+ msg =~ /No public key was found in #keys for '16325C61'/
+ }.should be_true
+ Backup::Logger.messages.any? {|msg|
+ msg =~ /No public key was found in #keys for '<backup03@foo.com>'/
+ }.should be_true
+
+ # issues warning about not being able to perform asymmetric encryption
+ Backup::Logger.messages.any? {|msg|
+ msg =~ /No recipients available for asymmetric encryption/
+ }.should be_true
+
+ # backup proceeded, since symmetric encryption could still be performed
+ File.exist?(archive_file_for(model)).should be_true
+
+ # with no private keys in the keyring,
+ # archive can be decrypted using the passphrase.
+ load_gpg_homedir(model.encryptor, :private, nil)
+ can_decrypt?(model, 'a secret').should be_true
+ end
+ end # context 'using :both mode with no valid asymmetric recipients'
+
+ context 'when using the deprecated #key accessor' do
+ let(:model) {
+ # See notes in spec-live/spec_helper.rb
+ h_set_single_model do
+ Backup::Model.new(:encryptor_gpg_deprecate_key, 'test_label') do
+ archive :test_archive, &Backup::SpecLive::ARCHIVE_JOB
+ encrypt_with 'GPG' do |e|
+ e.key = Backup::SpecLive::GPGKeys[:backup03][:public]
+ end
+ store_with 'Local'
+ end
+ end
+ }
+
+ it 'should log a warning and store an encrypted archive' do
+ model.perform!
+
+ Backup::Logger.has_warnings?.should be_true
+ Backup::Logger.messages.any? {|msg|
+ msg =~ /GPG#key has been deprecated/
+ }.should be_true
+
+ File.exist?(archive_file_for(model)).should be_true
+
+ load_gpg_homedir(model.encryptor, :private, :backup03)
+
+ can_decrypt?(model).should be_true
+ end
+ end # context 'when using the deprecated #key accessor'
+
+end
View
80 spec-live/notifier/mail_spec.rb
@@ -9,33 +9,49 @@
it 'should send a success email' do
model = h_set_trigger(trigger)
- expect do
- model.perform!
- end.not_to raise_error
+ model.perform!
+
+ Backup::Logger.has_warnings?.should be_false
+ Backup::Logger.messages.any? {|msg| msg =~ /\[error\]/ }.should be_false
end
it 'should send a warning email' do
model = h_set_trigger(trigger)
Backup::Logger.warn 'You have been warned!'
- expect do
- model.perform!
- end.not_to raise_error
+ model.perform!
+
+ Backup::Logger.has_warnings?.should be_true
+ Backup::Logger.messages.any? {|msg| msg =~ /\[error\]/ }.should be_false
end
it 'should send a failure email for non-fatal errors' do
model = h_set_trigger(trigger)
model.stubs(:databases).raises('A successful failure?')
- expect do
- model.perform!
- end.not_to raise_error
+ model.perform!
+
+ Backup::Logger.has_warnings?.should be_false
+ Backup::Logger.messages.any? {|msg|
+ msg =~ /\[error\]\s+A successful failure/
+ }.should be_true
+ Backup::Logger.messages.any? {|msg|
+ msg =~ /Backup will now attempt to continue/
+ }.should be_true
end
- it 'should send a failure email fatal errors' do
+ it 'should send a failure email for fatal errors' do
model = h_set_trigger(trigger)
model.stubs(:databases).raises(NoMemoryError, 'with increasing frequency...')
expect do
model.perform!
- end.to raise_error
+ end.to raise_error(SystemExit)
+
+ Backup::Logger.has_warnings?.should be_false
+ Backup::Logger.messages.any? {|msg|
+ msg =~ /with increasing frequency/
+ }.should be_true
+ Backup::Logger.messages.any? {|msg|
+ msg =~ /Backup will now exit/
+ }.should be_true
end
end # describe 'Notifier::Mail :smtp'
@@ -45,9 +61,11 @@
it 'should send a success email' do
model = h_set_trigger(trigger)
- expect do
- model.perform!
- end.not_to raise_error
+ model.perform!
+
+ Backup::Logger.has_warnings?.should be_false
+ Backup::Logger.messages.any? {|msg| msg =~ /\[error\]/ }.should be_false
+
File.exist?(test_email).should be_true
File.read(test_email).should match(/without any errors/)
end
@@ -55,9 +73,11 @@
it 'should send a warning email' do
model = h_set_trigger(trigger)
Backup::Logger.warn 'You have been warned!'
- expect do
- model.perform!
- end.not_to raise_error
+ model.perform!
+
+ Backup::Logger.has_warnings?.should be_true
+ Backup::Logger.messages.any? {|msg| msg =~ /\[error\]/ }.should be_false
+
File.exist?(test_email).should be_true
File.read(test_email).should match(/You have been warned/)
end
@@ -65,19 +85,35 @@
it 'should send a failure email for non-fatal errors' do
model = h_set_trigger(trigger)
model.stubs(:databases).raises('A successful failure?')
- expect do
- model.perform!
- end.not_to raise_error
+ model.perform!
+
+ Backup::Logger.has_warnings?.should be_false
+ Backup::Logger.messages.any? {|msg|
+ msg =~ /\[error\]\s+A successful failure/
+ }.should be_true
+ Backup::Logger.messages.any? {|msg|
+ msg =~ /Backup will now attempt to continue/
+ }.should be_true
+
File.exist?(test_email).should be_true
File.read(test_email).should match(/successful failure/)
end
- it 'should send a failure email fatal errors' do
+ it 'should send a failure email for fatal errors' do
model = h_set_trigger(trigger)
model.stubs(:databases).raises(NoMemoryError, 'with increasing frequency...')
expect do
model.perform!
- end.to raise_error
+ end.to raise_error(SystemExit)
+
+ Backup::Logger.has_warnings?.should be_false
+ Backup::Logger.messages.any? {|msg|
+ msg =~ /with increasing frequency/
+ }.should be_true
+ Backup::Logger.messages.any? {|msg|
+ msg =~ /Backup will now exit/
+ }.should be_true
+
File.exist?(test_email).should be_true
File.read(test_email).should match(/with increasing frequency/)
end
View
72 spec-live/spec_helper.rb
@@ -8,6 +8,11 @@
# Load Backup
require 'backup'
+# Backup::SpecLive::GPGKeys
+# Loaded here so these are available in backups/models.rb
+# as well as within encryptor/gpg_spec.rb
+require File.expand_path('../encryptor/gpg_keys.rb', __FILE__)
+
module Backup
module SpecLive
PATH = File.expand_path('..', __FILE__)
@@ -15,6 +20,11 @@ module SpecLive
TMP_PATH = PATH + '/tmp'
SYNC_PATH = PATH + '/sync'
+ ARCHIVE_JOB = lambda do |archive|
+ archive.add File.expand_path('../../lib/backup', __FILE__)
+ archive.exclude File.expand_path('../../lib/backup/storage', __FILE__)
+ end
+
config = PATH + '/backups/config.yml'
if File.exist?(config)
CONFIG = YAML.load_file(config)
@@ -24,14 +34,57 @@ module SpecLive
exit!
end
+ class << self
+ attr_accessor :load_models
+ end
+
module ExampleHelpers
+ # This method loads all defaults in config.rb and all the Models
+ # in models.rb, then returns the Model for the given trigger.
def h_set_trigger(trigger)
+ Backup::SpecLive.load_models = true
+ Backup::Logger.clear!
+ Backup::Model.all.clear
+ Backup::Config.load_config!
+ model = Backup::Model.find(trigger)
+ model.prepare!
+ model
+ end
+
+ # This method can be used to setup a test where you need to setup
+ # and perform a single Model that can not be setup in models.rb.
+ # This is primarily for Models used to test deprecations, since
+ # those warnings will be output when the Model is instantiated
+ # and will pollute the output of all other tests.
+ #
+ # Usage:
+ # model = h_set_single_model do
+ # Backup::Model.new(:test_trigger, 'test label') do
+ # ...setup model...
+ # end
+ # end
+ #
+ # The block doesn't have to return the model, as it will be retrieved
+ # from Model.all (since it will be the only one).
+ #
+ # Remember when defining the model that the DSL constants won't be
+ # available, as the block is not being evaluated in the context of
+ # the Backup::Config module. So, just use strings instead.
+ # e.g. store_with 'Local' vs store_with Local
+ #
+ # Note this will still load any defaults setup in config.rb, so don't
+ # do anything in config.rb that would generate a deprecation warning :)
+ #
+ def h_set_single_model(&block)
+ Backup::SpecLive.load_models = false
Backup::Logger.clear!
Backup::Model.all.clear
Backup::Config.load_config!
- FileUtils.mkdir_p(File.join(Backup::Config.data_path, trigger))
- Backup::Model.find(trigger)
+ block.call
+ model = Backup::Model.all.first
+ model.prepare!
+ model
end
def h_clean_data_paths!
@@ -82,4 +135,17 @@ def h_safety_check(path)
end
end
-puts "\n\nRuby version: #{RUBY_DESCRIPTION}\n\n"
+puts "\nRuby version: #{ RUBY_DESCRIPTION }\n"
+
+unless ENV['VERBOSE']
+ puts <<-EOS
+
+ Some of these tests can be slow, so be patient.
+ It's recommended you run these with:
+ $ VERBOSE=1 rspec spec-live/
+ For some tests, [error] and [warning] messages are normal.
+ Some could pass, but still have problems.
+ So, pay attention to the messages :)
+
+ EOS
+end
View
50 spec/cli/helpers_spec.rb
@@ -30,7 +30,7 @@
let(:stderr_messages) { '' }
it 'should return stdout and generate no additional log messages' do
- helpers.run(command).should == ''
+ helpers.send(:run, command).should == ''
end
end
@@ -42,7 +42,7 @@
Backup::Logger.expects(:message).with(
"cmd_name:STDOUT: out line1\ncmd_name:STDOUT: out line2"
)
- helpers.run(command).should == stdout_messages.strip
+ helpers.send(:run, command).should == stdout_messages.strip
end
end
@@ -54,7 +54,7 @@
Backup::Logger.expects(:warn).with(
"cmd_name:STDERR: err line1\ncmd_name:STDERR: err line2"
)
- helpers.run(command).should == ''
+ helpers.send(:run, command).should == ''
end
end
@@ -69,7 +69,7 @@
Backup::Logger.expects(:warn).with(
"cmd_name:STDERR: err line1\ncmd_name:STDERR: err line2"
)
- helpers.run(command).should == stdout_messages.strip
+ helpers.send(:run, command).should == stdout_messages.strip
end
end
end # context 'when the command is successful'
@@ -101,7 +101,7 @@
it 'should raise an error reporting no messages' do
expect do
- helpers.run(command)
+ helpers.send(:run, command)
end.to raise_error {|err|
err.message.should == message_head +
" STDOUT Messages: None\n" +
@@ -116,7 +116,7 @@
it 'should raise an error and report the stdout messages' do
expect do
- helpers.run(command)
+ helpers.send(:run, command)
end.to raise_error {|err|
err.message.should == message_head +
" STDOUT Messages: \n" +
@@ -133,7 +133,7 @@
it 'should raise an error and report the stderr messages' do
expect do
- helpers.run(command)
+ helpers.send(:run, command)
end.to raise_error {|err|
err.message.should == message_head +
" STDOUT Messages: None\n" +
@@ -150,7 +150,7 @@
it 'should raise an error and report the stdout and stderr messages' do
expect do
- helpers.run(command)
+ helpers.send(:run, command)
end.to raise_error {|err|
err.message.should == message_head +
" STDOUT Messages: \n" +
@@ -175,7 +175,7 @@
it 'should raise an error wrapping the system error raised' do
expect do
- helpers.run(command)
+ helpers.send(:run, command)
end.to raise_error {|err|
err.message.should == "CLI::SystemCallError: " +
"Failed to execute system command on #{ RUBY_PLATFORM }\n" +
@@ -193,24 +193,24 @@
context 'when a system path for the utility is available' do
it 'should return the system path with newline removed' do
helpers.expects(:`).with('which foo 2>/dev/null').returns("system_path\n")
- helpers.utility(:foo).should == 'system_path'
+ helpers.send(:utility, :foo).should == 'system_path'
end
it 'should cache the returned path' do
helpers.expects(:`).once.with('which cache_me 2>/dev/null').
returns("cached_path\n")
- helpers.utility(:cache_me).should == 'cached_path'
- helpers.utility(:cache_me).should == 'cached_path'