drebs 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ NDE5YTM0ZDEwMWJlNmZiMmY4NzQ2ZjhjOTI4NWQ0ODc4ZWExNzRmNg==
5
+ data.tar.gz: !binary |-
6
+ NDhlYmU3ZWFiYThiZTdiYmYxOTZiZjRiMWQ0NDM0YjEyN2VkZmM2Zg==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ NTlhYzQ1NWVhYzliOWE3YTlkNmNlMDlkMjQ2ZTdjNjY1NzE1OTViZjA4NDRi
10
+ ZDQ0MTM1NDFiZGI1YTFmYTAwNDBkMWQwY2Y2NTJiODZjNTY3MmM2NzI0MzA1
11
+ MGRiOTE0M2VkMzAwYTY4ODlmM2IxMzFjYmZiMDUyZGE3OTNhY2Q=
12
+ data.tar.gz: !binary |-
13
+ ZTAxZTc1NzUwNjllNTlmMzgzYTMwODQ0NTNjOWE2ZGNlYTdlY2I3MzkyYWU2
14
+ NjJjOWY0NTE0ZGE3NWE4NDAyZmUxNjBlYjVlYWQxODM5MTVlNGRkZWQ3Yzg3
15
+ NjRhZTM0NDFmY2UwOTk2YTI0MGQyMTVhMDA0YzAwMzUxNmY0MTg=
data/README.md CHANGED
@@ -8,14 +8,39 @@
8
8
 
9
9
  ## Installation & Setup
10
10
  1. Clone the repo or install the gem
11
- 1. Currently configuration is located at the top of the drebs bin. Add you ec2 key, etc.
12
- 1. Add Crontab entry: 0 * * * * drebs
13
-
14
- ## Issues
15
- * State including config is cached in drebs_state.json. This file will need to be deleted to get drebs to pick up new config. Doing so will orphan current snapshots which will need to be deleted manually.
11
+ 1. Output the example configuration to a file: drebs check_config example_config > your_config.yml
12
+ 1. Create an AWS account with authorization limited to create, list, & delete snapshots (Example comming soon)
13
+ 1. Add AWS API keys for above account to your_config.yml
14
+ 1. configure your_config.yml per your backup requirements
15
+ 1. test your configuration: drebs check_config your_config.yml && drebs check_cloud your_config.yml
16
+ 1. Add Crontab entry: 0 * * * * drebs execute your_config.yml
16
17
 
17
18
  ## Todo
18
- * Tests!
19
- * Refactor using main with db for state and external config
19
+ * Improve test coverage
20
20
  * Use Whenever gem for crontab setup
21
21
  * Arbitrary execution intervals (Snapshots every 5 minutes instead of every hour)
22
+ * AWS API keys and other config values from Instance Data: http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AESDG-chapter-instancedata.html
23
+ * Add example AWS user access configuration
24
+ * Implement support for pruning to occur from some other host so that the keys to delete snapshots are not available on host.
25
+
26
+ ## Testing notes
27
+
28
+ * __shell command__: If you do: `drebs shell some_config` you will end up at a shell with `@drebs` defined and you will be able to access `@drebs.db`, `@drebs.config`, & `@drebs.cloud`. If you set `@drebs.cloud` to be an instance of TestCloud from the test suite you should be able to execute various functions without actually hitting AWS and so work from your dev box.
29
+
30
+ * Due to the nature of drebs being designed to be run from an ec2 you will need to be on your ec2 instance to test many of the AWS interactions.
31
+
32
+ * You should be able to verify data on a snapshot by creating an ebs volume from the snapshot, attaching the volume to your instance and then mounting its file system on some mount point - [aws docs on using volumes](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-using-volumes.html)
33
+
34
+ ## Copyright 2014 [dojo4](www.dojo4.com)
35
+
36
+ Licensed under the Apache License, Version 2.0 (the "License");
37
+ you may not use this file except in compliance with the License.
38
+ You may obtain a copy of the License at
39
+
40
+ http://www.apache.org/licenses/LICENSE-2.0
41
+
42
+ Unless required by applicable law or agreed to in writing, software
43
+ distributed under the License is distributed on an "AS IS" BASIS,
44
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
45
+ See the License for the specific language governing permissions and
46
+ limitations under the License.
data/Rakefile CHANGED
@@ -1,8 +1,8 @@
1
1
  This.rubyforge_project = 'DREBS'
2
- This.author = "Garett Shulman"
3
- This.email = "garett@dojo4.com"
2
+ This.authors = ["Garett Shulman", "Miles Matthias"]
3
+ This.email = "miles@dojo4.com"
4
4
  This.homepage = "https://github.com/dojo4/#{ This.lib }"
5
-
5
+ This.licenses = "Apache-2.0"
6
6
 
7
7
  task :default do
8
8
  puts((Rake::Task.tasks.map{|task| task.name.gsub(/::/,':')} - ['default']).sort)
@@ -27,6 +27,7 @@ def run_tests!(which = nil)
27
27
  div = ('=' * 119)
28
28
  line = ('-' * 119)
29
29
 
30
+ puts "running #{ test_rbs.count } test files..."
30
31
  test_rbs.each_with_index do |test_rb, index|
31
32
  testno = index + 1
32
33
  command = "#{ File.basename(This.ruby) } -I ./lib -I ./test/lib #{ test_rb }"
@@ -56,7 +57,6 @@ def run_tests!(which = nil)
56
57
  end
57
58
  end
58
59
 
59
-
60
60
  task :gemspec do
61
61
  ignore_extensions = ['git', 'svn', 'tmp', /sw./, 'bak', 'gem']
62
62
  ignore_directories = ['pkg', 'db']
@@ -89,7 +89,7 @@ task :gemspec do
89
89
  #has_rdoc = true #File.exist?('doc')
90
90
  test_files = test(?e, "test/#{ lib }.rb") ? "test/#{ lib }.rb" : nil
91
91
  summary = object.respond_to?(:summary) ? object.summary : "summary: #{ lib } kicks the ass"
92
- description = object.respond_to?(:description) ? object.description : "description: #{ lib } kicks the ass"
92
+ description = object.respond_to?(:description) ? object.description : "#{ lib }: Disaster Recovery for Elastic Block Store. An AWS EBS backup script."
93
93
 
94
94
  if This.extensions.nil?
95
95
  This.extensions = []
