snapscatter 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/.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: