snapback 0.0.1 → 0.0.3

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