@@ -141,9 +141,10 @@ spec.add_dependency(*<%= Array(lib_version).flatten.inspect %>)
141
141
  spec.extensions.push(*<%= extensions.inspect %>)
142
142
 
143
143
  spec.rubyforge_project = <%= This.rubyforge_project.inspect %>
144
- spec.author = <%= This.author.inspect %>
144
+ spec.authors = <%= This.authors.inspect %>
145
145
  spec.email = <%= This.email.inspect %>
146
146
  spec.homepage = <%= This.homepage.inspect %>
147
+ spec.licenses = <%= This.licenses.inspect %>
147
148
  end
148
149
  __
149
150
  }
data/bin/drebs CHANGED
@@ -1,249 +1,129 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'rubygems'
4
- require 'right_aws'
5
- require 'logger'
6
4
  require 'main'
5
+ require 'logger'
7
6
  require 'systemu'
8
- require 'json'
7
+ require 'yaml'
9
8
  require 'socket'
10
9
  require 'net/smtp'
10
+ require 'pry'
11
11
 
12
- class DREBS
13
-
14
- ###### Begin User Config
15
- DREBS_HOST_NAME = 'An identifier for your host: www1'
16
- AWS_ACCESS_KEY_ID = 'YOUR ACCESS KEY ID'
17
- AWS_SECRET_ACCESS_KEY = 'YOUR SECRET ACCESS KEY'
18
- REGION = 'us-west-1'
19
- BACKUP_STRATEGY = [
20
- {
21
- 'hours_between'=>1, 'num_to_keep'=>5,
22
- 'mount_point'=>'/dev/sdh',
23
- 'pre_snapshot_tasks'=> [
24
- 'pg_dump some_app_production > /path/to/backups/on/snapshoted/volume/some_app_production.sql',
25
- 'mongodump -d another_app-production -o /path/to/backups/on/snapshoted/volume/'
26
- ]
27
- },
28
- {
29
- 'hours_between'=>6, 'num_to_keep'=>4,
30
- 'mount_point'=>'/dev/sdh',
31
- 'pre_snapshot_tasks'=> []
32
- },
33
- {
34
- 'hours_between'=>24, 'num_to_keep'=>4,
35
- 'mount_point'=>'/dev/sda1',
36
- 'pre_snapshot_tasks'=> []
37
- },
38
- {
39
- 'hours_between'=>96, 'num_to_keep'=>4,
40
- 'mount_point'=>'/dev/sda1',
41
- 'pre_snapshot_tasks'=> []
42
- }
43
- ]
44
- LOG_PATH = '/usr/local/var/drebs.log'
45
- BACKUP_STATE_FILE_PATH = '/usr/local/var/drebs_state.json'
46
- EMAIL_ON_EXCEPTION = 'admin@your.org'
47
- EMAIL_HOST = 'imap.gmail.com'
48
- EMAIL_PORT = 993
49
- EMAIL_USERNAME = 'your smpt username'
50
- EMAIL_PASSWORD = 'your smpt password'
51
- ###### End User Config
52
-
53
- def initialize(options = {})
54
- @drebs_host_name = options[:drebs_host_name] || options['drebs_host_name'] || DREBS_HOST_NAME
55
- @aws_access_key_id = options[:aws_access_key_id] || options['aws_access_key_id'] || AWS_ACCESS_KEY_ID
56
- @aws_secret_access_key = options[:aws_secret_access_key] || options['aws_secret_access_key'] || AWS_SECRET_ACCESS_KEY
57
- @region = options[:region] || options['region'] || REGION
58
- @backup_strategy = options[:backup_strategy] || options['backup_strategy'] || BACKUP_STRATEGY
59
- @log_path = options[:log_path] || options['log_path'] || LOG_PATH
60
- @backup_state_file_path = options[:backup_state_file_path] || options['backup_state_file_path'] || BACKUP_STATE_FILE_PATH
61
- @email_on_exception = options[:email_on_exception] || options['email_on_exception'] || EMAIL_ON_EXCEPTION
62
- @log = Logger.new(@log_path, 0, 10 * 1024 * 1024)
63
- end
64
-
65
- def setup_backup_data(backup_strategy=@backup_strategy)
66
- backup_data = Marshal.load(Marshal.dump(@backup_strategy))
67
- backup_data.collect{|a_backup_strategy|
68
- a_backup_strategy['hours_until_next_run'] = a_backup_strategy['hours_between']
69
- a_backup_strategy['previous_snapshots'] = []
70
- }
71
- return backup_data
72
- end
73
-
74
- def save_backup_data()
75
- open @backup_state_file_path, "w" do |h|
76
- h.write(@backup_data.to_json)
12
+ Main {
13
+
14
+ argument('config_path'){
15
+ argument :optional
16
+ description "A configuration file path. Passing 'example_config' uses an example configuration. Give it a try: >drebs check_config example_config"
17
+ }
18
+
19
+ mode 'execute' do
20
+ def run
21
+ setup
22
+ @drebs.execute
77
23
  end
78
24
  end
79
-
80
- def load_backup_data()
81
- @backup_data = JSON.parse(open(@backup_state_file_path).read)
25
+
26
+ # mode 'install_crontab' do
27
+ # def run
28
+ # setup
29
+ # @drebs.install_crontab
30
+ # end
31
+ # end
32
+
33
+ mode 'check_config' do
34
+ def run
35
+ setup
36
+ puts("No config errors found!")
37
+ end
82
38
  end
83
-
84
- def ec2(key_id=@aws_access_key_id, key=@aws_secret_access_key, region=@region)
85
- return RightAws::Ec2.new(key_id, key, {:region=>region})
39
+
40
+ mode 'check_cloud' do
41
+ def run
42
+ setup
43
+ @drebs.check_cloud
44
+ end
86
45
  end
87
-
88
- def find_local_instance()
89
- private_ip = UDPSocket.open {|s| s.connect("8.8.8.8", 1); s.addr.last}
90
- ec2.describe_instances.each {|instance|
91
- return instance if instance[:private_ip_address] == private_ip
92
- }
93
- return nil
46
+
47
+ mode 'shell' do
48
+ def run
49
+ setup
50
+ binding.pry(:hooks => Pry::Hooks.new, :prompt => proc{|*a| "drebs>"})
51
+ end
94
52
  end
95
-
96
- def find_local_ebs(mount_point='/dev/sdh')
97
- return nil if not local_instance = find_local_instance
98
- local_instance[:block_device_mappings].each {|volume|
99
- return volume if volume[:device_name] == mount_point
100
- }
101
- return nil
53
+
54
+ def run
55
+ help!
102
56
  end
103
-
104
- def get_snapshot(snapshot_id)
105
- ec2.describe_snapshots {|a_snapshot|
106
- return a_snapshot if a_snapshot[:aws_id] == snapshot_id
107
- }
57
+
58
+ db {
59
+ create_table :strategies do
60
+ String :config
61
+ String :snapshots
62
+ String :status
63
+ String :time_til_next_run
64
+ String :time_between_runs
65
+ String :num_to_keep
66
+ String :pre_snapshot_tasks
67
+ String :post_snapshot_tasks
68
+ String :mount_point
69
+ end unless table_exists? :strategies
70
+ }
71
+
72
+ def setup()
73
+ unless config_path = params['config_path'].value
74
+ raise 'Please provide a value for config_path.'
75
+ end
76
+ @config = load_config(config_path)
77
+
78
+ require File.join(lib_dir(), 'drebs', 'cloud.rb')
79
+ require File.join(lib_dir(), 'drebs', 'main.rb')
80
+
81
+ config_errors = Drebs::Main.check_config(example_config('print' => false), @config)
82
+ if config_errors.length == 0
83
+ @drebs = Drebs::Main.new('config' => @config, 'db' => db)
84
+ else
85
+ config_errors.each{|config_error| puts(config_error)}
86
+ end
108
87
  end
109
-
110
- def create_local_snapshot(pre_snapshot_tasks=nil, post_snapshot_tasks=nil, mount_point='/dev/sdh')
111
- local_instance=find_local_instance
112
- ip = local_instance[:ip_address]
113
- instance_id = local_instance[:aws_instance_id]
114
- volume_id = local_instance[:block_device_mappings].select{|m| m[:device_name]==mount_point}.first[:ebs_volume_id]
115
- return nil if not ebs = find_local_ebs(mount_point)
116
- pre_snapshot_tasks.each do |task|
117
- result = systemu(task)
118
- unless result[0].exitstatus == 0
119
- error_string = "Error while executing pre-snapshot task: #{task} on #{@drebs_host_name} #{ip}:#{mount_point} #{instance_id}:#{volume_id} #{result[1]} #{result[2]}"
120
- @log.error(error_string)
121
- send_email("DREBS Error!", error_string)
88
+
89
+ def base_dir(*args, &block)
90
+ basedir = File.dirname(File.expand_path(__FILE__))
91
+ File.join(basedir, *args)
92
+ ensure
93
+ if block
94
+ begin
95
+ $LOAD_PATH.unshift(basedir)
96
+ block.call()
97
+ ensure
98
+ $LOAD_PATH.shift()
122
99
  end
123
- end if pre_snapshot_tasks
124
- snapshot = ec2.create_snapshot(ebs[:ebs_volume_id], "DREBS #{@drebs_host_name} #{ip}:#{mount_point} #{instance_id}:#{volume_id}")
125
- Thread.new(snapshot[:aws_id], post_snapshot_tasks) {|snapshot_id, post_snapshot_tasks|
126
- 1.upto(500) {|a|
127
- sleep(3)
128
- break if get_snapshot(snapshot_id)[:aws_status] == 'completed'
129
- }
130
- post_snapshot_tasks.each do |task|
131
- result = systemu(task)
132
- unless result[0].exitstatus == 0
133
- error_string = "Error while executing post-snapshot task: #{task} on #{@drebs_host_name} #{ip}:#{mount_point} #{instance_id}:#{volume_id} #{result[1]} #{result[2]}"
134
- @log.error(error_string)
135
- send_email("DREBS Error!", error_string)
136
- end
137
- end if post_snapshot_tasks
138
- }
139
- return snapshot
140
- end
141
-
142
- def find_local_snapshots(mount_point='/dev/sdh')
143
- return nil if not ebs = find_local_ebs(mount_point)
144
- snapshots = []
145
- ec2.describe_snapshots.each {|snapshot|
146
- snapshots.push(snapshot) if snapshot[:aws_volume_id] == ebs[:ebs_volume_id]
147
- }
148
- return snapshots
100
+ end
149
101
  end
150
-
151
- def prune_backups(backup_data)
152
- to_prune = {}
153
- backup_data.collect {|a_backup_strategy|
154
- if a_backup_strategy['previous_snapshots'].count > a_backup_strategy['num_to_keep']
155
- to_prune[a_backup_strategy['previous_snapshots'].shift] = nil
156
- end
157
- }
158
- to_prune.each_key {|snapshot_to_prune|
159
- ec2.delete_snapshot(snapshot_to_prune) unless backup_data.any? {|a_backup_strategy|
160
- a_backup_strategy['previous_snapshots'].include?(snapshot_to_prune)
161
- }
162
- }
102
+
103
+ def lib_dir(&block)
104
+ base_dir(['..', 'lib'], &block)
163
105
  end
164
-
165
- def send_email(subject, body, options = {})
166
- host = options[:email_host] || options['email_host'] || EMAIL_HOST
167
- port = options[:email_port] || options['email_port'] || EMAIL_PORT
168
- username = options[:email_username] || options['email_username'] || EMAIL_USERNAME
169
- password = options[:email_password] || options['email_password'] || EMAIL_PASSWORD
170
-
171
-
172
- msg = "Subject: #{subject}\n\n#{body}"
173
- smtp = Net::SMTP.new 'smtp.gmail.com', 587
174
- smtp.enable_starttls
175
- smtp.start('gmail.com', username, password, :login) {|smtp|
176
- smtp.send_message(msg, username, @email_on_exception)
177
- }
106
+
107
+ def config_dir(&block)
108
+ base_dir(['..', 'config'], &block)
178
109
  end
179
110
 
180
- def run_drebs_cron
181
- begin
182
- unless File.exists?(@backup_state_file_path)
183
- @backup_data = setup_backup_data()
184
-
185
- mount_points = @backup_data.map {|strategy| strategy['mount_point']}.compact.flatten.uniq
186
-
187
- mount_points.map do |mount_point|
188
- strategies = @backup_data.select{|strategy| strategy['mount_point'] == mount_point}
189
- pre_snapshot_tasks = strategies.map {|strategy| strategy['pre_snapshot_tasks']}.compact.flatten.uniq
190
- post_snapshot_tasks = strategies.map {|strategy| strategy['post_snapshot_tasks']}.compact.flatten.uniq
191
- unless strategies.empty?
192
- @log.info("creating snapshot of #{mount_point}")
193
- snapshot = create_local_snapshot(pre_snapshot_tasks, post_snapshot_tasks, mount_point)
194
- end
195
- strategies.collect {|strategy|
196
- strategy['previous_snapshots'].push(snapshot[:aws_id])
197
- }
198
- end
199
-
200
-
201
- save_backup_data
202
- else
203
- load_backup_data
204
- @backup_data.collect {|strategy|
205
- strategy['hours_until_next_run'] -= 1
206
- }
207
- backup_now = @backup_data.collect {|strategy| strategy if strategy['hours_until_next_run'] <= 0}.compact
208
-
209
-
210
- mount_points = @backup_data.map {|strategy| strategy['mount_point']}.compact.flatten.uniq
211
-
212
- mount_points.map do |mount_point|
213
- strategies = @backup_data.select{|strategy| strategy['mount_point'] == mount_point}
214
- pre_snapshot_tasks = strategies.map {|strategy| strategy['pre_snapshot_tasks']}.compact.flatten.uniq
215
- post_snapshot_tasks = strategies.map {|strategy| strategy['post_snapshot_tasks']}.compact.flatten.uniq
216
- unless strategies.empty?
217
- @log.info("creating snapshot of #{mount_point}")
218
- snapshot = create_local_snapshot(pre_snapshot_tasks, post_snapshot_tasks, mount_point)
219
- end
220
- strategies.collect {|strategy|
221
- strategy['previous_snapshots'].push(snapshot[:aws_id])
222
- strategy['hours_until_next_run'] = strategy['hours_between']
223
- }
224
-
225
- end
226
-
227
- prune_backups(@backup_data)
228
- save_backup_data
229
- end
230
- rescue Exception => error
231
- @log.error("Exception occured during backup: #{error.message}\n#{error.backtrace.join("\n")}")
232
- send_email("DREBS Error! on #{@drebs_host_name}", "Host: #{@drebs_host_name} AWS Instance: #{find_local_instance[:aws_instance_id]}\n#{error.message}\n#{error.backtrace.join("\n")}")
111
+ def example_config(params)
112
+ config_path = File.join(config_dir, 'example.yml')
113
+ example_config = IO.read(config_path)
114
+ if params['print']
115
+ puts("Example YAML Config File (#{config_path}):")
116
+ puts(example_config)
117
+ puts()
233
118
  end
119
+ YAML::load(example_config)
234
120
  end
235
121
 
236
- end
237
-
238
- if __FILE__ == $0
239
- status = DATA.flock(File::LOCK_EX|File::LOCK_NB)
240
- exit(42) unless status == 0
241
- Main {
242
- def run
243
- drebs = DREBS.new
244
- drebs.run_drebs_cron
122
+ def load_config(config_path)
123
+ if config_path == 'example_config'
124
+ example_config('print' => true)
125
+ else
126
+ YAML::load(IO.read(config_path))
245
127
  end
246
- }
247
- end
248
-
249
- __END__
128
+ end
129
+ }
@@ -0,0 +1,28 @@
1
+ ---
2
+ aws_access_key_id: EXAMPLE_KEY_ID
3
+ aws_secret_access_key: EXAMPLE_ACCESS_KEY
4
+ region: us-west-1
5
+ strategies:
6
+ - hours_between: 1
7
+ num_to_keep: 5
8
+ mount_point: /dev/sdh
9
+ pre_snapshot_tasks:
10
+ - pg_dump some_app_prodction > /path/to/backups/on/volume/being/snapshoted
11
+ - mongodump --db some_app_prodction --out /path/to/backups/on/volume/being/snapshoted
12
+ post_snapshot_tasks:
13
+ - hours_between: 6
14
+ num_to_keep: 4
15
+ mount_point: /dev/sdh
16
+ - hours_between: 24
17
+ num_to_keep: 4
18
+ mount_point: /dev/sda1
19
+ - hours_between: 96
20
+ num_to_keep: 4
21
+ mount_point: /dev/sda1
22
+ log_path: /usr/local/var/drebs.log
23
+ email_on_exception: admin@your.org
24
+ email_host: 'smtp.gmail.com'
25
+ email_port: 587
26
+ email_domain: 'gmail.com'
27
+ email_user: = 'Your smtp username'
28
+ email_password: = 'Your smtp password'
data/drebs.gemspec CHANGED
@@ -3,19 +3,32 @@
3
3
 
