astrails-safe 0.0.7 → 0.0.8

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.
@@ -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