outback 0.0.14 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG +6 -0
  3. data/LICENSE +21 -0
  4. data/README.md +29 -3
  5. data/lib/outback.rb +16 -11
  6. data/lib/outback/archive.rb +6 -17
  7. data/lib/outback/backup.rb +24 -19
  8. data/lib/outback/cli.rb +6 -2
  9. data/lib/outback/configuration.rb +15 -10
  10. data/lib/outback/directory_source.rb +8 -7
  11. data/lib/outback/directory_target.rb +18 -11
  12. data/lib/outback/encryption_processor.rb +34 -0
  13. data/lib/outback/errors.rb +7 -0
  14. data/lib/outback/logging.rb +7 -0
  15. data/lib/outback/mysql_source.rb +9 -9
  16. data/lib/outback/processor.rb +17 -0
  17. data/lib/outback/s3_target.rb +18 -9
  18. data/lib/outback/sftp_target.rb +70 -0
  19. data/lib/outback/source.rb +9 -2
  20. data/lib/outback/source_archive.rb +17 -0
  21. data/lib/outback/support/attr_setter.rb +1 -1
  22. data/lib/outback/support/configurable.rb +5 -3
  23. data/lib/outback/target.rb +51 -14
  24. data/lib/outback/target_archive.rb +30 -0
  25. data/lib/outback/version.rb +3 -0
  26. data/lib/vendor/enumerable_ext.rb +9 -0
  27. data/lib/{outback/vendor → vendor}/metaclass.rb +1 -1
  28. data/lib/{outback/vendor → vendor}/methodphitamine.rb +1 -1
  29. data/lib/vendor/mysql.rb +1093 -0
  30. data/lib/vendor/mysql/charset.rb +325 -0
  31. data/lib/vendor/mysql/constants.rb +165 -0
  32. data/lib/vendor/mysql/error.rb +989 -0
  33. data/lib/vendor/mysql/packet.rb +78 -0
  34. data/lib/vendor/mysql/protocol.rb +770 -0
  35. data/lib/vendor/numeric_ext.rb +49 -0
  36. data/lib/vendor/string_ext.rb +19 -0
  37. metadata +53 -39
  38. data/MIT-LICENSE +0 -20
  39. data/VERSION +0 -1
  40. data/lib/outback/configuration_error.rb +0 -4
  41. data/lib/outback/directory_archive.rb +0 -8
  42. data/lib/outback/local_archive.rb +0 -6
  43. data/lib/outback/s3_archive.rb +0 -18
  44. data/lib/outback/temp_archive.rb +0 -5
  45. data/lib/outback/vendor/mysql.rb +0 -1214
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f2a87a0171b3d8506d69d5d8b0611916a7bccd8a
4
+ data.tar.gz: d86ec7bddb738e5b9574073e06aae4ffa65b25c8
5
+ SHA512:
6
+ metadata.gz: f252a3dab0597a9578c62c94ae4a9b739068c7090d357861ec534db8ae83f2327b6b5eead6cdc7e45e14f3fd502397cb6747d1ac7f900aba4692087d8004921d
7
+ data.tar.gz: 3e5a5ef024af75c835dbb4fae15f6b9e0d193c2e4865223beac15a33f83bef2b1c5345ff8a2795fc6b81634594d447ec294aedb04ae7a2b05dd2dd835140dd47
data/CHANGELOG CHANGED
@@ -1,3 +1,9 @@
1
+ *1.0.2 (September 20, 2016)*
2
+
3
+ * Add SCP target
4
+ * Add encryption with OpenSSL
5
+ * Drop activesupport dependency
6
+
1
7
  *0.0.12 (August 16, 2012)*
2
8
 
3
9
  * Fix warning caused by methodphitamine.rb
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2016 onrooby
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md CHANGED
@@ -28,7 +28,7 @@ instantiated during a single run.
28
28
 
29
29
  ```` ruby
30
30
  Outback::Configuration.new 'name' do
31
- source :directory, '/ver/www' do
31
+ source :directory, '/var/www' do
32
32
  exclude '/var/www/foo'
33
33
  exclude '/var/www/icons/*.png'
34
34
  end
@@ -45,6 +45,13 @@ Outback::Configuration.new 'name' do
45
45
  # database 'specific_database'
46
46
  end
47
47
 
48
+ processor :encryption do
49
+ password 'very secure encryption password'
50
+
51
+ # The default cipher is aes-256-cbc
52
+ #cipher 'openssl supported cipher'
53
+ end
54
+
48
55
  # Amazon S3 storage