4
4
  Gem::Specification::new do |spec|
5
5
  spec.name = "drebs"
6
- spec.version = "0.0.1"
6
+ spec.version = "0.1.0"
7
7
  spec.platform = Gem::Platform::RUBY
8
8
  spec.summary = "drebs"
9
- spec.description = "description: drebs kicks the ass"
9
+ spec.description = "drebs: Disaster Recovery for Elastic Block Store. An AWS EBS backup script."
10
10
 
11
11
  spec.files =
12
12
  ["README.md",
13
13
  "Rakefile",
14
14
  "bin",
15
15
  "bin/drebs",
16
+ "config",
17
+ "config/example.yml",
16
18
  "drebs.gemspec",
17
19
  "lib",
18
- "lib/drebs.rb"]
20
+ "lib/drebs",
21
+ "lib/drebs.rb",
22
+ "lib/drebs/cloud.rb",
23
+ "lib/drebs/main.rb",
24
+ "test",
25
+ "test/helper.rb",
26
+ "test/unit",
27
+ "test/unit/drebs",
28
+ "test/unit/drebs/drebs_test.rb",
29
+ "test/unit/drebs/main_test.rb",
30
+ "tmp_test_data",
31
+ "tmp_test_data/db.sqlite"]
19
32
 
20
33
  spec.executables = ["drebs"]
