namxam-backup 2.4.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. data/CHANGELOG +131 -0
  2. data/LICENSE +20 -0
  3. data/README.md +122 -0
  4. data/bin/backup +108 -0
  5. data/generators/backup/backup_generator.rb +69 -0
  6. data/generators/backup/templates/backup.rake +56 -0
  7. data/generators/backup/templates/backup.rb +253 -0
  8. data/generators/backup/templates/create_backup_tables.rb +18 -0
  9. data/generators/backup_update/backup_update_generator.rb +50 -0
  10. data/generators/backup_update/templates/migrations/update_backup_tables.rb +27 -0
  11. data/lib/backup.rb +132 -0
  12. data/lib/backup/adapters/archive.rb +34 -0
  13. data/lib/backup/adapters/base.rb +167 -0
  14. data/lib/backup/adapters/custom.rb +41 -0
  15. data/lib/backup/adapters/mongo_db.rb +139 -0
  16. data/lib/backup/adapters/mysql.rb +60 -0
  17. data/lib/backup/adapters/postgresql.rb +56 -0
  18. data/lib/backup/adapters/sqlite.rb +25 -0
  19. data/lib/backup/command_helper.rb +14 -0
  20. data/lib/backup/configuration/adapter.rb +21 -0
  21. data/lib/backup/configuration/adapter_options.rb +8 -0
  22. data/lib/backup/configuration/attributes.rb +19 -0
  23. data/lib/backup/configuration/base.rb +75 -0
  24. data/lib/backup/configuration/helpers.rb +24 -0
  25. data/lib/backup/configuration/mail.rb +20 -0
  26. data/lib/backup/configuration/smtp.rb +8 -0
  27. data/lib/backup/configuration/storage.rb +8 -0
  28. data/lib/backup/connection/cloudfiles.rb +75 -0
  29. data/lib/backup/connection/dropbox.rb +62 -0
  30. data/lib/backup/connection/s3.rb +88 -0
  31. data/lib/backup/core_ext/object.rb +5 -0
  32. data/lib/backup/environment/base.rb +12 -0
  33. data/lib/backup/environment/rails_configuration.rb +15 -0
  34. data/lib/backup/environment/unix_configuration.rb +109 -0
  35. data/lib/backup/mail/base.rb +97 -0
  36. data/lib/backup/mail/mail.txt +7 -0
  37. data/lib/backup/record/base.rb +65 -0
  38. data/lib/backup/record/cloudfiles.rb +28 -0
  39. data/lib/backup/record/dropbox.rb +27 -0
  40. data/lib/backup/record/ftp.rb +39 -0
  41. data/lib/backup/record/local.rb +26 -0
  42. data/lib/backup/record/s3.rb +25 -0
  43. data/lib/backup/record/scp.rb +33 -0
  44. data/lib/backup/record/sftp.rb +38 -0
  45. data/lib/backup/storage/base.rb +10 -0
  46. data/lib/backup/storage/cloudfiles.rb +16 -0
  47. data/lib/backup/storage/dropbox.rb +12 -0
  48. data/lib/backup/storage/ftp.rb +38 -0
  49. data/lib/backup/storage/local.rb +22 -0
  50. data/lib/backup/storage/s3.rb +15 -0
  51. data/lib/backup/storage/scp.rb +30 -0
  52. data/lib/backup/storage/sftp.rb +31 -0
  53. data/lib/backup/version.rb +3 -0
  54. data/lib/generators/backup/USAGE +10 -0
  55. data/lib/generators/backup/backup_generator.rb +47 -0
  56. data/lib/generators/backup/templates/backup.rake +56 -0
  57. data/lib/generators/backup/templates/backup.rb +236 -0
  58. data/lib/generators/backup/templates/create_backup_tables.rb +18 -0
  59. data/setup/backup.rb +255 -0
  60. data/setup/backup.sqlite3 +0 -0
  61. metadata +278 -0