49
56
  target :s3 do
50
57
  access_key 'S3 access key'
@@ -56,6 +63,19 @@ Outback::Configuration.new 'name' do
56
63
  # Just omit the definition to keep archives forever.
57
64
  ttl 1.month
58
65
  end
66
+
67
+ # SFTP storage
68
+ target :sftp, 'example.com' do
69
+ user 'backupuser'
70
+ # Usually, SSH keys will be used to connect to the server.
71
+ # Alternatively, a password can be given here.
72
+ #password 'your password'
73
+
74
+ # Default port is 22
75
+ # port 1234
76
+ ttl 1.day
77
+ path '/backups'
78
+ end
59
79
 
60
80
  # Store on a local filesystem path
61
81
  target :directory, '/media/backups/daily' do
@@ -79,7 +99,7 @@ Default configurations and commandline options
79
99
  If you place your backup configurations in the file `/etc/outback.conf` they
80
100
  will be read automatically when the outback executable is invoked. Make
81
101
  sure to have correct permissions on the configuration files, as they might
82
- include database passwords.
102
+ include database and encryption passwords.
83
103
 
84
104
  Alternatively, you can pass in the configuration file to read as a
85
105
  commandline argument. The default configuration file in /etc will then be
@@ -131,4 +151,10 @@ Other commandline options are:
131
151
  * `-t` `--test` test configuration, then exit
132
152
  * `-l` `--list` list available configurations, then exit
133
153
  * `-h` `--help` display help
134
- * `--version` display version
154
+ * `--version` display version
155
+
156
+ Known Issues
157
+ ------------
158
+
159
+ The s3 gem currently doesn't support V4 signatures, which are required for
160
+ s3 regions introduced after January 2014.
@@ -3,35 +3,40 @@ require 'fileutils'
3
3
  require 'tempfile'
4
4
  require 'tmpdir'
5
5
 
6
- require 'active_support/core_ext'
7
-
8
6
  require 's3'
9
7
 
10
- require 'outback/vendor/mysql'
11
- require 'outback/vendor/metaclass'
12
- require 'outback/vendor/methodphitamine'
8
+ require_relative 'vendor/string_ext'
9
+ require_relative 'vendor/numeric_ext'
10
+ require_relative 'vendor/enumerable_ext'
11
+ require_relative 'vendor/mysql'
12
+ require_relative 'vendor/metaclass'
13
+ require_relative 'vendor/methodphitamine'
13
14
 
15
+ require 'outback/version'
14
16
  require 'outback/support/returning'
15
17
  require 'outback/support/attr_setter'
16
18
  require 'outback/support/configurable'
17
19
  require 'outback/support/mysql_ext'
18
20
  require 'outback/support/pathname_ext'
21
+ require 'outback/errors'
22
+ require 'outback/logging'
19
23
  require 'outback/configuration'
20
- require 'outback/configuration_error'
24
+ require 'outback/archive'
21
25
  require 'outback/source'
26
+ require 'outback/source_archive'
22
27
  require 'outback/directory_source'
23
28
  require 'outback/mysql_source'
24
- require 'outback/archive'
25
- require 'outback/temp_archive'
29
+ require 'outback/processor'
30
+ require 'outback/encryption_processor'
26
31
  require 'outback/target'
32
+ require 'outback/target_archive'
27
33
  require 'outback/directory_target'
28
- require 'outback/directory_archive'
29
34
  require 'outback/s3_target'
30
- require 'outback/s3_archive'
35
+ require 'outback/sftp_target'
31
36
  require 'outback/backup'
32
37
 
33
38
  module Outback
34
- VERSION = Pathname.new(__FILE__).dirname.join('..', 'VERSION').read.strip
39
+ TIME_FORMAT = '%Y%m%d%H%M%S'.freeze
35
40
 
36
41
  class << self
37
42
  %w(verbose silent).each do |method|
@@ -1,30 +1,19 @@
1
1
  module Outback
2
2
  class Archive
3
- NAME_PATTERN = /([A-Za-z0-9.\-]+)_(\d{14})_(\w+)/
3
+ NAME_PATTERN = /\A([A-Za-z0-9.\-]+)_(\d{14})_(\w+)/
4
4
 
5
- attr_reader :filename, :backup_name, :timestamp, :source_name, :parent
5
+ attr_reader :filename, :backup_name, :timestamp, :source_name, :size
6
6
 