21
34
  spec.require_path = "lib"
@@ -23,21 +36,24 @@ spec.require_path = "lib"
23
36
  spec.test_files = nil
24
37
 
25
38
 
26
- spec.add_dependency(*["right_aws", " >= 3.0.0 "])
39
+ spec.add_dependency(*["right_aws", ">= 3.1.0"])
27
40
 
28
- spec.add_dependency(*["logger", " >= 1.2.8 "])
41
+ spec.add_dependency(*["logger", ">= 1.2.8"])
29
42
 
30
- spec.add_dependency(*["main", " >= 5.0.0 "])
43
+ spec.add_dependency(*["main", ">= 5.2.0"])
31
44
 
32
- spec.add_dependency(*["systemu", " >= 2.4.2 "])
45
+ spec.add_dependency(*["systemu", ">= 2.4.2"])
33
46
 
34
- spec.add_dependency(*["json", " >= 1.5.1 "])
47
+ spec.add_dependency(*["json", ">= 1.5.1"])
48
+
49
+ spec.add_dependency(*["pry", ">= 0.9.12.6"])
35
50
 
36
51
 
37
52
  spec.extensions.push(*[])
38
53
 
39
54
  spec.rubyforge_project = "DREBS"
40
- spec.author = "Garett Shulman"
41
- spec.email = "garett@dojo4.com"
55
+ spec.authors = ["Garett Shulman", "Miles Matthias"]
56
+ spec.email = "miles@dojo4.com"
42
57
  spec.homepage = "https://github.com/dojo4/drebs"
