astrails-safe 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README ADDED
@@ -0,0 +1,13 @@
1
+ Simple mysql and filesystem backups with S3 support
2
+
3
+ Usage:
4
+ astrails-safe [OPTIONS] CONFIG_FILE
5
+ Options:
6
+ -h, --help This help screen
7
+ -v, --verbose be verbose, duh!
8
+ -n, --dry-run just pretend, don't do anything.
9
+ -L, --local skip S3
10
+
11
+ Note: CONFIG_FILE will be created from template if missing
12
+
13
+ See template for configuration examples
data/lib/astrails/safe.rb CHANGED
@@ -1,12 +1,24 @@
1
1
  require 'extensions/mktmpdir'
2
2
  require 'astrails/safe/tmp_file'
3
+
3
4
  require 'astrails/safe/config/node'
4
5
  require 'astrails/safe/config/builder'
6
+
5
7
  require 'astrails/safe/stream'
6
- require 'astrails/safe/engine'
8
+
9
+ require 'astrails/safe/source'
7
10
  require 'astrails/safe/mysqldump'
8
11
  require 'astrails/safe/archive'
9
12
 
13
+ require 'astrails/safe/pipe'
14
+ require 'astrails/safe/gpg'
15
+ require 'astrails/safe/gzip'
16
+
17
+ require 'astrails/safe/sink'
18
+ require 'astrails/safe/local'
19
+ require 'astrails/safe/s3'
20
+
21
+
10
22
  module Astrails
11
23
  module Safe
12
24
  ROOT = File.join(File.dirname(__FILE__), "..", "..")
@@ -19,8 +31,9 @@ module Astrails
19
31
  config = Config::Node.new(&block)
20
32
  #config.dump
21
33
 
22
- Astrails::Safe::Mysqldump.run(config[:mysqldump, :databases], timestamp)
23
- Astrails::Safe::Archive.run(config[:tar, :archives], timestamp)
34
+ Astrails::Safe::Mysqldump.run(config[:mysqldump, :databases])
35
+ Astrails::Safe::Archive.run(config[:tar, :archives])
36
+
24
37
  Astrails::Safe::TmpFile.cleanup
25
38
  end
26
39
  end
@@ -1,12 +1,12 @@
1
1
  module Astrails
2
2
  module Safe
3
- class Archive < Engine
3
+ class Archive < Source
4
4
 
5
5
  def command
6
6
  "tar -cf - #{@config[:options]} #{tar_exclude_files} #{tar_files}"
7
7
  end
8
8
 
9
- def extension; 'tar'; end
9
+ def extension; '.tar'; end
10
10
 
11
11
  protected
12
12
 
@@ -3,8 +3,8 @@ module Astrails
3
3
  module Config
4
4
  class Builder
5
5
  COLLECTIONS = %w/database archive/
6
- ITEMS = %w/s3 key secret bucket path prefix gpg password keep local mysqldump options
7
- user socket tar files exclude/
6
+ ITEMS = %w/s3 key secret bucket path gpg password keep local mysqldump options
7
+ user socket tar files exclude filename/
8
8
  NAMES = COLLECTIONS + ITEMS
9
9
  def initialize(node)
10
10
  @node = node
@@ -3,6 +3,7 @@ module Astrails
3
3
  module Safe
4
4
  module Config
5
5
  class Node
6
+ attr_reader :parent
6
7
  def initialize(parent = nil, data = {}, &block)
7
8
  @parent, @data = parent, {}
8
9
  data.each { |k, v| self[k] = v }
@@ -41,6 +42,14 @@ module Astrails
41
42
  end
42
43
  include Enumerable
43
44
 
45
+ def to_hash
46
+ @data.keys.inject({}) do |res, key|
47
+ value = @data[key]
48
+ res[key] = value.is_a?(Node) ? value.to_hash : value
49
+ res
50
+ end
51
+ end
52
+
44
53
  def dump(indent = "")
45
54
  @data.each do |key, value|
46
55
  if value.is_a?(Node)