7
- def initialize(filename, parent)
8
- @filename, @parent = Pathname.new(filename), parent
7
+ def initialize(filename)
8
+ @filename = Pathname.new(filename)
9
9
  unless match_data = @filename.basename.to_s.match(NAME_PATTERN)
10
10
  raise ArgumentError, 'invalid name'
11
11
  end
12
12
  @backup_name, @timestamp, @source_name = match_data.captures[0..2]
13
13
  end
14
14
 
15
- def size
16
- filename.size
15
+ def to_s
16
+ "#{filename}"
17
17
  end
18
-
19
- def open
20
- filename.open
21
- end
22
-
23
- def outdated?
24
- if timestamp && parent && parent.ttl
25
- Time.now - Time.parse(timestamp) > parent.ttl
26
- end
27
- end
28
-
29
18
  end
30
19
  end
@@ -1,44 +1,49 @@
1
1
  module Outback
2
2
  class Backup
3
- attr_reader :configuration, :name, :timestamp, :archives, :tmpdir
4
- delegate :sources, :targets, :to => :configuration
3
+ include Logging
5
4
 
5
+ attr_reader :configuration, :name, :timestamp, :tmpdir
6
+
6
7
  def initialize(configuration)
7
8
  raise ArgumentError, "configuration required" unless configuration.is_a?(Outback::Configuration)
8
9
  @configuration = configuration
9
10
  @name = configuration.name
10
- @timestamp = Time.now.to_formatted_s(:number)
11
+ @timestamp = Time.now.strftime(Outback::TIME_FORMAT)
11
12
  end
12
13
 
13
14
  def run!
14
- @archives = []
15
- begin
16
- Outback.info "Using working directory #{configuration.tmpdir}" if configuration.tmpdir
17
- @tmpdir = Dir.mktmpdir([name, timestamp], configuration.tmpdir)
18
- @archives = create_archives
19
- Outback.info "Created #{@archives.size} archives"
20
- store_archives
21
- ensure
22
- FileUtils.remove_entry_secure(tmpdir)
23
- end
15
+ logger.info "Using working directory #{configuration.tmpdir}" if configuration.tmpdir
16
+ @tmpdir = Dir.mktmpdir([name, timestamp], configuration.tmpdir)
17
+ archives = create_archives
18
+ logger.info "Created #{archives.size} archives"
19
+ archives = process_archives(archives)
20
+ logger.info "Processed #{archives.size} archives"
21
+ store_archives(archives)
24
22
  purge_targets
23
+ ensure
24
+ FileUtils.remove_entry_secure(tmpdir)
25
+ end
26
+
27
+ [:sources, :processors, :targets].each do |collection|
28
+ define_method(collection) { configuration.public_send(collection) }
25
29
  end
26
30
 
27
31
  private
28
32
 
29
33
  def create_archives
30
- archives = sources.collect do |source|
31
- source.create_archives(name, timestamp, tmpdir)
32
- end
33
- archives.flatten.compact
34
+ sources.map { |source| source.create_archives(timestamp, tmpdir) }.flatten.compact
35
+ end
36
+
37
+ def process_archives(archives)
38
+ processors.inject(archives) { |archives, processor| processor.process!(archives) }.flatten.compact
34
39
  end
35
40
 
36
- def store_archives
41
+ def store_archives(archives)
37
42
  targets.each { |target| target.put(archives) }
38
43
  end
39
44
 
40
45
  def purge_targets
41
- targets.each { |target| target.purge!(name) }
46
+ targets.each { |target| target.purge! }
42
47
  end
43
48
 
44
49
  end
@@ -35,7 +35,11 @@ module Outback
35
35
  end
36
36
  option_parser.parse!
37
37
  Outback::Configuration.reset
38
- config_file = ARGV.first || DEFAULT_CONFIGURATION_FILE
38
+ config_file = if ARGV.first
39
+ ARGV.first.start_with?('/') ? ARGV.first : File.join(Dir.pwd, ARGV.first)
40
+ else
41
+ DEFAULT_CONFIGURATION_FILE
42
+ end
39
43
  begin
40
44
  load config_file
41
45
  rescue ConfigurationError => conf_error
@@ -83,4 +87,4 @@ module Outback
83
87
  end
84
88
 
85
89
  end
86
- end
90
+ end
@@ -1,10 +1,11 @@
1
1
  module Outback
2
2
  class Configuration