58
+ spec.licenses = "Apache-2.0"
43
59
  end
data/lib/drebs.rb CHANGED
@@ -5,7 +5,7 @@
5
5
  # DREBS libs
6
6
  #
7
7
  module DREBS
8
- Version = '0.0.1' unless defined?(Version)
8
+ Version = '0.1.0' unless defined?(Version)
9
9
 
10
10
  def version
11
11
  DREBS::Version
@@ -13,12 +13,13 @@
13
13
 
14
14
  def dependencies
15
15
  {
16
- 'right_aws' => [ 'right_aws' , ' >= 3.0.0 ' ] ,
17
- 'logger' => [ 'logger' , ' >= 1.2.8 ' ] ,
18
- 'main' => [ 'main' , ' >= 5.0.0 ' ] ,
19
- 'systemu' => [ 'systemu' , ' >= 2.4.2 ' ] ,
20
- 'json' => [ 'json' , ' >= 1.5.1 ' ] ,
21
- }
16
+ 'right_aws' => ['right_aws', '>= 3.1.0'],
17
+ 'logger' => ['logger' , '>= 1.2.8'],
18
+ 'main' => ['main' , '>= 5.2.0'],
19
+ 'systemu' => ['systemu' , '>= 2.4.2'],
20
+ 'json' => ['json' , '>= 1.5.1'],
21
+ 'pry' => ['pry' , '>= 0.9.12.6'],
22
+ }
22
23
  end
23
24
 
24
25
  def libdir(*args, &block)