@@ -0,0 +1,34 @@
1
+ module Backup
2
+ module Adapters
3
+ class Archive < Backup::Adapters::Base
4
+
5
+ attr_accessor :files, :exclude
6
+
7
+ private
8
+
9
+ # Archives and Compresses all files
10
+ def perform
11
+ log system_messages[:archiving]; log system_messages[:compressing]
12
+ run "tar -czf #{File.join(tmp_path, compressed_file)} #{exclude_files} #{tar_files}"
13
+ end
14
+
15
+ def load_settings
16
+ self.files = procedure.get_adapter_configuration.attributes['files']
17
+ self.exclude = procedure.get_adapter_configuration.attributes['exclude']
18
+ end
19
+
20
+ def performed_file_extension
21
+ ".tar"
22
+ end
23
+
24
+ def tar_files
25
+ [*files].map{|f| f.gsub(' ', '\ ')}.join(' ')
26
+ end
27
+
28
+ def exclude_files
29
+ [*exclude].compact.map{|x| "--exclude=#{x}"}.join(' ')
30
+ end
31
+
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,167 @@
1
+ require 'tempfile'
2
+
3
+ module Backup
4
+ module Adapters
5
+ class Base
6
+
7
+ include Backup::CommandHelper
8
+
9
+ attr_accessor :procedure, :timestamp, :options, :tmp_path, :encrypt_with_password, :encrypt_with_gpg_public_key, :keep_backups, :trigger
10
+
11
+ # IMPORTANT
12
+ # final_file must have the value of the final filename result
13
+ # so if a file gets compressed, then the file could look like this:
14
+ # myfile.gz
15
+ #
16
+ # and if a file afterwards gets encrypted, the file will look like:
17
+ # myfile.gz.enc (with a password)
18
+ # myfile.gz.gpg (with a gpg public key)
19
+ #
20
+ #
21
+ # It is important that, whatever the final filename of the file will be, that :final_file will contain it.
22
+ attr_accessor :performed_file, :compressed_file, :encrypted_file, :final_file
23
+
24
+ # Initializes the Backup Process
25
+ #
26
+ # This will first load in any prefixed settings from the Backup::Adapters::Base
27
+ # Then it will add it's own settings.
28
+ #
29
+ # First it will call the 'perform' method. This method is concerned with the backup, and must
30
+ # be implemented by derived classes!
31
+ # Then it will optionally encrypt the backed up file
32
+ # Then it will store it to the specified storage location
33
+ # Then it will record the data to the database
34
+ # Once this is all done, all the temporary files will be removed
35
+ #
36
+ # Wrapped inside of begin/ensure/end block to ensure the deletion of any files in the tmp directory
37
+ def initialize(trigger, procedure)
38
+ self.trigger = trigger
39
+ self.procedure = procedure
40
+ self.timestamp = Time.now.strftime("%Y%m%d%H%M%S")
41
+ self.tmp_path = File.join(BACKUP_PATH.gsub(' ', '\ '), 'tmp', 'backup', trigger)
42
+ self.encrypt_with_password = procedure.attributes['encrypt_with_password']
43
+ self.encrypt_with_gpg_public_key = procedure.attributes['encrypt_with_gpg_public_key']
44
+ self.keep_backups = procedure.attributes['keep_backups']
45
+
46
+ self.performed_file = "#{timestamp}.#{trigger.gsub(' ', '-')}#{performed_file_extension}"
47
+ self.compressed_file = "#{performed_file}.gz"
48
+ self.final_file = compressed_file
49
+
50
+ begin
51
+ create_tmp_folder
52
+ load_settings # if respond_to?(:load_settings)
53
+ handle_before_backup
54
+ perform
55
+ encrypt
56
+ store
57
+ handle_after_backup
58
+ record
59
+ notify
60
+ ensure
61
+ remove_tmp_files
62
+ end
63
+ end
64
+
65
+ # Creates the temporary folder for the specified adapter
66
+ def create_tmp_folder
67
+ #need to create with universal privlages as some backup tasks might create this path under sudo
68
+ run "mkdir -m 0777 -p #{tmp_path.sub(/\/[^\/]+$/, '')}" #this is the parent to the tmp_path
69
+ run "mkdir -m 0777 -p #{tmp_path}" #the temp path dir
70
+ end
71
+
72
+ # TODO make methods in derived classes public? respond_to cannot identify private methods
73
+ def load_settings
74
+ end
75
+
76
+ def skip_backup(msg)
77
+ log "Terminating backup early because: #{msg}"
78
+ exit 1
79
+ end
80
+
81
+ # Removes the files inside the temporary folder
82
+ def remove_tmp_files
83
+ run "rm -r #{File.join(tmp_path)}" if File.exists?(tmp_path) #just in case there isn't one because the process was skipped
84
+ end
85
+
86
+ def handle_before_backup
87
+ return unless self.procedure.before_backup_block
88
+ log system_messages[:before_backup_hook]
89
+ #run it through this instance so the block is run as a part of this adapter...which means it has access to all sorts of sutff
90
+ self.instance_eval &self.procedure.before_backup_block
91
+ end
92
+
93
+ def handle_after_backup
94
+ return unless self.procedure.after_backup_block
95
+ log system_messages[:after_backup_hook]
96
+ #run it through this instance so the block is run as a part of this adapter...which means it has access to all sorts of sutff
97
+ self.instance_eval &self.procedure.after_backup_block
98
+ end
99
+
100
+ # Encrypts the archive file
101
+ def encrypt
102
+ if encrypt_with_gpg_public_key.is_a?(String) && encrypt_with_password.is_a?(String)
103
+ puts "both 'encrypt_with_gpg_public_key' and 'encrypt_with_password' are set. Please choose one or the other. Exiting."
104
+ exit 1
105
+ end
106
+
107
+ if encrypt_with_gpg_public_key.is_a?(String)
108
+ if `which gpg` == ''
109
+ puts "Encrypting with a GPG public key requires that gpg be in your public path. gpg was not found. Exiting"
110
+ exit 1
111
+ end
112
+ log system_messages[:encrypting_w_key]
113
+ self.encrypted_file = "#{self.final_file}.gpg"
114
+
115
+ # tmp_file = Tempfile.new('backup.pub'){ |tmp_file| tmp_file << encrypt_with_gpg_public_key }
116
+ tmp_file = Tempfile.new('backup.pub')
117
+ tmp_file << encrypt_with_gpg_public_key
118
+ tmp_file.close
119
+ # that will either say the key was added OR that it wasn't needed, but either way we need to parse for the uid
120
+ # which will be wrapped in '<' and '>' like <someone_famous@me.com>
121
+ encryptionKeyId = `gpg --import #{tmp_file.path} 2>&1`.match(/<(.+)>/)[1]
122
+ run "gpg -e --trust-model always -o #{File.join(tmp_path, encrypted_file)} -r '#{encryptionKeyId}' #{File.join(tmp_path, compressed_file)}"
123
+ elsif encrypt_with_password.is_a?(String)
124
+ log system_messages[:encrypting_w_pass]
125
+ self.encrypted_file = "#{self.final_file}.enc"
126
+ run "openssl enc -des-cbc -in #{File.join(tmp_path, compressed_file)} -out #{File.join(tmp_path, encrypted_file)} -k #{encrypt_with_password}"
127
+ end
128
+ self.final_file = encrypted_file if encrypted_file
129
+ end
130
+
131
+ # Initializes the storing process
132
+ def store
133
+ procedure.initialize_storage(self)
134
+ end
135
+
136
+ # Records data on every individual file to the database
137
+ def record
138
+ record = procedure.initialize_record
139
+ record.load_adapter(self)
140
+ record.save
141
+ end
142
+
143
+ # Delivers a notification by email regarding the successfully stored backup
144
+ def notify
145
+ if Backup::Mail::Base.setup?
146
+ Backup::Mail::Base.notify!(self)
147
+ end
148
+ end
149
+
150
+ def system_messages
151
+ { :compressing => "Compressing backup..",
152
+ :archiving => "Archiving backup..",
153
+ :encrypting_w_pass => "Encrypting backup with password..",
154
+ :encrypting_w_key => "Encrypting backup with gpg public key..",
155
+ :mysqldump => "Creating MySQL dump..",
156
+ :mongo_dump => "Creating MongoDB dump..",
157
+ :mongo_copy => "Creating MongoDB disk level copy..",
158
+ :before_backup_hook => "Running before backup hook..",
159
+ :after_backup_hook => "Running after backup hook..",
160
+ :pgdump => "Creating PostgreSQL dump..",
161
+ :sqlite => "Copying and compressing SQLite database..",
162
+ :commands => "Executing commands.." }
163
+ end
164
+
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,41 @@
1
+ module Backup
2
+ module Adapters
3
+ class Custom < Backup::Adapters::Base
4
+
5
+ attr_accessor :commands
6
+
7
+ private
8
+
9
+ # Execute any given commands, then archive and compress every folder/file
10
+ def perform
11
+ execute_commands
12
+ targz
13
+ end
14
+
15
+ # Executes the commands
16
+ def execute_commands
17
+ return unless commands
18
+ log system_messages[:commands]
19
+ [*commands].each do |command|
20
+ run "#{command.gsub(':tmp_path', tmp_path)}"
21
+ end
22
+ end
23
+
24
+ # Archives and Compresses
25
+ def targz
26
+ log system_messages[:archiving]; log system_messages[:compressing]
27
+ run "tar -czf #{File.join(tmp_path, compressed_file)} #{File.join(tmp_path, '*')}"
28
+ end
29
+
30
+ def performed_file_extension
31
+ ".tar"
32
+ end
33
+
34
+ # Loads the initial settings
35
+ def load_settings
36
+ self.commands = procedure.get_adapter_configuration.attributes['commands']
37
+ end
38
+
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,139 @@
1
+ module Backup
2
+ module Adapters
3
+ class MongoDB < Backup::Adapters::Base
4
+ require 'json'
5
+
6
+ attr_accessor :user, :password, :database, :collections, :host, :port, :additional_options, :backup_method
7
+
8
+ private
9
+
10
+ BACKUP_METHOD_OPTIONS = [:mongodump, :disk_copy]
11
+
12
+ # Dumps and Compresses the Mongodump file
13
+ def perform
14
+ tmp_mongo_dir = "mongodump-#{Time.now.strftime("%Y%m%d%H%M%S")}"
15
+ tmp_dump_dir = File.join(tmp_path, tmp_mongo_dir)
16
+
17
+ case self.backup_method.to_sym
18
+ when :mongodump
19
+ #this is the default options
20
+ # PROS:
21
+ # * non-locking
22
+ # * much smaller archive sizes
23
+ # * can specifically target different databases or collections to dump
24
+ # * de-fragements the datastore
25
+ # * don't need to run under sudo
26
+ # * simple logic
27
+ # CONS:
28
+ # * a bit longer to restore as you have to do an import
29
+ # * does not include indexes or other meta data
30
+ log system_messages[:mongo_dump]
31
+ exit 1 unless run "#{mongodump} #{mongodump_options} #{collections_to_include} -o #{tmp_dump_dir} #{additional_options} > /dev/null 2>&1"
32
+ when :disk_copy
33
+ #this is a bit more complicated AND potentially a lot riskier:
34
+ # PROS:
35
+ # * byte level copy, so it includes all the indexes, meta data, etc
36
+ # * fast recovery; you just copy the files into place and startup mongo
37
+ # CONS:
38
+ # * locks the database, so ONLY use against a slave instance
39
+ # * copies everything; cannot specify a collection or a database
40
+ # * will probably need to run under sudo as the mongodb db_path file is probably under a different owner.
41
+ # If you do run under sudo, you will probably need to run rake RAILS_ENV=... if you aren't already
42
+ # * the logic is a bit brittle...
43
+ log system_messages[:mongo_copy]
44
+
45
+ cmd = "#{mongo} #{mongo_disk_copy_options} --quiet --eval 'printjson(db.isMaster());' admin"
46
+ output = JSON.parse(run(cmd, :exit_on_failure => true))
47
+ if output['ismaster']
48
+ puts "You cannot run in disk_copy mode against a master instance. This mode will lock the database. Please use :mongodump instead."
49
+ exit 1
50
+ end
51
+
52
+ begin
53
+ cmd = "#{mongo} #{mongo_disk_copy_options} --quiet --eval 'db.runCommand({fsync : 1, lock : 1}); printjson(db.runCommand({getCmdLineOpts:1}));' admin"
54
+ output = JSON.parse(run(cmd, :exit_on_failure => true))
55
+
56
+ #lets go find the dbpath. it is either going to be in the argv just returned OR we are going to have to parse through the mongo config file
57
+ cmd = "mongo --quiet --eval 'printjson(db.runCommand({getCmdLineOpts:1}));' admin"
58
+ output = JSON.parse(run(cmd, :exit_on_failure => true))
59
+ #see if --dbpath was passed in
60
+ db_path = output['argv'][output['argv'].index('--dbpath') + 1] if output['argv'].index('--dbpath')
61
+ #see if --config is passed in, and if so, lets parse it
62
+ db_path ||= $1 if output['argv'].index('--config') && File.read(output['argv'][output['argv'].index('--config') + 1]) =~ /dbpath\s*=\s*([^\s]*)/
63
+ db_path ||= "/data/db/" #mongo's default path
64
+ run "cp -rp #{db_path} #{tmp_dump_dir}"
65
+ ensure
66
+ #attempting to unlock
67
+ cmd = "#{mongo} #{mongo_disk_copy_options} --quiet --eval 'printjson(db.currentOp());' admin"
68
+ output = JSON.parse(run(cmd, :exit_on_failure => true))
69
+ (output['fsyncLock'] || 1).to_i.times do
70
+ run "#{mongo} #{mongo_disk_copy_options} --quiet --eval 'db.$cmd.sys.unlock.findOne();' admin"
71
+ end
72
+ end
73
+ else
74
+ puts "you did not enter a valid backup_method option. Your choices are: #{BACKUP_METHOD_OPTIONS.join(', ')}"
75
+ exit 1
76
+ end
77
+
78
+ log system_messages[:compressing]
79
+ run "tar -cz -C #{tmp_path} -f #{File.join(tmp_path, compressed_file)} #{tmp_mongo_dir}"
80
+ end
81
+
82
+ def mongodump
83
+ cmd = run("which mongodump").chomp
84
+ cmd = 'mongodump' if cmd.empty?
85
+ cmd
86
+ end
87
+
88
+ def mongo
89
+ cmd = run("which mongo").chomp
90
+ cmd = 'mongo' if cmd.empty?
91
+ cmd
92
+ end
93
+
94
+ def performed_file_extension
95
+ ".tar"
96
+ end
97
+
98
+ # Loads the initial settings
99
+ def load_settings
100
+ %w(user password database collections additional_options backup_method).each do |attribute|
101
+ send(:"#{attribute}=", procedure.get_adapter_configuration.attributes[attribute])
102
+ end
103
+
104
+ %w(host port).each do |attribute|
105
+ send(:"#{attribute}=", procedure.get_adapter_configuration.get_options.attributes[attribute])
106
+ end
107
+
108
+ self.backup_method ||= :mongodump
109
+ end
110
+
111
+ # Returns a list of options in Mongodump syntax
112
+ def mongodump_options
113
+ options = String.new
114
+ options += " --username='#{user}' " unless user.blank?
115
+ options += " --password='#{password}' " unless password.blank?
116
+ options += " --host='#{host}' " unless host.blank?
117
+ options += " --port='#{port}' " unless port.blank?
118
+ options += " --db='#{database}' " unless database.blank?
119
+ options
120
+ end
121
+
122
+ def mongo_disk_copy_options
123
+ options = String.new
124
+ options += " --username='#{user}' " unless user.blank?
125
+ options += " --password='#{password}' " unless password.blank?
126
+ options += " --host='#{host}' " unless host.blank?
127
+ options += " --port='#{port}' " unless port.blank?
128
+ options
129
+ end
130
+
131
+ # Returns a list of collections to include in Mongodump syntax
132
+ def collections_to_include
133
+ return "" unless collections
134
+ "--collection #{[*collections].join(" ")}"
135
+ end
136
+
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,60 @@
1
+ module Backup
2
+ module Adapters
3
+ class MySQL < Backup::Adapters::Base
4
+
5
+ attr_accessor :user, :password, :database, :skip_tables, :host, :port, :socket, :additional_options, :tables
6
+
7
+ private
8
+
9
+ # Dumps and Compresses the MySQL file
10
+ def perform
11
+ log system_messages[:mysqldump]; log system_messages[:compressing]
12
+ run "#{mysqldump} -u #{user} --password='#{password}' #{options} #{additional_options} #{database} #{tables_to_include} #{tables_to_skip} | gzip -f --best > #{File.join(tmp_path, compressed_file)}"
13
+ end
14
+
15
+ def mysqldump
16
+ # try to determine the full path, and fall back to myqsldump if not found
17
+ cmd = `which mysqldump`.chomp
18
+ cmd = 'mysqldump' if cmd.empty?
19
+ cmd
20
+ end
21
+
22
+ def performed_file_extension
23
+ ".sql"
24
+ end
25
+
26
+ # Loads the initial settings
27
+ def load_settings
28
+ %w(user password database tables skip_tables additional_options).each do |attribute|
29
+ send(:"#{attribute}=", procedure.get_adapter_configuration.attributes[attribute])
30
+ end
31
+
32
+ %w(host port socket).each do |attribute|
33
+ send(:"#{attribute}=", procedure.get_adapter_configuration.get_options.attributes[attribute])
34
+ end
35
+ end
36
+
37
+ # Returns a list of options in MySQL syntax
38
+ def options
39
+ options = String.new
40
+ options += " --host='#{host}' " unless host.blank?
41
+ options += " --port='#{port}' " unless port.blank?
42
+ options += " --socket='#{socket}' " unless socket.blank?
43
+ options
44
+ end
45
+
46
+ # Returns a list of tables to skip in MySQL syntax
47
+ def tables_to_skip
48
+ return "" unless skip_tables
49
+ [*skip_tables].map {|table| " --ignore-table='#{database}.#{table}' "}
50
+ end
51
+
52
+ # Returns a list of tables to include in MySQL syntax
53
+ def tables_to_include
54
+ return "" unless tables
55
+ [*tables].join(" ")
56
+ end
57
+
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,56 @@
1
+ module Backup
2
+ module Adapters
3
+ class PostgreSQL < Backup::Adapters::Base
4
+
5
+ attr_accessor :user, :password, :database, :skip_tables, :host, :port, :socket, :additional_options
6
+
7
+ private
8
+
9
+ # Dumps and Compresses the PostgreSQL file
10
+ def perform
11
+ log system_messages[:pgdump]; log system_messages[:compressing]
12
+ ENV['PGPASSWORD'] = password
13
+ run "#{pg_dump} -U #{user} #{options} #{additional_options} #{tables_to_skip} #{database} | gzip -f --best > #{File.join(tmp_path, compressed_file)}"
14
+ ENV['PGPASSWORD'] = nil
15
+ end
16
+
17
+ def pg_dump
18
+ # try to determine the full path, and fall back to pg_dump if not found
19
+ cmd = `which pg_dump`.chomp
20
+ cmd = 'pg_dump' if cmd.empty?
21
+ cmd
22
+ end
23
+
24
+ def performed_file_extension
25
+ ".sql"
26
+ end
27
+
28
+ # Loads the initial settings
29
+ def load_settings
30
+ %w(user password database skip_tables additional_options).each do |attribute|
31
+ send(:"#{attribute}=", procedure.get_adapter_configuration.attributes[attribute])
32
+ end
33
+
34
+ %w(host port socket).each do |attribute|
35
+ send(:"#{attribute}=", procedure.get_adapter_configuration.get_options.attributes[attribute])
36
+ end
37
+ end
38
+
39
+ # Returns a list of options in PostgreSQL syntax
40
+ def options
41
+ options = String.new
42
+ options += " --port='#{port}' " unless port.blank?
43
+ options += " --host='#{host}' " unless host.blank?
44
+ options += " --host='#{socket}' " unless socket.blank? unless options.include?('--host=')
45
+ options
46
+ end
47
+
48
+ # Returns a list of tables to skip in PostgreSQL syntax
49
+ def tables_to_skip
50
+ return "" unless skip_tables
51
+ [*skip_tables].map {|table| " -T \"#{table}\" "}
52
+ end
53
+
54
+ end
55
+ end
56
+ end