3
+
3
4
  @loaded = []
4
5
 
5
6
  class << self
6
7
  def add(configuration)
7
- raise ConfigurationError.new("duplicate configuration #{configuration.name}") if loaded.any?(&its.name == configuration.name)
8
+ raise ConfigurationError, "duplicate configuration #{configuration.name}" if loaded.any?(&its.name == configuration.name)
8
9
  loaded << configuration
9
10
  end
10
11
 
@@ -21,13 +22,13 @@ module Outback
21
22
  end
22
23
  end
23
24
 
24
- attr_reader :name, :sources, :targets, :errors
25
+ attr_reader :name, :sources, :targets, :processors, :errors
25
26
 
26
27
  def initialize(name, &block)
27
- raise(ConfigurationError.new("configuration name can't be blank")) if name.blank?
28
- @name = name.to_s
29
- raise(ConfigurationError.new('configuration name may not contain underscores')) if @name.include?('_')
30
- @sources, @targets, @errors = [], [], []
28
+ name = name.to_s
29
+ raise ConfigurationError, "Illegal configuration name #{name.inspect}" unless name.match(/\A[a-z][a-z0-9_\-.]+\z/)
30
+ @name = name
31
+ @sources, @processors, @targets, @errors = [], [], [], []
31
32
  if block_given?
32
33
  if block.arity == 1 then yield(self) else instance_eval(&block) end
33
34
  end
@@ -48,21 +49,25 @@ module Outback
48
49
  @tmpdir
49
50
  elsif args.size == 1
50
51
  dir = Pathname.new(args.first).realpath
51
- raise(ConfigurationError.new("tmpdir #{dir} is no directory")) unless dir.directory?
52
+ raise ConfigurationError, "tmpdir #{dir} is not a directory" unless dir.directory?
52
53
  @tmpdir = dir
53
54
  else
54
- raise ConfigurationError.new("tmpdir: wrong number of arguments(#{args.size} for 1)")
55
+ raise ConfigurationError, "tmpdir: wrong number of arguments(#{args.size} for 1)"
55
56
  end
56
57
  end
57
58
 
58
59
  protected
59
60
 
60
61
  def source(type, *args, &block)
61
- "Outback::#{type.to_s.classify}Source".constantize.configure(*args, &block).tap { |instance| sources << instance }
62
+ "Outback::#{type.to_s.classify}Source".constantize.configure(name, *args, &block).tap { |instance| sources << instance }
63
+ end
64
+
65
+ def processor(type, *args, &block)
66
+ "Outback::#{type.to_s.classify}Processor".constantize.configure(name, *args, &block).tap { |instance| processors << instance }
62
67
  end
63
68
 
64
69
  def target(type, *args, &block)
65
- "Outback::#{type.to_s.classify}Target".constantize.configure(*args, &block).tap { |instance| targets << instance }
70
+ "Outback::#{type.to_s.classify}Target".constantize.configure(name, *args, &block).tap { |instance| targets << instance }
66
71
  end
67
72
 
68
73
  def error(message)
@@ -2,7 +2,8 @@ module Outback
2
2
  class DirectorySource < Source
3
3
  attr_reader :path
4
4
 
5
- def initialize(path)
5
+ def initialize(backup_name, path)
6
+ super(backup_name)
6
7
  @path = path
7
8
  end
8
9
 
@@ -18,18 +19,18 @@ module Outback
18
19
  excludes.concat(paths.map(&:to_s)).uniq!
19
20
  end
20
21
 
21
- def create_archives(backup_name, timestamp, tmpdir)
22
+ def create_archives(timestamp, tmpdir)
22
23
  source_dir = Pathname.new(path).realpath
23
24
  archive_name = Pathname.new(tmpdir).join("#{backup_name}_#{timestamp}_#{source_name}.tar.gz")
24
25
  exclude_list = Pathname.new(tmpdir).join('exclude_list.txt')
