big_brother 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.
Files changed (48) hide show
  1. data/.gitignore +21 -0
  2. data/.rake_commit +1 -0
  3. data/.rvmrc +1 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE +22 -0
  6. data/README.md +29 -0
  7. data/Rakefile +6 -0
  8. data/big_brother.gemspec +33 -0
  9. data/bin/bigbro +6 -0
  10. data/bin/ocf_big_brother +174 -0
  11. data/config.ru +5 -0
  12. data/lib/big_brother.rb +49 -0
  13. data/lib/big_brother/app.rb +30 -0
  14. data/lib/big_brother/cli.rb +82 -0
  15. data/lib/big_brother/cluster.rb +70 -0
  16. data/lib/big_brother/configuration.rb +48 -0
  17. data/lib/big_brother/ipvs.rb +51 -0
  18. data/lib/big_brother/logger.rb +11 -0
  19. data/lib/big_brother/node.rb +30 -0
  20. data/lib/big_brother/shell_executor.rb +18 -0
  21. data/lib/big_brother/status_file.rb +26 -0
  22. data/lib/big_brother/ticker.rb +28 -0
  23. data/lib/big_brother/version.rb +3 -0
  24. data/lib/sinatra/synchrony.rb +40 -0
  25. data/lib/thin/backends/tcp_server_with_callbacks.rb +20 -0
  26. data/lib/thin/callback_rack_handler.rb +14 -0
  27. data/lib/thin/callbacks.rb +19 -0
  28. data/spec/big_brother/app_spec.rb +127 -0
  29. data/spec/big_brother/cluster_spec.rb +102 -0
  30. data/spec/big_brother/configuration_spec.rb +81 -0
  31. data/spec/big_brother/ipvs_spec.rb +26 -0
  32. data/spec/big_brother/node_spec.rb +44 -0
  33. data/spec/big_brother/shell_executor_spec.rb +21 -0
  34. data/spec/big_brother/status_file_spec.rb +39 -0
  35. data/spec/big_brother/ticker_spec.rb +60 -0
  36. data/spec/big_brother/version_spec.rb +7 -0
  37. data/spec/big_brother_spec.rb +119 -0
  38. data/spec/spec_helper.rb +57 -0
  39. data/spec/support/example_config.yml +34 -0
  40. data/spec/support/factories/cluster_factory.rb +13 -0
  41. data/spec/support/factories/node_factory.rb +9 -0
  42. data/spec/support/ipvsadm +3 -0
  43. data/spec/support/mock_session.rb +14 -0
  44. data/spec/support/null_logger.rb +7 -0
  45. data/spec/support/playback_executor.rb +13 -0
  46. data/spec/support/recording_executor.rb +11 -0
  47. data/spec/support/stub_server.rb +22 -0
  48. metadata +271 -0
