Fingertips-fingertips-backup 0.1.0
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 +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
|