bostonlogic-safe 0.3.0

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.
Files changed (41) hide show
  1. data/LICENSE +20 -0
  2. data/README.markdown +227 -0
  3. data/Rakefile +54 -0
  4. data/VERSION.yml +4 -0
  5. data/bin/astrails-safe +53 -0
  6. data/examples/example_helper.rb +19 -0
  7. data/examples/integration/archive_integration_example.rb +86 -0
  8. data/examples/integration/cleanup_example.rb +62 -0
  9. data/examples/unit/archive_example.rb +67 -0
  10. data/examples/unit/config_example.rb +184 -0
  11. data/examples/unit/gpg_example.rb +138 -0
  12. data/examples/unit/gzip_example.rb +64 -0
  13. data/examples/unit/local_example.rb +110 -0
  14. data/examples/unit/mysqldump_example.rb +83 -0
  15. data/examples/unit/pgdump_example.rb +45 -0
  16. data/examples/unit/rcloud_example.rb +110 -0
  17. data/examples/unit/s3_example.rb +112 -0
  18. data/examples/unit/svndump_example.rb +39 -0
  19. data/lib/astrails/safe.rb +71 -0
  20. data/lib/astrails/safe/archive.rb +24 -0
  21. data/lib/astrails/safe/backup.rb +20 -0
  22. data/lib/astrails/safe/config/builder.rb +62 -0
  23. data/lib/astrails/safe/config/node.rb +66 -0
  24. data/lib/astrails/safe/gpg.rb +45 -0
  25. data/lib/astrails/safe/gzip.rb +25 -0
  26. data/lib/astrails/safe/local.rb +48 -0
  27. data/lib/astrails/safe/mysqldump.rb +31 -0
  28. data/lib/astrails/safe/notification.rb +66 -0
  29. data/lib/astrails/safe/pgdump.rb +36 -0
  30. data/lib/astrails/safe/pipe.rb +13 -0
  31. data/lib/astrails/safe/rcloud.rb +73 -0
  32. data/lib/astrails/safe/s3.rb +68 -0
  33. data/lib/astrails/safe/sftp.rb +79 -0
  34. data/lib/astrails/safe/sink.rb +33 -0
  35. data/lib/astrails/safe/source.rb +46 -0
  36. data/lib/astrails/safe/stream.rb +19 -0
  37. data/lib/astrails/safe/svndump.rb +13 -0
  38. data/lib/astrails/safe/tmp_file.rb +48 -0
  39. data/lib/extensions/mktmpdir.rb +45 -0
  40. data/templates/script.rb +155 -0
  41. metadata +135 -0