@@ -0,0 +1,91 @@
1
+ require 'right_aws'
2
+
3
+ module Drebs
4
+ class Cloud
5
+ def initialize(config)
6
+ @config = config
7
+ end
8
+
9
+ def check_cloud
10
+ ec2
11
+ find_local_instance
12
+ end
13
+
14
+ def ec2
15
+ key_id = @config["aws_access_key_id"]
16
+ key = @config["aws_secret_access_key"]
17
+ region = @config["region"]
18
+ return RightAws::Ec2.new(key_id, key, {:region=>region})
19
+ end
20
+
21
+ def find_local_instance
22
+ #find a better way... right-aws?
23
+ private_ip = UDPSocket.open{|s| s.connect("8.8.8.8", 1); s.addr.last}
24
+ ec2.describe_instances.each do |instance|
25
+ return instance if instance[:private_ip_address] == private_ip
26
+ end
27
+ return nil
28
+ end
29
+
30
+ def find_local_ebs(mount_point)
31
+ return nil if not local_instance = find_local_instance
32
+ local_instance[:block_device_mappings].each do |volume|
33
+ return volume if volume[:device_name] == mount_point
34
+ end
35
+ return nil
36
+ end
37
+
38
+ def local_ebs_ids
39
+ @ebs_ids ||= find_local_instance[:block_device_mappings].map do |volume|
40
+ volume[:ebs_volume_id]
41
+ end rescue nil
42
+ end
43
+
44
+ def get_snapshot(snapshot_id)
45
+ ec2.describe_snapshots {|a_snapshot|
46
+ return a_snapshot if a_snapshot[:aws_id] == snapshot_id
47
+ }
48
+ end
49
+
50
+ def create_local_snapshot(pre_snapshot_tasks, post_snapshot_tasks, mount_point)
51
+ local_instance=find_local_instance
52
+ ip = local_instance[:ip_address]
53
+ instance_id = local_instance[:aws_instance_id]
54
+ volume_id = local_instance[:block_device_mappings].select{|m| m[:device_name]==mount_point}.first[:ebs_volume_id]
55
+ return nil if not ebs = find_local_ebs(mount_point)
56
+ pre_snapshot_tasks.each do |task|
57
+ result, stdout, stderr = systemu(task)
58
+ unless result.exitstatus == 0
59
+ raise Exception(
60
+ "Error while executing pre-snapshot task: #{task} on #{ip}:#{mount_point} #{instance_id}:#{volume_id} "
61
+ )
62
+ end
63
+ end if pre_snapshot_tasks
64
+ snapshot = ec2.create_snapshot(ebs[:ebs_volume_id], "DREBS #{ip}:#{mount_point} #{instance_id}:#{volume_id}")
65
+ Thread.new(snapshot[:aws_id], post_snapshot_tasks) do |snapshot_id, post_snapshot_tasks|
66
+ 1.upto(500) do |a|
67
+ sleep(3)
68
+ break if get_snapshot(snapshot_id)[:aws_status] == 'completed'
69
+ end
70
+ post_snapshot_tasks.each do |task|
71
+ result = systemu(task)
72
+ unless result.exitstatus == 0
73
+ raise Exception(
74
+ "Error while executing post-snapshot task: #{task} on #{ip}:#{mount_point} #{instance_id}:#{volume_id} "
75
+ )
76
+ end
77
+ end if post_snapshot_tasks
78
+ end
79
+ return snapshot
80
+ end
81
+
82
+ def find_local_snapshots(mount_point)
83
+ return nil if not ebs = find_local_ebs(mount_point)
84
+ snapshots = []
85
+ ec2.describe_snapshots.each {|snapshot|
86
+ snapshots.push(snapshot) if snapshot[:aws_volume_id] == ebs[:ebs_volume_id]
87
+ }
88
+ return snapshots
89
+ end
90
+ end
91
+ end
data/lib/drebs/main.rb ADDED
@@ -0,0 +1,179 @@
1
+ module Drebs
2
+ class Main
3
+ attr_reader :config
4
+ attr_reader :cloud
5
+ attr_reader :db
6
+
7
+ def initialize(params)
8
+ unless @config = params['config'].clone()
9
+ raise "No config_file_path passed!"
10
+ end
11
+ unless @db = params['db']
12
+ raise "No db passed!"
13
+ end
14
+ update_strategies(@config.delete("strategies"))
15
+ @cloud = Drebs::Cloud.new(@config)
16
+ @log = Logger.new(@config["log_path"])
17
+ @log.level = Logger::WARN
18
+ end
19
+
20
+ def check_cloud
21
+ @cloud.check_cloud()
22
+ end
23
+
24
+ def Main.check_config(reference_config, other_config)
25
+ reference_config = reference_config.clone()
26
+ reference_strategy = reference_config.delete('strategies').last
27
+
28
+ errors = []
29
+
30
+ config_ok = reference_config.keys.each do |key|
31
+ config_ok = other_config.has_key?(key) and other_config[key] != nil and other_config[key] != ""
32
+ errors.push("Missing key/value for key: #{key}") unless config_ok
33
+ end
34
+
35
+ strategies = other_config['strategies']
36
+ if strategies.is_a?(Array) and strategies.first()
37
+ strategies.each_with_index do |strategy, i|
38
+ strategies_ok = reference_strategy.keys.all?{|key|
39
+ unless strategy.has_key?(key) and strategy[key] != nil and strategy[key] != ""
40
+ errors.push("Missing key/value for key: #{key}") unless config_ok
41
+ end
42
+ }
43
+ end
44
+ else
45
+ errors.push("Missing strategies array")
46
+ end
47
+
48
+ return errors
49
+ end
50
+
51
+
52
+ def update_strategies(new_strategies)
53
+ new_strategies.each do |strategy|
54
+ exists = @db[:strategies].filter(:config=>strategy.to_yaml).update(:status=>"active")
55
+ if exists==0
56
+ pre_snapshot_tasks = strategy['pre_snapshot_tasks']
57
+ pre_snapshot_tasks = pre_snapshot_tasks ? pre_snapshot_tasks.join(",") : ""
58
+ post_snapshot_tasks = strategy['post_snapshot_tasks']
59
+ post_snapshot_tasks = post_snapshot_tasks ? post_snapshot_tasks.join(",") : ""
60
+ @db[:strategies].insert(
61
+ :config=>strategy.to_yaml,
62
+ :snapshots=>"",
63
+ :status=>"active",
64
+ :time_til_next_run => strategy['hours_between'],
65
+ :time_between_runs => strategy['hours_between'],
66
+ :num_to_keep => strategy['num_to_keep'],
67
+ :pre_snapshot_tasks => pre_snapshot_tasks,
68
+ :post_snapshot_tasks => post_snapshot_tasks,
69
+ :mount_point => strategy['mount_point']
70
+ )
71
+ end
72
+ end
73
+ deactivate_filter = new_strategies.map do |strategy|
74
+ "(config != '#{strategy.to_yaml}' and status == 'active')"
75
+ end.join(" and ")
76
+ @db[:strategies].filter(deactivate_filter).update(:status => 'inactive')
77
+ end
78
+
79
+ def send_email(subject, body)
80
+ host = @config['email_host']
81
+ port = @config['email_port']
82
+ domain = @config['email_domain']
83
+ username = @config['email_user']
84
+ password = @config['email_password']
85
+
86
+ msg = "Subject: #{subject}\n\n#{body}"
87
+ smtp = Net::SMTP.new(host, port)
88
+ smtp.enable_starttls
89
+ smtp.start(domain, username, password, :login) {|smtp|
90
+ smtp.send_message(msg, username, @config['email_on_exception'])
91
+ }
92
+ end
93
+
94
+ def prune_backups(strategies)
95
+ to_prune = []
96
+
97
+ strategies.each do |strategy|
98
+ snapshots = strategy[:snapshots].split(",")
99
+ if snapshots.uniq==[nil]
100
+ @db[:strategies].filter(:config=>strategy[:config]).update(:snapshots => "")
101
+ elsif snapshots == []
102
+ elsif snapshots.count > strategy[:num_to_keep].to_i
103
+ # only remove it from EC2 if there are no other strategies with this snapshot
104
+ to_prune.push(Map.for({
105
+ :snapshot => snapshots.first,
106
+ :strategy => strategy
107
+ }))
108
+ end
109
+ end
110
+
111
+ to_prune.each do |prune_obj|
112
+ snapshot = prune_obj.snapshot
113
+ strategy = prune_obj.strategy
114
+
115
+ # make sure that no other strategies have this snapshot
116
+ strategies_with_snapshot = @db[:strategies].all.select{|strategy| strategy[:snapshots].split(',').include?(snapshot)}
117
+
118
+ if strategies_with_snapshot.count == 1
119
+ begin
120
+ @cloud.ec2.delete_snapshot(snapshot.split(":")[0])
121
+ rescue RightAws::AwsError => e
122
+ type = e.errors.first.first rescue ''
123
+ raise unless type == "InvalidSnapshot.NotFound"
124
+ end
125
+ end
126
+
127
+ # update the strategy's snapshots to include all except given snapshot
128
+ new_snapshots = strategy[:snapshots].split(',').delete_if{|snap| snap == snapshot}.join(',')
129
+ @db[:strategies].filter(:config=>strategy[:config]).update(:snapshots => new_snapshots)
130
+ end
131
+ end
132
+
133
+ def execute
134
+ active_strategies = @db[:strategies].filter({:status=>"active"})
135
+
136
+ #Decrement time_til_next_run, save
137
+ active_strategies.each do |s|
138
+ @db[:strategies].filter(:config=>s[:config]).update(
139
+ :time_til_next_run => (s[:time_til_next_run].to_i - 1)
140
+ )
141
+ end
142
+
143
+ active_strategies = @db[:strategies].filter({:status=>"active"})
144
+
145
+ #backup_now = strategies where time_til_next_run <= 0
146
+ backup_now = active_strategies.to_a.select{|s| s[:time_til_next_run].to_i <= 0}
147
+
148
+ #loop over strategies grouped by mount_point
149
+ backup_now.group_by{|s| s[:mount_point]}.each do |mount_point, strategies|
150
+ pre_snapshot_tasks = strategies.map{|s| s[:pre_snapshot_tasks].split(",")}.flatten.uniq
151
+ post_snapshot_tasks = strategies.map{|s| s[:pre_snapshot_tasks].split(",")}.flatten.uniq
152
+
153
+ @log.info("creating snapshot of #{mount_point}")
154
+ begin
155
+ snapshot = @cloud.create_local_snapshot(pre_snapshot_tasks, post_snapshot_tasks, mount_point)
156
+
157
+ strategies.collect {|s|
158
+ snapshots = s[:snapshots].split(",")
159
+ snapshots.select!{|snapshot| @cloud.local_ebs_ids.include? snapshot.split(":")[1]}
160
+ snapshots.push(
161
+ s[:status]=='active' ?
162
+ "#{snapshot[:aws_id]}:#{snapshot[:aws_volume_id]}" : nil
163
+ )
164
+ @db[:strategies].filter(:config=>s[:config]).update(
165
+ :snapshots => snapshots.join(","),
166
+ :time_til_next_run => s[:time_between_runs]
167
+ )
168
+ }
169
+
170
+ rescue Exception => error
171
+ @log.error("Exception occured during backup: #{error.message}\n#{error.backtrace.join("\n")}")
172
+ send_email("DREBS Error!", "AWS Instance: #{@cloud.find_local_instance[:aws_instance_id]}\n#{error.message}\n#{error.backtrace.join("\n")}")
173
+ end
174
+ end
175
+
176
+ prune_backups(@db[:strategies])
177
+ end
178
+ end
179
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,126 @@
1
+ require "main"
2
+ require "./lib/drebs/main.rb"
3
+ require "./lib/drebs/cloud.rb"
4
+ require "test/unit"
5
+ require "yaml"
6
+
7
+ EXAMPLE_CONFIG_PATH = "./config/example.yml"
8
+ TMP_TEST_DATA_PATH = "./tmp_test_data/"
9
+
10
+ class TestEC2
11
+ def initialize(*args)
12
+ @snapshots = []
13
+ @instances = [
14
+ {
15
+ private_ip_address: UDPSocket.open{|s| s.connect("8.8.8.8", 1); s.addr.last},
16
+ ip_address: "127.0.0.1",
17
+ aws_instance_id: "fake_instance",
18
+ block_device_mappings: [
19
+ {device_name: "/dev/sda1", ebs_volume_id: "fake_sda1"},
20
+ {device_name: "/dev/sdh", ebs_volume_id: "fake_sdh"}
21
+ ]
22
+ }
23
+ ]
24
+ end
25
+
26
+ def describe_snapshots()
27
+ @snapshots
28
+ end
29
+
30
+ def describe_instances()
31
+ @instances
32
+ end
33
+
34
+ def create_snapshot(ebs_volume_id, snapshot_name)
35
+ new_snapshot = {
36
+ aws_id: "fake_snap-"+rand(36**6).to_s(36),
37
+ aws_status: 'completed',
38
+ aws_volume_id: ebs_volume_id
39
+ }
40
+ @snapshots.push(new_snapshot)
41
+ new_snapshot
42
+ end
43
+ end
44
+
45
+ class TestCloud < Drebs::Cloud
46
+ def ec2; @ec2 ||= TestEC2.new; end
47
+ end
48
+
49
+ class TestContext
50
+ def TestContext.main_context(&block)
51
+ Main.new() do |main|
52
+ config = YAML::load(IO.read(EXAMPLE_CONFIG_PATH))
53
+ main.dotdir(TMP_TEST_DATA_PATH)
54
+ main.db() do
55
+
56
+ drop_table :strategies if table_exists? :strategies
57
+ create_table :strategies do
58
+ String :config
59
+ String :snapshots
60
+ String :status
61
+ String :time_til_next_run
62
+ String :time_between_runs
63
+ String :num_to_keep
64
+ String :pre_snapshot_tasks
65
+ String :post_snapshot_tasks
66
+ end
67
+
68
+ drop_table :snapshots if table_exists? :snapshots
69
+ create_table :snapshots do
70
+ String :aws_id
71
+ String :volume
72
+ end
73
+ end
74
+
75
+ if block
76
+ begin
77
+ block.call(config, main.db())
78
+ ensure
79
+ begin
80
+ #fake main cleanup
81
+ main.db() do
82
+ drop_table :strategies if table_exists? :strategies
83
+ drop_table :snapshots if table_exists? :snapshots
84
+ end
85
+ rescue => e
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ def TestContext.cloud_context(config, &block)
93
+ cloud = TestCloud.new(config)
94
+ main_context do |main|
95
+ if block
96
+ begin
97
+ block.call(cloud)
98
+ ensure
99
+ begin
100
+ #fake cloud cleanup
101
+ rescue => e
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ def TestContext.drebs_context(&block)
109
+ main_context() do |config, db|
110
+ cloud_context(config) do |cloud|
111
+ drebs = Drebs::Main.new('config' => config, 'db' => db)
112
+
113
+ if block
114
+ begin
115
+ block.call(config, db, cloud, drebs)
116
+ ensure
117
+ begin
118
+ #fake drebs cleanup
119
+ rescue => e
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,11 @@
1
+ require "./test/helper.rb"
2
+
3
+ class DrebsMain < Test::Unit::TestCase
4
+
5
+ def test_backups_get_pruned
6
+ TestContext.drebs_context() do |config, db, cloud, drebs|
7
+ require 'pry'; binding.pry
8
+ end
9
+ end
10
+
11
+ end
@@ -0,0 +1,44 @@
1
+ require "./test/helper.rb"
2
+
3
+ class TestMain < Test::Unit::TestCase
4
+
5
+ def test_can_check_config
6
+ TestContext.drebs_context() do |config, db, cloud, drebs|
7
+ config_errors = drebs.class.check_config(config, config)
8
+ assert(config_errors == [])
9
+ end
10
+ end
11
+
12
+ def test_check_config_finds_missing_keys
13
+ TestContext.drebs_context() do |config, db, cloud, drebs|
14
+ bad_config = config.clone
15
+ bad_config.delete(bad_config.keys.first)
16
+ config_errors = drebs.class.check_config(config, bad_config)
17
+ assert(config_errors.length == 1)
18
+ assert(config_errors[0].include?("Missing key/value"))
19
+ end
20
+ end
21
+
22
+ def test_can_save_strategies
23
+ TestContext.main_context() do |config, db|
24
+ assert(db[:strategies].all == [])
25
+ drebs = Drebs::Main.new('config' => config, 'db' => db)
26
+ assert(db[:strategies].all.length > 0)
27
+ end
28
+ end
29
+
30
+ def test_removed_strategies_get_deactivated
31
+ TestContext.main_context() do |config, db|
32
+ drebs = Drebs::Main.new('config' => config, 'db' => db)
33
+ num_active_strategies = db[:strategies].where(:status=>"active").count
34
+ num_inactive_strategies = db[:strategies].where(:status=>"inactive").count
35
+ config['strategies'] = config['strategies'][1..-1]
36
+ drebs = Drebs::Main.new('config' => config, 'db' => db)
37
+ num_active_strategies_dec = db[:strategies].where(:status=>"active").count
38
+ num_inactive_strategies_inc = db[:strategies].where(:status=>"inactive").count
39
+ assert(num_active_strategies == num_active_strategies_dec + 1)
40
+ assert(num_inactive_strategies == num_inactive_strategies_inc - 1)
41
+ end
42
+ end
43
+
44
+ end
Binary file
metadata CHANGED
@@ -1,73 +1,103 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: drebs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
5
- prerelease:
4
+ version: 0.1.0
6
5
  platform: ruby
