snapback 0.0.1 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,155 @@
1
+ desc 'Take a database snapshot'
2
+ arg_name 'database [, ...]'
3
+ command :snapshot do |c|
4
+
5
+ c.desc 'Disk size of changes expected'
6
+ c.flag [:s, :size]
7
+
8
+ c.action do |global_options,options,args|
9
+ help_now!('parameter -s for size is required') if options[:s].nil?
10
+ help_now!('database(s) required') if args.empty?
11
+
12
+ # Load the configuration
13
+ config = Snapback::ConfigurationLoader.factory global_options[:config]
14
+
15
+ # Connect to MySQL
16
+ mysql_client = config.mysql_client
17
+
18
+ args.each do |database|
19
+
20
+ # Start the transaction
21
+ Snapback::Transaction.new do
22
+
23
+ run_command "Selecting database: #{database}" do
24
+ mysql_client.database_select(database)
25
+ end
26
+
27
+ vg_name = config.lvm_volume_group
28
+ lv_name = "#{config.lvm_database_prefix}-#{database}"
29
+ lv_path = "/dev/#{vg_name}/#{lv_name}"
30
+
31
+ mount_database_directory = config.filesystem_mount_directory(database)
32
+ mysql_database_directory = "#{mysql_client.get_data_directory}/#{database}"
33
+
34
+ # Check
35
+
36
+ lv_available = run_command "Checking logical volume name is available" do
37
+ !File.exists?("/dev/#{vg_name}/#{config.lvm_snapshot_prefix}-#{database}")
38
+ end
39
+
40
+ if !lv_available then
41
+ raise "Logical volume #{lv_path.colorize(:red)} already exist"
42
+ end
43
+
44
+ # Drop tablespaces
45
+ tables = run_command "Getting list of tables in database" do
46
+ mysql_client.database_tables
47
+ end
48
+
49
+ tables.each do |table|
50
+ table = table.to_s
51
+ run_command "Changing table engine to MyISAM: #{table}" do
52
+ mysql_client.table_set_engine(table, "MyISAM")
53
+ end
54
+ end
55
+
56
+ # Flush
57
+ run_command "Flush tables with read lock" do
58
+ mysql_client.flush_tables
59
+ end
60
+
61
+ # Stop MySQL
62
+ run_command "Stop MySQL server" do
63
+ Snapback::MySQL::ServiceControl.stop
64
+ end
65
+
66
+ revert do
67
+ run_command "Start MySQL server" do
68
+ Snapback::MySQL::ServiceControl.start
69
+ end
70
+ end
71
+
72
+ # Unlink
73
+ run_command "Unlinking mysql data directory",
74
+ "unlink #{mysql_database_directory}"
75
+
76
+ revert do
77
+ run_command "Linking mysql data directory",
78
+ "ln -s #{mount_database_directory} #{mysql_database_directory}"
79
+ end
80
+
81
+ # Unmount
82
+ run_command "Unmounting logical volume",
83
+ "umount #{mount_database_directory}"
84
+
85
+ revert do
86
+ run_command "Mounting logical volume",
87
+ "mount #{lv_path} #{mount_database_directory}"
88
+ end
89
+
90
+ # Deactivate
91
+ run_command "De-activating logical volume",
92
+ "lvchange -an #{lv_path}"
93
+
94
+ revert do
95
+ run_command "Re-activating logical volume",
96
+ "lvchange -ay #{lv_path}"
97
+ end
98
+
99
+ # Branch the logical volume with a snapshot
100
+ run_command "Snapshoting to #{config.lvm_snapshot_prefix}-#{database}",
101
+ "lvcreate -L #{options[:s]} -s -n #{config.lvm_snapshot_prefix}-#{database} #{lv_path}"
102
+
103
+ revert do
104
+ run_command "Removing the snapshot",
105
+ "lvremove -f /dev/#{vg_name}/#{config.lvm_snapshot_prefix}-#{database}"
106
+ end
107
+
108
+ # Active the master drive
109
+ run_command "Activating logical volume",
110
+ "lvchange -ay #{lv_path}"
111
+
112
+ revert do
113
+ run_command "Deactivating logical volume",
114
+ "lvchange -an #{lv_path}"
115
+ end
116
+
117
+ # Mount the master drive
118
+ run_command "Mounting logical volume",
119
+ "mount #{lv_path} #{mount_database_directory}"
120
+
121
+ revert do
122
+ run_command "Unmounting logical volume",
123
+ "umount #{mount_database_directory}"
124
+ end
125
+
126
+ # Symbolic-link the MySQL data directory to the new logical volume
127
+ run_command "Linking mysql data directory",
128
+ "ln -s #{mount_database_directory} #{mysql_database_directory}"
129
+
130
+ revert do
131
+ run_command "Unlinking mysql data directory",
132
+ "unlink #{mysql_database_directory}"
133
+ end
134
+
135
+ # Change the permissions & ownership to MySQL
136
+ run_command "Changing owner to mysql: #{mysql_database_directory}",
137
+ "chown -R mysql:mysql #{mysql_database_directory}"
138
+
139
+ run_command "Changing owner to mysql: #{mount_database_directory}",
140
+ "chown -R mysql:mysql #{mount_database_directory}"
141
+
142
+ # Start MySQL
143
+ run_command "Starting MySQL server" do
144
+ Snapback::MySQL::ServiceControl.start
145
+ end
146
+
147
+ revert do
148
+ run_command "Stopping MySQL server" do
149
+ Snapback::MySQL::ServiceControl.stop
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,86 @@
1
+ desc 'Unmount an existing database'
2
+ arg_name 'database [, ...]'
3
+ command :unmount do |c|
4
+ c.action do |global_options,options,args|
5
+ help_now!('database(s) required') if args.empty?
6
+
7
+ # Load the configuration
8
+ config = Snapback::ConfigurationLoader.factory global_options[:config]
9
+
10
+ # Connect to MySQL
11
+ mysql_client = config.mysql_client
12
+
13
+ # For each database
14
+ args.each do |database|
15
+
16
+ # Start the transaction
17
+ Snapback::Transaction.new do
18
+ run_command "Selecting database: #{database}" do
19
+ mysql_client.database_select(database)
20
+ end
21
+
22
+ vg_name = config.lvm_volume_group
23
+ lv_name = config.lvm_logical_database database
24
+ lv_path = "/dev/#{vg_name}/#{lv_name}"
25
+
26
+ mount_database_directory = config.filesystem_mount_directory(database)
27
+ mysql_database_directory = "#{mysql_client.get_data_directory}/#{database}"
28
+
29
+ lv_exists = run_command "Checking logical volume exists" do
30
+ File.exists?(lv_path)
31
+ end
32
+
33
+ if !lv_exists then
34
+ raise "Logical volume #{lv_path.colorize(:red)} does not exist"
35
+ end
36
+
37
+ # Flush
38
+ run_command "Flush tables with read lock" do
39
+ mysql_client.flush_tables
40
+ end
41
+
42
+ # Stop MySQL
43
+ run_command "Stop MySQL server" do
44
+ Snapback::MySQL::ServiceControl.stop
45
+ end
46
+
47
+ revert do
48
+ run_command "Start MySQL server" do
49
+ Snapback::MySQL::ServiceControl.start
50
+ end
51
+ end
52
+
53
+ # Unlink
54
+ run_command "Unlinking data directory",
55
+ "unlink #{mysql_database_directory}"
56
+
57
+ revert do
58
+ run_command "Re-linking data directory" do
59
+ "ln -s #{mount_database_directory} #{mysql_database_directory}"
60
+ end
61
+ end
62
+
63
+ # Unmount
64
+ run_command "Unmounting directory",
65
+ "unmount #{mount_database_directory}"
66
+
67
+ revert do
68
+ run_command "Re-mounting data directory" do
69
+ "mount #{lv_path} #{mount_database_directory}"
70
+ end
71
+ end
72
+
73
+ # Start MySQL
74
+ run_command "Start MySQL server" do
75
+ Snapback::MySQL::ServiceControl.start
76
+ end
77
+
78
+ revert do
79
+ run_command "Stop MySQL server" do
80
+ Snapback::MySQL::ServiceControl.stop
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,99 @@
1
+ require 'snapback/mysql/client_control'
2
+
3
+ module Snapback
4
+ module Configuration
5
+ class Configuration_0_0_3
6
+
7
+ def initialize(yaml)
8
+ @@yaml = yaml
9
+ end
10
+
11
+ def version
12
+ @@yaml['version']
13
+ end
14
+
15
+ def lvm_volume_group
16
+ begin
17
+ @@yaml['lvm']['volume_group']
18
+ rescue
19
+ nil
20
+ end
21
+ end
22
+
23
+ def lvm_logical_database(database)
24
+ begin
25
+ "#{@@yaml['lvm']['database_prefix']}-#{database}"
26
+ rescue
27
+ "snapback-database-#{database}"
28
+ end
29
+ end
30
+
31
+ def lvm_logical_snapshot(database)
32
+ begin
33
+ "#{@@yaml['lvm']['snapshot_prefix']}-#{database}"
34
+ rescue
35
+ "snapback-snapshot-#{database}"
36
+ end
37
+ end
38
+
39
+ def lvm_database_prefix
40
+ begin
41
+ @@yaml['lvm']['database_prefix']
42
+ rescue
43
+ nil
44
+ end
45
+ end
46
+
47
+ def lvm_snapshot_prefix
48
+ begin
49
+ @@yaml['lvm']['snapshot_prefix']
50
+ rescue
51
+ nil
52
+ end
53
+ end
54
+
55
+ def mysql_client
56
+ client = Snapback::MySQL::ClientControl.instance
57
+
58
+ client.hostname = mysql_hostname
59
+ client.username = mysql_username
60
+ client.password = mysql_password
61
+ client.connect
62
+
63
+ client
64
+ end
65
+
66
+ def mysql_hostname
67
+ begin
68
+ @@yaml['mysql']['hostname']
69
+ rescue
70
+ nil
71
+ end
72
+ end
73
+
74
+ def mysql_username
75
+ begin
76
+ @@yaml['mysql']['username']
77
+ rescue
78
+ nil
79
+ end
80
+ end
81
+
82
+ def mysql_password
83
+ begin
84
+ @@yaml['mysql']['password']
85
+ rescue
86
+ nil
87
+ end
88
+ end
89
+
90
+ def filesystem_mount_directory(database)
91
+ begin
92
+ "#{@@yaml['filesystem']['mount']}/#{database}"
93
+ rescue
94
+ "/mnt/snapback-#{database}"
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,24 @@
1
+ require 'snapback/configuration/configuration_0_0_3'
2
+
3
+ module Snapback
4
+ class ConfigurationLoader
5
+ def initialize(options={}) #:nodoc:
6
+ end
7
+
8
+ def self.factory filename
9
+ begin
10
+ # Read confirmation options
11
+ configuration = YAML.load_file filename
12
+ rescue
13
+ raise "Could not load configuration file: #{filename}"
14
+ end
15
+
16
+ case configuration['version']
17
+ when "0.0.3"
18
+ ::Snapback::Configuration::Configuration_0_0_3.new configuration
19
+ else
20
+ raise "Unknown configuration version: #{configuration['version']}"
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,39 @@
1
+ module Snapback
2
+ class Filesystem
3
+
4
+ LOCKED_FILES = ['.', '..', 'lost+found']
5
+
6
+ def self.mount(device, directory)
7
+ # If it's already mounted, don't worry
8
+ mount_status = Open4::popen4("mountpoint -q #{directory}") do |pid, stdin, stdout, stderr|
9
+ end
10
+
11
+ if mount_status == 0 then
12
+ return true
13
+ end
14
+
15
+ `mount #{device} #{directory}`
16
+ end
17
+
18
+ def self.unmount(directory)
19
+ # If it's already unmounted, don't worry
20
+ mount_status = Open4::popen4("mountpoint -q #{directory}") do |pid, stdin, stdout, stderr|
21
+ end
22
+
23
+ if mount_status != 0 then
24
+ return true
25
+ end
26
+
27
+ `umount #{directory}`
28
+ end
29
+
30
+ def self.move_mysql_files(from_directory, to_directory)
31
+ Dir.foreach(from_directory) do |filename|
32
+ if !LOCKED_FILES.include?(filename) then
33
+ run_command "Moving #{from_directory}/#{filename} to #{to_directory}",
34
+ "mv #{from_directory}/#{filename} #{to_directory}"
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,125 @@
1
+ require 'singleton'
2
+ require 'mysql'
3
+
4
+ module Snapback
5
+ module MySQL
6
+ class ClientControl
7
+ include Singleton
8
+
9
+ attr_accessor :hostname, :username, :password
10
+
11
+ # Connection
12
+
13
+ @connection = nil
14
+
15
+ @hostname = ""
16
+ @username = ""
17
+ @password = ""
18
+
19
+ def connect
20
+ @connection = Mysql.new @hostname, @username, @password
21
+ end
22
+
23
+ # Maintenance
24
+
25
+ def flush_tables
26
+ @connection.query "FLUSH TABLES WITH READ LOCK"
27
+ true
28
+ end
29
+
30
+ def unlock_tables
31
+ @connection.query "UNLOCK TABLES"
32
+ true
33
+ end
34
+
35
+ # Configuration
36
+
37
+ def get_data_directory
38
+ rs = @connection.query "SHOW VARIABLES LIKE 'datadir'"
39
+ rs.each_hash do |row|
40
+ return row['Value'].gsub(/\/$/, '')
41
+ end
42
+ end
43
+
44
+ def has_innodb_file_per_table?
45
+ rs = @connection.query "SHOW VARIABLES LIKE 'innodb_file_per_table'"
46
+ rs.each_hash do |row|
47
+ return row['Value'] == "ON"
48
+ end
49
+ end
50
+
51
+ def set_innodb_file_per_table(on)
52
+ rs = @connection.prepare "SET GLOBAL innodb_file_per_table = ?"
53
+ rs.execute(on ? 'ON' : 'OFF')
54
+ end
55
+
56
+ # Database
57
+
58
+ def database_exists?(database)
59
+ @connection.list_dbs(database).size == 1
60
+ end
61
+
62
+ def database_create(database)
63
+ begin
64
+ @connection.query "CREATE DATABASE `#{Mysql.escape_string database}`"
65
+ true
66
+ rescue
67
+ false
68
+ end
69
+ end
70
+
71
+ def database_select(database)
72
+ @connection.select_db(database)
73
+ end
74
+
75
+ def database_tables
76
+ @connection.query "SHOW TABLES"
77
+ end
78
+
79
+ # Tables
80
+
81
+ def table_drop(table)
82
+ begin
83
+ @connection.query "DROP TABLE `#{Mysql.escape_string table}`"
84
+ true
85
+ rescue
86
+ false
87
+ end
88
+ end
89
+
90
+ def table_optimize(table)
91
+ begin
92
+ @connection.query "ALTER TABLE `#{Mysql.escape_string table}` IMPORT TABLESPACE"
93
+ rescue
94
+ # If this fails, it's usually just because the table is not InnoDB, so ignore errors
95
+ end
96
+
97
+ begin
98
+ @connection.query "OPTIMIZE TABLE `#{Mysql.escape_string table}`"
99
+ true
100
+ rescue
101
+ false
102
+ end
103
+ end
104
+
105
+ def table_set_engine(table, engine)
106
+ begin
107
+ @connection.query "ALTER TABLE `#{Mysql.escape_string table}` ENGINE = #{Mysql.escape_string engine};"
108
+ true
109
+ rescue
110
+ false
111
+ end
112
+ end
113
+
114
+ # Tablespace
115
+ def tablespace_discard(table)
116
+ begin
117
+ @connection.query "ALTER TABLE `#{Mysql.escape_string table}` DISCARD TABLESPACE"
118
+ true
119
+ rescue
120
+ false
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end