25
26
  File.open(exclude_list, 'w') { |f| f << excludes.map { |e| e.to_s.sub(/\A\//, '') }.join("\n") }
26
27
  verbose_switch = Outback.verbose? ? 'v' : ''
27
28
  commandline = "tar -cz#{verbose_switch}pf #{archive_name} --exclude-from=#{exclude_list} --directory=/ #{source_dir.to_s.sub(/\A\//, '')}"
28
- Outback.debug "executing command: #{commandline}"
29
+ logger.debug "executing command: #{commandline}"
29
30
  result = `#{commandline}`
30
- Outback.debug "result: #{result}"
31
- Outback.info "Archived directory #{path}"
32
- [TempArchive.new(archive_name, self)]
31
+ logger.debug "result: #{result}"
32
+ logger.info "Archived directory #{path}"
33
+ [SourceArchive.new(archive_name)]
33
34
  end
34
35
  end
35
- end
36
+ end
@@ -3,7 +3,8 @@ module Outback
3
3
  attr_reader :path
4
4
  attr_setter :user, :group, :directory_permissions, :archive_permissions, :ttl, :move
5
5
 
6
- def initialize(path)
6
+ def initialize(backup_name, path)
7
+ super(backup_name)
7
8
  @path = Pathname.new(path)
8
9
  end
9
10
 
@@ -11,39 +12,45 @@ module Outback
11
12
  (user and group) or (not user and not group)
12
13
  end
13
14
 
14
- def display_name
15
+ def to_s
15
16
  "directory:#{path}"
16
17
  end
17
18
 
18
19
  def put(archives)
19
- Dir.mkdir(path) unless path.directory?
20
+ FileUtils.mkdir_p(path) unless path.directory?
20
21
  FileUtils.chmod directory_permissions || 0700, path
21
22
  size = 0
22
23
  archives.each do |archive|
23
24
  basename = Pathname.new(archive.filename).basename
24
25
  if move
25
- Outback.debug "moving #{archive.filename} to #{path}"
26
+ logger.debug "moving #{archive.filename} to #{path}"
26
27
  FileUtils.mv archive.filename, path
27
28
  else
28
- Outback.debug "copying #{archive.filename} to #{path}"
29
+ logger.debug "copying #{archive.filename} to #{path}"
29
30
  FileUtils.cp_r archive.filename, path
30
31
  end
31
32
  archived_file = path.join(basename)
32
- Outback.debug "setting permissions for #{archived_file}"
33
+ logger.debug "setting permissions for #{archived_file}"
33
34
  FileUtils.chmod archive_permissions || 0600, archived_file
34
35
  if user && group
35
- Outback.debug "setting owner #{user}, group #{group} for #{archived_file}"
36
+ logger.debug "setting owner #{user}, group #{group} for #{archived_file}"
36
37
  FileUtils.chown user, group, archived_file
37
38
  end
38
39
  size += archived_file.size
39
40
  end
40
- Outback.info "#{move ? 'Moved' : 'Copied'} #{archives.size} archives (#{size} bytes) to #{display_name}"
41
+ logger.info "#{move ? 'Moved' : 'Copied'} #{archives.size} archives (#{size} bytes) to #{self}"
41
42
  archives.size
42
43
  end
44
+
45
+ private
43
46
 
44
- def list_archives(name)
45
- path.files(Archive::NAME_PATTERN).map { |f| DirectoryArchive.new(f, self) }.select(&its.backup_name == name)
47
+ def list_all_archives
48
+ path.files(Archive::NAME_PATTERN).map { |f| build_archive(f.to_s, f.size) }
49
+ end
50
+
51
+ def unlink_archive!(archive)
52
+ archive.filename.unlink
46
53
  end
47
54
 
48
55
  end
49
- end
56
+ end
@@ -0,0 +1,34 @@
1
+ require 'openssl'
2
+ require 'open3'
3
+
4
+ module Outback
5
+ class EncryptionProcessor < Processor
6
+
7
+ attr_setter :password, :cipher
8
+
9
+ def cipher
10
+ @cipher ||= 'aes-256-cbc'
11
+ end
12
+
13
+ def to_s
14
+ "encryption:#{cipher}"
15
+ end
16
+
17
+ private
18
+
19
+ def process_archive!(archive)
20
+ result = nil
21
+ outfile = Pathname.new("#{archive.filename}.enc")
22
+ logger.debug "Encrypting #{archive} with #{self}"
23
+ Open3.popen3("openssl enc -#{cipher} -pass stdin -in #{archive.filename} -out #{outfile}") do |stdin, stdout, stderr, wait_thr|
24
+ stdin << password
25
+ stdin.close
26
+ result = wait_thr.value
27
+ end
28
+ raise ProcessingError, "error processing archive #{archive} in #{self}" unless result.success?
29
+ raise ProcessingError, "outfile #{outfile} not found" unless outfile.file?
30
+ archive.unlink
31
+ SourceArchive.new(outfile)
32
+ end
33
+ end
34
+ end