astrails-safe 0.0.7 → 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,26 @@
1
+ require 'astrails/safe/tmp_file'
2
+ require 'astrails/safe/config/node'
3
+ require 'astrails/safe/config/builder'
4
+ require 'astrails/safe/stream'
5
+ require 'astrails/safe/engine'
6
+ require 'astrails/safe/mysqldump'
7
+ require 'astrails/safe/archive'
8
+
9
+ module Astrails
10
+ module Safe
11
+ ROOT = File.join(File.dirname(__FILE__), "..", "..")
12
+
13
+ def timestamp
14
+ @timestamp ||= Time.now.strftime("%y%m%d-%H%M")
15
+ end
16
+
17
+ def safe(&block)
18
+ config = Config::Node.new(&block)
19
+ #config.dump
20
+
21
+ Astrails::Safe::Mysqldump.run(config[:mysqldump, :databases], timestamp)
22
+ Astrails::Safe::Archive.run(config[:tar, :archives], timestamp)
23
+ end
24
+ end
25
+ end
26
+
@@ -0,0 +1,22 @@
1
+ module Astrails
2
+ module Safe
3
+ class Archive < Engine
4
+
5
+ def command
6
+ "tar -cf - #{@config[:options]} #{tar_exclude_files} #{tar_files}"
7
+ end
8
+
9
+ protected
10
+
11
+ def tar_exclude_files
12
+ [*@config[:exclude]].compact.map{|x| "--exclude=#{x}"} * " "
13
+ end
14
+
15
+ def tar_files
16
+ raise RuntimeError, "missing files for tar" unless @config[:files]
17
+ [*@config[:files]] * " "
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,58 @@
1
+ module Astrails
2
+ module Safe
3
+ module Config
4
+ class Builder
5
+ COLLECTIONS = %w/database archive/
6
+ ITEMS = %w/s3 key secret bucket path gpg password keep local mysqldump options
7
+ user socket tar files exclude/
8
+ NAMES = COLLECTIONS + ITEMS
9
+ def initialize(node)
10
+ @node = node
11
+ end
12
+
13
+ # supported args:
14
+ # args = [value]
15
+ # args = [id, data]
16
+ # args = [data]
17
+ # id/value - simple values, data - hash
18
+ def method_missing(sym, *args, &block)
19
+ return super unless NAMES.include?(sym.to_s)
20
+
21
+ # do we have id or value?
22
+ unless args.first.is_a?(Hash)
23
+ id_or_value = args.shift # nil for args == []
24
+ end
25
+
26
+ # do we have data hash?
27
+ if data = args.shift
28
+ die "#{sym}: hash expected: #{data.inspect}" unless data.is_a?(Hash)
29
+ end
30
+
31
+ #puts "#{sym}: args=#{args.inspect}, id_or_value=#{id_or_value}, data=#{data.inspect}, block=#{block.inspect}"
32
+
33
+ die "#{sym}: unexpected: #{args.inspect}" unless args.empty?
34
+ die "#{sym}: missing arguments" unless id_or_value || data || block
35
+
36
+ if COLLECTIONS.include?(sym.to_s) && id_or_value
37
+ data ||= {}
38
+ end
39
+
40
+ if !data && !block
41
+ # simple value assignment
42
+ @node[sym] = id_or_value
43
+
44
+ elsif id_or_value
45
+ # collection element with id => create collection node and a subnode in it
46
+ key = sym.to_s + "s"
47
+ collection = @node[key] || @node.set(key, {})
48
+ collection.set(id_or_value, data || {}, &block)
49
+
50
+ else
51
+ # simple subnode
52
+ @node.set(sym, data || {}, &block)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,58 @@
1
+ require 'astrails/safe/config/builder'
2
+ module Astrails
3
+ module Safe
4
+ module Config
5
+ class Node
6
+ def initialize(parent = nil, data = {}, &block)
7
+ @parent, @data = parent, {}
8
+ data.each { |k, v| self[k] = v }
9
+ Builder.new(self).instance_eval(&block) if block
10
+ end
11
+
12
+ # looks for the path from this node DOWN. will not delegate to parent
13
+ def get(*path)
14
+ key = path.shift
15
+ value = @data[key.to_s]
16
+ return value if value && path.empty?
17
+
18
+ value && value.get(*path)
19
+ end
20
+
21
+ # recursive find
22
+ # starts at the node and continues to the parent
23
+ def find(*path)
24
+ get(*path) || @parent && @parent.find(*path)
25
+ end
26
+ alias :[] :find
27
+
28
+ def set(key, value, &block)
29
+ @data[key.to_s] =
30
+ if value.is_a?(Hash)
31
+ Node.new(self, value, &block)
32
+ else
33
+ raise(ArgumentError, "#{key}: no block supported for simple values") if block
34
+ value
35
+ end
36
+ end
37
+ alias :[]= :set
38
+
39
+ def each(&block)
40
+ @data.each(&block)
41
+ end
42
+ include Enumerable
43
+
44
+ def dump(indent = "")
45
+ @data.each do |key, value|
46
+ if value.is_a?(Node)
47
+ puts "#{indent}#{key}:"
48
+ value.dump(indent + " ")
49
+ else
50
+ puts "#{indent}#{key}: #{value.inspect}"
51
+ end
52
+ end
53
+ end
54
+
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,133 @@
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, File.join(path, backup_filename))
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 kind
38
+ self.class.name.split('::').last.downcase
39
+ end
40
+
41
+ def backup_filename
42
+ @backup_filename ||= "#{kind}-#{id}.#{timestamp}.tar"
43
+ end
44
+
45
+ def expand(path)
46
+ path.gsub(/:kind\b/, kind).gsub(/:id\b/, id)
47
+ end
48
+
49
+ def s3_path
50
+ @s3_path ||= expand(@config[:s3, :path] || ":kind/:id")
51
+ end
52
+
53
+ def path
54
+ @path ||= File.expand_path(expand(@config[:path] || raise(RuntimeError, "missing :path in configuration")))
55
+ end
56
+
57
+
58
+ def upload(filename)
59
+
60
+ bucket = @config[:s3, :bucket]
61
+ key = @config[:s3, :key]
62
+ secret = @config[:s3, :secret]
63
+
64
+ return unless bucket && key && secret
65
+
66
+ upload_path = File.join(s3_path, File.basename(filename))
67
+
68
+ puts "Uploading file #{filename} to #{bucket}/#{upload_path}" if $_VERBOSE || $DRY_RUN
69
+ if $LOCAL
70
+ puts "skip upload (local operation)"
71
+ else
72
+ # needed in cleanup even on dry run
73
+ AWS::S3::Base.establish_connection!(:access_key_id => key, :secret_access_key => secret, :use_ssl => true)
74
+
75
+ unless $DRY_RUN
76
+ AWS::S3::Bucket.create(bucket)
77
+ AWS::S3::S3Object.store(s3_path, open(filename), bucket)
78
+ end
79
+ end
80
+ puts "...done" if $_VERBOSE
81
+ end
82
+
83
+ # call block on files to be removed (all except for the LAST 'limit' files
84
+ def cleanup_files(files, limit, &block)
85
+ return unless files.size > limit
86
+
87
+ to_remove = files[0..(files.size - limit - 1)]
88
+ to_remove.each(&block)
89
+ end
90
+
91
+ def cleanup_local(filename)
92
+ return unless keep = @config[:keep, :local]
93
+
94
+ dir = File.dirname(filename)
95
+ base = File.basename(filename).split(".").first
96
+
97
+ files = Dir[File.join(dir, "#{base}*")] .
98
+ select{|f| File.file?(f)} .
99
+ sort
100
+
101
+ cleanup_files(files, keep) do |f|
102
+ puts "removing local file #{f}" if $DRY_RUN || $_VERBOSE
103
+ File.unlink(f) unless $DRY_RUN
104
+ end
105
+ end
106
+
107
+ def cleanup_s3(filename)
108
+
109
+ return unless keep = @config[:keep, :s3]
110
+
111
+ bucket = @config[:s3, :bucket]
112
+
113
+ base = File.basename(filename).split(".").first
114
+
115
+ puts "listing files in #{bucket}:#{s3_path}"
116
+ files = AWS::S3::Bucket.objects(bucket, :prefix => s3_path, :max_keys => keep * 2)
117
+ puts files.collect(&:key) if $_VERBOSE
118
+
119
+ files = files.
120
+ collect(&:key).
121
+ select{|f| File.basename(f)[0..(base.length - 1)] == base}.
122
+ sort
123
+
124
+ cleanup_files(files, keep) do |f|
125
+ puts "removing s3 file #{bucket}:#{f}" if $DRY_RUN || $_VERBOSE
126
+ AWS::S3::Bucket.find(bucket)[f].delete unless $DRY_RUN
127
+ end
128
+ end
129
+
130
+ end
131
+ end
132
+ end
133
+
@@ -0,0 +1,33 @@
1
+ module Astrails
2
+ module Safe
3
+ class Mysqldump < Engine
4
+
5
+ def command
6
+ "mysqldump --defaults-extra-file=#{password_file} #{@config[:options]} #{mysql_skip_tables} #{@id}"
7
+ end
8
+
9
+ protected
10
+
11
+ def password_file
12
+ Astrails::Safe::TmpFile.create("mysqldump") do |file|
13
+ file.puts "[mysqldump]"
14
+ %w/user password socket host port/.each do |k|
15
+ v = @config[k]
16
+ file.puts "#{k} = #{v}" if v
17
+ end
18
+ end
19
+ end
20
+
21
+ def mysql_skip_tables
22
+ if skip_tables = @config[:skip_tables]
23
+ [*skip_tables].map { |t| "--ignore-table=#{@id}.#{t}" } * " "
24
+ end
25
+ end
26
+
27
+ def mysqldump_extra_options
28
+ @config[:options] + " " if @config[:options]
29
+ end
30
+
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,65 @@
1
+ module Astrails
2
+ module Safe
3
+ class Stream
4
+
5
+ attr_accessor :config, :command, :filename
6
+ def initialize(config, command, filename)
7
+ @config, @command, @filename = config, command, filename
8
+ end
9
+
10
+ def run
11
+ encrypt || compress # use gpg or gzip
12
+ redirect
13
+ execute
14
+ end
15
+
16
+ private
17
+
18
+ def gpg_password_file(pass)
19
+ Astrails::Safe::TmpFile.create("gpg-pass") { |file| file.write(pass) }
20
+ end
21
+
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
43
+ end
44
+
45
+ def compress
46
+ @filename << ".gz"
47
+ @command << "|gzip"
48
+ end
49
+
50
+ def redirect
51
+ @command << ">" << @filename
52
+ end
53
+
54
+ def execute
55
+ dir = File.dirname(filename)
56
+ FileUtils.mkdir_p(dir) unless File.directory?(dir) || $DRY_RUN
57
+
58
+ # EXECUTE
59
+ puts "Backup command: #{@command}" if $DRY_RUN || $_VERBOSE
60
+ system @command unless $DRY_RUN
61
+ end
62
+
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,21 @@
1
+ require 'tmpdir'
2
+ module Astrails
3
+ module Safe
4
+ module TmpFile
5
+ @KEEP_FILES = []
6
+ TMPDIR = Dir.mktmpdir
7
+
8
+ def self.create(name)
9
+ # create temp directory
10
+
11
+ file = Tempfile.new(name, TMPDIR)
12
+
13
+ yield file
14
+
15
+ file.close
16
+ @KEEP_FILES << file # so that it will not get gcollected and removed from filesystem until the end
17
+ file.path
18
+ end
19
+ end
20
+ end
21
+ end
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "safe"
3
- s.version = "0.0.7"
3
+ s.version = "0.0.8"
4
4
  s.date = "2009-03-15"