@@ -0,0 +1,42 @@
1
+ module Astrails
2
+ module Safe
3
+ class Gpg < Pipe
4
+
5
+ def compressed?
6
+ active? || @parent.compressed?
7
+ end
8
+
9
+ protected
10
+
11
+ def pipe
12
+ if key
13
+ rise RuntimeError, "can't use both gpg password and pubkey" if password
14
+ "|gpg -e -r #{key}"
15
+ elsif password
16
+ "|gpg -c --passphrase-file #{gpg_password_file(password)}"
17
+ end
18
+ end
19
+
20
+ def extension
21
+ ".gpg" if active?
22
+ end
23
+
24
+ def active?
25
+ password || key
26
+ end
27
+
28
+ def password
29
+ @password ||= config[:gpg, :password]
30
+ end
31
+
32
+ def key
33
+ @key ||= config[:gpg, :key]
34
+ end
35
+
36
+ def gpg_password_file(pass)
37
+ return "TEMP_GENERATED_FILENAME" if $DRY_RUN
38
+ Astrails::Safe::TmpFile.create("gpg-pass") { |file| file.write(pass) }
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,25 @@
1
+ module Astrails
2
+ module Safe
3
+ class Gzip < Pipe
4
+
5
+ def compressed?
6
+ true
7
+ end
8
+
9
+ protected
10
+
11
+ def pipe
12
+ "|gzip" if active?
13
+ end
14
+
15
+ def extension
16
+ ".gz" if active?
17
+ end
18
+
19
+ def active?
20
+ !@parent.compressed?
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,54 @@
1
+ module Astrails
2
+ module Safe
3
+ class Local < Sink
4
+
5
+ def open(&block)
6
+ return @parent.open(&block) unless active?
7
+ run
8
+ File.open(path, &block) unless $DRY_RUN
9
+ end
10
+
11
+ protected
12
+
13
+ def active?
14
+ # S3 can't upload from pipe. it needs to know file size, so we must pass through :local
15
+ # will change once we add SSH sink
16
+ true
17
+ end
18
+
19
+ def prefix
20
+ @prefix ||= File.expand_path(expand(@config[:local, :path] || raise(RuntimeError, "missing :local/:path in configuration")))
21
+ end
22
+
23
+ def command
24
+ "#{@parent.command} > #{path}"
25
+ end
26
+
27
+ def save
28
+ puts "command: #{command}" if $_VERBOSE
29
+ unless $DRY_RUN
30
+ FileUtils.mkdir_p(prefix) unless File.directory?(prefix)
31
+ system command
32
+ end
33
+ end
34
+
35
+ def cleanup
36
+ return unless keep = @config[:keep, :local]
37
+
38
+ base = File.basename(filename).split(".").first
39
+
40
+ pattern = File.join(prefix, "#{base}*")
41
+ puts "listing files #{pattern.inspect}" if $_VERBOSE
42
+ files = Dir[pattern] .
43
+ select{|f| File.file?(f)} .
44
+ sort
45
+
46
+ cleanup_with_limit(files, keep) do |f|
47
+ puts "removing local file #{f}" if $DRY_RUN || $_VERBOSE
48
+ File.unlink(f) unless $DRY_RUN
49
+ end
50
+ end
51
+
52
+ end
53
+ end
54
+ end
@@ -1,16 +1,16 @@
1
1
  module Astrails
2
2
  module Safe
3
- class Mysqldump < Engine
3
+ class Mysqldump < Source
4
4
 
5
5
  def command
6
- "mysqldump --defaults-extra-file=#{password_file} #{@config[:options]} #{mysql_skip_tables} #{@id}"
6
+ @commanbd ||= "mysqldump --defaults-extra-file=#{mysql_password_file} #{@config[:options]} #{mysql_skip_tables} #{@id}"
7
7
  end
8
8
 
9
- def extension; 'sql'; end
9
+ def extension; '.sql'; end
10
10
 
11
11
  protected
12
12
 
