ebs-snapshoter 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/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