7
6
  authors:
8
7
  - Garett Shulman
8
+ - Miles Matthias
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-03-01 00:00:00.000000000 Z
12
+ date: 2014-06-16 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: right_aws
16
- requirement: &2152789220 !ruby/object:Gem::Requirement
17
- none: false
16
+ requirement: !ruby/object:Gem::Requirement
18
17
  requirements:
19
18
  - - ! '>='
20
19
  - !ruby/object:Gem::Version
21
- version: 3.0.0
20
+ version: 3.1.0
22
21
  type: :runtime
23
22
  prerelease: false
24
- version_requirements: *2152789220
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ! '>='
26
+ - !ruby/object:Gem::Version
27
+ version: 3.1.0
25
28
  - !ruby/object:Gem::Dependency
26
29
  name: logger
27
- requirement: &2152788720 !ruby/object:Gem::Requirement
28
- none: false
30
+ requirement: !ruby/object:Gem::Requirement
29
31
  requirements:
30
32
  - - ! '>='
31
33
  - !ruby/object:Gem::Version
32
34
  version: 1.2.8
33
35
  type: :runtime
34
36
  prerelease: false
35
- version_requirements: *2152788720
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ! '>='
40
+ - !ruby/object:Gem::Version
41
+ version: 1.2.8
36
42
  - !ruby/object:Gem::Dependency