13
- def password_file
13
+ def mysql_password_file
14
14
  Astrails::Safe::TmpFile.create("mysqldump") do |file|
15
15
  file.puts "[mysqldump]"
16
16
  %w/user password socket host port/.each do |k|
@@ -26,10 +26,6 @@ module Astrails
26
26
  end
27
27
  end
28
28
 
29
- def mysqldump_extra_options
30
- @config[:options] + " " if @config[:options]
31
- end
32
-
33
29
  end
34
30
  end
35
31
  end
@@ -0,0 +1,15 @@
1
+ module Astrails
2
+ module Safe
3
+ class Pipe < Stream
4
+
5
+ def command
6
+ "#{@parent.command}#{pipe}"
7
+ end
8
+
9
+ def filename
10
+ "#{@parent.filename}#{extension}"
11
+ end
12
+
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,68 @@
1
+ module Astrails
2
+ module Safe
3
+ class S3 < Sink
4
+
5
+ protected
6
+
7
+ def active?
8
+ bucket && key && secret
9
+ end
10
+
11
+ def prefix
12
+ @prefix ||= expand(config[:s3, :path] || expand(config[:local, :path] || ":kind/:id"))
13
+ end
14
+
15
+ def save
16
+ # needed in cleanup even on dry run
17
+ AWS::S3::Base.establish_connection!(:access_key_id => key, :secret_access_key => secret, :use_ssl => true) unless $LOCAL
18
+
19
+ file = @parent.open
20
+ puts "Uploading #{bucket}:#{path}" if $_VERBOSE || $DRY_RUN
21
+ unless $DRY_RUN || $LOCAL
22
+ AWS::S3::Bucket.create(bucket)
23
+ AWS::S3::S3Object.store(path, file, bucket)
24
+ puts "...done" if $_VERBOSE
25
+ end
26
+ file.close if file
27
+
28
+ end
29
+
30
+ def cleanup
31
+
32
+ return if $LOCAL
33
+
34
+ return unless keep = @config[:keep, :s3]
35
+
36
+ bucket = @config[:s3, :bucket]
37
+
38
+ base = File.basename(filename).split(".").first
39
+
40
+ puts "listing files in #{bucket}:#{prefix}/#{base}"
41
+ files = AWS::S3::Bucket.objects(bucket, :prefix => "#{prefix}/#{base}", :max_keys => keep * 2)
42
+ puts files.collect {|x| x.key} if $_VERBOSE
43
+
44
+ files = files.
45
+ collect {|x| x.key}.
46
+ sort
47
+
48
+ cleanup_with_limit(files, keep) do |f|
49
+ puts "removing s3 file #{bucket}:#{f}" if $DRY_RUN || $_VERBOSE
50
+ AWS::S3::Bucket.find(bucket)[f].delete unless $DRY_RUN || $LOCAL
51
+ end
52
+ end
53
+
54
+ def bucket
55
+ config[:s3, :bucket]
56
+ end
57
+
58
+ def key
59
+ config[:s3, :key]
60
+ end
61
+
62
+ def secret
63
+ config[:s3, :secret]
64
+ end
65
+
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,33 @@
1
+ module Astrails
2
+ module Safe
3
+ class Sink < Stream
4
+
5
+ def run
6
+ if active?
7
+ save
8
+ cleanup
9
+ else
10
+ @parent.run
11
+ end
12
+ end
13
+
14
+ protected
15
+
16
+ # prefix is defined in subclass
17
+ def path
18
+ @path ||= File.join(prefix, filename)
19
+ end
20
+
21
+ # call block on files to be removed (all except for the LAST 'limit' files
22
+ def cleanup_with_limit(files, limit, &block)
23
+ return unless files.size > limit
24
+
25
+ to_remove = files[0..(files.size - limit - 1)]
26
+ # TODO: validate here
27
+ to_remove.each(&block)
28
+ end
29
+
30
+ end
31
+ end
32
+ end
33
+
@@ -0,0 +1,31 @@
1
+ module Astrails
2
+ module Safe
3
+ class Source < Stream
4
+
5
+ def initialize(id, config)
6
+ @id, @config = id, config
7
+ end
8
+
9
+ def filename
10
+ @filename ||= expand(":kind-:id.:timestamp#{extension}")
11
+ end
12
+
13
+ # process each config key as source (with full pipe)
14
+ def self.run(config)
15
+ unless config
16
+ puts "No configuration found for #{kind}"
17
+ return
18
+ end
19
+
20
+ config.each do |key, value|
21
+ stream = [Gpg, Gzip, Local, S3].inject(new(key, value)) do |res, klass|
22
+ klass.new(res)
23
+ end
24
+ stream.run
25
+ end
26
+ end
27
+
28
+ end
29
+ end
30
+ end
31
+
@@ -2,64 +2,44 @@ module Astrails
2
2
  module Safe
3
3
  class Stream
4
4
 
5
- attr_accessor :config, :command, :filename
6
- def initialize(config, command, filename)
7
- @config, @command, @filename = config, command, filename
5
+ def initialize(parent)
6
+ @parent = parent
8
7
  end
9
8
 
10
- def run
11
- encrypt || compress # use gpg or gzip
12
- redirect
13
- execute
9
+ def id
10
+ @id ||= @parent.id
14
11
  end
15
12
 
16
- private
17
-
18
- def gpg_password_file(pass)
19
- Astrails::Safe::TmpFile.create("gpg-pass") { |file| file.write(pass) }
13
+ def config
14
+ @config ||= @parent.config
20
15
  end
21
16
 
22
- def encrypt
23
-
24
- password = @config[:gpg, :password]
25
- key = @config[:gpg, :key]
26
-
27
- return false unless key || password
28
-
29
- if key
30
- rise RuntimeError, "can't use both gpg password and pubkey" if password
31
-
32
- @filename << ".gpg"
33
- @command << "|gpg -e -r #{key}"
34
- else
35
- @filename << ".gpg"
36
- unless $DRY_RUN
37
- @command << "|gpg -c --passphrase-file #{gpg_password_file(password)}"
38
- else
39
- @command << "|gpg -c --passphrase-file TEMP_GENERATED_FILENAME"
40
- end
41
- end
42
- true
17
+ def filename
18
+ @parent.filename
43
19
  end
44
20
 
45
- def compress
46
- @filename << ".gz"
47
- @command << "|gzip"
21
+ def compressed?
22
+ @parent && @parent.compressed?
48
23
  end
49
24
 
50
- def redirect
51
- @command << ">" << @filename
25
+ protected
26
+
27
+ def name
28
+ self.class.name.split('::').last.downcase
52
29
  end
53
30
 
54
- def execute
55
- dir = File.dirname(filename)
56
- FileUtils.mkdir_p(dir) unless File.directory?(dir) || $DRY_RUN
31
+ def kind
32
+ @parent ? @parent.kind : name
33
+ end
57
34
 
58
- # EXECUTE
59
- puts "Backup command: #{@command}" if $DRY_RUN || $_VERBOSE
60
- system @command unless $DRY_RUN
35
+ def expand(path)
36
+ path .
37
+ gsub(/:kind\b/, kind) .
38
+ gsub(/:id\b/, id) .
39
+ gsub(/:timestamp\b/, timestamp)
61
40
  end
62
41
 
63
42
  end
64
43
  end
65
44
  end
45
+
data/safe.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "safe"
3
- s.version = "0.1.0"
3
+ s.version = "0.1.1"
4
4
  s.date = "2009-03-15"
5
5
  s.summary = "Astrails Safe"
6
6
  s.email = "we@astrails.com"
@@ -9,16 +9,23 @@ Gem::Specification.new do |s|
9
9
  s.has_rdoc = false
10
10
  s.authors = ["Astrails Ltd."]
11
11
  s.files = files = %w(
12
+ README
12
13
  bin/astrails-safe
14
+ lib/extensions/mktmpdir.rb
13
15
  lib/astrails/safe.rb
16
+ lib/astrails/safe/s3.rb
17
+ lib/astrails/safe/gpg.rb
14
18
  lib/astrails/safe/mysqldump.rb
15
19
  lib/astrails/safe/stream.rb
16
20
  lib/astrails/safe/config/builder.rb
17
21
  lib/astrails/safe/config/node.rb
18
- lib/astrails/safe/engine.rb
22
+ lib/astrails/safe/sink.rb
23
+ lib/astrails/safe/pipe.rb
24
+ lib/astrails/safe/source.rb
19
25
  lib/astrails/safe/archive.rb
26
+ lib/astrails/safe/local.rb
20
27
  lib/astrails/safe/tmp_file.rb
21
- lib/extensions/mktmpdir.rb
28
+ lib/astrails/safe/gzip.rb
22
29
  templates/script.rb
23
30
  safe.gemspec
24
31
  )
data/templates/script.rb CHANGED
@@ -1,21 +1,24 @@
1
1
  safe do
2
- # backup file path (full, including filename)
3
- # Note: do not include .tar, .sql, .gz or .pgp, it will be added automatically
2
+
3
+ # backup file path (not including filename)
4
4
  # supported substitutions:
5
5
  # :kind -> backup 'engine' kind, e.g. "mysqldump" or "archive"
6
6
  # :id -> backup 'id', e.g. "blog", "production", etc.
7
7
  # :timestamp -> current run timestamp (same for all the backups in the same 'run')
8
8
  # you can set separate :path for all backups (or once globally here)
9
- path "/backup/:kind/:id-:timestamp"
9
+ local do
10
+ path "/backup/:kind/"
11
+ end
10
12
 
11
13
  ## uncomment to enable uploads to Amazon S3
12
14
  ## Amazon S3 auth (optional)
15
+ ## don't forget to add :s3 to the 'store' list
13
16
  # s3 do
14
17
  # key YOUR_S3_KEY
15
18
  # secret YOUR_S3_SECRET
16
19
  # bucket S3_BUCKET
17
- # # prefix for uploads to S3. supports same substitution like :path
18
- # prefix ":kind/:id" # this is default
20
+ # # path for uploads to S3. supports same substitution like :local/:path
21
+ # path ":kind/" # this is default
19
22
  # end
20
23
 
21
24
  ## alternative style:
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: astrails-safe
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Astrails Ltd.
@@ -31,16 +31,23 @@ extensions: []
31
31
  extra_rdoc_files: []
32
32
 
33
33
  files:
34
+ - README
34
35
  - bin/astrails-safe
36
+ - lib/extensions/mktmpdir.rb
35
37
  - lib/astrails/safe.rb
38
+ - lib/astrails/safe/s3.rb
39
+ - lib/astrails/safe/gpg.rb
36
40
  - lib/astrails/safe/mysqldump.rb
37
41
  - lib/astrails/safe/stream.rb
38
42
  - lib/astrails/safe/config/builder.rb
39
43
  - lib/astrails/safe/config/node.rb
40
- - lib/astrails/safe/engine.rb
44
+ - lib/astrails/safe/sink.rb
45
+ - lib/astrails/safe/pipe.rb
46
+ - lib/astrails/safe/source.rb
41
47
  - lib/astrails/safe/archive.rb
48
+ - lib/astrails/safe/local.rb
42
49
  - lib/astrails/safe/tmp_file.rb
43
- - lib/extensions/mktmpdir.rb
50
+ - lib/astrails/safe/gzip.rb
44
51
  - templates/script.rb
45
52
  - safe.gemspec
46
53
  has_rdoc: false
@@ -1,134 +0,0 @@
1
- module Astrails
2
- module Safe
3
- class Engine
4
-
5
- def self.run(config, timestamp)
6
- unless config
7
- puts "No configuration found for #{kind}"
8
- return
9
- end
10
- config.each do |key, value|
11
- new(key, value, timestamp).run
12
- end
13
- end
14
-
15
- attr_accessor :timestamp
16
- attr_reader :id, :config
17
- def initialize(id, config, timestamp)
18
- @config, @id, @timestamp = config, id, timestamp
19
- end
20
-
21
- def run
22
- puts "#{kind}: #{@id}" if $_VERBOSE
23
-
24
- stream = Stream.new(@config, command, backup_filepath)
25
- stream.run # execute backup comand. result is file stream.filename
26
-
27
- # UPLOAD
28
- upload(stream.filename)
29
-
30
- # CLEANUP
31
- cleanup_s3(stream.filename)
32
- cleanup_local(stream.filename)
33
- end
34
-
35
- protected
36
-
37
- def self.kind
38
- name.split('::').last.downcase
39
- end
40
- def kind
41
- self.class.kind
42
- end
43
-
44
- def expand(path)
45
- path .
46
- gsub(/:kind\b/, kind) .
47
- gsub(/:id\b/, id) .
48
- gsub(/:timestamp\b/, timestamp)
49
- end
50
-
51
- def s3_prefix
52
- @s3_prefix ||= expand(@config[:s3, :prefix] || ":kind/:id")
53
- end
54
-
55
- def backup_filepath
56
- @backup_filepath ||= File.expand_path(expand(@config[:path] || raise(RuntimeError, "missing :path in configuration"))) << "." << extension
57
- end
58
-
59
-
60
- def upload(filename)
61
-
62
- bucket = @config[:s3, :bucket]
63
- key = @config[:s3, :key]
64
- secret = @config[:s3, :secret]
65
-
66
- return unless bucket && key && secret
67
-
68
- upload_path = File.join(s3_prefix, File.basename(filename))
69
-
70
- puts "Uploading file #{filename} to #{bucket}/#{upload_path}" if $_VERBOSE || $DRY_RUN
71
- if $LOCAL
72
- puts "skip upload (local operation)"
73
- else
74
- # needed in cleanup even on dry run
75
- AWS::S3::Base.establish_connection!(:access_key_id => key, :secret_access_key => secret, :use_ssl => true)
76
-
77
- unless $DRY_RUN
78
- AWS::S3::Bucket.create(bucket)
79
- AWS::S3::S3Object.store(upload_path, open(filename), bucket)
80
- end
81
- end
82
- puts "...done" if $_VERBOSE
83
- end
84
-
85
- # call block on files to be removed (all except for the LAST 'limit' files
86
- def cleanup_files(files, limit, &block)
87
- return unless files.size > limit
88
-
89
- to_remove = files[0..(files.size - limit - 1)]
90
- to_remove.each(&block)
91
- end
92
-
93
- def cleanup_local(filename)
94
- return unless keep = @config[:keep, :local]
95
-
96
- dir = File.dirname(filename)
97
- base = File.basename(filename).split(".").first
98
-
99
- files = Dir[File.join(dir, "#{base}*")] .
100
- select{|f| File.file?(f)} .
101
- sort
102
-
103
- cleanup_files(files, keep) do |f|
104
- puts "removing local file #{f}" if $DRY_RUN || $_VERBOSE
105
- File.unlink(f) unless $DRY_RUN
106
- end
107
- end
108
-
109
- def cleanup_s3(filename)
110
-
111
- return unless keep = @config[:keep, :s3]
112
-
113
- bucket = @config[:s3, :bucket]
114
-
115
- base = File.basename(filename).split(".").first
116
-
117
- puts "listing files in #{bucket}:#{s3_prefix}"
118
- files = AWS::S3::Bucket.objects(bucket, :prefix => "#{s3_prefix}/#{base}", :max_keys => keep * 2)
119
- puts files.collect {|x| x.key} if $_VERBOSE
120
-
121
- files = files.
122
- collect {|x| x.key}.
123
- sort
124
-
125
- cleanup_files(files, keep) do |f|
126
- puts "removing s3 file #{bucket}:#{f}" if $DRY_RUN || $_VERBOSE
127
- AWS::S3::Bucket.find(bucket)[f].delete unless $DRY_RUN
128
- end
129
- end
130
-
131
- end
132
- end
133
- end
134
-