cluster 0.5.33
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.
- data/Rakefile +69 -0
- data/VERSION +1 -0
- data/bin/cluster +14 -0
- data/bin/ec2-consistent-snapshot +676 -0
- data/bin/periodic.sh +19 -0
- data/examples/cacerts.pem +19 -0
- data/examples/credentials.yml +24 -0
- data/examples/monitor.god +88 -0
- data/examples/users.sh +42 -0
- data/lib/cluster.rb +267 -0
- data/lib/cluster/cli.rb +206 -0
- data/lib/cluster/configuration.rb +52 -0
- data/lib/cluster/infrastructure.rb +160 -0
- data/lib/cluster/infrastructures/amazon.rb +568 -0
- data/lib/cluster/infrastructures/amazon_instance.rb +270 -0
- data/lib/cluster/infrastructures/amazon_release.rb +63 -0
- data/lib/cluster/instance.rb +97 -0
- data/lib/cluster/release.rb +30 -0
- data/lib/cluster/version.rb +25 -0
- data/lib/ext/array.rb +13 -0
- data/lib/ext/cluster_extensions.rb +13 -0
- metadata +206 -0
data/Rakefile
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
PKG_NAME = ENV['PKG_NAME'] || 'cluster'
|
2
|
+
require File.join(File.dirname(__FILE__), 'lib', PKG_NAME, 'version')
|
3
|
+
|
4
|
+
namespace :cluster do
|
5
|
+
desc "Display #{Cluster::NAME} current version."
|
6
|
+
task :version do
|
7
|
+
puts "#{Cluster::NAME} is at version #{Cluster::Version::CURRENT}"
|
8
|
+
end
|
9
|
+
|
10
|
+
desc "Update the #{Cluster::NAME}.gem file at S3."
|
11
|
+
task :update => :package do
|
12
|
+
# Using right_aws directly to get around having to
|
13
|
+
# have a fantastic require structure to use the
|
14
|
+
# cluster tool itself to do this.
|
15
|
+
#
|
16
|
+
# Most particularly this means that error messages will
|
17
|
+
# show up...
|
18
|
+
require 'right_aws'
|
19
|
+
|
20
|
+
Rightscale::HttpConnection.params = Rightscale::HttpConnection.params.merge(:ca_file => File.expand_path(File.join(File.dirname(__FILE__), 'lib', 'cluster', 'infrastructures', 'aws-certificates.crt')))
|
21
|
+
|
22
|
+
oven = RightAws::S3.new(ENV['AMAZON_ACCESS_KEY_ID'], ENV['AMAZON_SECRET_ACCESS_KEY']).bucket(Cluster::BUCKET)
|
23
|
+
|
24
|
+
unless oven
|
25
|
+
STDERR.puts "Bucket by the name of #{Cluster::BUCKET} cannot be found or accessed."
|
26
|
+
exit 1
|
27
|
+
end
|
28
|
+
|
29
|
+
filename = File.join('pkg', "#{Cluster::NAME}-#{Cluster::Version::STRING}.gem")
|
30
|
+
file = open(filename)
|
31
|
+
oven.put("#{Cluster::NAME}.gem", file, {}, 'public-read')
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# FIXME these RightAws calls could be refactored into one.
|
36
|
+
# Not only that but they should probably be using the cluster
|
37
|
+
# infrastructure...
|
38
|
+
namespace :images do
|
39
|
+
desc "List the current images used"
|
40
|
+
task :show do
|
41
|
+
require 'right_aws'
|
42
|
+
|
43
|
+
oven = RightAws::S3.new(ENV['AMAZON_ACCESS_KEY_ID'], ENV['AMAZON_SECRET_ACCESS_KEY']).bucket(Cluster::BUCKET)
|
44
|
+
|
45
|
+
unless oven
|
46
|
+
STDERR.puts "Bucket by the name of #{Cluster::BUCKET} cannot be found or accessed."
|
47
|
+
exit 1
|
48
|
+
end
|
49
|
+
|
50
|
+
filename = 'cluster_images.yml'
|
51
|
+
puts oven.get(filename)
|
52
|
+
end
|
53
|
+
|
54
|
+
desc "Update the ec2 base image data"
|
55
|
+
task :update do
|
56
|
+
require 'right_aws'
|
57
|
+
|
58
|
+
oven = RightAws::S3.new(ENV['AMAZON_ACCESS_KEY_ID'], ENV['AMAZON_SECRET_ACCESS_KEY']).bucket(Cluster::BUCKET)
|
59
|
+
|
60
|
+
unless oven
|
61
|
+
STDERR.puts "Bucket by the name of #{Cluster::BUCKET} cannot be found or accessed."
|
62
|
+
exit 1
|
63
|
+
end
|
64
|
+
|
65
|
+
filename = 'cluster_images.yml'
|
66
|
+
file = open(filename)
|
67
|
+
oven.put(filename, file, {}, 'public-read')
|
68
|
+
end
|
69
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.5.32
|
data/bin/cluster
ADDED
@@ -0,0 +1,676 @@
|
|
1
|
+
#!/usr/bin/perl
|
2
|
+
#
|
3
|
+
# Copyright (C) 2008-2009 Eric Hammond
|
4
|
+
#
|
5
|
+
use strict;
|
6
|
+
use warnings;
|
7
|
+
(our $Prog) = ($0 =~ m%([^/]+)$%);
|
8
|
+
use Getopt::Long;
|
9
|
+
use Pod::Usage;
|
10
|
+
use File::Slurp;
|
11
|
+
use Time::HiRes qw(ualarm usleep);
|
12
|
+
use Net::Amazon::EC2 0.11;
|
13
|
+
use POSIX ':signal_h';
|
14
|
+
my $SIGALRM = 14;
|
15
|
+
|
16
|
+
#---- OPTIONS ----
|
17
|
+
|
18
|
+
my $Help = 0;
|
19
|
+
my $Debug = 0;
|
20
|
+
my $Quiet = 0;
|
21
|
+
my $Noaction = 0;
|
22
|
+
|
23
|
+
my $aws_access_key_id = $ENV{AWS_ACCESS_KEY_ID};
|
24
|
+
my $aws_secret_access_key = $ENV{AWS_SECRET_ACCESS_KEY};
|
25
|
+
my $aws_access_key_id_file = $ENV{AWS_ACCESS_KEY_ID};
|
26
|
+
my $aws_secret_access_key_file = $ENV{AWS_SECRET_ACCESS_KEY};
|
27
|
+
my $region = undef;
|
28
|
+
my $description = $Prog;
|
29
|
+
my $xfs_filesystem = undef;
|
30
|
+
my $mysql = 0;
|
31
|
+
my $mysql_defaults_file = undef;
|
32
|
+
my $mysql_master_status_file = undef;
|
33
|
+
my $mysql_username = undef;
|
34
|
+
my $mysql_password = undef;
|
35
|
+
my $mysql_host = undef;
|
36
|
+
my $mysql_stop = 0;
|
37
|
+
my $snapshot_timeout = 10.0; # seconds
|
38
|
+
my $lock_timeout = 0.5; # seconds
|
39
|
+
my $lock_tries = 60;
|
40
|
+
my $lock_sleep = 5.0; # seconds
|
41
|
+
|
42
|
+
Getopt::Long::config('no_ignore_case');
|
43
|
+
GetOptions(
|
44
|
+
'help|?' => \$Help,
|
45
|
+
'debug' => \$Debug,
|
46
|
+
'quiet' => \$Quiet,
|
47
|
+
'noaction' => \$Noaction,
|
48
|
+
|
49
|
+
'aws-access-key-id=s' => \$aws_access_key_id,
|
50
|
+
'aws-secret-access-key=s' => \$aws_secret_access_key,
|
51
|
+
'aws-access-key-id-file=s' => \$aws_access_key_id_file,
|
52
|
+
'aws-secret-access-key-file=s' => \$aws_secret_access_key_file,
|
53
|
+
'region=s' => \$region,
|
54
|
+
'description=s' => \$description,
|
55
|
+
'xfs-filesystem=s' => \$xfs_filesystem,
|
56
|
+
'mysql' => \$mysql,
|
57
|
+
'mysql-defaults-file=s' => \$mysql_defaults_file,
|
58
|
+
'mysql-master-status-file=s' => \$mysql_master_status_file,
|
59
|
+
'mysql-username=s' => \$mysql_username,
|
60
|
+
'mysql-password=s' => \$mysql_password,
|
61
|
+
'mysql-host=s' => \$mysql_host,
|
62
|
+
'mysql-stop' => \$mysql_stop,
|
63
|
+
'snapshot-timeout=s' => \$snapshot_timeout,
|
64
|
+
'lock-timeout=s' => \$lock_timeout,
|
65
|
+
'lock-tries=s' => \$lock_tries,
|
66
|
+
'lock-sleep=s' => \$lock_sleep,
|
67
|
+
) or pod2usage(2);
|
68
|
+
|
69
|
+
pod2usage(1) if $Help;
|
70
|
+
|
71
|
+
my @volume_ids = @ARGV;
|
72
|
+
pod2usage(2) unless scalar @volume_ids;
|
73
|
+
|
74
|
+
#---- MAIN ----
|
75
|
+
|
76
|
+
($aws_access_key_id, $aws_secret_access_key) = determine_access_keys(
|
77
|
+
$aws_access_key_id, $aws_secret_access_key,
|
78
|
+
$aws_access_key_id_file, $aws_secret_access_key_file,
|
79
|
+
);
|
80
|
+
die "$Prog: ERROR: Can't find AWS access key or secret access key"
|
81
|
+
unless $aws_access_key_id and $aws_secret_access_key;
|
82
|
+
|
83
|
+
my $mysql_stopped = undef;
|
84
|
+
my $mysql_dbh = undef;
|
85
|
+
if ( $mysql_stop ) {
|
86
|
+
$mysql_stopped = mysql_stop();
|
87
|
+
} elsif ( $mysql ) {
|
88
|
+
$mysql_dbh = mysql_connect($mysql_host, $mysql_username, $mysql_password);
|
89
|
+
mysql_lock($mysql_dbh);
|
90
|
+
}
|
91
|
+
|
92
|
+
# sync may help flush changed blocks, increasing the consistency on
|
93
|
+
# non-XFS file systems, and reducing the time the freeze locks changes
|
94
|
+
# out on XFS file systems.
|
95
|
+
run_command(['sync']);
|
96
|
+
|
97
|
+
xfs_freeze($xfs_filesystem) if $xfs_filesystem;
|
98
|
+
|
99
|
+
ebs_snapshot(\@volume_ids, $region, $description);
|
100
|
+
|
101
|
+
exit 0;
|
102
|
+
|
103
|
+
END {
|
104
|
+
xfs_thaw($xfs_filesystem) if $xfs_filesystem;
|
105
|
+
|
106
|
+
if ( defined($mysql_master_status_file) ) {
|
107
|
+
$Debug and warn "$Prog: ", scalar localtime,
|
108
|
+
": removing master info file: $mysql_master_status_file\n";
|
109
|
+
if ( not $Noaction ) {
|
110
|
+
unlink $mysql_master_status_file
|
111
|
+
or die "$Prog: Couldn't remove file: $mysql_master_status_file: $!\n";
|
112
|
+
}
|
113
|
+
}
|
114
|
+
|
115
|
+
if ( $mysql_stopped ) {
|
116
|
+
mysql_start();
|
117
|
+
} elsif ( $mysql_dbh ) {
|
118
|
+
mysql_unlock($mysql_dbh);
|
119
|
+
$Debug and warn "$Prog: ", scalar localtime, ": MySQL disconnect\n";
|
120
|
+
$mysql_dbh->disconnect;
|
121
|
+
}
|
122
|
+
|
123
|
+
$Debug and warn "$Prog: ", scalar localtime, ": done\n";
|
124
|
+
}
|
125
|
+
|
126
|
+
#---- METHODS ----
|
127
|
+
|
128
|
+
sub ebs_snapshot {
|
129
|
+
my ($volume_ids, $region, $description) = @_;
|
130
|
+
|
131
|
+
$Debug and warn "$Prog: ", scalar localtime, ": create EC2 object\n";
|
132
|
+
|
133
|
+
# EC2 API object
|
134
|
+
my $ec2 = Net::Amazon::EC2->new(
|
135
|
+
AWSAccessKeyId => $aws_access_key_id,
|
136
|
+
SecretAccessKey => $aws_secret_access_key,
|
137
|
+
($region ? (base_url => "http://$region.ec2.amazonaws.com") : ()),
|
138
|
+
# ($Debug ? (debug => 1) : ()),
|
139
|
+
);
|
140
|
+
|
141
|
+
VOLUME:
|
142
|
+
for my $volume_id ( @$volume_ids ) {
|
143
|
+
# Snapshot
|
144
|
+
$Debug and
|
145
|
+
warn "$Prog: ", scalar localtime, ": ec2-create-snapshot $volume_id\n";
|
146
|
+
if ( $Noaction ) {
|
147
|
+
warn "snapshot SKIPPED (--noaction)\n";
|
148
|
+
next VOLUME;
|
149
|
+
}
|
150
|
+
|
151
|
+
my $snapshot;
|
152
|
+
eval {
|
153
|
+
ualarm($snapshot_timeout * 1_000_000);
|
154
|
+
$snapshot = $ec2->create_snapshot(
|
155
|
+
VolumeId => $volume_id,
|
156
|
+
Description => $description,
|
157
|
+
);
|
158
|
+
ualarm(0);
|
159
|
+
};
|
160
|
+
ualarm(0);
|
161
|
+
if ( $@ ){
|
162
|
+
warn "$Prog: ERROR: create_snapshot: $@";
|
163
|
+
return undef;
|
164
|
+
}
|
165
|
+
|
166
|
+
my $snapshot_id;
|
167
|
+
if ( not defined $snapshot ) {
|
168
|
+
warn "$Prog: ERROR: create_snapshot returned undef\n";
|
169
|
+
} elsif ( not ref $snapshot ) {
|
170
|
+
warn "$Prog: ERROR: create_snapshot returned '$snapshot'\n";
|
171
|
+
} elsif ( $snapshot->can('snapshot_id') ) {
|
172
|
+
$snapshot_id = $snapshot->snapshot_id;
|
173
|
+
$Quiet or print "$snapshot_id\n";
|
174
|
+
} else {
|
175
|
+
for my $error ( @{$snapshot->errors} ) {
|
176
|
+
warn "$Prog: ERROR: ".$error->message."\n";
|
177
|
+
}
|
178
|
+
}
|
179
|
+
}
|
180
|
+
}
|
181
|
+
|
182
|
+
sub mysql_connect {
|
183
|
+
my ($mysql_host, $mysql_username, $mysql_password) = @_;
|
184
|
+
|
185
|
+
require DBI;
|
186
|
+
DBI->import;
|
187
|
+
|
188
|
+
if ( defined($mysql_defaults_file) )
|
189
|
+
{
|
190
|
+
($mysql_host, $mysql_username, $mysql_password) = read_mydefaultsfile(
|
191
|
+
$mysql_defaults_file,
|
192
|
+
);
|
193
|
+
} else {
|
194
|
+
($mysql_host, $mysql_username, $mysql_password) = read_mycnf(
|
195
|
+
$mysql_host, $mysql_username, $mysql_password,
|
196
|
+
);
|
197
|
+
}
|
198
|
+
|
199
|
+
$mysql_host ||= 'localhost';
|
200
|
+
|
201
|
+
$Debug and warn "$Prog: ", scalar localtime,
|
202
|
+
": MySQL connect as $mysql_username\n";
|
203
|
+
|
204
|
+
my $mysql_dbh = DBI->connect("DBI:mysql:;host=$mysql_host",
|
205
|
+
$mysql_username, $mysql_password)
|
206
|
+
or die "$Prog: ERROR: Unable to connect to MySQL"
|
207
|
+
. " on $mysql_host as $mysql_username";
|
208
|
+
|
209
|
+
return $mysql_dbh;
|
210
|
+
}
|
211
|
+
|
212
|
+
sub mysql_lock {
|
213
|
+
my ($mysql_dbh) = @_;
|
214
|
+
|
215
|
+
# Don't pass FLUSH TABLES statements on to replication slaves
|
216
|
+
# as this can interfere with long-running queries on the slaves.
|
217
|
+
$mysql_dbh->do(q{ SET SQL_LOG_BIN=0 }) unless $Noaction;
|
218
|
+
|
219
|
+
# Try a flush first without locking so the later flush with lock
|
220
|
+
# goes faster. This may not be needed as it seems to interfere with
|
221
|
+
# some statements anyway.
|
222
|
+
sql_timeout_retry(
|
223
|
+
q{ FLUSH LOCAL TABLES },
|
224
|
+
"MySQL flush",
|
225
|
+
$lock_timeout,
|
226
|
+
$lock_tries,
|
227
|
+
$lock_sleep,
|
228
|
+
);
|
229
|
+
|
230
|
+
# Get a lock on the entire database
|
231
|
+
sql_timeout_retry(
|
232
|
+
q{ FLUSH LOCAL TABLES WITH READ LOCK },
|
233
|
+
"MySQL flush & lock",
|
234
|
+
$lock_timeout,
|
235
|
+
$lock_tries,
|
236
|
+
$lock_sleep,
|
237
|
+
);
|
238
|
+
|
239
|
+
my ($mysql_logfile, $mysql_position,
|
240
|
+
$mysql_binlog_do_db, $mysql_binlog_ignore_db);
|
241
|
+
if ( not $Noaction ) {
|
242
|
+
# This might be a slave database already
|
243
|
+
my $slave_status = $mysql_dbh->selectrow_hashref(q{ SHOW SLAVE STATUS });
|
244
|
+
$mysql_logfile = $slave_status->{Master_Log_File};
|
245
|
+
$mysql_position = $slave_status->{Read_Master_Log_Pos};
|
246
|
+
$mysql_binlog_do_db = $slave_status->{Replicate_Do_DB};
|
247
|
+
$mysql_binlog_ignore_db = $slave_status->{Replicate_Ignore_DB};
|
248
|
+
|
249
|
+
# or this might be the master
|
250
|
+
($mysql_logfile, $mysql_position,
|
251
|
+
$mysql_binlog_do_db, $mysql_binlog_ignore_db) =
|
252
|
+
$mysql_dbh->selectrow_array(q{ SHOW MASTER STATUS })
|
253
|
+
unless $mysql_logfile;
|
254
|
+
}
|
255
|
+
|
256
|
+
$mysql_dbh->do(q{ SET SQL_LOG_BIN=1 }) unless $Noaction;
|
257
|
+
|
258
|
+
print "$Prog: master_log_file=\"$mysql_logfile\",",
|
259
|
+
" master_log_pos=$mysql_position\n"
|
260
|
+
if $mysql_logfile and not $Quiet;
|
261
|
+
|
262
|
+
if ( defined($mysql_master_status_file) && $mysql_logfile ) {
|
263
|
+
$Debug and warn "$Prog: ", scalar localtime,
|
264
|
+
": writing MASTER STATUS to $mysql_master_status_file\n";
|
265
|
+
if ( not $Noaction ) {
|
266
|
+
open(MYSQLMASTERSTATUS,"> $mysql_master_status_file") or
|
267
|
+
die "$Prog: Unable to open for write: $mysql_master_status_file: $!\n";
|
268
|
+
print MYSQLMASTERSTATUS <<"EOM";
|
269
|
+
master_log_file="$mysql_logfile"
|
270
|
+
master_log_pos="$mysql_position"
|
271
|
+
master_binlog_do_db="$mysql_binlog_do_db"
|
272
|
+
master_binlog_ignore_db="$mysql_binlog_ignore_db"
|
273
|
+
EOM
|
274
|
+
close(MYSQLMASTERSTATUS);
|
275
|
+
}
|
276
|
+
}
|
277
|
+
|
278
|
+
return ($mysql_logfile, $mysql_position);
|
279
|
+
}
|
280
|
+
|
281
|
+
sub mysql_unlock {
|
282
|
+
my ($mysql_dbh) = @_;
|
283
|
+
|
284
|
+
$Debug and warn "$Prog: ", scalar localtime, ": MySQL unlock\n";
|
285
|
+
$mysql_dbh->do(q{ UNLOCK TABLES }) unless $Noaction;
|
286
|
+
|
287
|
+
}
|
288
|
+
|
289
|
+
sub mysql_stop {
|
290
|
+
$Debug and warn "$Prog: ", scalar localtime, ": MySQL stop\n";
|
291
|
+
|
292
|
+
my $result = system('/usr/bin/mysqladmin', 'shutdown');
|
293
|
+
|
294
|
+
if ( $result != 0 ) {
|
295
|
+
die "$Prog: Unable to stop mysqld: $? $!";
|
296
|
+
}
|
297
|
+
|
298
|
+
return 1;
|
299
|
+
}
|
300
|
+
|
301
|
+
sub mysql_start {
|
302
|
+
$Debug and warn "$Prog: ", scalar localtime, ": MySQL start\n";
|
303
|
+
|
304
|
+
my $result = system('/etc/init.d/mysql', 'start');
|
305
|
+
|
306
|
+
if ( $result != 0 ) {
|
307
|
+
warn "$Prog: Unable to start mysqld: $? $!";
|
308
|
+
}
|
309
|
+
}
|
310
|
+
|
311
|
+
# See also:
|
312
|
+
#http://search.cpan.org/dist/DBI/DBI.pm#Signal_Handling_and_Canceling_Operations
|
313
|
+
sub sql_timeout_retry {
|
314
|
+
my ($sql, $description, $lock_timeout, $lock_tries, $lock_sleep) = @_;
|
315
|
+
|
316
|
+
my $action = POSIX::SigAction->new(
|
317
|
+
sub { die "timeout" },
|
318
|
+
POSIX::SigSet->new( SIGALRM ),
|
319
|
+
);
|
320
|
+
my $oldaction = POSIX::SigAction->new();
|
321
|
+
sigaction($SIGALRM, $action, $oldaction);
|
322
|
+
LOCK:
|
323
|
+
while ( $lock_tries -- ) {
|
324
|
+
$Debug and warn "$Prog: ", scalar localtime, ": $description\n";
|
325
|
+
eval {
|
326
|
+
ualarm($lock_timeout * 1_000_000);
|
327
|
+
$mysql_dbh->do($sql) unless $Noaction;
|
328
|
+
ualarm(0);
|
329
|
+
};
|
330
|
+
ualarm(0);
|
331
|
+
last LOCK unless $@ =~ /timeout/;
|
332
|
+
|
333
|
+
} continue {
|
334
|
+
$Quiet or warn "$Prog: MySQL timeout at $lock_timeout sec\n";
|
335
|
+
$Debug and warn "$Prog: Trying again in $lock_sleep seconds\n";
|
336
|
+
usleep($lock_sleep * 1_000_000);
|
337
|
+
#TBD: May need to reopen $mysql_dbh here as the handle may not be clean.
|
338
|
+
}
|
339
|
+
sigaction($SIGALRM, $oldaction);
|
340
|
+
die "$Prog: ERROR: MySQL failure: $@" if $@;
|
341
|
+
}
|
342
|
+
|
343
|
+
sub xfs_freeze {
|
344
|
+
my ($xfs_filesystem) = @_;
|
345
|
+
|
346
|
+
run_command(['xfs_freeze', '-f', $xfs_filesystem]);
|
347
|
+
}
|
348
|
+
|
349
|
+
sub xfs_thaw {
|
350
|
+
my ($xfs_filesystem) = @_;
|
351
|
+
|
352
|
+
run_command(['xfs_freeze', '-u', $xfs_filesystem]);
|
353
|
+
}
|
354
|
+
|
355
|
+
# mysql defaults-file
|
356
|
+
sub read_mydefaultsfile {
|
357
|
+
my ($mysql_defaults_file) = @_;
|
358
|
+
|
359
|
+
open(MYCNF, $mysql_defaults_file)
|
360
|
+
or die "$Prog: ERROR: Couldn't open defaults-file $mysql_defaults_file\n";
|
361
|
+
while ( defined (my $line = <MYCNF>) ) {
|
362
|
+
$mysql_host = $1 if $line =~ m%^\s*host\s*=\s*"?(\S+?)"?$%
|
363
|
+
and not defined $mysql_host;
|
364
|
+
$mysql_username = $1 if $line =~ m%^\s*user\s*=\s*"?(\S+?)"?$%
|
365
|
+
and not defined $mysql_username;
|
366
|
+
$mysql_password = $1 if $line =~ m%^\s*password\s*=\s*"?(\S+?)"?$%
|
367
|
+
and not defined $mysql_password;
|
368
|
+
}
|
369
|
+
close(MYCNF);
|
370
|
+
|
371
|
+
return ($mysql_host, $mysql_username, $mysql_password);
|
372
|
+
}
|
373
|
+
|
374
|
+
# Look for the MySQL username/password in $HOME/.my.cnf
|
375
|
+
sub read_mycnf {
|
376
|
+
my ($mysql_host, $mysql_username, $mysql_password) = @_;
|
377
|
+
|
378
|
+
open(MYCNF, "$ENV{HOME}/.my.cnf")
|
379
|
+
or return($mysql_host, $mysql_username, $mysql_password);
|
380
|
+
while ( defined (my $line = <MYCNF>) ) {
|
381
|
+
$mysql_host = $1 if $line =~ m%^\s*host\s*=\s*"?(\S+?)"?$%
|
382
|
+
and not defined $mysql_host;
|
383
|
+
$mysql_username = $1 if $line =~ m%^\s*user\s*=\s*"?(\S+?)"?$%
|
384
|
+
and not defined $mysql_username;
|
385
|
+
$mysql_password = $1 if $line =~ m%^\s*password\s*=\s*"?(\S+?)"?$%
|
386
|
+
and not defined $mysql_password;
|
387
|
+
}
|
388
|
+
close(MYCNF);
|
389
|
+
|
390
|
+
return ($mysql_host, $mysql_username, $mysql_password);
|
391
|
+
}
|
392
|
+
|
393
|
+
|
394
|
+
# Figure out which AWS access keys to use
|
395
|
+
sub determine_access_keys {
|
396
|
+
my ($aws_access_key_id, $aws_secret_access_key,
|
397
|
+
$aws_access_key_id_file, $aws_secret_access_key_file,
|
398
|
+
) = @_;
|
399
|
+
|
400
|
+
# 1. --aws-access-key-id and --aws-secret-access-key
|
401
|
+
return ($aws_access_key_id, $aws_secret_access_key)
|
402
|
+
if $aws_access_key_id;
|
403
|
+
|
404
|
+
# 2. --aws-access-key-id-file and --aws-secret-access-key-file
|
405
|
+
if ( $aws_access_key_id_file ) {
|
406
|
+
die "$Prog: Please provide both --aws-access-key-id-file and --aws-secret-access-key-file"
|
407
|
+
unless $aws_secret_access_key_file;
|
408
|
+
$aws_access_key_id = File::Slurp::read_file($aws_access_key_id_file);
|
409
|
+
$aws_secret_access_key= File::Slurp::read_file($aws_secret_access_key_file);
|
410
|
+
chomp($aws_access_key_id);
|
411
|
+
chomp($aws_secret_access_key);
|
412
|
+
return ($aws_access_key_id, $aws_secret_access_key);
|
413
|
+
}
|
414
|
+
|
415
|
+
# 3. $HOME/.awssecret
|
416
|
+
return read_awssecret();
|
417
|
+
}
|
418
|
+
|
419
|
+
|
420
|
+
# Look for the access keys in ~/.awssecret
|
421
|
+
sub read_awssecret {
|
422
|
+
my $filename = "$ENV{HOME}/.awssecret";
|
423
|
+
my ($aws_access_key_id, $aws_secret_access_key);
|
424
|
+
eval {
|
425
|
+
($aws_access_key_id, $aws_secret_access_key) =
|
426
|
+
File::Slurp::read_file($filename);
|
427
|
+
chomp $aws_access_key_id;
|
428
|
+
chomp $aws_secret_access_key;
|
429
|
+
};
|
430
|
+
return ($aws_access_key_id, $aws_secret_access_key);
|
431
|
+
}
|
432
|
+
|
433
|
+
# Run a system command, warning if there are problems.
|
434
|
+
# Pass in reference to an array of command args.
|
435
|
+
sub run_command {
|
436
|
+
my ($command) = @_;
|
437
|
+
|
438
|
+
$Debug and warn "$Prog: ", scalar localtime, ": @$command\n";
|
439
|
+
return if $Noaction;
|
440
|
+
eval {
|
441
|
+
system(@$command) and die "failed($?)\n";
|
442
|
+
};
|
443
|
+
warn "$Prog: ERROR: @$command: $@" if $@;
|
444
|
+
}
|
445
|
+
|
446
|
+
|
447
|
+
=head1 NAME
|
448
|
+
|
449
|
+
ec2-consistent-snapshot - Create snapshot, locking db and freezing file system
|
450
|
+
|
451
|
+
=head1 SYNOPSIS
|
452
|
+
|
453
|
+
ec2-consistent-snapshot [opts] VOLUMEID...
|
454
|
+
|
455
|
+
=head1 OPTIONS
|
456
|
+
|
457
|
+
-h --help Print help and exit.
|
458
|
+
-d --debug Debug mode.
|
459
|
+
-q --quiet Quiet mode.
|
460
|
+
-n --noaction Don't do it. Just say what you would have done.
|
461
|
+
|
462
|
+
--aws-access-key-id KEY
|
463
|
+
--aws-secret-access-key SECRET
|
464
|
+
|
465
|
+
Amazon AWS access key and secret access key. Defaults to
|
466
|
+
environment variables or .awssecret file contents described below.
|
467
|
+
|
468
|
+
--aws-access-key-id-file KEYFILE
|
469
|
+
--aws-secret-access-key-file SECRETFILE
|
470
|
+
|
471
|
+
Files containing Amazon AWS access key and secret access key.
|
472
|
+
Defaults to environment variables or .awssecret file contents
|
473
|
+
described below.
|
474
|
+
|
475
|
+
--region REGION
|
476
|
+
|
477
|
+
Specify a different region like "eu-west-1". Defaults to
|
478
|
+
"us-east-1".
|
479
|
+
|
480
|
+
--description DESCRIPTION
|
481
|
+
|
482
|
+
Specify a description string for the EBS snapshot. Defaults to the
|
483
|
+
name of the program.
|
484
|
+
|
485
|
+
--xfs-filesystem MOUNTPOINT
|
486
|
+
|
487
|
+
Indicates that the volume contains an XFS file system at the specified
|
488
|
+
mount point, which will be flushed and frozen during the snapshot.
|
489
|
+
|
490
|
+
--mysql
|
491
|
+
|
492
|
+
Indicates that the volume contains data files for a running MySQL
|
493
|
+
database, which will be flushed and locked during the snapshot.
|
494
|
+
|
495
|
+
--mysql-defaults-file FILE
|
496
|
+
|
497
|
+
MySQL defaults file, containing host, username and password, this
|
498
|
+
option will ignore the --mysql-host, --mysql-username,
|
499
|
+
--mysql-password parameters
|
500
|
+
|
501
|
+
--mysql-host HOST
|
502
|
+
--mysql-username USER
|
503
|
+
--mysql-password PASS
|
504
|
+
|
505
|
+
MySQL host, username, and password used to flush logs and lock
|
506
|
+
tables. User must have appropriate permissions. Defaults to
|
507
|
+
$HOME/.my.cnf file contents.
|
508
|
+
|
509
|
+
--mysql-master-status-file FILE
|
510
|
+
|
511
|
+
Store the MASTER STATUS output in a file on the snapshot. It will
|
512
|
+
be removed after the EBS snapshot is taken. This option will be
|
513
|
+
ignored with --mysql-stop
|
514
|
+
|
515
|
+
--mysql-stop
|
516
|
+
|
517
|
+
Indicates that the volume contains data files for a running MySQL
|
518
|
+
database. The database is shutdown before the snapshot is initiated
|
519
|
+
and restarted afterwards. [EXPERIMENTAL]
|
520
|
+
|
521
|
+
--snapshot-timeout SECONDS
|
522
|
+
|
523
|
+
How many seconds to wait for the snapshot-create to return.
|
524
|
+
Defaults to 10.0
|
525
|
+
|
526
|
+
--lock-timeout SECONDS
|
527
|
+
|
528
|
+
How many seconds to wait for a database lock. Defaults to 0.5.
|
529
|
+
Making this too large can force other processes to wait while this
|
530
|
+
process waits for a lock. Better to make it small and try lots of
|
531
|
+
times.
|
532
|
+
|
533
|
+
--lock-tries COUNT
|
534
|
+
|
535
|
+
How many times to try to get a database lock before failing.
|
536
|
+
Defaults to 60.
|
537
|
+
|
538
|
+
--lock-sleep SECONDS
|
539
|
+
|
540
|
+
How many seconds to sleep between database lock tries. Defaults
|
541
|
+
to 5.0.
|
542
|
+
|
543
|
+
=head1 ARGUMENTS
|
544
|
+
|
545
|
+
VOLUMEID EBS volume id(s) for which to create a snapshot.
|
546
|
+
|
547
|
+
=head1 DESCRIPTION
|
548
|
+
|
549
|
+
This program creates an EBS snapshot for an Amazon EC2 EBS volume. To
|
550
|
+
help ensure consistent data in the snapshot, it tries to flush and
|
551
|
+
freeze the file system first as well as flushing and locking the
|
552
|
+
database, if applicable.
|
553
|
+
|
554
|
+
There are a number of timeouts to reduce the risk of interfering with
|
555
|
+
the normal database operation while improving the chances of getting a
|
556
|
+
consistent snapshot.
|
557
|
+
|
558
|
+
If you have multiple EBS volumes in a RAID configuration, you can
|
559
|
+
specify all of the volume ids on the command line and it will create
|
560
|
+
snapshots for each while the filesystem and database are locked. Note
|
561
|
+
that it is your responsibility to keep track of the resulting snapshot
|
562
|
+
ids and to figure out how to put these back together when you need to
|
563
|
+
restore the RAID setup.
|
564
|
+
|
565
|
+
If you have multiple EBS volumes which are hosting different file
|
566
|
+
systems, it might be better to simply run the command once for each
|
567
|
+
volume id.
|
568
|
+
|
569
|
+
=head1 EXAMPLES
|
570
|
+
|
571
|
+
Snapshot a volume which has a MySQL database on XFS under /vol:
|
572
|
+
|
573
|
+
ec2-consistent-snapshot --mysql --xfs-filesystem /vol vol-VOLUMEID
|
574
|
+
|
575
|
+
Snapshot a volume with XFS on /var/local but no MySQL database:
|
576
|
+
|
577
|
+
ec2-consistent-snapshot --xfs-filesystem /var/local vol-VOLUMEID
|
578
|
+
|
579
|
+
Snapshot four European volumes in a RAID configuration with MySQL on
|
580
|
+
XFS, saving the snapshots with a description marking the current time:
|
581
|
+
|
582
|
+
ec2-consistent-snapshot \
|
583
|
+
--mysql \
|
584
|
+
--xfs-filesystem /vol \
|
585
|
+
--region eu-west-1 \
|
586
|
+
--description "RAID snapshot $(date +'%Y-%m-%d %H:%M:%S')" \
|
587
|
+
vol-VOL1 vol-VOL2 vol-VOL3 vol-VOL4
|
588
|
+
|
589
|
+
=head1 ENVIRONMENT
|
590
|
+
|
591
|
+
$AWS_ACCESS_KEY_ID
|
592
|
+
|
593
|
+
Default value for access key
|
594
|
+
Can be overridden by command line options.
|
595
|
+
|
596
|
+
$AWS_SECRET_ACCESS_KEY
|
597
|
+
|
598
|
+
Default value for secret access key
|
599
|
+
Can be overridden by command line options.
|
600
|
+
|
601
|
+
=head1 FILES
|
602
|
+
|
603
|
+
$HOME/.my.cnf
|
604
|
+
|
605
|
+
Default values for MySQL user and password are sought here in the
|
606
|
+
standard format.
|
607
|
+
|
608
|
+
$HOME/.awssecret
|
609
|
+
|
610
|
+
Default values for access key and secret access keys are sought
|
611
|
+
here. Can be overridden by environment variables and command line
|
612
|
+
options.
|
613
|
+
|
614
|
+
=head1 INSTALLATION
|
615
|
+
|
616
|
+
On most Ubuntu releases, the B<ec2-consistent-snapshot> package can be
|
617
|
+
installed directly from the Alestic.com PPA using the following
|
618
|
+
commands:
|
619
|
+
|
620
|
+
code=$(lsb_release -cs)
|
621
|
+
echo "deb http://ppa.launchpad.net/alestic/ppa/ubuntu $code main"|
|
622
|
+
sudo tee /etc/apt/sources.list.d/alestic-ppa.list
|
623
|
+
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys BE09C571
|
624
|
+
sudo apt-get update
|
625
|
+
sudo apt-get install -y ec2-consistent-snapshot
|
626
|
+
|
627
|
+
This program also requires the installation of the Net::Amazon::EC2
|
628
|
+
Perl package from CPAN. On an Ubuntu system this can be accomplished
|
629
|
+
with the following commands.:
|
630
|
+
|
631
|
+
sudo PERL_MM_USE_DEFAULT=1 cpan Net::Amazon::EC2
|
632
|
+
|
633
|
+
On Ubuntu 8.04 Hardy, use the following commands instead:
|
634
|
+
|
635
|
+
sudo apt-get install -y build-essential
|
636
|
+
sudo cpan Net::Amazon::EC2
|
637
|
+
|
638
|
+
The default values can be accepted for most of the prompts, though it
|
639
|
+
is necessary to select a CPAN mirror on Hardy.
|
640
|
+
|
641
|
+
=head1 SEE ALSO
|
642
|
+
|
643
|
+
Amazon EC2
|
644
|
+
Amazon EC2 EBS (Elastic Block Store)
|
645
|
+
ec2-create-snapshot
|
646
|
+
|
647
|
+
=head1 CAVEATS
|
648
|
+
|
649
|
+
This program currently supports the MySQL database and the XFS file
|
650
|
+
system. Patches are welcomed for other databases and file systems.
|
651
|
+
|
652
|
+
=head1 BUGS
|
653
|
+
|
654
|
+
Please report bugs at https://bugs.launchpad.net/ec2-consistent-snapshot
|
655
|
+
|
656
|
+
=head1 CREDITS
|
657
|
+
|
658
|
+
Thanks to the following for performing tests on early versions,
|
659
|
+
providing feedback, and patches:
|
660
|
+
|
661
|
+
David Erickson
|
662
|
+
Steve Caldwell
|
663
|
+
Gryp
|
664
|
+
|
665
|
+
=head1 AUTHOR
|
666
|
+
|
667
|
+
Eric Hammond <ehammond@thinksome.com>
|
668
|
+
|
669
|
+
=head1 LICENSE
|
670
|
+
|
671
|
+
Copyright (C) 2009 Eric Hammond <ehammond@thinksome.com>
|
672
|
+
|
673
|
+
Licensed under the Apache License, Version 2.0, see
|
674
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
675
|
+
|
676
|
+
=cut
|