snap-ebs 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
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,5 @@
1
+ #!/usr/bin/env ruby
2
+ $:.unshift File.join File.dirname(__FILE__), '..'
3
+ require 'lib/snap_ebs.rb'
4
+
5
+ SnapEbs.new.execute
@@ -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: []