Fingertips-fingertips-backup 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +25 -0
- data/VERSION.yml +4 -0
- data/bin/fingertips-backup +10 -0
- data/lib/backup.rb +112 -0
- data/lib/ec2.rb +84 -0
- data/lib/logger.rb +48 -0
- data/test/backup_test.rb +222 -0
- data/test/ec2_test.rb +80 -0
- data/test/fixtures/attach-volume +1 -0
- data/test/fixtures/config.yml +17 -0
- data/test/fixtures/describe-instances +2 -0
- data/test/fixtures/describe-volumes +2 -0
- data/test/fixtures/run-instances +2 -0
- data/test/fixtures/terminate-instances +1 -0
- data/test/logger_test.rb +32 -0
- data/test/test_helper.rb +20 -0
- metadata +91 -0
data/Rakefile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'rake/testtask'
|
2
|
+
|
3
|
+
task :default => :test
|
4
|
+
|
5
|
+
Rake::TestTask.new do |t|
|
6
|
+
t.test_files = FileList['test/**/*_test.rb']
|
7
|
+
t.verbose = true
|
8
|
+
end
|
9
|
+
|
10
|
+
begin
|
11
|
+
require 'jeweler'
|
12
|
+
Jeweler::Tasks.new do |s|
|
13
|
+
s.name = "fingertips-backup"
|
14
|
+
s.description = "A simple tool to backup MySQL databases and files to an Amazon EBS instance through an EC2 instance."
|
15
|
+
s.summary = "A simple tool to backup a webserver."
|
16
|
+
s.homepage = "http://fingertips.github.com"
|
17
|
+
|
18
|
+
s.authors = ["Eloy Duran"]
|
19
|
+
s.email = "eloy@fngtps.com"
|
20
|
+
|
21
|
+
s.add_dependency 'Fingertips-executioner'
|
22
|
+
s.add_dependency 'aws-s3'
|
23
|
+
end
|
24
|
+
rescue LoadError
|
25
|
+
end
|
data/VERSION.yml
ADDED
data/lib/backup.rb
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
require "yaml"
|
2
|
+
|
3
|
+
require "rubygems"
|
4
|
+
require "executioner"
|
5
|
+
require "aws/s3"
|
6
|
+
|
7
|
+
require "logger"
|
8
|
+
require "ec2"
|
9
|
+
|
10
|
+
module Fingertips
|
11
|
+
class Backup
|
12
|
+
include Executioner
|
13
|
+
Executioner::SEARCH_PATHS << '/usr/local/mysql/bin'
|
14
|
+
executable :mysql
|
15
|
+
executable :mysqldump
|
16
|
+
executable :rsync
|
17
|
+
executable :ssh, :switch_stdout_and_stderr => true
|
18
|
+
|
19
|
+
MYSQL_DUMP_DIR = '/tmp/mysql_backup_dumps'
|
20
|
+
|
21
|
+
attr_reader :config, :ec2, :logger, :s3
|
22
|
+
attr_accessor :ec2_instance_id
|
23
|
+
|
24
|
+
def initialize(config_file)
|
25
|
+
@logger = Executioner.logger = Fingertips::Logger.new
|
26
|
+
@config = YAML.load(File.read(config_file))
|
27
|
+
@ec2 = Fingertips::EC2.new(@config['ec2']['zone'], @config['ec2']['private_key_file'], @config['ec2']['certificate_file'], @config['java_home'])
|
28
|
+
@s3 = AWS::S3::Base.establish_connection!(:access_key_id => @config['s3']['access_key_id'], :secret_access_key => @config['s3']['secret_access_key'])
|
29
|
+
rescue Exception => e
|
30
|
+
failed(e)
|
31
|
+
end
|
32
|
+
|
33
|
+
def finished
|
34
|
+
@logger.debug "The backup finished."
|
35
|
+
publish_log!
|
36
|
+
end
|
37
|
+
|
38
|
+
def failed(exception)
|
39
|
+
@logger.debug "#{exception.message} #{exception.backtrace.join("\n")}"
|
40
|
+
@logger.debug "[!] The backup has failed."
|
41
|
+
publish_log!
|
42
|
+
raise exception
|
43
|
+
end
|
44
|
+
|
45
|
+
def write_feed!
|
46
|
+
@logger.write_feed(@config['log_feed'])
|
47
|
+
end
|
48
|
+
|
49
|
+
def publish_log!
|
50
|
+
write_feed!
|
51
|
+
AWS::S3::S3Object.store('backup_feed.xml', File.open(@config['log_feed']), @config['s3']['bucket'], :content_type => 'application/atom+xml', :access => :public_read)
|
52
|
+
end
|
53
|
+
|
54
|
+
def run!
|
55
|
+
begin
|
56
|
+
create_mysql_dump!
|
57
|
+
bring_backup_volume_online!
|
58
|
+
sync!
|
59
|
+
rescue Exception => e
|
60
|
+
failed(e)
|
61
|
+
ensure
|
62
|
+
take_backup_volume_offline! if ec2_instance_id
|
63
|
+
end
|
64
|
+
finished
|
65
|
+
end
|
66
|
+
|
67
|
+
def bring_backup_volume_online!
|
68
|
+
launch_ec2_instance!
|
69
|
+
attach_backup_volume!
|
70
|
+
mount_backup_volume!
|
71
|
+
end
|
72
|
+
|
73
|
+
def launch_ec2_instance!
|
74
|
+
@ec2_instance_id = @ec2.run_instance(@config['ec2']['ami'], @config['ec2']['keypair_name'])
|
75
|
+
sleep 5 until @ec2.running?(@ec2_instance_id)
|
76
|
+
end
|
77
|
+
|
78
|
+
def attach_backup_volume!
|
79
|
+
@ec2.attach_volume(@config['ec2']['ebs'], ec2_instance_id, "/dev/sdh")
|
80
|
+
sleep 2.5 until @ec2.attached?(@config['ec2']['ebs'])
|
81
|
+
end
|
82
|
+
|
83
|
+
def mount_backup_volume!
|
84
|
+
ssh "-o 'StrictHostKeyChecking=no' -i '#{@config['ec2']['keypair_file']}' root@#{ec2_host} 'mkdir /mnt/data-store && mount /dev/sdh /mnt/data-store'"
|
85
|
+
end
|
86
|
+
|
87
|
+
def take_backup_volume_offline!
|
88
|
+
@ec2.terminate_instance @ec2_instance_id
|
89
|
+
end
|
90
|
+
|
91
|
+
def ec2_host
|
92
|
+
@ec2_host ||= @ec2.host_of_instance(ec2_instance_id)
|
93
|
+
end
|
94
|
+
|
95
|
+
def mysql_databases
|
96
|
+
@mysql_databases ||= mysql('-u root --batch --skip-column-names -e "show databases"').strip.split("\n")
|
97
|
+
end
|
98
|
+
|
99
|
+
def create_mysql_dump!
|
100
|
+
FileUtils.rm_rf(MYSQL_DUMP_DIR)
|
101
|
+
FileUtils.mkdir_p(MYSQL_DUMP_DIR)
|
102
|
+
|
103
|
+
mysql_databases.each do |database|
|
104
|
+
mysqldump("-u root #{database} --add-drop-table > '#{File.join(MYSQL_DUMP_DIR, database)}.sql'")
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def sync!
|
109
|
+
rsync "-avz -e \"ssh -i '#{@config['ec2']['keypair_file']}'\" '#{MYSQL_DUMP_DIR}' '#{@config['backup'].join("' '")}' root@#{ec2_host}:/mnt/data-store"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
data/lib/ec2.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "executioner"
|
3
|
+
|
4
|
+
module Fingertips
|
5
|
+
class EC2
|
6
|
+
PRIVATE_KEY_FILE = '/Volumes/Fingertips Confidential/aws/fingertips/pk-6LN7EWTYKIDRU25OJYMTY6P75S43WA45.pem'
|
7
|
+
CERTIFICATE_FILE = '/Volumes/Fingertips Confidential/aws/fingertips/cert-6LN7EWTYKIDRU25OJYMTY6P75S43WA45.pem'
|
8
|
+
|
9
|
+
HOME = '/opt/ec2'
|
10
|
+
BIN = File.join(HOME, 'bin')
|
11
|
+
|
12
|
+
include Executioner
|
13
|
+
Executioner::SEARCH_PATHS << BIN
|
14
|
+
|
15
|
+
attr_reader :zone, :private_key_file, :certificate_file, :java_home
|
16
|
+
|
17
|
+
def initialize(zone, private_key_file, certificate_file, java_home)
|
18
|
+
@zone, @private_key_file, @certificate_file, @java_home = zone, private_key_file, certificate_file, java_home
|
19
|
+
end
|
20
|
+
|
21
|
+
def env
|
22
|
+
@env ||= {
|
23
|
+
'JAVA_HOME' => @java_home,
|
24
|
+
'EC2_HOME' => HOME,
|
25
|
+
'EC2_PRIVATE_KEY' => @private_key_file,
|
26
|
+
'EC2_CERT' => @certificate_file,
|
27
|
+
'EC2_URL' => "https://#{@zone[0..-2]}.ec2.amazonaws.com"
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
# EC2
|
32
|
+
|
33
|
+
def run_instance(ami, keypair_name, options = {})
|
34
|
+
ec2_run_instances("#{ami} -z #{@zone} -k #{keypair_name}", :env => env)[1][1]
|
35
|
+
end
|
36
|
+
|
37
|
+
def describe_instance(instance_id)
|
38
|
+
ec2_describe_instances(instance_id, :env => env).detect { |line| line[1] == instance_id }
|
39
|
+
end
|
40
|
+
|
41
|
+
def terminate_instance(instance_id)
|
42
|
+
ec2_terminate_instances(instance_id, :env => env)[0][3]
|
43
|
+
end
|
44
|
+
|
45
|
+
def running?(instance_id)
|
46
|
+
describe_instance(instance_id)[5] == 'running'
|
47
|
+
end
|
48
|
+
|
49
|
+
def host_of_instance(instance_id)
|
50
|
+
describe_instance(instance_id)[3]
|
51
|
+
end
|
52
|
+
|
53
|
+
# EBS
|
54
|
+
|
55
|
+
def attach_volume(volume_id, instance_id, device)
|
56
|
+
ec2_attach_volume("#{volume_id} -i #{instance_id} -d #{device}", :env => env)
|
57
|
+
end
|
58
|
+
|
59
|
+
def describe_volume(volume_id)
|
60
|
+
ec2_describe_volumes(volume_id, :env => env).detect { |line| line[0] == 'ATTACHMENT' && line[1] == volume_id }
|
61
|
+
end
|
62
|
+
|
63
|
+
def attached?(volume_id)
|
64
|
+
describe_volume(volume_id)[4] == 'attached'
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
executable 'ec2-run-instances'
|
70
|
+
executable 'ec2-describe-instances'
|
71
|
+
executable 'ec2-terminate-instances'
|
72
|
+
|
73
|
+
executable 'ec2-attach-volume'
|
74
|
+
executable 'ec2-describe-volumes'
|
75
|
+
|
76
|
+
def parse(text)
|
77
|
+
text.strip.split("\n").map { |line| line.split("\t") }
|
78
|
+
end
|
79
|
+
|
80
|
+
def execute(command, options = {})
|
81
|
+
parse(super)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
data/lib/logger.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require "builder"
|
2
|
+
require "rss"
|
3
|
+
|
4
|
+
module Fingertips
|
5
|
+
class Logger
|
6
|
+
class << self
|
7
|
+
attr_accessor :print
|
8
|
+
end
|
9
|
+
self.print = true
|
10
|
+
|
11
|
+
attr_reader :logged
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@logged = []
|
15
|
+
end
|
16
|
+
|
17
|
+
def debug(message)
|
18
|
+
@logged << message
|
19
|
+
puts(message) if self.class.print
|
20
|
+
end
|
21
|
+
|
22
|
+
def feed
|
23
|
+
output = ''
|
24
|
+
xml = Builder::XmlMarkup.new(:target => output, :indent => 2)
|
25
|
+
xml.instruct!
|
26
|
+
xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do
|
27
|
+
xml.id "http://github.com/Fingertips/fingertips-backup"
|
28
|
+
xml.link "rel" => "self", "href" => "http://github.com/Fingertips/fingertips-backup"
|
29
|
+
xml.updated Time.now.iso8601
|
30
|
+
xml.author { xml.name "Backup" }
|
31
|
+
xml.title "Backup feed"
|
32
|
+
|
33
|
+
xml.entry do
|
34
|
+
xml.id "http://github.com/Fingertips/fingertips-backup"
|
35
|
+
xml.updated Time.now.iso8601
|
36
|
+
xml.title @logged.last
|
37
|
+
xml.summary @logged.last
|
38
|
+
xml.content @logged.join("\n")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
output
|
42
|
+
end
|
43
|
+
|
44
|
+
def write_feed(path)
|
45
|
+
File.open(path, 'w') { |f| f << feed }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/test/backup_test.rb
ADDED
@@ -0,0 +1,222 @@
|
|
1
|
+
require File.expand_path('../test_helper', __FILE__)
|
2
|
+
|
3
|
+
describe "Fingertips::Backup, in general" do
|
4
|
+
before do
|
5
|
+
@config = YAML.load(fixture_read('config.yml'))
|
6
|
+
@backup = Fingertips::Backup.new(fixture('config.yml'))
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should have instantiated a Logger and assigned it to Executioner" do
|
10
|
+
@backup.logger.should.be.instance_of Fingertips::Logger
|
11
|
+
Executioner.logger.should.be @backup.logger
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should return the config" do
|
15
|
+
@backup.config.should == @config
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should return a configured Fingertips::EC2 instance" do
|
19
|
+
@backup.ec2.should.be.instance_of Fingertips::EC2
|
20
|
+
@backup.ec2.zone.should == @config['ec2']['zone']
|
21
|
+
@backup.ec2.private_key_file.should == @config['ec2']['private_key_file']
|
22
|
+
@backup.ec2.certificate_file.should == @config['ec2']['certificate_file']
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should return a configured AWS::S3 connection" do
|
26
|
+
@backup.s3.should.be.instance_of AWS::S3::Connection
|
27
|
+
@backup.s3.access_key_id.should == @config['s3']['access_key_id']
|
28
|
+
@backup.s3.secret_access_key.should == @config['s3']['secret_access_key']
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should return a list of all MySQL databases" do
|
32
|
+
databases = @backup.mysql_databases
|
33
|
+
databases.should.include 'information_schema'
|
34
|
+
databases.should == `mysql -u root --batch --skip-column-names -e "show databases"`.strip.split("\n")
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should return the host of the EC2 instance" do
|
38
|
+
@backup.ec2_instance_id = 'i-nonexistant'
|
39
|
+
@backup.ec2.expects(:host_of_instance).with('i-nonexistant').returns('instance.amazon.com')
|
40
|
+
@backup.ec2_host.should == 'instance.amazon.com'
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should perform a full run and report that the backup finished" do
|
44
|
+
@backup.expects(:create_mysql_dump!)
|
45
|
+
@backup.expects(:bring_backup_volume_online!)
|
46
|
+
@backup.ec2_instance_id = 'i-nonexistant'
|
47
|
+
@backup.expects(:sync!)
|
48
|
+
@backup.expects(:take_backup_volume_offline!)
|
49
|
+
@backup.expects(:finished)
|
50
|
+
|
51
|
+
@backup.run!
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should catch any type of exception that was raised during initialization and call #failed" do
|
55
|
+
Fingertips::Backup.any_instance.expects(:failed).with { |exception| exception.backtrace.to_s.include?('initialize') }
|
56
|
+
backup = Fingertips::Backup.new(nil)
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should catch any type of exception that was raised during the run and terminate the EC2 instance if one was launched and call #failed" do
|
60
|
+
@backup.stubs(:finished)
|
61
|
+
|
62
|
+
@backup.ec2_instance_id = 'i-nonexistant'
|
63
|
+
@backup.stubs(:create_mysql_dump!).raises 'oh noes!'
|
64
|
+
@backup.expects(:failed).with { |exception| exception.message == 'oh noes!' }
|
65
|
+
@backup.expects(:take_backup_volume_offline!)
|
66
|
+
@backup.run!
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should report that the backup failed and re-raise the exception" do
|
70
|
+
exception = nil
|
71
|
+
begin; raise 'oh noes!'; rescue Exception => e; exception = e; end
|
72
|
+
|
73
|
+
@backup.expects(:publish_log!)
|
74
|
+
lambda { @backup.failed(exception) }.should.raise exception.class
|
75
|
+
@backup.logger.logged.first.should.include 'oh noes!'
|
76
|
+
@backup.logger.logged.last.should == "[!] The backup has failed."
|
77
|
+
end
|
78
|
+
|
79
|
+
it "should report that the backup has finished" do
|
80
|
+
@backup.expects(:publish_log!)
|
81
|
+
@backup.finished
|
82
|
+
@backup.logger.logged.last.should == "The backup finished."
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should write the feed of the current log" do
|
86
|
+
@backup.logger.expects(:write_feed).with(@config['log_feed'])
|
87
|
+
@backup.write_feed!
|
88
|
+
end
|
89
|
+
|
90
|
+
it "should push the log to S3" do
|
91
|
+
@backup.expects(:write_feed!)
|
92
|
+
|
93
|
+
file = stub('Feed file')
|
94
|
+
File.expects(:open).with(@config['log_feed']).returns(file)
|
95
|
+
|
96
|
+
AWS::S3::S3Object.expects(:store).with('backup_feed.xml', file, @config['s3']['bucket'], :content_type => 'application/atom+xml', :access => :public_read)
|
97
|
+
|
98
|
+
@backup.publish_log!
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
describe "Fingertips::Backup, concerning the MySQL backup" do
|
103
|
+
before do
|
104
|
+
@backup = Fingertips::Backup.new(fixture('config.yml'))
|
105
|
+
@backup.stubs(:mysql_databases).returns(%w{ information_schema })
|
106
|
+
end
|
107
|
+
|
108
|
+
after do
|
109
|
+
FileUtils.rm_rf(Fingertips::Backup::MYSQL_DUMP_DIR)
|
110
|
+
end
|
111
|
+
|
112
|
+
it "should first remove the tmp mysql dump dir" do
|
113
|
+
# have to use at_least_once because it's also called in the after filter
|
114
|
+
FileUtils.expects(:rm_rf).with(Fingertips::Backup::MYSQL_DUMP_DIR).at_least_once
|
115
|
+
@backup.create_mysql_dump!
|
116
|
+
end
|
117
|
+
|
118
|
+
it "should create the tmp mysql dump dir" do
|
119
|
+
@backup.create_mysql_dump!
|
120
|
+
File.should.exist Fingertips::Backup::MYSQL_DUMP_DIR
|
121
|
+
File.should.be.directory Fingertips::Backup::MYSQL_DUMP_DIR
|
122
|
+
end
|
123
|
+
|
124
|
+
it "should dump each database into its own file" do
|
125
|
+
@backup.create_mysql_dump!
|
126
|
+
|
127
|
+
actual = strip_comments(`mysqldump -u root information_schema --add-drop-table`)
|
128
|
+
dump = strip_comments(File.read(File.join(Fingertips::Backup::MYSQL_DUMP_DIR, 'information_schema.sql')))
|
129
|
+
|
130
|
+
actual.should == dump
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
def strip_comments(sql)
|
136
|
+
sql.gsub(/^--.*?$/, '').strip
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
describe "Fingertips::Backup, concerning the EBS volume" do
|
141
|
+
before do
|
142
|
+
@backup = Fingertips::Backup.new(fixture('config.yml'))
|
143
|
+
@config = @backup.config
|
144
|
+
@ec2 = @backup.ec2
|
145
|
+
|
146
|
+
@backup.stubs(:sleep)
|
147
|
+
|
148
|
+
@ec2.stubs(:run_instance).returns("i-nonexistant")
|
149
|
+
@ec2.stubs(:running?).returns(true)
|
150
|
+
|
151
|
+
@ec2.stubs(:attach_volume)
|
152
|
+
@ec2.stubs(:attached?).returns(true)
|
153
|
+
end
|
154
|
+
|
155
|
+
it "should run an EC2 instance and wait till it's online" do
|
156
|
+
@backup.stubs(:mount_backup_volume!)
|
157
|
+
|
158
|
+
@ec2.expects(:run_instance).with('ami-nonexistant', 'fingertips').returns("i-nonexistant")
|
159
|
+
|
160
|
+
@ec2.expects(:running?).with do |id|
|
161
|
+
# next time it's queried it will be running
|
162
|
+
def @ec2.running?(id)
|
163
|
+
true
|
164
|
+
end
|
165
|
+
|
166
|
+
id == "i-nonexistant"
|
167
|
+
end.returns(false)
|
168
|
+
@backup.expects(:sleep).with(5).once
|
169
|
+
|
170
|
+
@backup.launch_ec2_instance!
|
171
|
+
@backup.ec2_instance_id.should == "i-nonexistant"
|
172
|
+
end
|
173
|
+
|
174
|
+
it "should attach the existing EBS instance and wait till it's online" do
|
175
|
+
@backup.ec2_instance_id = 'i-nonexistant'
|
176
|
+
@ec2.expects(:attach_volume).with("vol-nonexistant", "i-nonexistant", "/dev/sdh")
|
177
|
+
|
178
|
+
@ec2.expects(:attached?).with do |id|
|
179
|
+
# next time it's queried it will be attached
|
180
|
+
def @ec2.attached?(id)
|
181
|
+
true
|
182
|
+
end
|
183
|
+
|
184
|
+
id == "vol-nonexistant"
|
185
|
+
end.returns(false)
|
186
|
+
@backup.expects(:sleep).with(2.5).once
|
187
|
+
|
188
|
+
@backup.attach_backup_volume!
|
189
|
+
end
|
190
|
+
|
191
|
+
it "should mount the attached EBS volume on the running instance" do
|
192
|
+
@backup.stubs(:ec2_host).returns('instance.amazon.com')
|
193
|
+
@backup.expects(:ssh).with("-o 'StrictHostKeyChecking=no' -i '#{@config['ec2']['keypair_file']}' root@instance.amazon.com 'mkdir /mnt/data-store && mount /dev/sdh /mnt/data-store'")
|
194
|
+
@backup.mount_backup_volume!
|
195
|
+
end
|
196
|
+
|
197
|
+
it "should run all steps to bring the backup volume online" do
|
198
|
+
@backup.expects(:launch_ec2_instance!)
|
199
|
+
@backup.expects(:attach_backup_volume!)
|
200
|
+
@backup.expects(:mount_backup_volume!)
|
201
|
+
|
202
|
+
@backup.bring_backup_volume_online!
|
203
|
+
end
|
204
|
+
|
205
|
+
it "should take the backup volume offline by terminating the EC2 instance" do
|
206
|
+
@backup.ec2_instance_id = 'i-nonexistant'
|
207
|
+
@backup.ec2.expects(:terminate_instance).with('i-nonexistant')
|
208
|
+
@backup.take_backup_volume_offline!
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
describe "Fingertips::Backup, concerning syncing" do
|
213
|
+
before do
|
214
|
+
@backup = Fingertips::Backup.new(fixture('config.yml'))
|
215
|
+
@backup.stubs(:ec2_host).returns('instance.amazon.com')
|
216
|
+
end
|
217
|
+
|
218
|
+
it "should sync all configured paths and the mysql dump dir to the backup volume" do
|
219
|
+
@backup.expects(:rsync).with("-avz -e \"ssh -i '#{@backup.config['ec2']['keypair_file']}'\" '#{Fingertips::Backup::MYSQL_DUMP_DIR}' '/var/www/apps' '/root' root@instance.amazon.com:/mnt/data-store")
|
220
|
+
@backup.sync!
|
221
|
+
end
|
222
|
+
end
|
data/test/ec2_test.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
require File.expand_path('../test_helper', __FILE__)
|
2
|
+
|
3
|
+
describe "Fingertips::EC2, in general" do
|
4
|
+
before do
|
5
|
+
@ami = 'ami-nonexistant'
|
6
|
+
@instance = Fingertips::EC2.new('eu-west-1a', '/path/to/private_key_file', '/path/to/certificate_file', '/Library/Java/Home')
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should return the right env variables to be able to use the Amazon CLI tools" do
|
10
|
+
@instance.env.should == {
|
11
|
+
'JAVA_HOME' => '/Library/Java/Home',
|
12
|
+
'EC2_HOME' => '/opt/ec2',
|
13
|
+
'EC2_PRIVATE_KEY' => '/path/to/private_key_file',
|
14
|
+
'EC2_CERT' => '/path/to/certificate_file',
|
15
|
+
'EC2_URL' => 'https://eu-west-1.ec2.amazonaws.com'
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should return an array of lines splitted at tabs" do
|
20
|
+
@instance.send(:parse, " line1item1\tline1item2\nline2item1\tline2item2 ").should ==
|
21
|
+
[['line1item1', 'line1item2'], ['line2item1', 'line2item2']]
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should override #execute so it returns the response parsed" do
|
25
|
+
@instance.send(:execute, 'ls').should == @instance.send(:parse, `ls`)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "Fingertips::EC2, concerning the pre-defined commands" do
|
30
|
+
before do
|
31
|
+
@instance = Fingertips::EC2.new('eu-west-1a', '/path/to/private_key_file', '/path/to/certificate_file', '/Library/Java/Home')
|
32
|
+
@options = { :switch_stdout_and_stderr => false, :env => @instance.env }
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should run an instance of the given AMI with the given options and return the instance ID" do
|
36
|
+
expect_call('run-instances', "ami-nonexistant -z eu-west-1a -k fingertips")
|
37
|
+
@instance.run_instance('ami-nonexistant', 'fingertips').should == 'i-0992a760'
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should return the status of an instance" do
|
41
|
+
expect_call('describe-instances', 'i-nonexistant')
|
42
|
+
@instance.describe_instance("i-nonexistant").should == @instance.send(:parse, fixture_read('describe-instances'))[1]
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should terminate an instance" do
|
46
|
+
expect_call('terminate-instances', 'i-nonexistant')
|
47
|
+
@instance.terminate_instance('i-nonexistant').should == 'shutting-down'
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should return if an instance is running" do
|
51
|
+
expect_call('describe-instances', 'i-nonexistant')
|
52
|
+
@instance.running?('i-nonexistant').should.be true
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should return the public host address of an instance" do
|
56
|
+
expect_call('describe-instances', 'i-nonexistant')
|
57
|
+
@instance.host_of_instance('i-nonexistant').should == 'ec2-174-129-88-205.compute-1.amazonaws.com'
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should attach an EBS volume to an EC2 instance" do
|
61
|
+
expect_call('attach-volume', 'vol-nonexistant -i i-nonexistant -d /dev/sdh')
|
62
|
+
@instance.attach_volume('vol-nonexistant', 'i-nonexistant', '/dev/sdh')
|
63
|
+
end
|
64
|
+
|
65
|
+
it "should return the status of a volume" do
|
66
|
+
expect_call('describe-volumes', 'vol-nonexistant')
|
67
|
+
@instance.describe_volume('vol-nonexistant').should == @instance.send(:parse, fixture_read('describe-volumes'))[1]
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should return if a volume is attached" do
|
71
|
+
expect_call('describe-volumes', 'vol-nonexistant')
|
72
|
+
@instance.attached?('vol-nonexistant').should.be true
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def expect_call(name, args)
|
78
|
+
@instance.expects(:execute).with("/opt/ec2/bin/ec2-#{name} #{args}", @options).returns(@instance.send(:parse, fixture_read(name)))
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
ATTACHMENT vol-21e70e48 i-ad2f19c4 /dev/sdh attaching 2009-07-09T13:46:45+0000
|
@@ -0,0 +1,17 @@
|
|
1
|
+
java_home: /Library/Java/Home
|
2
|
+
log_feed: /var/www/backup_feed/backup_feed.xml
|
3
|
+
s3:
|
4
|
+
bucket: backup_feed
|
5
|
+
access_key_id: ANID
|
6
|
+
secret_access_key: SOSECRET
|
7
|
+
ec2:
|
8
|
+
zone: eu-west-1a
|
9
|
+
ebs: vol-nonexistant
|
10
|
+
ami: ami-nonexistant
|
11
|
+
keypair_name: fingertips
|
12
|
+
keypair_file: /Volumes/Fingertips Confidential/aws/fingertips/keys/fingertips
|
13
|
+
private_key_file: /Volumes/Fingertips Confidential/aws/fingertips/pk-6LN7EWTYKIDRU25OJYMTY6P75S43WA45.pem
|
14
|
+
certificate_file: /Volumes/Fingertips Confidential/aws/fingertips/cert-6LN7EWTYKIDRU25OJYMTY6P75S43WA45.pem
|
15
|
+
backup:
|
16
|
+
- /var/www/apps
|
17
|
+
- /root
|
@@ -0,0 +1,2 @@
|
|
1
|
+
RESERVATION r-bfda9cd6 895951156444 default
|
2
|
+
INSTANCE i-nonexistant ami-0d729464 ec2-174-129-88-205.compute-1.amazonaws.com domU-12-31-39-00-84-B7.compute-1.internal running fingertips 0 m1.small 2009-07-08T13:06:22+0000 us-east-1b aki-a71cf9ce ari-a51cf9cc monitoring-disabled
|
@@ -0,0 +1 @@
|
|
1
|
+
INSTANCE i-7b576012 running shutting-down
|
data/test/logger_test.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require File.expand_path('../test_helper', __FILE__)
|
2
|
+
|
3
|
+
describe "Fingertips::Logger" do
|
4
|
+
TMP_FEED = '/tmp/backup_feed'
|
5
|
+
|
6
|
+
before do
|
7
|
+
@logger = Fingertips::Logger.new
|
8
|
+
@logger.debug "foo"
|
9
|
+
@logger.debug "bar"
|
10
|
+
@logger.debug "baz"
|
11
|
+
end
|
12
|
+
|
13
|
+
after do
|
14
|
+
FileUtils.rm_rf TMP_FEED
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should store any debug messages" do
|
18
|
+
@logger.logged.should == %w{ foo bar baz }
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should create a feed" do
|
22
|
+
@logger.feed.should.include "foo\nbar\nbaz"
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should write the feed to a given path" do
|
26
|
+
now = Time.now
|
27
|
+
Time.stubs(:now).returns(now)
|
28
|
+
|
29
|
+
@logger.write_feed(TMP_FEED)
|
30
|
+
File.read(TMP_FEED).should == @logger.feed
|
31
|
+
end
|
32
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "test/spec"
|
3
|
+
require "mocha"
|
4
|
+
|
5
|
+
$:.unshift File.expand_path('../../lib', __FILE__)
|
6
|
+
require "backup"
|
7
|
+
|
8
|
+
Fingertips::Logger.print = false
|
9
|
+
|
10
|
+
FIXTURES = File.expand_path('../fixtures', __FILE__)
|
11
|
+
|
12
|
+
class Test::Unit::TestCase
|
13
|
+
def fixture(fixture)
|
14
|
+
File.join(FIXTURES, fixture)
|
15
|
+
end
|
16
|
+
|
17
|
+
def fixture_read(fixture)
|
18
|
+
File.read(self.fixture(fixture))
|
19
|
+
end
|
20
|
+
end
|
metadata
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: Fingertips-fingertips-backup
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Eloy Duran
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-08-18 00:00:00 -07:00
|
13
|
+
default_executable: fingertips-backup
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: Fingertips-executioner
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: aws-s3
|
27
|
+
type: :runtime
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: "0"
|
34
|
+
version:
|
35
|
+
description: A simple tool to backup MySQL databases and files to an Amazon EBS instance through an EC2 instance.
|
36
|
+
email: eloy@fngtps.com
|
37
|
+
executables:
|
38
|
+
- fingertips-backup
|
39
|
+
extensions: []
|
40
|
+
|
41
|
+
extra_rdoc_files: []
|
42
|
+
|
43
|
+
files:
|
44
|
+
- Rakefile
|
45
|
+
- VERSION.yml
|
46
|
+
- bin/fingertips-backup
|
47
|
+
- lib/backup.rb
|
48
|
+
- lib/ec2.rb
|
49
|
+
- lib/logger.rb
|
50
|
+
- test/backup_test.rb
|
51
|
+
- test/ec2_test.rb
|
52
|
+
- test/fixtures/attach-volume
|
53
|
+
- test/fixtures/config.yml
|
54
|
+
- test/fixtures/describe-instances
|
55
|
+
- test/fixtures/describe-volumes
|
56
|
+
- test/fixtures/run-instances
|
57
|
+
- test/fixtures/terminate-instances
|
58
|
+
- test/logger_test.rb
|
59
|
+
- test/test_helper.rb
|
60
|
+
has_rdoc: true
|
61
|
+
homepage: http://fingertips.github.com
|
62
|
+
licenses:
|
63
|
+
post_install_message:
|
64
|
+
rdoc_options:
|
65
|
+
- --charset=UTF-8
|
66
|
+
require_paths:
|
67
|
+
- lib
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: "0"
|
73
|
+
version:
|
74
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: "0"
|
79
|
+
version:
|
80
|
+
requirements: []
|
81
|
+
|
82
|
+
rubyforge_project:
|
83
|
+
rubygems_version: 1.3.5
|
84
|
+
signing_key:
|
85
|
+
specification_version: 2
|
86
|
+
summary: A simple tool to backup a webserver.
|
87
|
+
test_files:
|
88
|
+
- test/backup_test.rb
|
89
|
+
- test/ec2_test.rb
|
90
|
+
- test/logger_test.rb
|
91
|
+
- test/test_helper.rb
|