@@ -0,0 +1,36 @@
1
+ module Astrails
2
+ module Safe
3
+ class Pgdump < Source
4
+
5
+ def command
6
+ if @config["password"]
7
+ ENV['PGPASSWORD'] = @config["password"]
8
+ else
9
+ ENV['PGPASSWORD'] = nil
10
+ end
11
+ "pg_dump #{postgres_options} #{postgres_username} #{postgres_host} #{postgres_port} #{@id}"
12
+ end
13
+
14
+ def extension; '.sql'; end
15
+
16
+ protected
17
+
18
+ def postgres_options
19
+ @config[:options]
20
+ end
21
+
22
+ def postgres_host
23
+ @config["host"] && "--host='#{@config["host"]}'"
24
+ end
25
+
26
+ def postgres_port
27
+ @config["port"] && "--port='#{@config["port"]}'"
28
+ end
29
+
30
+ def postgres_username
31
+ @config["user"] && "--username='#{@config["user"]}'"
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,13 @@
1
+ module Astrails
2
+ module Safe
3
+ class Pipe < Stream
4
+ def process
5
+ return unless active?
6
+
7
+ @backup.command << pipe
8
+ @backup.extension << extension
9
+ post_process
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,73 @@
1
+ module Astrails
2
+ module Safe
3
+ class Rcloud < Sink
4
+
5
+ protected
6
+
7
+ def active?
8
+ container && username && api_key
9
+ end
10
+
11
+ def path
12
+ @path ||= expand(config[:rcloud, :path] || config[:local, :path] || ":kind/:id")
13
+ end
14
+
15
+ def save
16
+ raise RuntimeError, "pipe-streaming not supported for Rackspace Cloudfiles." unless @backup.path
17
+
18
+ # needed in cleanup even on dry run
19
+ rcf = CloudFiles::Connection.new(username, api_key)
20
+
21
+ puts "Uploading #{container}:#{full_path}" if $_VERBOSE || $DRY_RUN
22
+ unless $DRY_RUN || $LOCAL
23
+ benchmark = Benchmark.realtime do
24
+ if rcf.container_exists?(container)
25
+ rcont = rcf.container(container)
26
+ else
27
+ rcont = rcf.create_container(container)
28
+ end
29
+ rfile = rcont.create_object(full_path)
30
+ rfile.load_from_filename(@backup.path)
31
+ end
32
+ puts "...done" if $_VERBOSE
33
+ puts("Upload took " + sprintf("%.2f", benchmark) + " second(s).") if $_VERBOSE
34
+ end
35
+ end
36
+
37
+ def cleanup
38
+ return if $LOCAL
39
+
40
+ return unless keep = @config[:keep, :rcloud]
41
+
42
+ rcf = CloudFiles::Connection.new(username, api_key)
43
+ rcont = rcf.container(container)
44
+
45
+ puts "listing files: #{container}:#{base}*" if $_VERBOSE
46
+ rfiles = rcont.objects_detail(:prefix => base)
47
+ puts rfiles.collect {|x| x[0]} if $_VERBOSE
48
+
49
+ files = rfiles.
50
+ collect {|x| x[0]}.
51
+ sort
52
+
53
+ cleanup_with_limit(files, keep) do |f|
54
+ puts "removing Rackspace Cloudfile #{container}:#{f}" if $DRY_RUN || $_VERBOSE
55
+ rcont.delete_object(f) unless $DRY_RUN || $LOCAL
56
+ end
57
+ end
58
+
59
+ def container
60
+ @config[:rcloud, :container]
61
+ end
62
+
63
+ def username
64
+ @config[:rcloud, :username]
65
+ end
66
+
67
+ def api_key
68
+ @config[:rcloud, :api_key]
69
+ end
70
+
71
+ end
72
+ end
73
+ 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 path
12
+ @path ||= expand(config[:s3, :path] || config[:local, :path] || ":kind/:id")
13
+ end
14
+
15
+ def save
16
+ raise RuntimeError, "pipe-streaming not supported for S3." unless @backup.path
17
+
18
+ # needed in cleanup even on dry run
19
+ AWS::S3::Base.establish_connection!(:access_key_id => key, :secret_access_key => secret, :use_ssl => true) unless $LOCAL
20
+
21
+ puts "Uploading #{bucket}:#{full_path}" if $_VERBOSE || $DRY_RUN
22
+ unless $DRY_RUN || $LOCAL
23
+ benchmark = Benchmark.realtime do
24
+ AWS::S3::Bucket.create(bucket)
25
+ File.open(@backup.path) do |file|
26
+ AWS::S3::S3Object.store(full_path, file, bucket)
27
+ end
28
+ end
29
+ puts "...done" if $_VERBOSE
30
+ puts("Upload took " + sprintf("%.2f", benchmark) + " second(s).") if $_VERBOSE
31
+ end
32
+ end
33
+
34
+ def cleanup
35
+ return if $LOCAL
36
+
37
+ return unless keep = @config[:keep, :s3]
38
+
39
+
40
+ puts "listing files: #{bucket}:#{base}*" if $_VERBOSE
41
+ files = AWS::S3::Bucket.objects(bucket, :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,79 @@
1
+ module Astrails
2
+ module Safe
3
+ class Sftp < Sink
4
+
5
+ protected
6
+
7
+ def active?
8
+ host && user
9
+ end
10
+
11
+ def path
12
+ @path ||= expand(config[:sftp, :path] || config[:local, :path] || ":kind/:id")
13
+ end
14
+
15
+ def save
16
+ raise RuntimeError, "pipe-streaming not supported for SFTP." unless @backup.path
17
+
18
+ puts "Uploading #{host}:#{full_path} via SFTP" if $_VERBOSE || $DRY_RUN
19
+
20
+ unless $DRY_RUN || $LOCAL
21
+ opts = {}
22
+ opts[:password] = password if password
23
+ Net::SFTP.start(host, user, opts) do |sftp|
24
+ puts "Sending #{@backup.path} to #{full_path}" if $_VERBOSE
25
+ begin
26
+ sftp.upload! @backup.path, full_path
27
+ rescue Net::SFTP::StatusException
28
+ puts "Ensuring remote path (#{path}) exists" if $_VERBOSE
29
+ # mkdir -p
30
+ folders = path.split('/')
31
+ folders.each_index do |i|
32
+ folder = folders[0..i].join('/')
33
+ puts "Creating #{folder} on remote" if $_VERBOSE
34
+ sftp.mkdir!(folder) rescue Net::SFTP::StatusException
35
+ end
36
+ retry
37
+ end
38
+ end
39
+ puts "...done" if $_VERBOSE
40
+ end
41
+ end
42
+
43
+ def cleanup
44
+ return if $LOCAL || $DRY_RUN
45
+
46
+ return unless keep = @config[:keep, :sftp]
47
+
48
+ puts "listing files: #{host}:#{base}*" if $_VERBOSE
49
+ Net::SFTP.start(host, user, :password => password) do |sftp|
50
+ files = sftp.dir.glob(path, File.basename("#{base}*"))
51
+
52
+ puts files.collect {|x| x.name } if $_VERBOSE
53
+
54
+ files = files.
55
+ collect {|x| x.name }.
56
+ sort
57
+
58
+ cleanup_with_limit(files, keep) do |f|
59
+ file = File.join(path, f)
60
+ puts "removing sftp file #{host}:#{file}" if $DRY_RUN || $_VERBOSE
61
+ sftp.remove!(file) unless $DRY_RUN || $LOCAL
62
+ end
63
+ end
64
+ end
65
+
66
+ def host
67
+ @config[:sftp, :host]
68
+ end
69
+
70
+ def user
71
+ @config[:sftp, :user]
72
+ end
73
+
74
+ def password
75
+ @config[:sftp, :password]
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,33 @@
1
+ module Astrails
2
+ module Safe
3
+ class Sink < Stream
4
+
5
+ def process
6
+ return unless active?
7
+
8
+ save
9
+ cleanup
10
+ end
11
+
12
+ protected
13
+
14
+ # path is defined in subclass
15
+ def base
16
+ @base ||= File.join(path, File.basename(@backup.filename).split(".").first + '.')
17
+ end
18
+
19
+ def full_path
20
+ @full_path ||= File.join(path, @backup.filename) + @backup.extension
21
+ end
22
+
23
+ # call block on files to be removed (all except for the LAST 'limit' files
24
+ def cleanup_with_limit(files, limit, &block)
25
+ return unless files.size > limit
26
+
27
+ to_remove = files[0..(files.size - limit - 1)]
28
+ # TODO: validate here
29
+ to_remove.each(&block)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,46 @@
1
+ module Astrails
2
+ module Safe
3
+ class Source < Stream
4
+
5
+ attr_accessor :id
6
+ def initialize(id, config)
7
+ @id, @config = id.to_s, config
8
+ end
9
+
10
+ def timestamp
11
+ Time.now.strftime("%y%m%d-%H%M")
12
+ end
13
+
14
+ def kind
15
+ self.class.human_name
16
+ end
17
+
18
+ def filename
19
+ @filename ||= expand(":kind-:id.:timestamp")
20
+ end
21
+
22
+ def backup
23
+ return @backup if @backup
24
+ @backup = Backup.new(
25
+ :id => @id,
26
+ :kind => kind,
27
+ :extension => extension,
28
+ :command => command,
29
+ :timestamp => timestamp
30
+ )
31
+ # can't do this in the initializer hash above since
32
+ # filename() calls expand() which requires @backup
33
+ @backup.filename = filename
34
+ @backup
35
+ end
36
+
37
+ protected
38
+
39
+ def self.human_name
40
+ name.split('::').last.downcase
41
+ end
42
+
43
+ end
44
+ end
45
+ end
46
+
@@ -0,0 +1,19 @@
1
+ module Astrails
2
+ module Safe
3
+ class Stream
4
+
5
+ attr_accessor :config, :backup
6
+ def initialize(config, backup)
7
+ @config, @backup = config, backup
8
+ end
9
+
10
+ def expand(path)
11
+ path .
12
+ gsub(/:kind\b/, @backup.kind.to_s) .
13
+ gsub(/:id\b/, @backup.id.to_s) .
14
+ gsub(/:timestamp\b/, @backup.timestamp)
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,13 @@
1
+ module Astrails
2
+ module Safe
3
+ class Svndump < Source
4
+
5
+ def command
6
+ "svnadmin dump #{@config[:options]} #{@config[:repo_path]}"
7
+ end
8
+
9
+ def extension; '.svn'; end
10
+
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,48 @@
1
+ require 'tmpdir'
2
+ module Astrails
3
+ module Safe
4
+ module TmpFile
5
+ @keep_files = []
6
+
7
+ def self.tmproot
8
+ @tmproot ||= Dir.mktmpdir
9
+ end
10
+
11
+ def self.cleanup
12
+ begin
13
+ FileUtils.remove_entry_secure tmproot
14
+ rescue ArgumentError => e
15
+ if e.message =~ /parent directory is world writable/
16
+ puts <<-ERR
17
+
18
+
19
+ ********************************************************************************
20
+ It looks like you have wrong permissions on your TEMP directory. The usual
21
+ case is when you have world writable TEMP directory withOUT the sticky bit.
22
+
23
+ Try "chmod +t" on it.
24
+
25
+ ********************************************************************************
26
+
27
+ ERR
28
+ else
29
+ raise
30
+ end
31
+ end
32
+ @tmproot = nil
33
+ end
34
+
35
+ def self.create(name)
36
+ # create temp directory
37
+
38
+ file = Tempfile.new(name, tmproot)
39
+
40
+ yield file
41
+
42
+ file.close
43
+ @keep_files << file # so that it will not get gcollected and removed from filesystem until the end
44
+ file.path
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,45 @@
1
+ require 'tmpdir'
2
+
3
+ unless Dir.respond_to?(:mktmpdir)
4
+ # backward compat for 1.8.6
5
+ class Dir
6
+ def Dir.mktmpdir(prefix_suffix=nil, tmpdir=nil)
7
+ case prefix_suffix
8
+ when nil
9
+ prefix = "d"
10
+ suffix = ""
11
+ when String
12
+ prefix = prefix_suffix
13
+ suffix = ""
14
+ when Array
15
+ prefix = prefix_suffix[0]
16
+ suffix = prefix_suffix[1]
17
+ else
18
+ raise ArgumentError, "unexpected prefix_suffix: #{prefix_suffix.inspect}"
19
+ end
20
+ tmpdir ||= Dir.tmpdir
21
+ t = Time.now.strftime("%Y%m%d")
22
+ n = nil
23
+ begin
24
+ path = "#{tmpdir}/#{prefix}#{t}-#{$$}-#{rand(0x100000000).to_s(36)}"
25
+ path << "-#{n}" if n
26
+ path << suffix
27
+ Dir.mkdir(path, 0700)
28
+ rescue Errno::EEXIST
29
+ n ||= 0
30
+ n += 1
31
+ retry
32
+ end
33
+
34
+ if block_given?
35
+ begin
36
+ yield path
37
+ ensure
38
+ FileUtils.remove_entry_secure path
39
+ end
40
+ else
41
+ path
42
+ end
43
+ end
44
+ end
45
+ end