snapscatter 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in snapscatter.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Alex Escalante
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # Snapscatter
2
+
3
+ This script creates snapshots from EBS volumes and copies them across regions for disaster recovery.
4
+ If your volume is used for database storage, this script can make sure your backup is consistent by
5
+ flushing and stopping writes until the snapshot is complete.
6
+
7
+ This script also purges old snapshots according to a simple retention policy specified by you, to keep
8
+ your Amazon AWS bills under control.
9
+
10
+ ## Installation
11
+
12
+ Just use the gem command to install:
13
+
14
+ $ gem install snapscatter
15
+
16
+ ## Usage
17
+
18
+ You can have a look at the commands and options by just typing the name of the executable:
19
+
20
+ Commands:
21
+ snapscatter create # Create snapshots, optionally copying them to destination region
22
+ snapscatter help [COMMAND] # Describe available commands or one specific command
23
+ snapscatter list # Show available snapshots
24
+ snapscatter purge # purge snapshots older than the specified number of days
25
+ snapscatter targets # Show volumes tagged for backup
26
+
27
+ The best way to use this script is to make a shell wrapper for it that exports your AWS credentials
28
+ as environment variables and then put it under the control of the cron demon.
29
+
30
+ ### Specifying volumes to backup
31
+
32
+ You should use the AWS console to mark the volumes you want to be backed up using the tag `Backup` with a
33
+ value of `true`. You can then check the list of these volumes using the command `targets`.
34
+
35
+ ### Taking snapshots
36
+
37
+ Use the command `create` to take snapshots of all the tagged volumes. Snapshots will be taken to your default
38
+ AWS region, but you can optionally supply the '--destination' flag to create a copy onto another
39
+ region for disaster recovery.
40
+
41
+ Every snapshot taken will have the `PurgeAllow` tag set with the value of `true`. If for some reason you want
42
+ a snapshot not to be purged indefinitely, you can set this tag to any other value, or even remove the tag
43
+ altogether.
44
+
45
+ ### Purging snapshots
46
+
47
+ You can call the `purge` command to delete any snapshots older than 30 days. This is the default retention policy
48
+ but you can change it by using the optional '-d' flag (for `--days).
49
+
50
+ You can also run this command with the `-n` (for `--noaction`) to only list the snapshots that would be purged
51
+ under the specified retention policy, no snapshot will be purged.
52
+
53
+ ### Listing snapshots
54
+
55
+ The `list` command will show all the snapshots subject to be purged, that is, all snapshots with the `PurgAllow`
56
+ tag set to `true`. The `-f`option for this command gives more information on every snapshot: snapshot id, volume id
57
+ and date of creation.
58
+
59
+ ### Consistent backups
60
+
61
+ If the volumes your attempting to snapshot are being used by a database, then you want to force a flush to disk and
62
+ stop writing so you can have a consistent backup. This script can do that for you (currently it only supports MongoDB).
63
+
64
+ To use this feature, you can tag the volume with the `Consistent` tag. The value of this tag contains the connection
65
+ information, as key-value pairs separated by commas, so that the script can have access to the database, flush and
66
+ stop writes until the snapshot has been taken. Here's an example:
67
+
68
+ strategy: mongo, host: 127.0.0.1, port: 27017, usr: admin, pwd: 12345
69
+
70
+ ## Example
71
+
72
+ Create a shell file like the following and put it under cron's control:
73
+
74
+ #!/bin/sh
75
+
76
+ export AWS_ACCESS_KEY_ID="YOURACCESSKEY"
77
+ export AWS_SECRET_ACCESS_KEY="YOURSECRETACCESSKEY"
78
+
79
+ snapscatter purge -d 20
80
+ snapscatter create --destination="us-west-1"
81
+
82
+ You have to make two calls because the script won't purge and create snapshots on a single call.
83
+
84
+ ## TODO
85
+
86
+ * Take consistency speficication out of the volume and put it in a configuration file
87
+ * More database connectors
88
+
89
+ ## Contributing
90
+
91
+ 1. Fork it
92
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
93
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
94
+ 4. Push to the branch (`git push origin my-new-feature`)
95
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/snapscatter ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'snapscatter/cli'
4
+ Snapscatter::CLI.start
@@ -0,0 +1,19 @@
1
+ @announce-stdout
2
+ Feature: snapscatter
3
+ In order to create and distribute snapshots
4
+ As a CLI
5
+ I can execute several different commands
6
+
7
+ Scenario: show available commands and options
8
+ When I run `snapscatter`
9
+ Then the output should contain "targets"
10
+ And the output should contain "list"
11
+ And the output should contain "go"
12
+
13
+ Scenario: show all volumes available for snapshot
14
+ When I run `snapscatter targets`
15
+ Then the output should match /vol-[0-9a-f]+/
16
+
17
+ Scenario: list all current snapshots
18
+ When I run `snapscatter list`
19
+ Then the output should match /snap-[0-9a-f]+/
@@ -0,0 +1,5 @@
1
+ require 'aruba/cucumber'
2
+
3
+ Before do
4
+ @aruba_timeout_seconds = 60
5
+ end
@@ -0,0 +1,80 @@
1
+ require 'thor'
2
+ require_relative '../snapscatter'
3
+
4
+ module Snapscatter
5
+ class CLI < Thor
6
+
7
+ desc 'targets', 'Show volumes tagged for backup'
8
+ method_option :keys, type: :hash, banner: 'AWS security keys'
9
+ def targets
10
+ targets = Snapscatter.targets create_ec2
11
+ targets.each { |target| puts target.id }
12
+ end
13
+
14
+ desc 'list', 'Show available snapshots'
15
+ method_option :keys, type: :hash, banner: 'AWS security keys'
16
+ method_option :full, type: :boolean, aliases: '-f', banner: 'Show useful info about snapshots'
17
+ def list
18
+ snapshots = Snapscatter.list create_ec2
19
+ snapshots.each do |snapshot|
20
+ output = [ snapshot.id ]
21
+ if options[:full]
22
+ output << snapshot.volume_id
23
+ output << snapshot.start_time.strftime("%Y-%m-%d")
24
+ end
25
+ puts output.join(" ")
26
+ end
27
+ end
28
+
29
+ desc 'purge', 'purge snapshots older than the specified number of days'
30
+ method_option :keys, type: :hash, banner: 'AWS security keys'
31
+ method_option :days, type: :numeric, default: 30, aliases: '-d', banner: 'retention policy in days'
32
+ method_option :noaction, type: :boolean, default: false, aliases: '-n', banner: 'do not purge, just show'
33
+ def purge
34
+ purged = Snapscatter.purge create_ec2, options[:days], true # options[:noaction] # remove in production
35
+ purged.each { |snapshot| puts "#{snapshot.id}" }
36
+ end
37
+
38
+ desc 'create', 'Create snapshots, optionally copying them to destination region'
39
+ method_option :keys, type: :hash, banner: 'AWS security keys'
40
+ method_option :destination, type: :string, banner: 'region to copy snapshots to'
41
+ def create
42
+ source_ec2 = create_ec2
43
+ targets = Snapscatter.targets source_ec2
44
+ targets.each do |volume|
45
+ snapshot = nil
46
+ description = nil
47
+
48
+ Snapscatter.in_lock volume.tags['Consistent'] do
49
+ volume_name = volume.tags['Name']
50
+ date_as_string = Date.today.strftime("%Y-%m-%d")
51
+ description = "#{volume_name} #{date_as_string}"
52
+
53
+ snapshot = volume.create_snapshot description
54
+ snapshot.add_tag 'VolumeName', value: volume_name
55
+ snapshot.add_tag 'PurgeAllow', value: "true"
56
+
57
+ sleep 1 until [:completed, :error].include?(snapshot.status)
58
+ end
59
+
60
+ if snapshot.status == :completed
61
+ output = ["created", snapshot.id, description]
62
+ if options.has_key? 'destination'
63
+ destination_ec2 = create_ec2(region: options[:destination])
64
+ Snapscatter.copy destination_ec2, source_ec2.client.config.region, snapshot, description
65
+ output << "#{options[:destination]}"
66
+ end
67
+ puts output.join(" ")
68
+ else
69
+ puts "#{volume.id} (#{volume_name}): snapshot failed"
70
+ end
71
+ end
72
+ end
73
+
74
+ private
75
+ def create_ec2 ec2_options={}
76
+ ec2_options.merge! options[:keys] if options.has_key? :keys
77
+ ec2 = AWS::EC2.new(ec2_options)
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,60 @@
1
+ require 'mongo'
2
+
3
+ module Snapscatter
4
+
5
+ class Locker
6
+
7
+ def initialize spec
8
+ strategy = spec[:strategy] && spec.delete(:strategy)
9
+ case strategy
10
+ when 'mongo'
11
+ @strategy = MongoLocker.new spec
12
+ else
13
+ @strategy = NoOpLocker.new
14
+ end
15
+ end
16
+
17
+ def lock
18
+ @strategy.lock
19
+ end
20
+
21
+ def unlock
22
+ @strategy.unlock
23
+ end
24
+ end
25
+
26
+ class NoOpLocker
27
+ def method_missing sym
28
+ end
29
+ end
30
+
31
+ class MongoLocker
32
+ def initialize spec
33
+ @host = spec[:host] && spec.delete(:host)
34
+ @port = spec[:port] && spec.delete(:port)
35
+ user = spec[:usr] && spec.delete(:usr)
36
+ password = spec[:pwd] && spec.delete(:pwd)
37
+
38
+ if @host
39
+ @client = Mongo::MongoClient.new @host, @port, spec # spec contains the options
40
+ else
41
+ @client = Mongo::MongoClient.new
42
+ end
43
+
44
+ if user
45
+ @client.add_auth 'admin', user, password, nil
46
+ end
47
+ end
48
+
49
+ def lock
50
+ @client.lock!
51
+ puts "locked mongo instance at #{@host}"
52
+ end
53
+
54
+ def unlock
55
+ @client.unlock!
56
+ puts "unlocked mongo instance at #{@host}"
57
+ end
58
+ end
59
+
60
+ end
@@ -0,0 +1,3 @@
1
+ module Snapscatter
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,70 @@
1
+ require 'snapscatter/version'
2
+ require 'snapscatter/locker'
3
+ require 'aws'
4
+
5
+ module Snapscatter
6
+
7
+ private
8
+
9
+ def parse_spec str
10
+ str ||= ""
11
+ spec = {}
12
+ str.split(',').map do |i|
13
+ k, v = i.split(':').map { |i| i.strip }
14
+ spec[k.to_sym] = v
15
+ end
16
+ return spec
17
+ end
18
+
19
+ public
20
+
21
+ def targets ec2
22
+ ec2.volumes.tagged('Backup').tagged_values('true')
23
+ end
24
+
25
+ def list ec2
26
+ ec2.snapshots.tagged('PurgeAllow').tagged_values('true')
27
+ end
28
+
29
+ def copy ec2, region, snapshot, description
30
+ options = {
31
+ source_region: region,
32
+ source_snapshot_id: snapshot.id,
33
+ description: description
34
+ }
35
+
36
+ response = ec2.client.copy_snapshot options
37
+ copied_snapshot = ec2.snapshots[response.data[:snapshot_id]]
38
+ copied_snapshot.tags.set snapshot.tags
39
+ end
40
+
41
+ def purge ec2, purge_after_days, list_only
42
+ purged = []
43
+ snapshots = Snapscatter.list ec2
44
+ snapshots.each do |snapshot|
45
+ purge_date = snapshot.start_time.to_date + purge_after_days
46
+ # puts "#{Date.today} > #{purge_date} == #{Date.today > purge_date}"
47
+ if Date.today > purge_date
48
+ snapshot.delete if not list_only
49
+ purged << snapshot
50
+ end
51
+ end
52
+
53
+ return purged
54
+ end
55
+
56
+ # consistency spec should look like the following (all parameters but host, optional)
57
+ # strategy: mongo, host: 127.0.0.1, port: 27017, usr: admin, pwd: 12345
58
+ def in_lock consistency_spec
59
+ locker = Locker.new Snapscatter.parse_spec(consistency_spec)
60
+ locker.lock
61
+ begin
62
+ yield
63
+ ensure
64
+ locker.unlock
65
+ end
66
+ end
67
+
68
+ module_function :targets, :list, :copy, :purge, :in_lock, :parse_spec
69
+
70
+ end
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'snapscatter/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "snapscatter"
8
+ spec.version = Snapscatter::VERSION
9
+ spec.authors = ["Alex Escalante"]
10
+ spec.email = ["alex.escalante@gmail.com"]
11
+ spec.description = %q{Geographically distributed and consistent AWS snapshots}
12
+ spec.summary = %q{Creates consistent snapshots from EBS volumes and copies them across regions for disaster recovery}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "thor"
22
+ spec.add_dependency 'aws-sdk', '~> 1.0'
23
+ spec.add_dependency "mongo"
24
+ spec.add_dependency "bson_ext"
25
+
26
+ spec.add_development_dependency "bundler", "~> 1.3"
27
+ spec.add_development_dependency "rake"
28
+ spec.add_development_dependency "cucumber"
29
+ spec.add_development_dependency "aruba"
30
+ end
metadata ADDED
@@ -0,0 +1,192 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: snapscatter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Alex Escalante
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-10-22 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: thor
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: aws-sdk
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '1.0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '1.0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: mongo
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: bson_ext
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: bundler
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ~>
84
+ - !ruby/object:Gem::Version
85
+ version: '1.3'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ~>
92
+ - !ruby/object:Gem::Version
93
+ version: '1.3'
94
+ - !ruby/object:Gem::Dependency
95
+ name: rake
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: cucumber
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ - !ruby/object:Gem::Dependency
127
+ name: aruba
128
+ requirement: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ! '>='
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ type: :development
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ! '>='
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ description: Geographically distributed and consistent AWS snapshots
143
+ email:
144
+ - alex.escalante@gmail.com
145
+ executables:
146
+ - snapscatter
147
+ extensions: []
148
+ extra_rdoc_files: []
149
+ files:
150
+ - .gitignore
151
+ - Gemfile
152
+ - LICENSE.txt
153
+ - README.md
154
+ - Rakefile
155
+ - bin/snapscatter
156
+ - features/snapscatter.feature
157
+ - features/support/setup.rb
158
+ - lib/snapscatter.rb
159
+ - lib/snapscatter/cli.rb
160
+ - lib/snapscatter/locker.rb
161
+ - lib/snapscatter/version.rb
162
+ - snapscatter.gemspec
163
+ homepage: ''
164
+ licenses:
165
+ - MIT
166
+ post_install_message:
167
+ rdoc_options: []
168
+ require_paths:
169
+ - lib
170
+ required_ruby_version: !ruby/object:Gem::Requirement
171
+ none: false
172
+ requirements:
173
+ - - ! '>='
174
+ - !ruby/object:Gem::Version
175
+ version: '0'
176
+ required_rubygems_version: !ruby/object:Gem::Requirement
177
+ none: false
178
+ requirements:
179
+ - - ! '>='
180
+ - !ruby/object:Gem::Version
181
+ version: '0'
182
+ requirements: []
183
+ rubyforge_project:
184
+ rubygems_version: 1.8.23
185
+ signing_key:
186
+ specification_version: 3
187
+ summary: Creates consistent snapshots from EBS volumes and copies them across regions
188
+ for disaster recovery
189
+ test_files:
190
+ - features/snapscatter.feature
191
+ - features/support/setup.rb
192
+ has_rdoc: