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 ADDED
@@ -0,0 +1,4 @@
1
+ == 1.0.0 / 2009-04-12
2
+
3
+ * 1 major enhancement
4
+ * Birthday!
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
@@ -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