@@ -0,0 +1,102 @@
1
+ require 'spec_helper'
2
+
3
+ describe BigBrother::Cluster do
4
+ describe "#start_monitoring!" do
5
+ it "marks the cluster as monitored" do
6
+ cluster = Factory.cluster
7
+ cluster.should_not be_monitored
8
+ cluster.start_monitoring!
9
+ cluster.should be_monitored
10
+ end
11
+
12
+ it "starts the service in IPVS" do
13
+ cluster = Factory.cluster(:fwmark => 100, :scheduler => 'wrr')
14
+
15
+ cluster.start_monitoring!
16
+ @recording_executor.commands.should include('ipvsadm --add-service --fwmark-service 100 --scheduler wrr')
17
+ end
18
+ end
19
+
20
+ describe "#stop_monitoring!" do
21
+ it "marks the cluster as unmonitored" do
22
+ cluster = Factory.cluster(:fwmark => 100)
23
+
24
+ cluster.start_monitoring!
25
+ cluster.should be_monitored
26
+
27
+ cluster.stop_monitoring!
28
+ cluster.should_not be_monitored
29
+ @recording_executor.commands.should include("ipvsadm --delete-service --fwmark-service 100")
30
+ end
31
+ end
32
+
33
+ describe "#needs_check?" do
34
+ it "requires the cluster to be monitored" do
35
+ cluster = Factory.cluster
36
+ cluster.needs_check?.should be_false
37
+ cluster.start_monitoring!
38
+ cluster.needs_check?.should be_true
39
+ end
40
+ end
41
+
42
+ describe "#monitor_nodes" do
43
+ it "marks the cluster as no longer requiring monitoring" do
44
+ cluster = Factory.cluster
45
+ cluster.start_monitoring!
46
+ cluster.needs_check?.should be_true
47
+ cluster.monitor_nodes
48
+ cluster.needs_check?.should be_false
49
+ end
50
+
51
+ it "updates the weight for each node" do
52
+ node = Factory.node(:address => '127.0.0.1')
53
+ node.should_receive(:current_health).and_return(56)
54
+ cluster = Factory.cluster(:fwmark => 100, :nodes => [node])
55
+
56
+ cluster.monitor_nodes
57
+
58
+ @recording_executor.commands.should include("ipvsadm --edit-server --fwmark-service 100 --real-server 127.0.0.1 --ipip --weight 56")
59
+ end
60
+
61
+ it "sets the weight to 100 for each node if an upfile exists" do
62
+ node = Factory.node(:address => '127.0.0.1')
63
+ node.stub(:current_health).and_return(56)
64
+ cluster = Factory.cluster(:name => 'test', :fwmark => 100, :nodes => [node])
65
+
66
+ BigBrother::StatusFile.new('up', 'test').create('Up for testing')
67
+
68
+ cluster.monitor_nodes
69
+
70
+ @recording_executor.commands.should include("ipvsadm --edit-server --fwmark-service 100 --real-server 127.0.0.1 --ipip --weight 100")
71
+ end
72
+
73
+ it "sets the weight to 0 for each node if a downfile exists" do
74
+ node = Factory.node(:address => '127.0.0.1')
75
+ node.stub(:current_health).and_return(56)
76
+ cluster = Factory.cluster(:name => 'test', :fwmark => 100, :nodes => [node])
77
+
78
+ BigBrother::StatusFile.new('down', 'test').create('Down for testing')
79
+
80
+ cluster.monitor_nodes
81
+
82
+ @recording_executor.commands.should include("ipvsadm --edit-server --fwmark-service 100 --real-server 127.0.0.1 --ipip --weight 0")
83
+ end
84
+ end
85
+
86
+ describe "#resume_monitoring!" do
87
+ it "marks the cluster as monitored" do
88
+ cluster = Factory.cluster
89
+
90
+ cluster.monitored?.should be_false
91
+ cluster.resume_monitoring!
92
+ cluster.monitored?.should be_true
93
+ end
94
+ end
95
+
96
+ describe "#to_s" do
97
+ it "is the clusters name and fwmark" do
98
+ cluster = Factory.cluster(:name => 'name', :fwmark => 100)
99
+ cluster.to_s.should == "name (100)"
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,81 @@
1
+ require 'spec_helper'
2
+
3
+ describe BigBrother::Configuration do
4
+ describe '.evaluate' do
5
+ it 'returns a hash of clusters' do
6
+ clusters = BigBrother::Configuration.evaluate(TEST_CONFIG)
7
+
8
+ clusters['test1'].check_interval.should == 1
9
+ clusters['test1'].scheduler.should == 'wrr'
10
+ clusters['test1'].fwmark.should == 1
11
+
12
+ clusters['test2'].check_interval.should == 1
13
+ clusters['test2'].scheduler.should == 'wrr'
14
+ clusters['test2'].fwmark.should == 2
15
+
16
+ clusters['test3'].check_interval.should == 1
17
+ clusters['test3'].scheduler.should == 'wrr'
18
+ clusters['test3'].fwmark.should == 3
19
+ end
20
+
21
+ it 'populates a clusters nodes' do
22
+ clusters = BigBrother::Configuration.evaluate(TEST_CONFIG)
23
+
24
+ clusters['test1'].nodes.length.should == 2
25
+
26
+ clusters['test1'].nodes[0].address == '127.0.0.1'
27
+ clusters['test1'].nodes[0].port == '9001'
28
+ clusters['test1'].nodes[0].path == '/test/valid'
29
+
30
+ clusters['test1'].nodes[1].address == '127.0.0.1'
31
+ clusters['test1'].nodes[1].port == '9002'
32
+ clusters['test1'].nodes[1].path == '/test/valid'
33
+ end
34
+ end
35
+
36
+ describe '.synchronize_with_ipvs' do
37
+ it "monitors clusters that were already monitored" do
38
+ clusters = {}
39
+ clusters['one'] = Factory.cluster(:fwmark => 1)
40
+ clusters['two'] = Factory.cluster(:fwmark => 2)
41
+ clusters['three'] = Factory.cluster(:fwmark => 3)
42
+
43
+ ipvs_state = {}
44
+ ipvs_state['1'] = ['127.0.0.1']
45
+ ipvs_state['3'] = ['127.0.0.1']
46
+
47
+ BigBrother::Configuration.synchronize_with_ipvs(clusters, ipvs_state)
48
+
49
+ clusters['one'].should be_monitored
50
+ clusters['two'].should_not be_monitored
51
+ clusters['three'].should be_monitored
52
+ end
53
+
54
+ it "does not attempt to re-add the services it was monitoring" do
55
+ clusters = { 'one' => Factory.cluster(:fwmark => 1, :nodes => [Factory.node(:address => '127.0.0.1')]) }
56
+ ipvs_state = { '1' => ['127.0.0.1'] }
57
+
58
+ BigBrother::Configuration.synchronize_with_ipvs(clusters, ipvs_state)
59
+
60
+ @recording_executor.commands.should be_empty
61
+ end
62
+
63
+ it "removes nodes that are no longer part of the cluster" do
64
+ clusters = { 'one' => Factory.cluster(:fwmark => 1, :nodes => [Factory.node(:address => '127.0.0.1')]) }
65
+ ipvs_state = { '1' => ['127.0.1.1', '127.0.0.1'] }
66
+
67
+ BigBrother::Configuration.synchronize_with_ipvs(clusters, ipvs_state)
68
+
69
+ @recording_executor.commands.last.should == "ipvsadm --delete-server --fwmark-service 1 --real-server 127.0.1.1"
70
+ end
71
+
72
+ it "adds new nodes to the cluster" do
73
+ clusters = { 'one' => Factory.cluster(:fwmark => 1, :nodes => [Factory.node(:address => '127.0.0.1'), Factory.node(:address => "127.0.1.1")]) }
74
+ ipvs_state = { '1' => ['127.0.0.1'] }
75
+
76
+ BigBrother::Configuration.synchronize_with_ipvs(clusters, ipvs_state)
77
+
78
+ @recording_executor.commands.should include("ipvsadm --add-server --fwmark-service 1 --real-server 127.0.1.1 --ipip --weight 100")
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+
3
+ describe BigBrother::IPVS do
4
+ describe "#running_configuration" do
5
+ it "returns a parsed version of the running config" do
6
+ playback = PlaybackExecutor.new
7
+ playback.add_response(<<-OUTPUT, 0)
8
+ -A -f 3 -s wrr
9
+ -a -f 3 -r 10.0.1.220:80 -i -w 1
10
+ -a -f 3 -r 10.0.1.221:80 -i -w 1
11
+ -a -f 3 -r 10.0.1.222:80 -i -w 1
12
+ -A -f 1 -s wrr
13
+ -a -f 1 -r 10.0.1.223:80 -i -w 1
14
+ -a -f 1 -r 10.0.1.224:80 -i -w 1
15
+ -A -f 2 -s wrr
16
+ -a -f 2 -r 10.0.1.225:80 -i -w 1
17
+ OUTPUT
18
+ config = BigBrother::IPVS.new(playback).running_configuration
19
+
20
+ config.size.should == 3
21
+ config['3'].should == ['10.0.1.220', '10.0.1.221', '10.0.1.222']
22
+ config['1'].should == ['10.0.1.223', '10.0.1.224']
23
+ config['2'].should == ['10.0.1.225']
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,44 @@
1
+ require 'spec_helper'
2
+
3
+ describe BigBrother::Node do
4
+ describe "#current_health" do
5
+ run_in_reactor
6
+
7
+ it "returns its health" do
8
+ StubServer.new(<<-HTTP, 0.25)
9
+ HTTP/1.0 200 OK
10
+ Connection: close
11
+
12
+ Health: 59
13
+ HTTP
14
+ node = BigBrother::Node.new("127.0.0.1", 8081, "/test/status")
15
+ node.current_health.should == 59
16
+ end
17
+
18
+ it "returns 0 for an unknown service" do
19
+ StubServer.new(<<-HTTP, 0.25)
20
+ HTTP/1.0 503 Service Unavailable
21
+ Connection: close
22
+ HTTP
23
+ node = BigBrother::Node.new("127.0.0.1", 8081, "/test/status")
24
+ node.current_health.should == 0
25
+ end
26
+
27
+ it "returns 0 for an unknown DNS entry" do
28
+ node = BigBrother::Node.new("junk.local", 8081, "/test/status")
29
+ node.current_health.should == 0
30
+ end
31
+
32
+ it "returns the health if it is passed in a header" do
33
+ StubServer.new(<<-HTTP, 0.25)
34
+ HTTP/1.0 200 OK
35
+ Connection: close
36
+ X-Health: 61
37
+
38
+ This part is for people.
39
+ HTTP
40
+ node = BigBrother::Node.new("127.0.0.1", 8081, "/test/status")
41
+ node.current_health.should == 61
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,21 @@
1
+ require 'spec_helper'
2
+
3
+ describe BigBrother::ShellExecutor do
4
+ describe "#invoke" do
5
+ run_in_reactor
6
+
7
+ it "runs a command" do
8
+ executor = BigBrother::ShellExecutor.new
9
+ output, exit_status = executor.invoke('echo hi')
10
+ output.should == "hi\n"
11
+ exit_status.should == 0
12
+ end
13
+
14
+ it "failed command" do
15
+ executor = BigBrother::ShellExecutor.new
16
+ output, exit_status = executor.invoke('test -e /this/isnt/here')
17
+ output.should == ""
18
+ exit_status.should == 1
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,39 @@
1
+ require 'spec_helper'
2
+
3
+ describe BigBrother::StatusFile do
4
+ describe "create" do
5
+ it "creates a nested file" do
6
+ status_file = BigBrother::StatusFile.new("foo", "bar")
7
+ status_file.create("for testing")
8
+
9
+ status_file.exists?.should == true
10
+ end
11
+
12
+ it "creates a file" do
13
+ status_file = BigBrother::StatusFile.new("foo")
14
+ status_file.create("for testing")
15
+
16
+ status_file.exists?.should == true
17
+ end
18
+
19
+ it "writes the content" do
20
+ status_file = BigBrother::StatusFile.new("foo")
21
+ status_file.create("for testing")
22
+
23
+ status_file.content.should == "for testing"
24
+ end
25
+ end
26
+
27
+ describe "delete" do
28
+ it "removes the file" do
29
+ status_file = BigBrother::StatusFile.new("foo")
30
+ status_file.create("for testing")
31
+
32
+ status_file.exists?.should be_true
33
+
34
+ status_file.delete
35
+
36
+ status_file.exists?.should be_false
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,60 @@
1
+ require 'spec_helper'
2
+
3
+ describe BigBrother::Ticker do
4
+ describe ".schedule!" do
5
+ it "schedules the ticker to run ten times every second" do
6
+ EventMachine.should_receive(:add_periodic_timer).with(0.1)
7
+
8
+ BigBrother::Ticker.schedule!
9
+ end
10
+ end
11
+
12
+ describe ".tick" do
13
+ describe "end-to-end" do
14
+ run_in_reactor
15
+ with_litmus_server '127.0.0.1', 8081, 74
16
+ with_litmus_server public_ip_address, 8082, 76
17
+
18
+ it "monitors clusters requiring monitoring" do
19
+ BigBrother.clusters['test'] = Factory.cluster(
20
+ :fwmark => 100,
21
+ :nodes => [
22
+ Factory.node(:address => '127.0.0.1', :port => 8081),
23
+ Factory.node(:address => public_ip_address, :port => 8082)
24
+ ]
25
+ )
26
+ BigBrother.clusters['test'].start_monitoring!
27
+ @recording_executor.commands.clear
28
+
29
+ BigBrother::Ticker.tick
30
+
31
+ @recording_executor.commands.should include("ipvsadm --edit-server --fwmark-service 100 --real-server 127.0.0.1 --ipip --weight 74")
32
+ @recording_executor.commands.should include("ipvsadm --edit-server --fwmark-service 100 --real-server #{public_ip_address} --ipip --weight 76")
33
+ end
34
+
35
+ it "only monitors a cluster once in the given interval" do
36
+ BigBrother.clusters['test'] = Factory.cluster(
37
+ :fwmark => 100,
38
+ :nodes => [Factory.node(:address => '127.0.0.1', :port => 8081)]
39
+ )
40
+ BigBrother.clusters['test'].start_monitoring!
41
+ @recording_executor.commands.clear
42
+
43
+ BigBrother::Ticker.tick
44
+ BigBrother::Ticker.tick
45
+
46
+ @recording_executor.commands.should == ["ipvsadm --edit-server --fwmark-service 100 --real-server 127.0.0.1 --ipip --weight 74"]
47
+ end
48
+ end
49
+
50
+ it "monitors clusters requiring monitoring" do
51
+ BigBrother.clusters['one'] = Factory.cluster
52
+ BigBrother.clusters['two'] = Factory.cluster
53
+ BigBrother.clusters['two'].start_monitoring!
54
+
55
+ BigBrother.clusters['two'].should_receive(:monitor_nodes)
56
+
57
+ BigBrother::Ticker.tick
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ describe BigBrother::VERSION do
4
+ it "is not nil" do
5
+ BigBrother::VERSION.should_not be_nil
6
+ end
7
+ end
@@ -0,0 +1,119 @@
1
+ require 'spec_helper'
2
+
3
+ describe BigBrother do
4
+ describe '.configure' do
5
+ it "reads the configuration file" do
6
+ BigBrother.configure(TEST_CONFIG)
7
+ BigBrother.clusters.size.should == 3
8
+ end
9
+
10
+ it "synchronizes the configuration with the current state of IPVS" do
11
+ playback = PlaybackExecutor.new
12
+ playback.add_response(<<-OUTPUT, 0)
13
+ -A -f 1 -s wrr
14
+ -a -f 1 -r 10.0.1.223:80 -i -w 1
15
+ -a -f 1 -r 10.0.1.224:80 -i -w 1
16
+ -A -f 2 -s wrr
17
+ -a -f 2 -r 10.0.1.225:80 -i -w 1
18
+ OUTPUT
19
+ BigBrother.ipvs = BigBrother::IPVS.new(playback)
20
+ BigBrother.configure(TEST_CONFIG)
21
+
22
+ BigBrother.clusters['test1'].should be_monitored
23
+ BigBrother.clusters['test2'].should be_monitored
24
+ end
25
+ end
26
+
27
+ describe '.reconfigure' do
28
+ run_in_reactor
29
+ around(:each) do |spec|
30
+ response_time = 1 #seconds
31
+ server = StubServer.new(<<HTTP, response_time, 9001, '127.0.0.1')
32
+ HTTP/1.0 200 OK
33
+ Connection: close
34
+
35
+ Health: 50
36
+ HTTP
37
+ spec.run
38
+ server.stop
39
+ end
40
+ it "reconfigures the clusters" do
41
+ config_file = Tempfile.new('config.yml')
42
+ File.open(config_file, 'w') do |f|
43
+ f.puts(<<-EOF)
44
+ ---
45
+ test1:
46
+ checkInterval: 1
47
+ scheduler: wrr
48
+ fwmark: 1
49
+ nodes:
50
+ - address: 127.0.0.1
51
+ port: 9001
52
+ path: /test/valid
53
+ EOF
54
+ end
55
+ BigBrother.configure(config_file)
56
+ BigBrother.start_ticker!
57
+ BigBrother.clusters['test1'].nodes.first.path.should == "/test/valid"
58
+
59
+ File.open(config_file, 'w') do |f|
60
+ f.puts(<<-EOF)
61
+ ---
62
+ test1:
63
+ checkInterval: 1
64
+ scheduler: wrr
65
+ fwmark: 1
66
+ nodes:
67
+ - address: 127.0.0.1
68
+ port: 9001
69
+ path: /test/another/path
70
+ EOF
71
+ end
72
+ BigBrother.reconfigure
73
+ BigBrother.clusters['test1'].nodes.first.path.should == "/test/another/path"
74
+ end
75
+
76
+ it "stops the ticker and reconfigures after it has finished all its ticks" do
77
+ config_file = Tempfile.new('config.yml')
78
+ File.open(config_file, 'w') do |f|
79
+ f.puts(<<-EOF)
80
+ ---
81
+ test1:
82
+ checkInterval: 1
83
+ scheduler: wrr
84
+ fwmark: 1
85
+ nodes:
86
+ - address: 127.0.0.1
87
+ port: 9001
88
+ path: /test/valid
89
+ EOF
90
+ end
91
+ BigBrother.configure(config_file)
92
+ BigBrother.clusters['test1'].start_monitoring!
93
+ @recording_executor.commands.clear
94
+
95
+ BigBrother.start_ticker!
96
+
97
+ EM::Synchrony.sleep(0.2)
98
+
99
+ File.open(config_file, 'w') do |f|
100
+ f.puts(<<-EOF)
101
+ ---
102
+ test1:
103
+ checkInterval: 1
104
+ scheduler: wrr
105
+ fwmark: 1
106
+ nodes:
107
+ - address: 127.0.0.1
108
+ port: 9001
109
+ path: /test/another/path
110
+ EOF
111
+ end
112
+ BigBrother.reconfigure
113
+ BigBrother.clusters['test1'].nodes.first.path.should == "/test/another/path"
114
+
115
+ @recording_executor.commands.first.should include("--weight 50")
116
+ @recording_executor.commands.last.should == "ipvsadm --save --numeric"
117
+ end
118
+ end
119
+ end