snapback 0.0.1 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -7
- data/README.md +6 -11
- data/bin/snapback +51 -67
- data/lib/snapback.rb +130 -0
- data/lib/snapback/cli/commit.rb +72 -0
- data/lib/snapback/cli/create.rb +157 -0
- data/lib/snapback/cli/drop.rb +94 -0
- data/lib/snapback/cli/install.rb +179 -0
- data/lib/snapback/cli/mount.rb +97 -0
- data/lib/snapback/cli/rollback.rb +161 -0
- data/lib/snapback/cli/snapshot.rb +155 -0
- data/lib/snapback/cli/unmount.rb +86 -0
- data/lib/snapback/configuration/Configuration_0_0_3.rb +99 -0
- data/lib/snapback/configuration_loader.rb +24 -0
- data/lib/snapback/filesystem.rb +39 -0
- data/lib/snapback/mysql/client_control.rb +125 -0
- data/lib/snapback/mysql/service_control.rb +14 -0
- data/lib/snapback/transaction.rb +25 -10
- data/lib/snapback/version.rb +3 -0
- metadata +110 -67
- data/lib/snapback/app/commit.rb +0 -35
- data/lib/snapback/app/create.rb +0 -117
- data/lib/snapback/app/drop.rb +0 -51
- data/lib/snapback/app/install.rb +0 -156
- data/lib/snapback/app/mount.rb +0 -62
- data/lib/snapback/app/rollback.rb +0 -63
- data/lib/snapback/app/snapshot.rb +0 -74
- data/lib/snapback/app/unmount.rb +0 -39
- data/lib/snapback/database.rb +0 -102
- data/lib/snapback/dsl.rb +0 -208
- data/lib/snapback/options.rb +0 -85
@@ -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
|