5
5
  s.summary = "Astrails Safe"
6
6
  s.email = "we@astrails.com"
@@ -9,9 +9,18 @@ Gem::Specification.new do |s|
9
9
  s.has_rdoc = false
10
10
  s.authors = ["Astrails Ltd."]
11
11
  s.files = files = %w(
12
- bin/astrails-safe
13
- safe.gemspec
14
- ) + Dir['lib/**/*'] + Dir['template/**/*']
12
+ bin/astrails-safe
13
+ lib/astrails/safe.rb
14
+ lib/astrails/safe/mysqldump.rb
15
+ lib/astrails/safe/stream.rb
16
+ lib/astrails/safe/config/builder.rb
17
+ lib/astrails/safe/config/node.rb
18
+ lib/astrails/safe/engine.rb
19
+ lib/astrails/safe/archive.rb
20
+ lib/astrails/safe/tmp_file.rb
21
+ templates/script.rb
22
+ safe.gemspec
23
+ )
15
24
  s.executables = files.grep(/^bin/).map {|x| x.gsub(/^bin\//, "")}
16
25
 
17
26
  s.test_files = []
@@ -0,0 +1,106 @@
1
+ safe do
2
+ # global path
3
+ # supported substitutions:
4
+ # :kind -> backup 'engine' kind, e.g. "mysqldump" or "archive"
5
+ # :id -> backup 'id', e.g. "blog", "production", etc.
6
+ # you can set separate :path for all backups (or once globally here)
7
+ path "/backup/:kind/:id"
8
+
9
+ ## uncomment to enable uploads to Amazon S3
10
+ ## Amazon S3 auth (optional)
11
+ # s3 do
12
+ # key YOUR_S3_KEY
13
+ # secret YOUR_S3_SECRET
14
+ # bucket S3_BUCKET
15
+ # end
16
+
17
+ ## alternative style:
18
+ # s3 :key => YOUR_S3_KEY, :secret => YOUR_S3_SECRET, :bucket => S3_BUCKET
19
+
20
+ ## uncomment to enable GPG encryption.
21
+ ## Note: you can use public 'key' or symmetric password but not both!
22
+ # gpg do
23
+ # # key "backup@astrails.com"
24
+ # password "astrails"
25
+ # end
26
+
27
+ ## uncomment to enable backup rotation. keep only given number of latest
28
+ ## backups. remove the rest
29
+ # keep do
30
+ # local 4 # keep 4 local backups
31
+ # s3 20 # keep 20 S3 backups
32
+ # end
33
+
34
+ # backup mysql databases with mysqldump
35
+ mysqldump do
36
+ # you can override any setting from parent in a child:
37
+ path "/backup/mysql/:id"
38
+
39
+ options "-ceKq --single-transaction --create-options"
40
+
41
+ user "astrails"
42
+ password ""
43
+ # host "localhost"
44
+ # port 3306
45
+ socket "/var/run/mysqld/mysqld.sock"
46
+
47
+ # database is a 'collection' element. it must have a hash or block parameter
48
+ # it will be 'collected' in a 'databases', with database id (1st arg) used as hash key
49
+ # the following code will create mysqldump/databases/blog and mysqldump/databases/mysql ocnfiguration 'nodes'
50
+
51
+ # backup database with default values
52
+ # database :blog
53
+
54
+ # backup overriding some values
55
+ # database :production do
56
+ # # you can override 'partially'
57
+ # keep :local => 3
58
+ # # keep/local is 3, and keep/s3 is 20 (from parent)
59
+
60
+ # # local override for gpg password
61
+ # gpg do
62
+ # password "custom-production-pass"
63
+ # end
64
+
65
+ # skip_tables [:logger_exceptions, :request_logs] # skip those tables during backup
66
+ # end
67
+
68
+ end
69
+
70
+
71
+ tar do
72
+ path "/backup/archives"
73
+
74
+ # 'archive' is a collection item, just like 'database'
75
+ # archive "git-repositories" do
76
+ # # files and directories to backup
77
+ # files "/home/git/repositories"
78
+ # end
79
+
80
+ # archive "etc-files" do
81
+ # files "/etc"
82
+ # # exlude those files/directories
83
+ # exclude "/etc/puppet/other"
84
+ # end
85
+
86
+ # archive "dot-configs" do
87
+ # files "/home/*/.[^.]*"
88
+ # end
89
+
90
+ # archive "blog" do
91
+ # files "/var/www/blog.astrails.com/"
92
+ # # specify multiple files/directories as array
93
+ # exclude ["/var/www/blog.astrails.com/log", "/var/www/blog.astrails.com/tmp"]
94
+ # end
95
+
96
+ # archive "site" do
97
+ # files "/var/www/astrails.com/"
98
+ # exclude ["/var/www/astrails.com/log", "/var/www/astrails.com/tmp"]
99
+ # end
100
+
101
+ # archive :misc do
102
+ # files [ "/backup/*.rb" ]
103
+ # end
104
+ end
105
+
106
+ end
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.0.7
4
+ version: 0.0.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Astrails Ltd.
@@ -32,6 +32,15 @@ extra_rdoc_files: []
32
32
 
33
33
  files:
34
34
  - bin/astrails-safe
35
+ - lib/astrails/safe.rb
36
+ - lib/astrails/safe/mysqldump.rb
37
+ - lib/astrails/safe/stream.rb
38
+ - lib/astrails/safe/config/builder.rb
39
+ - lib/astrails/safe/config/node.rb
40
+ - lib/astrails/safe/engine.rb
41
+ - lib/astrails/safe/archive.rb
42
+ - lib/astrails/safe/tmp_file.rb
43
+ - templates/script.rb
35
44
  - safe.gemspec
36
45
  has_rdoc: false
37
46
  homepage: http://github.com/astrails/safe