37
43
  name: main
38
- requirement: &2152788240 !ruby/object:Gem::Requirement
39
- none: false
44
+ requirement: !ruby/object:Gem::Requirement
40
45
  requirements:
41
46
  - - ! '>='
42
47
  - !ruby/object:Gem::Version
43
- version: 5.0.0
48
+ version: 5.2.0
44
49
  type: :runtime
45
50
  prerelease: false
46
- version_requirements: *2152788240
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ! '>='
54
+ - !ruby/object:Gem::Version
55
+ version: 5.2.0
47
56
  - !ruby/object:Gem::Dependency
48
57
  name: systemu
49
- requirement: &2152787760 !ruby/object:Gem::Requirement
50
- none: false
58
+ requirement: !ruby/object:Gem::Requirement
51
59
  requirements:
52
60
  - - ! '>='
53
61
  - !ruby/object:Gem::Version
54
62
  version: 2.4.2
55
63
  type: :runtime
56
64
  prerelease: false
57
- version_requirements: *2152787760
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: 2.4.2
58
70
  - !ruby/object:Gem::Dependency
59
71
  name: json
60
- requirement: &2152787280 !ruby/object:Gem::Requirement
61
- none: false
72
+ requirement: !ruby/object:Gem::Requirement
62
73
  requirements:
63
74
  - - ! '>='
64
75
  - !ruby/object:Gem::Version
65
76
  version: 1.5.1
66
77
  type: :runtime
67
78
  prerelease: false
68
- version_requirements: *2152787280
69
- description: ! 'description: drebs kicks the ass'
70
- email: garett@dojo4.com
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ! '>='
82
+ - !ruby/object:Gem::Version
83
+ version: 1.5.1
84
+ - !ruby/object:Gem::Dependency
85
+ name: pry
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ! '>='
89
+ - !ruby/object:Gem::Version
90
+ version: 0.9.12.6
91
+ type: :runtime
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ! '>='
96
+ - !ruby/object:Gem::Version
97
+ version: 0.9.12.6
98
+ description: ! 'drebs: Disaster Recovery for Elastic Block Store. An AWS EBS backup
99
+ script.'
100
+ email: miles@dojo4.com
71
101
  executables:
72
102
  - drebs
73
103
  extensions: []
@@ -76,30 +106,37 @@ files:
76
106
  - README.md
77
107
  - Rakefile
78
108
  - bin/drebs
109
+ - config/example.yml
79
110
  - drebs.gemspec
80
111
  - lib/drebs.rb
112
+ - lib/drebs/cloud.rb
113
+ - lib/drebs/main.rb
114
+ - test/helper.rb
115
+ - test/unit/drebs/drebs_test.rb
116
+ - test/unit/drebs/main_test.rb
117
+ - tmp_test_data/db.sqlite
81
118
  homepage: https://github.com/dojo4/drebs
82
- licenses: []
119
+ licenses:
120
+ - Apache-2.0
121
+ metadata: {}
83
122
  post_install_message:
84
123
  rdoc_options: []
85
124
  require_paths:
86
125
  - lib
87
126
  required_ruby_version: !ruby/object:Gem::Requirement
88
- none: false
89
127
  requirements:
90
128
  - - ! '>='
91
129
  - !ruby/object:Gem::Version
92
130
  version: '0'
93
131
  required_rubygems_version: !ruby/object:Gem::Requirement
94
- none: false
95
132
  requirements:
96
133
  - - ! '>='
97
134
  - !ruby/object:Gem::Version
98
135
  version: '0'
99
136
  requirements: []
100
137
  rubyforge_project: DREBS
101
- rubygems_version: 1.8.11
138
+ rubygems_version: 2.1.8
102
139
  signing_key:
103
- specification_version: 3
140
+ specification_version: 4
104
141
  summary: drebs
105
142
  test_files: []