nat-monitor 1.0.0

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: 3665bd673f7f721528355cfacc695941a2f98063
4
+ data.tar.gz: aa462845fffc6a241368013dc1b90b609bb3b1bd
5
+ SHA512:
6
+ metadata.gz: f6bd6266e6104e3f42baea9a2c408ec45b9a714511cfc1263ae7f2937b26fdbb87c856817edc214d5f0a0ade6307e65190a1ccc457d6897e63dc0d40d9f5346a
7
+ data.tar.gz: 3c86db404315f8e6dfc0aeae92fd0ac9969b3823b258791913f24e2110fe198ba053eb93bf78e45e0b3ddfb0fe4aac47e6c0261546efa34d11767eab3d076db4
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in nat-monitor.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Eric Herot
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,55 @@
1
+ # Nat::Monitor
2
+
3
+ Monitors a quorum of NAT servers for an outage and reassigns the specified EC2 route table to point to a working server.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'nat-monitor'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install nat-monitor
20
+
21
+ ## Usage
22
+
23
+ Run it as a service after creating the configuration YAML file.
24
+
25
+ E.g.:
26
+
27
+ $ nat-monitor [OPTIONAL CONF_FILE]
28
+
29
+ By default it will check `/etc/nat_monitor.yml` for its configuration.
30
+
31
+ ## Example Configuration
32
+
33
+ ```yaml
34
+ ---
35
+ route_table_id: rtb-00000001
36
+ nodes:
37
+ i-00000001: 10.0.0.1
38
+ i-00000002: 10.0.1.1
39
+ i-00000003: 10.0.2.1
40
+ ```
41
+
42
+ Optional properties include:
43
+ ```yaml
44
+ pings: 3
45
+ ping_timeout: 1
46
+ heartbeat_interval: 10
47
+ ```
48
+
49
+ ## Contributing
50
+
51
+ 1. Fork it ( https://github.com/[my-github-username]/nat-monitor/fork )
52
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
53
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
54
+ 4. Push to the branch (`git push origin my-new-feature`)
55
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
data/bin/nat-monitor ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'nat-monitor'
4
+
5
+ EtTools::NatMonitor.new(ARGV[0]).run
@@ -0,0 +1,121 @@
1
+ module EtTools
2
+ class NatMonitor
3
+ require 'net/http'
4
+ require 'net/ping'
5
+ require 'fog'
6
+ require 'yaml'
7
+
8
+ def initialize(conf_file = nil)
9
+ @conf = YAML.load_file(conf_file || '/etc/nat_monitor.yml')
10
+ @conf = defaults.merge @conf
11
+ end
12
+
13
+ def run
14
+ validate!
15
+ output 'Starting NAT Monitor'
16
+ main_loop
17
+ end
18
+
19
+ def validate!
20
+ case
21
+ when !@conf['route_table_id']
22
+ output 'route_table_id not specified'
23
+ exit 1
24
+ when !route_exists?(@conf['route_table_id'])
25
+ output "Route #{@conf['route_table_id']} not found"
26
+ exit 2
27
+ when @conf['nodes'].count < 3
28
+ output '3 or more nodes are required to create a quorum'
29
+ exit 3
30
+ end
31
+ end
32
+
33
+ def output(message)
34
+ puts message
35
+ end
36
+
37
+ def defaults
38
+ { 'pings' => 3,
39
+ 'ping_timeout' => 1,
40
+ 'heartbeat_interval' => 10 }
41
+ end
42
+
43
+ def main_loop
44
+ loop do
45
+ heartbeat
46
+ sleep @conf['heartbeat_interval']
47
+ end
48
+ end
49
+
50
+ def heartbeat
51
+ return if am_i_master?
52
+ un = unreachable_nodes
53
+ return if (un.count == other_nodes.keys.count) || # Next if I'm unreachable...
54
+ !un.include?(current_master) # ...unless master is unreachable
55
+ steal_route
56
+ end
57
+
58
+ def steal_route
59
+ output 'Stealing route 0.0.0.0/0 on route table ' \
60
+ "#{@conf['route_table_id']}"
61
+ connection.replace_route(
62
+ @conf['route_table_id'],
63
+ '0.0.0.0/0',
64
+ my_instance_id
65
+ )
66
+ end
67
+
68
+ def unreachable_nodes
69
+ other_nodes.select { |_node, ip| !pingable?(ip) }
70
+ end
71
+
72
+ def other_nodes
73
+ @other_nodes ||= begin
74
+ nodes = @conf['nodes'].dup
75
+ nodes.delete my_instance_id
76
+ nodes
77
+ end
78
+ end
79
+
80
+ def pingable?(ip)
81
+ p = Net::Ping::External.new(ip)
82
+ p.timeout = @conf['ping_timeout']
83
+ p.ping?
84
+ end
85
+
86
+ def route_exists?(route_id)
87
+ connection.route_tables.map(&:id).include? route_id
88
+ end
89
+
90
+ def connection
91
+ @connection ||= begin
92
+ Fog::Compute::AWS.new(aws_access_key_id: 'AWS_ACCESS_KEY_ID',
93
+ aws_secret_access_key: 'AWS_SECRET_ACCESS_KEY')
94
+ end
95
+ end
96
+
97
+ def current_master
98
+ default_r = connection.route_tables.get(route_table_id).routes.find do |r|
99
+ r['destinationCidrBlock'] == '0.0.0.0/0'
100
+ end
101
+ default_r['instanceId']
102
+ end
103
+
104
+ def my_instance_id
105
+ @my_instance_id ||= begin
106
+ Net::HTTP.get(
107
+ '169.254.169.254',
108
+ '/latest/meta-data/instance-id'
109
+ )
110
+ end
111
+ end
112
+
113
+ def am_i_master?
114
+ master_node? my_instance_id
115
+ end
116
+
117
+ def master_node?(node_id)
118
+ current_master == node_id
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,5 @@
1
+ module EtTools
2
+ class NatMonitor
3
+ VERSION = '1.0.0'
4
+ end
5
+ end
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'nat-monitor/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'nat-monitor'
8
+ spec.version = EtTools::NatMonitor::VERSION
9
+ spec.authors = ['Eric Herot']
10
+ spec.email = ['eric.github@herot.com']
11
+ spec.summary = 'A service for providing an HA NAT in EC2'
12
+ spec.description = spec.summary
13
+ spec.homepage = ''
14
+ spec.license = 'Apache 2.0'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(/^(test|spec|features)\//)
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_development_dependency 'bundler', '~> 1.7'
22
+ spec.add_development_dependency 'rake', '~> 10.0'
23
+
24
+ spec.add_runtime_dependency 'fog', '~> 1.23'
25
+ end
@@ -0,0 +1,178 @@
1
+ require 'spec_helper'
2
+ require 'byebug'
3
+ require 'nat-monitor'
4
+
5
+ describe EtTools::NatMonitor do
6
+ before(:each) do
7
+ @route_table_id = 'rtb-00000000'
8
+ @my_instance_id = 'i-00000001'
9
+
10
+ @other_nodes = { 'i-00000002' => '1.1.1.2',
11
+ 'i-00000003' => '1.1.1.3' }
12
+
13
+ @yaml_conf = { 'route_table_id' => @route_table_id,
14
+ 'nodes' => (
15
+ { @my_instance_id => '1.1.1.1' }
16
+ ).merge(@other_nodes) }
17
+
18
+ filepath = 'bogus_filename.yml'
19
+ allow(YAML).to receive(:load_file).with(filepath)
20
+ .and_return(@yaml_conf)
21
+
22
+ @nat_monitor = EtTools::NatMonitor.new(filepath)
23
+
24
+ @defaults = { 'pings' => 3,
25
+ 'ping_timeout' => 1,
26
+ 'heartbeat_interval' => 10 }
27
+
28
+ allow(@nat_monitor).to receive(:my_instance_id).and_return(@my_instance_id)
29
+ allow(@nat_monitor).to receive(:steal_route).and_return(true)
30
+ end
31
+
32
+ context 'fewer than 3 nodes are specified' do
33
+ before do
34
+ allow(YAML).to receive(:load_file).with(any_args)
35
+ @nat_monitor.instance_variable_set(
36
+ :@conf,
37
+ ({ 'route_table_id' => @route_table_id,
38
+ 'nodes' => @other_nodes })
39
+ )
40
+ allow(@nat_monitor).to receive(:route_exists?).with(any_args)
41
+ .and_return true
42
+ end
43
+
44
+ it 'exits with status 3' do
45
+ expect(@nat_monitor).to receive(:exit).with(3)
46
+ @nat_monitor.validate!
47
+ end
48
+ end
49
+
50
+ context 'invalid route is specified' do
51
+ # connection.route_tables.map(&:id).include? route_id
52
+ before do
53
+ allow_any_instance_of(Fog::Compute::AWS).to receive(:route_tables)
54
+ .and_return [
55
+ double('route_tables',
56
+ id: 'rtb-99999999')
57
+ ]
58
+ end
59
+
60
+ it 'exits with status 2' do
61
+ expect(@nat_monitor).to receive(:route_exists?).with(@route_table_id)
62
+ .and_return false
63
+ expect(@nat_monitor).to receive(:exit).with(2)
64
+ @nat_monitor.validate!
65
+ end
66
+ end
67
+
68
+ context 'no route is specified' do
69
+ before do
70
+ @nat_monitor.instance_variable_set(
71
+ :@conf,
72
+ 'nodes' => ({ @my_instance_id => '1.1.1.1' }).merge(@other_nodes)
73
+ )
74
+ end
75
+
76
+ it 'exits with status 1' do
77
+ expect(@nat_monitor).to receive(:exit).with(1)
78
+ @nat_monitor.validate!
79
+ end
80
+ end
81
+
82
+ context 'local node is the master' do
83
+ before do
84
+ allow(@nat_monitor).to(
85
+ receive(:current_master).and_return(@my_instance_id)
86
+ )
87
+ end
88
+
89
+ it 'sets @conf correctly' do
90
+ expect(@nat_monitor.instance_variable_get(:@conf)).to eq(
91
+ @yaml_conf.merge(@defaults)
92
+ )
93
+ end
94
+
95
+ it 'and knows that it is master' do
96
+ expect(@nat_monitor).to receive(:am_i_master?).and_return(true)
97
+ @nat_monitor.heartbeat
98
+ end
99
+
100
+ it 'and does not check for unreachable nodes' do
101
+ expect(@nat_monitor).to_not receive(:unreachable_nodes)
102
+ @nat_monitor.heartbeat
103
+ end
104
+ end
105
+
106
+ context 'local node is not master' do
107
+ before do
108
+ allow(@nat_monitor).to(
109
+ receive(:current_master).and_return('i-00000002')
110
+ )
111
+ end
112
+
113
+ context 'and can ping everything' do
114
+ before do
115
+ allow(@nat_monitor).to receive(:pingable?).with(any_args)
116
+ .and_return true
117
+ end
118
+
119
+ it 'does not try to steal the route' do
120
+ expect(@nat_monitor).to_not receive(:steal_route)
121
+ @nat_monitor.heartbeat
122
+ end
123
+ end
124
+
125
+ context 'and can\'t ping anything' do
126
+ before do
127
+ allow(@nat_monitor).to receive(:pingable?).with(any_args)
128
+ .and_return false
129
+ end
130
+
131
+ it 'counts unreachable nodes correctly' do
132
+ allow(@nat_monitor).to receive(:other_nodes)
133
+ .and_return(@other_nodes)
134
+ expect(@nat_monitor).to receive(:unreachable_nodes)
135
+ .and_return(@other_nodes)
136
+ @nat_monitor.heartbeat
137
+ end
138
+
139
+ it 'does not try to steal the route' do
140
+ expect(@nat_monitor).to receive(:unreachable_nodes)
141
+ .and_return(@other_nodes)
142
+ expect(@nat_monitor).to_not receive(:steal_route)
143
+ @nat_monitor.heartbeat
144
+ end
145
+ end
146
+
147
+ context 'and can\'t ping the master' do
148
+ before do
149
+ allow(@nat_monitor).to receive(:pingable?).with('1.1.1.2')
150
+ .and_return false
151
+ allow(@nat_monitor).to receive(:pingable?).with('1.1.1.3')
152
+ .and_return true
153
+ allow_any_instance_of(Fog::Compute::AWS).to receive(:replace_route)
154
+ .with(@route_table_id, '0.0.0.0', @my_instance_id)
155
+ .and_return true
156
+ end
157
+
158
+ it 'computes the list of other nodes correctly' do
159
+ expect(@nat_monitor).to receive(:other_nodes)
160
+ .exactly(2).times.and_return(@other_nodes)
161
+ @nat_monitor.heartbeat
162
+ end
163
+
164
+ it 'finds i-00000002 unreachable' do
165
+ expect(@nat_monitor).to receive(:unreachable_nodes)
166
+ .and_return('i-00000002' => '1.1.1.2')
167
+ @nat_monitor.heartbeat
168
+ end
169
+
170
+ it 'tries to steal the route' do
171
+ allow(@nat_monitor).to receive(:other_nodes)
172
+ .and_return(@other_nodes)
173
+ expect(@nat_monitor).to receive(:steal_route)
174
+ @nat_monitor.heartbeat
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,4 @@
1
+ require 'rspec'
2
+ require 'simplecov'
3
+
4
+ SimpleCov.start
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nat-monitor
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Eric Herot
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-02-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.7'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: fog
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.23'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.23'
55
+ description: A service for providing an HA NAT in EC2
56
+ email:
57
+ - eric.github@herot.com
58
+ executables:
59
+ - nat-monitor
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - ".gitignore"
64
+ - Gemfile
65
+ - LICENSE.txt
66
+ - README.md
67
+ - Rakefile
68
+ - bin/nat-monitor
69
+ - lib/nat-monitor.rb
70
+ - lib/nat-monitor/version.rb
71
+ - nat-monitor.gemspec
72
+ - spec/lib/nat-monitor_spec.rb
73
+ - spec/spec_helper.rb
74
+ homepage: ''
75
+ licenses:
76
+ - Apache 2.0
77
+ metadata: {}
78
+ post_install_message:
79
+ rdoc_options: []
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ requirements: []
93
+ rubyforge_project:
94
+ rubygems_version: 2.4.5
95
+ signing_key:
96
+ specification_version: 4
97
+ summary: A service for providing an HA NAT in EC2
98
+ test_files:
99
+ - spec/lib/nat-monitor_spec.rb
100
+ - spec/spec_helper.rb