astrails-safe 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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
-