namxam-backup 2.4.5

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 (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