snap-ebs 0.0.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +56 -0
- data/bin/snap-ebs +5 -0
- data/lib/plugins/mongo_plugin.rb +140 -0
- data/lib/plugins/mysql_plugin.rb +22 -0
- data/lib/snap_ebs/options.rb +50 -0
- data/lib/snap_ebs/plugin.rb +49 -0
- data/lib/snap_ebs/snapshotter.rb +80 -0
- data/lib/snap_ebs.rb +65 -0
- metadata +108 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 6d2d9b7df8d19e53288b89d957cc9c56d7942d23
|
4
|
+
data.tar.gz: 07e9374eae9478236ef061a582a050d349015a49
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5f8986b7b2a0be56714219a524d8ef65fa6746b2a78b93fae5350c0e8893fe721a93c24550a10e40b4f9c13a482c02add2bdbd79fab5b0cf4ef4f667a2faefa7
|
7
|
+
data.tar.gz: c46faf076355911857de4ca88dedf6649ac5ebaf7e54a1df8c961ba8bc341d7ac3c64fc244ac3b7c56ee0150e6cbe20ee5832e7f64b7747318c95319b4dac8f4
|
data/README.md
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
snap-ebsc2-ebs-automatic-consistent-snapshot
|
2
|
+
===
|
3
|
+
|
4
|
+
Easier to use than it is to say, this project aims to provide easy, automatic,
|
5
|
+
and consistent snapshots for AWS EBS volumes on EC2 instances.
|
6
|
+
|
7
|
+
Some specific goals and how they are achieved:
|
8
|
+
|
9
|
+
- *Safety*: refuses to operate unless everything seems ok, and tries desperately to leave your system no worse than it started
|
10
|
+
- *Reliability*: comprehensive test sweet makes sure that SnapEbs behaves as expected, even in unexpected conditions.
|
11
|
+
- *Visibility*: verbose logging options to inspect the decision-making process for any action
|
12
|
+
- *Ease of Installation*: just install the gem and add one line to your crontab
|
13
|
+
- *Ease of Use*: automatically detects volumes mounted to the machine
|
14
|
+
- *Ease of Monitoring*: 100% visibility of operation can be gained from off-the-shelf monitoring solution plugins
|
15
|
+
- *Maintainability*: well-organized code structure and a modern language
|
16
|
+
- *Extensibility*: plugin architecture makes it easy to add lock support for services
|
17
|
+
- *Isolation*: plugin execution is isolated, so that an error in one is very unlikely to affect the others
|
18
|
+
|
19
|
+
Install
|
20
|
+
===
|
21
|
+
|
22
|
+
Dependencies - Amazon Linux
|
23
|
+
---
|
24
|
+
|
25
|
+
```
|
26
|
+
sudo yum install gcc \
|
27
|
+
glibc-devel \
|
28
|
+
make \
|
29
|
+
mysql-devel \
|
30
|
+
patch \
|
31
|
+
ruby-devel \
|
32
|
+
zlib-devel
|
33
|
+
```
|
34
|
+
|
35
|
+
```
|
36
|
+
gem install snap-ebsc2-ebs-automatic-consistent-snapshot
|
37
|
+
crontab -e
|
38
|
+
```
|
39
|
+
|
40
|
+
Testing
|
41
|
+
===
|
42
|
+
|
43
|
+
Because you'll be running this against production servers with critical data, it's important that the functionality is well-tested. A thorough, pessimistic, multi-layer test suite hopes to assuage your concerns about letting a computer handle such an important task. The test suite is unquestionably the most complex part of this project.
|
44
|
+
|
45
|
+
Unit Tests
|
46
|
+
---
|
47
|
+
|
48
|
+
Like any good Ruby software, this tool has a unit test suite that seeks mostly to verify the plumbing and ensure that there are no runtime errors on the expected execution paths. This is only the tip of the iceberg...
|
49
|
+
|
50
|
+
Vagrant Integration Testing
|
51
|
+
---
|
52
|
+
|
53
|
+
The integration layer contains an Ansible + Vagrant setup to configure clusters of services for live-fire testing (the AWS bits are mocked out via SnapEbs's `--mock` flag). Simply running `vagrant up` will build a cluster of servers running MySQL, MongoDB, etc, configured in a master/slave architecture as approprite for the given system.
|
54
|
+
|
55
|
+
There is also a set of Ansible tasks that verify the operation of each plugin under **both ideal and pathological** conditions. This means that SnapEbs runs reliably, even when the services it operates on do not. Things like timeouts and service restart failures are modeled via `socat`, and assertions are made on the correct error output for each condition. For more info on how this is done, check `roles/integration-test/tasks/*.yml`
|
56
|
+
|
data/bin/snap-ebs
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
require 'pp'
|
2
|
+
class SnapEbs::Plugin::MongoPlugin < SnapEbs::Plugin
|
3
|
+
WIRED_TIGER_KEY = 'wiredTiger'
|
4
|
+
attr_accessor :client
|
5
|
+
def defined_options
|
6
|
+
{
|
7
|
+
service: 'Service to start after shutting down server',
|
8
|
+
shutdown: 'Shutdown mongodb server (this is required if your data and journal are on different volumes',
|
9
|
+
user: 'Mongo user',
|
10
|
+
password: 'Mongo password',
|
11
|
+
port: 'Mongo port',
|
12
|
+
host: 'Mongo host',
|
13
|
+
server_selection_timeout: 'Timeout in seconds while choosing a server to connect to (default 30)',
|
14
|
+
wait_queue_timeout: 'Timeout in seconds while waiting for a connection in the pool (default 1)',
|
15
|
+
connection_timeout: 'Timeout in seconds to wait for a socket to connect (default 5)',
|
16
|
+
socket_timeout: 'Timeout in seconds to wait for an operation to execute on a socket (default 5)'
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
def default_options
|
21
|
+
{
|
22
|
+
service: 'mongodb',
|
23
|
+
port: '27017',
|
24
|
+
shutdown: false,
|
25
|
+
host: 'localhost',
|
26
|
+
server_selection_timeout: 30,
|
27
|
+
wait_queue_timeout: 1,
|
28
|
+
connection_timeout: 5,
|
29
|
+
socket_timeout: 5
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def client
|
34
|
+
@client ||= Mongo::Client.new [ "#{options.host}:#{options.port}" ], client_options
|
35
|
+
end
|
36
|
+
|
37
|
+
def client_options
|
38
|
+
{
|
39
|
+
connect: :direct,
|
40
|
+
user: options.user,
|
41
|
+
password: options.password,
|
42
|
+
server_selection_timeout: options.server_selection_timeout.to_i,
|
43
|
+
wait_queue_timeout: options.wait_queue_timeout.to_i,
|
44
|
+
connection_timeout: options.connection_timeout.to_i,
|
45
|
+
socket_timeout: options.socket_timeout.to_i
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
def before
|
50
|
+
require 'mongo'
|
51
|
+
Mongo::Logger.logger = logger
|
52
|
+
return unless safe_to_operate?
|
53
|
+
|
54
|
+
if wired_tiger?
|
55
|
+
logger.info "Wired Tiger storage engine detected"
|
56
|
+
carefully('stop mongo') { stop_mongo } if options.shutdown
|
57
|
+
else
|
58
|
+
logger.info "MMAPv1 storage engine detected"
|
59
|
+
carefully('lock mongo') { lock_mongo }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def after
|
64
|
+
if wired_tiger?
|
65
|
+
carefully('start mongo') { start_mongo } if options.shutdown
|
66
|
+
else
|
67
|
+
carefully('unlock mongo') { unlock_mongo }
|
68
|
+
end
|
69
|
+
|
70
|
+
if carefully('check that mongo is still accessible') { client.command(serverStatus: 1).first }
|
71
|
+
logger.info "Received status from mongo, everything appears to be ok"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def name
|
76
|
+
"Mongo"
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def safe_to_operate?
|
82
|
+
# we check for strict equality with booleans here, because nil means an
|
83
|
+
# error occurred while checking, and it is unsafe to operate
|
84
|
+
return true if (primary? == false) or (standalone? == true)
|
85
|
+
logger.error "This appears to be a primary member, refusing to operate"
|
86
|
+
false
|
87
|
+
end
|
88
|
+
|
89
|
+
def wired_tiger?
|
90
|
+
if @wired_tiger.nil?
|
91
|
+
@wired_tiger = client.command(serverStatus: 1).first.has_key? WIRED_TIGER_KEY
|
92
|
+
end
|
93
|
+
@wired_tiger
|
94
|
+
end
|
95
|
+
|
96
|
+
def primary?
|
97
|
+
carefully 'check whether this node is a primary' do
|
98
|
+
if @primary.nil?
|
99
|
+
@primary = client.command(isMaster: 1).first['ismaster']
|
100
|
+
end
|
101
|
+
@primary
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def standalone?
|
106
|
+
# this will raise an error on a non-RS mongod
|
107
|
+
client.command(replSetGetStatus: 1)
|
108
|
+
false
|
109
|
+
rescue
|
110
|
+
true
|
111
|
+
end
|
112
|
+
|
113
|
+
def stop_mongo
|
114
|
+
logger.info 'Stopping mongodb'
|
115
|
+
begin
|
116
|
+
# this will always raise an exception after it completes
|
117
|
+
client.command shutdown: 1
|
118
|
+
rescue Mongo::Error::SocketError => e
|
119
|
+
logger.debug "Received expected socket error after shutting down"
|
120
|
+
end
|
121
|
+
|
122
|
+
# we need a new connection now since the server has shut down
|
123
|
+
@client = nil
|
124
|
+
end
|
125
|
+
|
126
|
+
def start_mongo
|
127
|
+
logger.info "Starting mongodb via 'service #{options[:service]} start'"
|
128
|
+
system "service #{options[:service]} start"
|
129
|
+
end
|
130
|
+
|
131
|
+
def lock_mongo
|
132
|
+
logger.info "Locking mongo"
|
133
|
+
client.command(fsync: 1, lock: true)
|
134
|
+
end
|
135
|
+
|
136
|
+
def unlock_mongo
|
137
|
+
logger.info "Unlocking mongo"
|
138
|
+
client.database['$cmd.sys.unlock'].find().read
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class SnapEbs::Plugin::MysqlPlugin < SnapEbs::Plugin
|
2
|
+
def defined_options
|
3
|
+
{
|
4
|
+
user: 'MySql Username',
|
5
|
+
pass: 'MySql Password',
|
6
|
+
port: 'MySql port',
|
7
|
+
host: 'MySql host'
|
8
|
+
}
|
9
|
+
end
|
10
|
+
|
11
|
+
def before
|
12
|
+
require 'mysql'
|
13
|
+
Mysql.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def after
|
17
|
+
end
|
18
|
+
|
19
|
+
def name
|
20
|
+
"Mysql"
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
class SnapEbs
|
4
|
+
module Options
|
5
|
+
def option_parser
|
6
|
+
unless @option_parser
|
7
|
+
@option_parser = OptionParser.new do |o|
|
8
|
+
o.banner = "Usage: #{$0} [options]"
|
9
|
+
|
10
|
+
o.on("-v", "--[no-]verbose", "Run verbosely") do |val|
|
11
|
+
options[:verbose] = val
|
12
|
+
end
|
13
|
+
|
14
|
+
o.on("-a", "--access-key <AWS ACCESS KEY>", "AWS access key") do |val|
|
15
|
+
options[:access_key] = val
|
16
|
+
end
|
17
|
+
|
18
|
+
o.on("-s", "--secret-key <AWS SECRET KEY>", "AWS secret key") do |val|
|
19
|
+
options[:secret_key] = val
|
20
|
+
end
|
21
|
+
|
22
|
+
o.on("-c", "--credentials-file <FILE>", "Load AWS credentials from the downloaded CSV file (overrides -a and -s)") do |val|
|
23
|
+
options[:credentials_file] = val
|
24
|
+
end
|
25
|
+
|
26
|
+
o.on("-m", "--[no-]mock", "Mock out AWS calls for testing in Vagrant") do |val|
|
27
|
+
options[:mock] = val
|
28
|
+
end
|
29
|
+
|
30
|
+
o.on("-l", "--logfile FILE", "Path to a file used for logging") do |filename|
|
31
|
+
options.logfile = filename
|
32
|
+
logger.debug filename
|
33
|
+
end
|
34
|
+
|
35
|
+
o.on("-d", "--directory PATH", "Only snap volumes mounted to PATH, a comma-separated list of directories") do |d|
|
36
|
+
options.directory = d
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
plugins.each { |plugin| plugin.collect_options @option_parser }
|
41
|
+
end
|
42
|
+
|
43
|
+
@option_parser
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def options
|
48
|
+
@options ||= OpenStruct.new
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
class SnapEbs::Plugin
|
3
|
+
@@registered_plugins = []
|
4
|
+
|
5
|
+
attr_reader :options, :logger
|
6
|
+
|
7
|
+
def self.inherited(klass)
|
8
|
+
registered_plugins.unshift klass
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.registered_plugins
|
12
|
+
@@registered_plugins
|
13
|
+
end
|
14
|
+
|
15
|
+
def default_options
|
16
|
+
{ }
|
17
|
+
end
|
18
|
+
|
19
|
+
def options
|
20
|
+
@options ||= OpenStruct.new default_options
|
21
|
+
end
|
22
|
+
|
23
|
+
def logger
|
24
|
+
SnapEbs.logger false
|
25
|
+
end
|
26
|
+
|
27
|
+
def collect_options option_parser
|
28
|
+
option_parser.on "--#{name.downcase}", "Enable the #{name} plugin" do
|
29
|
+
options.enable = true
|
30
|
+
end
|
31
|
+
|
32
|
+
defined_options.each do |option_name, description|
|
33
|
+
option_parser.on "--#{name.downcase}-#{option_name.to_s.gsub('_','-')} #{option_name.upcase}", description do |val|
|
34
|
+
options[option_name.to_sym] = val
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def carefully msg
|
40
|
+
yield
|
41
|
+
rescue Exception => e
|
42
|
+
logger.error "Error while trying to #{msg}"
|
43
|
+
logger.error e
|
44
|
+
nil
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
require 'plugins/mysql_plugin'
|
49
|
+
require 'plugins/mongo_plugin'
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'csv'
|
2
|
+
require 'httparty'
|
3
|
+
module SnapEbs::Snapshotter
|
4
|
+
AWS_INSTANCE_ID_URL = 'http://169.254.169.254/latest/dynamic/instance-identity/document'
|
5
|
+
|
6
|
+
def take_snapshots
|
7
|
+
attached_volumes.collect do |vol|
|
8
|
+
next unless should_snap vol
|
9
|
+
logger.debug "Snapping #{vol.id}"
|
10
|
+
snapshot = compute.snapshots.new
|
11
|
+
snapshot.volume_id = vol.id
|
12
|
+
snapshot.description = snapshot_name(vol)
|
13
|
+
snapshot.save
|
14
|
+
snapshot
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# lazy loaders
|
19
|
+
def compute
|
20
|
+
require 'fog/aws'
|
21
|
+
if options[:mock]
|
22
|
+
Fog.mock!
|
23
|
+
@region = 'us-east-1'
|
24
|
+
@instance_id = 'i-deadbeef'
|
25
|
+
@instance_name = 'totally-not-the-cia'
|
26
|
+
end
|
27
|
+
|
28
|
+
@compute ||= Fog::Compute.new({
|
29
|
+
:aws_access_key_id => access_key,
|
30
|
+
:aws_secret_access_key => secret_key,
|
31
|
+
:region => region,
|
32
|
+
:provider => "AWS"
|
33
|
+
})
|
34
|
+
end
|
35
|
+
|
36
|
+
def attached_volumes
|
37
|
+
@attached_volumes ||= compute.volumes.select { |vol| vol.server_id == instance_id }
|
38
|
+
end
|
39
|
+
|
40
|
+
def access_key
|
41
|
+
@access_key ||= if options[:credentials_file] then credentials.first["Access Key Id"] else options[:access_key] end
|
42
|
+
end
|
43
|
+
|
44
|
+
def secret_key
|
45
|
+
@secret_key ||= if options[:credentials_file] then credentials.first["Secret Access Key"] else options[:secret_key] end
|
46
|
+
end
|
47
|
+
|
48
|
+
def credentials
|
49
|
+
@credentials ||= CSV.parse(File.read(options[:credentials_file]), :headers => true)
|
50
|
+
end
|
51
|
+
|
52
|
+
def instance_id
|
53
|
+
@instance_id ||= JSON.parse(HTTParty.get(AWS_INSTANCE_ID_URL))["instanceId"]
|
54
|
+
end
|
55
|
+
|
56
|
+
def instance_name
|
57
|
+
@instance_name ||= compute.servers.get(instance_id).tags['Name']
|
58
|
+
end
|
59
|
+
|
60
|
+
def region
|
61
|
+
@region ||= JSON.parse(HTTParty.get(AWS_INSTANCE_ID_URL))["region"]
|
62
|
+
end
|
63
|
+
|
64
|
+
def snapshot_name vol
|
65
|
+
id = instance_name
|
66
|
+
id = instance_id if id.nil? or id.empty?
|
67
|
+
"#{Time.now.strftime "%Y%m%d%H%M%S"}-#{id}-#{vol.device}"
|
68
|
+
end
|
69
|
+
|
70
|
+
def should_snap vol
|
71
|
+
normalized_device = vol.device.gsub('/dev/s', '/dev/xv') rescue vol.device
|
72
|
+
options.directory.nil? or devices_to_snap.include?(normalized_device)
|
73
|
+
end
|
74
|
+
|
75
|
+
def devices_to_snap
|
76
|
+
@devices_to_snap ||= options.directory.split(',').map { |dir| `df --output=source #{dir} | grep dev`.strip }
|
77
|
+
logger.debug @devices_to_snap
|
78
|
+
@devices_to_snap
|
79
|
+
end
|
80
|
+
end
|
data/lib/snap_ebs.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
$:.unshift File.dirname __FILE__
|
2
|
+
require 'logger'
|
3
|
+
require 'ostruct'
|
4
|
+
require 'snap_ebs/options'
|
5
|
+
require 'snap_ebs/snapshotter'
|
6
|
+
require 'snap_ebs/plugin'
|
7
|
+
|
8
|
+
class SnapEbs
|
9
|
+
include SnapEbs::Options
|
10
|
+
include SnapEbs::Snapshotter
|
11
|
+
|
12
|
+
@@logger = nil
|
13
|
+
def self.logger logfile
|
14
|
+
unless @@logger
|
15
|
+
@@logger = Logger.new(logfile || STDOUT)
|
16
|
+
@@logger.level = Logger::DEBUG
|
17
|
+
@@logger.formatter = proc do |severity, datetime, progname, msg|
|
18
|
+
"[#{severity}] #{datetime.strftime("%Y-%m-%d %H:%M:%S")} #{msg}\n"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
@@logger
|
23
|
+
end
|
24
|
+
|
25
|
+
def plugins
|
26
|
+
@plugins ||= registered_plugins.collect { |klass| klass.new }
|
27
|
+
end
|
28
|
+
|
29
|
+
def registered_plugins
|
30
|
+
SnapEbs::Plugin.registered_plugins
|
31
|
+
end
|
32
|
+
|
33
|
+
def run
|
34
|
+
plugins.each do |plugin|
|
35
|
+
begin
|
36
|
+
plugin.before if plugin.options.enable
|
37
|
+
rescue Exception => e
|
38
|
+
logger.error "Encountered error while running the #{plugin.name} plugin's before hook"
|
39
|
+
logger.error e
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
take_snapshots
|
44
|
+
|
45
|
+
plugins.each do |plugin|
|
46
|
+
begin
|
47
|
+
plugin.after if plugin.options.enable
|
48
|
+
rescue Exception => e
|
49
|
+
logger.error "Encountered error while running the #{plugin.name} plugin's after hook"
|
50
|
+
logger.error e
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def execute
|
56
|
+
option_parser.parse!
|
57
|
+
logger.debug "Debug logging enabled"
|
58
|
+
run
|
59
|
+
end
|
60
|
+
|
61
|
+
def logger
|
62
|
+
# HACK -- the logfile argument only gets used on the first invocation
|
63
|
+
SnapEbs.logger options.logfile
|
64
|
+
end
|
65
|
+
end
|
metadata
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: snap-ebs
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.8
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Bryan Conrad
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-06-26 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: fog
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.31'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.31'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: httparty
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.13'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.13'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: mysql
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '2.9'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '2.9'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: mongo
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '2.0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '2.0'
|
69
|
+
description: Easy EBS snapshots that work
|
70
|
+
email: bryan.conrad@synctree.com
|
71
|
+
executables:
|
72
|
+
- snap-ebs
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- README.md
|
77
|
+
- bin/snap-ebs
|
78
|
+
- lib/plugins/mongo_plugin.rb
|
79
|
+
- lib/plugins/mysql_plugin.rb
|
80
|
+
- lib/snap_ebs.rb
|
81
|
+
- lib/snap_ebs/options.rb
|
82
|
+
- lib/snap_ebs/plugin.rb
|
83
|
+
- lib/snap_ebs/snapshotter.rb
|
84
|
+
homepage: http://rubygems.org/gems/snap-ebs
|
85
|
+
licenses:
|
86
|
+
- MIT
|
87
|
+
metadata: {}
|
88
|
+
post_install_message:
|
89
|
+
rdoc_options: []
|
90
|
+
require_paths:
|
91
|
+
- lib
|
92
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - ">="
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
requirements: []
|
103
|
+
rubyforge_project:
|
104
|
+
rubygems_version: 2.4.6
|
105
|
+
signing_key:
|
106
|
+
specification_version: 4
|
107
|
+
summary: Easy EBS snapshots that work
|
108
|
+
test_files: []
|