ebs-snapshoter 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +4 -0
- data/README.txt +84 -0
- data/Rakefile +61 -0
- data/VERSION +1 -0
- data/bin/snapshoter +52 -0
- data/lib/snapshoter/config.rb +60 -0
- data/lib/snapshoter/manager.rb +134 -0
- data/lib/snapshoter/provider/ec2.rb +76 -0
- data/lib/snapshoter/volume.rb +61 -0
- data/lib/snapshoter.rb +70 -0
- data/spec/snapshoter_spec.rb +67 -0
- data/spec/spec_helper.rb +16 -0
- data/test/test_snapshoter.rb +0 -0
- metadata +97 -0
data/History.txt
ADDED
data/README.txt
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
snapshoter
|
2
|
+
by Kris Rasmussen
|
3
|
+
http://www.dreamthis.com
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
Provides EBS snapshot automation that can be configured and run on an EC2 instance.
|
8
|
+
|
9
|
+
Snapshot Process:
|
10
|
+
|
11
|
+
* Read configuration
|
12
|
+
* Fetch snapshot descriptions from ec2
|
13
|
+
* For each volume in configuration
|
14
|
+
* - If it is time to run a new snapshot based on the most recent snapshot date
|
15
|
+
* -- freeze mysql (if using mysql)
|
16
|
+
* -- freeze xfs
|
17
|
+
* -- create the snapshot
|
18
|
+
* -- unfreeze xfs
|
19
|
+
* -- unfreeze mysql
|
20
|
+
* -- delete old snapshots
|
21
|
+
|
22
|
+
== FEATURES/PROBLEMS:
|
23
|
+
|
24
|
+
You can configure the following options:
|
25
|
+
|
26
|
+
* EBS volumes to backup
|
27
|
+
* Backup frequency: currently only daily or weekly
|
28
|
+
* Number of backups to keep (up to ec2 limits)
|
29
|
+
* Mysql database freezing
|
30
|
+
|
31
|
+
== SYNOPSIS:
|
32
|
+
|
33
|
+
/etc/snapshoter.yml
|
34
|
+
|
35
|
+
aws_public_key: VSdfslkfjsdf...
|
36
|
+
aws_private_key: df23knlkvjsdf...
|
37
|
+
|
38
|
+
vol-VVVV1113:
|
39
|
+
mount_point: /data1
|
40
|
+
frequency: daily
|
41
|
+
freeze_mysql: true
|
42
|
+
mysql_user: root
|
43
|
+
mysql_password: XDFDSE32
|
44
|
+
keep: 20
|
45
|
+
|
46
|
+
vol-BBBBB1112:
|
47
|
+
mount_point: /data2
|
48
|
+
frequency: weekly
|
49
|
+
|
50
|
+
== REQUIREMENTS:
|
51
|
+
|
52
|
+
* EBS volumes using XFS as filesystem
|
53
|
+
|
54
|
+
== INSTALL:
|
55
|
+
|
56
|
+
* sudo gem install snapshoter
|
57
|
+
* Configure /etc/snapshoter.yml
|
58
|
+
* create the following cron entry: 0 1 * * * /usr/bin/snapshoter
|
59
|
+
* OR: call the snapshoter script directly after some other work processing every day
|
60
|
+
|
61
|
+
== LICENSE:
|
62
|
+
|
63
|
+
(The MIT License)
|
64
|
+
|
65
|
+
Copyright (c) 2008 Kris Rasmussen
|
66
|
+
|
67
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
68
|
+
a copy of this software and associated documentation files (the
|
69
|
+
'Software'), to deal in the Software without restriction, including
|
70
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
71
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
72
|
+
permit persons to whom the Software is furnished to do so, subject to
|
73
|
+
the following conditions:
|
74
|
+
|
75
|
+
The above copyright notice and this permission notice shall be
|
76
|
+
included in all copies or substantial portions of the Software.
|
77
|
+
|
78
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
79
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
80
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
81
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
82
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
83
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
84
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |s|
|
7
|
+
s.name = "ebs-snapshoter"
|
8
|
+
s.executables = "snapshoter"
|
9
|
+
s.summary = "Provides EBS snapshot automation that can be configured and run on an EC2 instance."
|
10
|
+
s.email = "kristopher.rasmussen@gmail.com"
|
11
|
+
s.homepage = "http://github.com/krisr/ebs-snapshoter"
|
12
|
+
s.description = "Provides EBS snapshot automation that can be configured and run on an EC2 instance."
|
13
|
+
s.authors = ["Kris Rasmussen"]
|
14
|
+
s.files = FileList["[A-Z]*", "{bin,generators,lib,test}/**/*", 'lib/jeweler/templates/.gitignore']
|
15
|
+
s.add_dependency 'right_aws'
|
16
|
+
s.add_dependency 'mysql'
|
17
|
+
end
|
18
|
+
rescue LoadError
|
19
|
+
puts "Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
20
|
+
end
|
21
|
+
|
22
|
+
require 'lib/snapshoter'
|
23
|
+
|
24
|
+
task :default => 'spec:run'
|
25
|
+
|
26
|
+
require 'rake/testtask'
|
27
|
+
Rake::TestTask.new(:test) do |test|
|
28
|
+
test.libs << 'lib' << 'test'
|
29
|
+
test.pattern = 'test/**/test_*.rb'
|
30
|
+
test.verbose = true
|
31
|
+
end
|
32
|
+
|
33
|
+
begin
|
34
|
+
require 'rcov/rcovtask'
|
35
|
+
Rcov::RcovTask.new do |test|
|
36
|
+
test.libs << 'test'
|
37
|
+
test.pattern = 'test/**/test_*.rb'
|
38
|
+
test.verbose = true
|
39
|
+
end
|
40
|
+
rescue LoadError
|
41
|
+
task :rcov do
|
42
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
task :test => :check_dependencies
|
47
|
+
|
48
|
+
task :default => :test
|
49
|
+
|
50
|
+
require 'rake/rdoctask'
|
51
|
+
Rake::RDocTask.new do |rdoc|
|
52
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
53
|
+
|
54
|
+
rdoc.rdoc_dir = 'rdoc'
|
55
|
+
rdoc.title = "foo #{version}"
|
56
|
+
rdoc.rdoc_files.include('README*')
|
57
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
# EOF
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/bin/snapshoter
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require File.expand_path(
|
4
|
+
File.join(File.dirname(__FILE__), %w[.. lib snapshoter]))
|
5
|
+
|
6
|
+
require 'optparse'
|
7
|
+
|
8
|
+
# Options
|
9
|
+
# -------
|
10
|
+
|
11
|
+
config_file = '/etc/snapshoter.yml'
|
12
|
+
verbose = false
|
13
|
+
|
14
|
+
# Option Parsing
|
15
|
+
# --------------
|
16
|
+
|
17
|
+
opts = OptionParser.new { |opts|
|
18
|
+
opts.banner = "Usage: snapshoter [options]"
|
19
|
+
|
20
|
+
opts.separator ""
|
21
|
+
opts.separator "Specific options:"
|
22
|
+
|
23
|
+
opts.on("-c", "--config FILE",
|
24
|
+
"The configuration to use (default #{config_file})") do |file|
|
25
|
+
config_file = file
|
26
|
+
end
|
27
|
+
|
28
|
+
opts.on("-v", "--verbose", "Run with verbose logging") do
|
29
|
+
verbose = true
|
30
|
+
end
|
31
|
+
|
32
|
+
opts.separator ""
|
33
|
+
opts.separator "Common options:"
|
34
|
+
|
35
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
36
|
+
puts opts
|
37
|
+
exit
|
38
|
+
end
|
39
|
+
|
40
|
+
opts.on_tail("--version", "Show version") do
|
41
|
+
puts Snapshoter.version
|
42
|
+
exit
|
43
|
+
end
|
44
|
+
}.parse!(ARGV)
|
45
|
+
|
46
|
+
# Run
|
47
|
+
# ---
|
48
|
+
|
49
|
+
config = Snapshoter::Config.read(config_file)
|
50
|
+
provider = Snapshoter::Provider::EC2Provider.new(config)
|
51
|
+
manager = Snapshoter::Manager.new(config, provider)
|
52
|
+
manager.run
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
module Snapshoter
|
5
|
+
class ConfigError < Exception
|
6
|
+
end
|
7
|
+
|
8
|
+
class Config
|
9
|
+
attr_reader :aws_public_key, :aws_private_key, :volumes
|
10
|
+
|
11
|
+
def initialize(config)
|
12
|
+
@aws_public_key = config.delete('aws_public_key')
|
13
|
+
@aws_private_key = config.delete('aws_private_key')
|
14
|
+
@log_file = config.delete('log_file')
|
15
|
+
@log_level = config.delete('log_level')
|
16
|
+
|
17
|
+
@volumes = []
|
18
|
+
|
19
|
+
config.keys.each do |volume_id|
|
20
|
+
volume_options = config[volume_id]
|
21
|
+
if volume_options.is_a? Hash
|
22
|
+
begin
|
23
|
+
@volumes << Snapshoter::Volume.new(volume_id, config[volume_id])
|
24
|
+
rescue Snapshoter::VolumeInvalid => e
|
25
|
+
raise ConfigError.new(e.message)
|
26
|
+
end
|
27
|
+
config.delete(volume_id)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
if config.any?
|
32
|
+
raise ConfigError.new("Invalid config option(s) #{config.keys.map{|k| "'#{k}'"}.join(',')}")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def logger
|
37
|
+
@logger ||= begin
|
38
|
+
logger = Logger.new(@log_file || STDOUT)
|
39
|
+
logger.level = case @log_level
|
40
|
+
when 'warn'
|
41
|
+
Logger::WARN
|
42
|
+
when 'error'
|
43
|
+
Logger::ERROR
|
44
|
+
when 'FATAL'
|
45
|
+
Logger::FATAL
|
46
|
+
when 'debug'
|
47
|
+
Logger::DEBUG
|
48
|
+
else
|
49
|
+
Logger::INFO
|
50
|
+
end
|
51
|
+
logger
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def Config.read(path='/etc/snapshoter.yml')
|
56
|
+
config = YAML.load(File.read(path))
|
57
|
+
Config.new(config)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
require 'mysql'
|
2
|
+
|
3
|
+
module Snapshoter
|
4
|
+
class Manager
|
5
|
+
def initialize(config, provider)
|
6
|
+
@config = config
|
7
|
+
@provider = provider
|
8
|
+
end
|
9
|
+
|
10
|
+
def run
|
11
|
+
# ensure we have the latest snapshot data
|
12
|
+
@provider.refresh
|
13
|
+
|
14
|
+
# First create all the snapshots
|
15
|
+
@config.volumes.each do |volume|
|
16
|
+
if should_take_snapshot(volume)
|
17
|
+
if volume.freeze_mysql
|
18
|
+
mysql = Mysql.connect(
|
19
|
+
'localhost',
|
20
|
+
volume.mysql_user,
|
21
|
+
volume.mysql_password,
|
22
|
+
volume.mysql_port,
|
23
|
+
volume.mysql_sock)
|
24
|
+
else
|
25
|
+
mysql = nil
|
26
|
+
end
|
27
|
+
|
28
|
+
logger.debug "[Volume #{volume}] Starting snapshot"
|
29
|
+
success = false
|
30
|
+
begin
|
31
|
+
lock_mysql(mysql) if mysql
|
32
|
+
freeze_xfs(volume.mount_point)
|
33
|
+
@provider.snapshot_volume(volume)
|
34
|
+
rescue Exception => e
|
35
|
+
logger.error "[Volume #{volume}] There was a problem creating snapshot!"
|
36
|
+
logger.error e.message
|
37
|
+
logger.error e.backtrace.join("\n")
|
38
|
+
ensure
|
39
|
+
unfreeze_xfs(volume.mount_point)
|
40
|
+
if mysql
|
41
|
+
unlock_mysql(mysql)
|
42
|
+
mysql.close
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# ensure we have the latest snapshot data
|
49
|
+
@provider.refresh
|
50
|
+
|
51
|
+
# Then cleanup old snapshots
|
52
|
+
@config.volumes.each do |volume|
|
53
|
+
delete_old_snapshots(volume)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def logger
|
60
|
+
@config.logger
|
61
|
+
end
|
62
|
+
|
63
|
+
def freeze_xfs(mount_point)
|
64
|
+
logger.debug "Freezing filesystem at #{mount_point}"
|
65
|
+
`xfs_freeze -f #{mount_point}`
|
66
|
+
logger.debug "Filesystem frozen at #{mount_point}"
|
67
|
+
end
|
68
|
+
|
69
|
+
def unfreeze_xfs(mount_point)
|
70
|
+
logger.debug "unfreezing xfs on mount point #{mount_point}"
|
71
|
+
`xfs_freeze -u #{mount_point}`
|
72
|
+
end
|
73
|
+
|
74
|
+
def lock_mysql(mysql)
|
75
|
+
logger.debug "locking mysql tables"
|
76
|
+
mysql.query "FLUSH TABLES WITH READ LOCK"
|
77
|
+
# TODO: capture mysql master bin log position and log it somewhere
|
78
|
+
end
|
79
|
+
|
80
|
+
def unlock_mysql(mysql)
|
81
|
+
logger.debug "unfreezing mysql"
|
82
|
+
mysql.query "UNLOCK TABLES"
|
83
|
+
end
|
84
|
+
|
85
|
+
def delete_old_snapshots(volume)
|
86
|
+
logger.debug "Deleting old snapshots of volume #{volume}"
|
87
|
+
begin
|
88
|
+
n = @provider.delete_old_snapshots(volume)
|
89
|
+
logger.info "[Volume #{volume}] Deleted #{n} old snapshots" if n > 0
|
90
|
+
rescue Exception => e
|
91
|
+
logger.error "[Volume #{volume}] There was a problem deleting old snaphots"
|
92
|
+
logger.error e.message
|
93
|
+
logger.error e.backtrace.join("\n")
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def should_take_snapshot(volume)
|
98
|
+
if last_snapshot_at = @provider.last_snapshot_at(volume)
|
99
|
+
|
100
|
+
last = last_snapshot_at.localtime
|
101
|
+
now = Time.now
|
102
|
+
|
103
|
+
if volume.frequency == :hourly
|
104
|
+
time_of_last_snapshot = seconds_to_hours(last)
|
105
|
+
current_time = seconds_to_hours(now)
|
106
|
+
elsif volume.frequency == :daily
|
107
|
+
time_of_last_snapshot = seconds_to_days(last)
|
108
|
+
current_time = seconds_to_days(now)
|
109
|
+
elsif volume.frequency == :weekly
|
110
|
+
time_of_last_snapshot = seconds_to_weeks(last)
|
111
|
+
current_time = seconds_to_weeks(now)
|
112
|
+
else
|
113
|
+
raise "Unknown frequency #{volume.frequency}"
|
114
|
+
end
|
115
|
+
|
116
|
+
time_of_last_snapshot != current_time
|
117
|
+
else
|
118
|
+
true
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def seconds_to_hours(s)
|
123
|
+
s.strftime("%Y%m%d%H")
|
124
|
+
end
|
125
|
+
|
126
|
+
def seconds_to_days(s)
|
127
|
+
s.strftime("%Y%m%d")
|
128
|
+
end
|
129
|
+
|
130
|
+
def seconds_to_weeks(s)
|
131
|
+
s.strftime("%Y%m") + ((s.day - 1) / 7).to_s
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'right_aws'
|
2
|
+
|
3
|
+
module Snapshoter
|
4
|
+
module Provider
|
5
|
+
class EC2Provider
|
6
|
+
def initialize(config)
|
7
|
+
@config = config
|
8
|
+
end
|
9
|
+
|
10
|
+
# Returns the most recent time a snapshot was started for a given volume or nil if there are none
|
11
|
+
def last_snapshot_at(volume)
|
12
|
+
snapshots = snapshots_by_volume_id(volume.id)
|
13
|
+
snapshots.any? ? snapshots.first[:aws_started_at] : nil
|
14
|
+
end
|
15
|
+
|
16
|
+
def snapshot_volume(volume)
|
17
|
+
logger.info "[Volume #{volume.id}] Snapshot started"
|
18
|
+
if ec2.create_snapshot(volume.id)
|
19
|
+
logger.info "[Volume #{volume.id}] Snapshot complete"
|
20
|
+
else
|
21
|
+
logger.error "[Volume #{volume.id}] Snapshot failed!"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def delete_old_snapshots(volume)
|
26
|
+
deleted_count = 0
|
27
|
+
old_snapshots = snapshots_by_volume_id(volume.id)[volume.keep..-1]
|
28
|
+
if old_snapshots && old_snapshots.any?
|
29
|
+
logger.info "[Volume #{volume.id}] Deleting #{old_snapshots.length} old snapshots for #{volume.id}"
|
30
|
+
old_snapshots.each do |snapshot|
|
31
|
+
if snapshot[:aws_status] == 'completed'
|
32
|
+
logger.debug "[Volume #{volume.id}][Snapshot #{snapshot[:aws_id]}] Deleting snapshot created on #{snapshot[:aws_started_at]} #{snapshot[:aws_id]}"
|
33
|
+
if ec2.delete_snapshot(snapshot[:aws_id])
|
34
|
+
deleted_count += 1
|
35
|
+
logger.debug "[Volume #{volume.id}][Snapshot #{snapshot[:aws_id]}] Snapshot deleted"
|
36
|
+
else
|
37
|
+
logger.error "[Volume #{volume.id}][Snapshot #{snapshot.id}] Snapshot delete failed!"
|
38
|
+
end
|
39
|
+
else
|
40
|
+
logger.info "[Volume #{volume.id}][Snapshot #{snapshot.id}] Could not be deleted because status is '#{snapshot[:aws_status]}'"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
else
|
44
|
+
logger.debug "[Volume #{volume.id}] No old snapshots to delete"
|
45
|
+
end
|
46
|
+
deleted_count
|
47
|
+
end
|
48
|
+
|
49
|
+
def refresh
|
50
|
+
snapshots(true) if @snapshots
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def logger
|
56
|
+
@config.logger
|
57
|
+
end
|
58
|
+
|
59
|
+
def snapshots_by_volume_id(volume_id)
|
60
|
+
snapshots.select {|s| s[:aws_volume_id] == volume_id}.sort_by{|s| s[:aws_started_at]}.reverse
|
61
|
+
end
|
62
|
+
|
63
|
+
def snapshots(refresh=false)
|
64
|
+
if @snapshots && !refresh
|
65
|
+
@snapshots
|
66
|
+
else
|
67
|
+
@snapshots = ec2.describe_snapshots
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def ec2
|
72
|
+
@ec2 ||= RightAws::Ec2.new(@config.aws_public_key, @config.aws_private_key)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Snapshoter
|
2
|
+
class VolumeInvalid < Exception
|
3
|
+
def initialize(id, errors)
|
4
|
+
@id = id
|
5
|
+
@errors = errors
|
6
|
+
end
|
7
|
+
|
8
|
+
def message
|
9
|
+
"Error with configuration of volume #{@id || '<unknown>'}:\n #{@errors.join("\n ")}\n"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Volume
|
14
|
+
ValidFrequencies = [:hourly, :daily, :weekly]
|
15
|
+
|
16
|
+
attr_reader :id, :mount_point, :frequency, :freeze_mysql, :mysql_user, :mysql_password, :mysql_port, :mysql_sock, :keep
|
17
|
+
|
18
|
+
def initialize(volume_id, options={})
|
19
|
+
options = options.symbolize_keys
|
20
|
+
|
21
|
+
@id = volume_id
|
22
|
+
@mount_point = options.delete(:mount_point)
|
23
|
+
@frequency = options.delete(:frequency) || 'daily'
|
24
|
+
@freeze_mysql = options.delete(:freeze_mysql) || false
|
25
|
+
@mysql_user = options.delete(:mysql_user) || 'root'
|
26
|
+
@mysql_password = options.delete(:mysql_password)
|
27
|
+
@mysql_port = options.delete(:mysql_port)
|
28
|
+
@mysql_sock = options.delete(:mysql_sock)
|
29
|
+
@keep = options.delete(:keep) || 7
|
30
|
+
|
31
|
+
@frequency = @frequency.to_sym
|
32
|
+
|
33
|
+
validate!(options)
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_s
|
37
|
+
"##{@id}"
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def validate!(extra_options)
|
43
|
+
errors = []
|
44
|
+
|
45
|
+
errors << 'volume must have an id' if @id.nil?
|
46
|
+
errors << "frequency must be one of #{ValidFrequencies.join(",")}" unless ValidFrequencies.include?(@frequency)
|
47
|
+
errors << 'mount_point must be specified' if @mount_point.nil?
|
48
|
+
errors << 'freeze_mysql must be true or false' if @freeze_mysql != true && @freeze_mysql != false
|
49
|
+
errors << "keep must be greater than 0" if keep <= 0
|
50
|
+
|
51
|
+
extra_options.keys.each do |key|
|
52
|
+
errors << "unknown volume attribute #{key}"
|
53
|
+
end
|
54
|
+
|
55
|
+
if errors.any?
|
56
|
+
raise VolumeInvalid.new(@id, errors)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
data/lib/snapshoter.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
|
2
|
+
module Snapshoter
|
3
|
+
|
4
|
+
# :stopdoc:
|
5
|
+
LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
|
6
|
+
PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
|
7
|
+
# :startdoc:
|
8
|
+
|
9
|
+
# Returns the version string for the library.
|
10
|
+
#
|
11
|
+
def self.version
|
12
|
+
VERSION
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns the library path for the module. If any arguments are given,
|
16
|
+
# they will be joined to the end of the libray path using
|
17
|
+
# <tt>File.join</tt>.
|
18
|
+
#
|
19
|
+
def self.libpath( *args )
|
20
|
+
args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns the lpath for the module. If any arguments are given,
|
24
|
+
# they will be joined to the end of the path using
|
25
|
+
# <tt>File.join</tt>.
|
26
|
+
#
|
27
|
+
def self.path( *args )
|
28
|
+
args.empty? ? PATH : ::File.join(PATH, args.flatten)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Utility method used to require all files ending in .rb that lie in the
|
32
|
+
# directory below this file that has the same name as the filename passed
|
33
|
+
# in. Optionally, a specific _directory_ name can be passed in such that
|
34
|
+
# the _filename_ does not have to be equivalent to the directory.
|
35
|
+
#
|
36
|
+
def self.require_all_libs_relative_to( fname, dir = nil )
|
37
|
+
dir ||= ::File.basename(fname, '.*')
|
38
|
+
search_me = ::File.expand_path(
|
39
|
+
::File.join(::File.dirname(fname), dir, '**', '*.rb'))
|
40
|
+
|
41
|
+
Dir.glob(search_me).sort.each {|rb| require rb}
|
42
|
+
end
|
43
|
+
|
44
|
+
end # module Snapshoter
|
45
|
+
|
46
|
+
class Hash
|
47
|
+
def symbolize_keys
|
48
|
+
inject({}) do |options, (key, value)|
|
49
|
+
options[(key.to_sym rescue key) || key] = value
|
50
|
+
options
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def except!(*keys)
|
55
|
+
keys.map! { |key| convert_key(key) } if respond_to?(:convert_key)
|
56
|
+
keys.each { |key| delete(key) }
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
def except(*keys)
|
61
|
+
dup.except!(*keys)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
|
67
|
+
|
68
|
+
Snapshoter.require_all_libs_relative_to(__FILE__)
|
69
|
+
|
70
|
+
# EOF
|
@@ -0,0 +1,67 @@
|
|
1
|
+
|
2
|
+
require File.join(File.dirname(__FILE__), %w[spec_helper])
|
3
|
+
|
4
|
+
describe Snapshoter do
|
5
|
+
|
6
|
+
end
|
7
|
+
|
8
|
+
describe Snapshoter::Volume do
|
9
|
+
before do
|
10
|
+
@valid_attributes = {
|
11
|
+
'mount_point' => '/data',
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should initialize successfully with valid attributes" do
|
16
|
+
Snapshoter::Volume.new('vol-test', @valid_attributes)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should raise a Snapshoter::VolumeInvalid when initialized without an id" do
|
20
|
+
lambda {
|
21
|
+
Snapshoter::Volume.new(nil, @valid_attributes)
|
22
|
+
}.should raise_error Snapshoter::VolumeInvalid, /volume must have an id/
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should raise a Snapshoter::VolumeInvalid when the frequency is anything but hourly, daily or weekly" do
|
26
|
+
Snapshoter::Volume.new('vol-test', @valid_attributes.merge(:frequency => 'hourly'))
|
27
|
+
Snapshoter::Volume.new('vol-test', @valid_attributes.merge(:frequency => 'daily'))
|
28
|
+
Snapshoter::Volume.new('vol-test', @valid_attributes.merge(:frequency => 'weekly'))
|
29
|
+
|
30
|
+
lambda {
|
31
|
+
Snapshoter::Volume.new('vol-test', @valid_attributes.merge(:frequency => 'yearly'))
|
32
|
+
}.should raise_error Snapshoter::VolumeInvalid, /frequency must be one of/
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should raise a Snapshoter::VolumeInvalid when freeze_mysql is anything but true or false" do
|
36
|
+
Snapshoter::Volume.new('vol-test', @valid_attributes.merge(:freeze_mysql => true))
|
37
|
+
Snapshoter::Volume.new('vol-test', @valid_attributes.merge(:freeze_mysql => false))
|
38
|
+
|
39
|
+
lambda {
|
40
|
+
Snapshoter::Volume.new('vol-test', @valid_attributes.merge(:freeze_mysql => 33))
|
41
|
+
}.should raise_error Snapshoter::VolumeInvalid, /freeze_mysql must be/
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should raise a Snapshoter::VolumeInvalid when keep is less than or equal to 0" do
|
45
|
+
Snapshoter::Volume.new('vol-test', @valid_attributes.merge(:keep => 33))
|
46
|
+
|
47
|
+
lambda {
|
48
|
+
Snapshoter::Volume.new('vol-test', @valid_attributes.merge(:keep => 0))
|
49
|
+
}.should raise_error Snapshoter::VolumeInvalid, /keep must/
|
50
|
+
|
51
|
+
lambda {
|
52
|
+
Snapshoter::Volume.new('vol-test', @valid_attributes.merge(:keep => -2))
|
53
|
+
}.should raise_error Snapshoter::VolumeInvalid, /keep must/
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should raise a a Snapshoter::VolumeInvalid when initialized with an unknown option" do
|
57
|
+
lambda {
|
58
|
+
Snapshoter::Volume.new('vol-test', @valid_attributes.merge(:keep2 => 0))
|
59
|
+
}.should raise_error Snapshoter::VolumeInvalid, /unknown/
|
60
|
+
|
61
|
+
lambda {
|
62
|
+
Snapshoter::Volume.new('vol-test', @valid_attributes.merge('apple' => 0))
|
63
|
+
}.should raise_error Snapshoter::VolumeInvalid, /unknown/
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# EOF
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
|
2
|
+
require File.expand_path(
|
3
|
+
File.join(File.dirname(__FILE__), %w[.. lib snapshoter]))
|
4
|
+
|
5
|
+
Spec::Runner.configure do |config|
|
6
|
+
# == Mock Framework
|
7
|
+
#
|
8
|
+
# RSpec uses it's own mocking framework by default. If you prefer to
|
9
|
+
# use mocha, flexmock or RR, uncomment the appropriate line:
|
10
|
+
#
|
11
|
+
# config.mock_with :mocha
|
12
|
+
# config.mock_with :flexmock
|
13
|
+
# config.mock_with :rr
|
14
|
+
end
|
15
|
+
|
16
|
+
# EOF
|
File without changes
|
metadata
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ebs-snapshoter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
version: 0.1.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Kris Rasmussen
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-05-10 00:00:00 -07:00
|
18
|
+
default_executable: snapshoter
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: right_aws
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 0
|
29
|
+
version: "0"
|
30
|
+
type: :runtime
|
31
|
+
version_requirements: *id001
|
32
|
+
- !ruby/object:Gem::Dependency
|
33
|
+
name: mysql
|
34
|
+
prerelease: false
|
35
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
segments:
|
40
|
+
- 0
|
41
|
+
version: "0"
|
42
|
+
type: :runtime
|
43
|
+
version_requirements: *id002
|
44
|
+
description: Provides EBS snapshot automation that can be configured and run on an EC2 instance.
|
45
|
+
email: kristopher.rasmussen@gmail.com
|
46
|
+
executables:
|
47
|
+
- snapshoter
|
48
|
+
extensions: []
|
49
|
+
|
50
|
+
extra_rdoc_files:
|
51
|
+
- README.txt
|
52
|
+
files:
|
53
|
+
- History.txt
|
54
|
+
- README.txt
|
55
|
+
- Rakefile
|
56
|
+
- VERSION
|
57
|
+
- bin/snapshoter
|
58
|
+
- lib/snapshoter.rb
|
59
|
+
- lib/snapshoter/config.rb
|
60
|
+
- lib/snapshoter/manager.rb
|
61
|
+
- lib/snapshoter/provider/ec2.rb
|
62
|
+
- lib/snapshoter/volume.rb
|
63
|
+
- test/test_snapshoter.rb
|
64
|
+
has_rdoc: true
|
65
|
+
homepage: http://github.com/krisr/ebs-snapshoter
|
66
|
+
licenses: []
|
67
|
+
|
68
|
+
post_install_message:
|
69
|
+
rdoc_options:
|
70
|
+
- --charset=UTF-8
|
71
|
+
require_paths:
|
72
|
+
- lib
|
73
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
segments:
|
78
|
+
- 0
|
79
|
+
version: "0"
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
segments:
|
85
|
+
- 0
|
86
|
+
version: "0"
|
87
|
+
requirements: []
|
88
|
+
|
89
|
+
rubyforge_project:
|
90
|
+
rubygems_version: 1.3.6
|
91
|
+
signing_key:
|
92
|
+
specification_version: 3
|
93
|
+
summary: Provides EBS snapshot automation that can be configured and run on an EC2 instance.
|
94
|
+
test_files:
|
95
|
+
- spec/snapshoter_spec.rb
|
96
|
+
- spec/spec_helper.rb
|
97
|
+
- test/test_snapshoter.rb
|