big_brother 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.2
4
+ - 1.9.3
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # BigBrother
2
2
 
3
+ [![Build Status](https://secure.travis-ci.org/braintree/big_brother.png)](http://travis-ci.org/braintree/big_brother)
4
+
3
5
  TODO: Write a gem description
4
6
 
5
7
  ## Installation
@@ -46,25 +46,20 @@ module BigBrother
46
46
  end
47
47
 
48
48
  def monitor_nodes
49
- @nodes.each do |node|
50
- BigBrother.ipvs.edit_node(@fwmark, node.address, _determine_weight(node))
51
- end
52
-
49
+ @nodes.each { |node| node.monitor(self) }
53
50
  @last_check = Time.now
54
51
  end
55
52
 
56
- def _determine_weight(node)
57
- if @up_file.exists?
58
- 100
59
- elsif @down_file.exists?
60
- 0
61
- else
62
- node.current_health
63
- end
64
- end
65
-
66
53
  def to_s
67
54
  "#{@name} (#{@fwmark})"
68
55
  end
56
+
57
+ def up_file_exists?
58
+ @up_file.exists?
59
+ end
60
+
61
+ def down_file_exists?
62
+ @down_file.exists?
63
+ end
69
64
  end
70
65
  end
@@ -11,15 +11,19 @@ module BigBrother
11
11
  end
12
12
 
13
13
  def self.synchronize_with_ipvs(clusters, ipvs_state)
14
- clusters.values.each do |cluster|
15
- if ipvs_state.has_key?(cluster.fwmark.to_s)
16
- cluster.resume_monitoring!
14
+ ipvs_state.each do |fwmark, running_nodes|
15
+ running_cluster = clusters.values.detect { |cluster| cluster.fwmark == fwmark.to_i }
17
16
 
18
- running_nodes = ipvs_state[cluster.fwmark.to_s]
19
- cluster_nodes = cluster.nodes.map(&:address)
17
+ if running_cluster.nil?
18
+ BigBrother.ipvs.stop_cluster(fwmark)
19
+ else
20
+ running_cluster.resume_monitoring!
20
21
 
21
- _remove_nodes(cluster, running_nodes - cluster_nodes)
22
- _add_nodes(cluster, cluster_nodes - running_nodes)
22
+ running_nodes = ipvs_state[running_cluster.fwmark.to_s]
23
+ cluster_nodes = running_cluster.nodes.map(&:address)
24
+
25
+ _remove_nodes(running_cluster, running_nodes - cluster_nodes)
26
+ _add_nodes(running_cluster, cluster_nodes - running_nodes)
23
27
  end
24
28
  end
25
29
  end
@@ -0,0 +1,13 @@
1
+ module BigBrother
2
+ class HealthFetcher
3
+ def self.current_health(address, port, path)
4
+ http_response = EventMachine::HttpRequest.new("http://#{address}:#{port}#{path}").get
5
+
6
+ if http_response.response_header.has_key?('X_HEALTH')
7
+ http_response.response_header['X_HEALTH'].to_i
8
+ else
9
+ http_response.response.slice(/Health: (\d+)/, 1).to_i
10
+ end
11
+ end
12
+ end
13
+ end
@@ -8,22 +8,24 @@ module BigBrother
8
8
  @address = address
9
9
  @port = port
10
10
  @path = path
11
+ @weight = nil
11
12
  end
12
13
 
13
- def current_health
14
- response = _get("http://#{@address}:#{@port}#{@path}")
15
- _parse_health(response)
16
- end
17
-
18
- def _get(url)
19
- EventMachine::HttpRequest.new(url).get
14
+ def monitor(cluster)
15
+ new_weight = _determine_weight(cluster)
16
+ if new_weight != @weight
17
+ BigBrother.ipvs.edit_node(cluster.fwmark, address, _determine_weight(cluster))
18
+ @weight = new_weight
19
+ end
20
20
  end
21
21
 
22
- def _parse_health(http_response)
23
- if http_response.response_header.has_key?('X_HEALTH')
24
- http_response.response_header['X_HEALTH'].to_i
22
+ def _determine_weight(cluster)
23
+ if cluster.up_file_exists?
24
+ 100
25
+ elsif cluster.down_file_exists?
26
+ 0
25
27
  else
26
- http_response.response.slice(/Health: (\d+)/, 1).to_i
28
+ BigBrother::HealthFetcher.current_health(@address, @port, @path)
27
29
  end
28
30
  end
29
31
  end
@@ -1,3 +1,3 @@
1
1
  module BigBrother
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
data/lib/big_brother.rb CHANGED
@@ -10,6 +10,7 @@ require 'sinatra/synchrony'
10
10
  require 'big_brother/app'
11
11
  require 'big_brother/cluster'
12
12
  require 'big_brother/configuration'
13
+ require 'big_brother/health_fetcher'
13
14
  require 'big_brother/ipvs'
14
15
  require 'big_brother/logger'
15
16
  require 'big_brother/node'
@@ -48,38 +48,15 @@ describe BigBrother::Cluster do
48
48
  cluster.needs_check?.should be_false
49
49
  end
50
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])
51
+ it "calls monitor on each of the nodes" do
52
+ node1 = Factory.node
53
+ node2 = Factory.node
54
+ cluster = Factory.cluster(:nodes => [node1, node2])
55
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')
56
+ node1.should_receive(:monitor).with(cluster)
57
+ node2.should_receive(:monitor).with(cluster)
79
58
 
80
59
  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
60
  end
84
61
  end
85
62
 
@@ -99,4 +76,26 @@ describe BigBrother::Cluster do
99
76
  cluster.to_s.should == "name (100)"
100
77
  end
101
78
  end
79
+
80
+ describe "#up_file_exists?" do
81
+ it "returns true when an up file exists" do
82
+ cluster = Factory.cluster(:name => 'name')
83
+ cluster.up_file_exists?.should be_false
84
+
85
+ BigBrother::StatusFile.new('up', 'name').create('Up for testing')
86
+
87
+ cluster.up_file_exists?.should be_true
88
+ end
89
+ end
90
+
91
+ describe "#down_file_exists?" do
92
+ it "returns true when an down file exists" do
93
+ cluster = Factory.cluster(:name => 'name')
94
+ cluster.down_file_exists?.should be_false
95
+
96
+ BigBrother::StatusFile.new('down', 'name').create('down for testing')
97
+
98
+ cluster.down_file_exists?.should be_true
99
+ end
100
+ end
102
101
  end
@@ -77,5 +77,14 @@ describe BigBrother::Configuration do
77
77
 
78
78
  @recording_executor.commands.should include("ipvsadm --add-server --fwmark-service 1 --real-server 127.0.1.1 --ipip --weight 100")
79
79
  end
80
+
81
+ it "will remove clusters that are no longer configured" do
82
+ clusters = { 'two' => Factory.cluster(:fwmark => 2) }
83
+ ipvs_state = { '1' => ['127.0.0.1'] }
84
+
85
+ BigBrother::Configuration.synchronize_with_ipvs(clusters, ipvs_state)
86
+
87
+ @recording_executor.commands.should include("ipvsadm --delete-service --fwmark-service 1")
88
+ end
80
89
  end
81
90
  end
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+
3
+ describe BigBrother::HealthFetcher 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
+ BigBrother::HealthFetcher.current_health("127.0.0.1", 8081, "/test/status").should == 59
15
+ end
16
+
17
+ it "returns 0 for an unknown service" do
18
+ StubServer.new(<<-HTTP, 0.25)
19
+ HTTP/1.0 503 Service Unavailable
20
+ Connection: close
21
+ HTTP
22
+ BigBrother::HealthFetcher.current_health("127.0.0.1", 8081, "/test/status").should == 0
23
+ end
24
+
25
+ it "returns 0 for an unknown DNS entry" do
26
+ BigBrother::HealthFetcher.current_health("junk.local", 8081, "/test/status").should == 0
27
+ end
28
+
29
+ it "returns the health if it is passed in a header" do
30
+ StubServer.new(<<-HTTP, 0.25)
31
+ HTTP/1.0 200 OK
32
+ Connection: close
33
+ X-Health: 61
34
+
35
+ This part is for people.
36
+ HTTP
37
+ BigBrother::HealthFetcher.current_health("127.0.0.1", 8081, "/test/status").should == 61
38
+ end
39
+ end
40
+
41
+ end
@@ -1,44 +1,67 @@
1
1
  require 'spec_helper'
2
2
 
3
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
4
+
5
+ describe "#monitor" do
6
+ it "updates the weight for the node" do
7
+ BigBrother::HealthFetcher.stub(:current_health).and_return(56)
8
+ node = Factory.node(:address => '127.0.0.1')
9
+ cluster = Factory.cluster(:fwmark => 100, :nodes => [node])
10
+
11
+ node.monitor(cluster)
12
+
13
+ @recording_executor.commands.should include("ipvsadm --edit-server --fwmark-service 100 --real-server 127.0.0.1 --ipip --weight 56")
14
+ end
15
+
16
+ it "sets the weight to 100 for each node if an up file exists" do
17
+ BigBrother::HealthFetcher.stub(:current_health).and_return(56)
18
+ node = Factory.node(:address => '127.0.0.1')
19
+ cluster = Factory.cluster(:fwmark => 100, :nodes => [node])
20
+
21
+ BigBrother::StatusFile.new('up', 'test').create('Up for testing')
22
+
23
+ node.monitor(cluster)
24
+
25
+ @recording_executor.commands.should include("ipvsadm --edit-server --fwmark-service 100 --real-server 127.0.0.1 --ipip --weight 100")
16
26
  end
17
27
 
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
28
+ it "sets the weight to 0 for each node if a down file exists" do
29
+ BigBrother::HealthFetcher.stub(:current_health).and_return(56)
30
+ node = Factory.node(:address => '127.0.0.1')
31
+ cluster = Factory.cluster(:fwmark => 100, :nodes => [node])
32
+
33
+ BigBrother::StatusFile.new('down', 'test').create('Down for testing')
34
+
35
+ node.monitor(cluster)
36
+
37
+ @recording_executor.commands.should include("ipvsadm --edit-server --fwmark-service 100 --real-server 127.0.0.1 --ipip --weight 0")
25
38
  end
26
39
 
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
40
+ it "does not run multiple ipvsadm commands if the health does not change" do
41
+ BigBrother::HealthFetcher.stub(:current_health).and_return(56)
42
+ node = Factory.node(:address => '127.0.0.1')
43
+ cluster = Factory.cluster(:fwmark => 100, :nodes => [node])
44
+
45
+ node.monitor(cluster)
46
+ node.monitor(cluster)
47
+
48
+ @recording_executor.commands.should == ["ipvsadm --edit-server --fwmark-service 100 --real-server 127.0.0.1 --ipip --weight 56"]
30
49
  end
31
50
 
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
51
+ it "will run multiple ipvsadm commands if the health does change" do
52
+ BigBrother::HealthFetcher.stub(:current_health).and_return(56)
53
+ node = Factory.node(:address => '127.0.0.1')
54
+ cluster = Factory.cluster(:fwmark => 100, :nodes => [node])
55
+
56
+ node.monitor(cluster)
57
+ node.monitor(cluster)
58
+ BigBrother::HealthFetcher.stub(:current_health).and_return(41)
59
+ node.monitor(cluster)
37
60
 
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
61
+ @recording_executor.commands.should == [
62
+ "ipvsadm --edit-server --fwmark-service 100 --real-server 127.0.0.1 --ipip --weight 56",
63
+ "ipvsadm --edit-server --fwmark-service 100 --real-server 127.0.0.1 --ipip --weight 41"
64
+ ]
42
65
  end
43
66
  end
44
67
  end
@@ -7,5 +7,6 @@ class RecordingExecutor
7
7
 
8
8
  def invoke(command)
9
9
  @commands << command
10
+ ["", 0]
10
11
  end
11
12
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: big_brother
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-05-15 00:00:00.000000000 Z
12
+ date: 2012-05-16 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: thin
16
- requirement: &70321466649500 !ruby/object:Gem::Requirement
16
+ requirement: &70346477113640 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: 1.3.1
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70321466649500
24
+ version_requirements: *70346477113640
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: async-rack
27
- requirement: &70321466648640 !ruby/object:Gem::Requirement
27
+ requirement: &70346477113100 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ~>
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: 0.5.1
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *70321466648640
35
+ version_requirements: *70346477113100
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: sinatra
38
- requirement: &70321466648140 !ruby/object:Gem::Requirement
38
+ requirement: &70346477112480 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ~>
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: '1.0'
44
44
  type: :runtime
45
45
  prerelease: false
46
- version_requirements: *70321466648140
46
+ version_requirements: *70346477112480
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: rack-fiber_pool
49
- requirement: &70321466647520 !ruby/object:Gem::Requirement
49
+ requirement: &70346477111740 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ~>
@@ -54,10 +54,10 @@ dependencies:
54
54
  version: '0.9'
55
55
  type: :runtime
56
56
  prerelease: false
57
- version_requirements: *70321466647520
57
+ version_requirements: *70346477111740
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: eventmachine
60
- requirement: &70321466646780 !ruby/object:Gem::Requirement
60
+ requirement: &70346477111000 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - ! '>'
@@ -68,10 +68,10 @@ dependencies:
68
68
  version: 1.0.0.beta.100
69
69
  type: :runtime
70
70
  prerelease: false
71
- version_requirements: *70321466646780
71
+ version_requirements: *70346477111000
72
72
  - !ruby/object:Gem::Dependency
73
73
  name: em-http-request
74
- requirement: &70321466645620 !ruby/object:Gem::Requirement
74
+ requirement: &70346477109640 !ruby/object:Gem::Requirement
75
75
  none: false
76
76
  requirements:
77
77
  - - ~>
@@ -79,10 +79,10 @@ dependencies:
79
79
  version: '1.0'
80
80
  type: :runtime
81
81
  prerelease: false
82
- version_requirements: *70321466645620
82
+ version_requirements: *70346477109640
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: em-synchrony
85
- requirement: &70321466644700 !ruby/object:Gem::Requirement
85
+ requirement: &70346477108780 !ruby/object:Gem::Requirement
86
86
  none: false
87
87
  requirements:
88
88
  - - ~>
@@ -90,10 +90,10 @@ dependencies:
90
90
  version: '1.0'
91
91
  type: :runtime
92
92
  prerelease: false
93
- version_requirements: *70321466644700
93
+ version_requirements: *70346477108780
94
94
  - !ruby/object:Gem::Dependency
95
95
  name: em-resolv-replace
96
- requirement: &70321466643840 !ruby/object:Gem::Requirement
96
+ requirement: &70346477107660 !ruby/object:Gem::Requirement
97
97
  none: false
98
98
  requirements:
99
99
  - - ~>
@@ -101,10 +101,10 @@ dependencies:
101
101
  version: '1.1'
102
102
  type: :runtime
103
103
  prerelease: false
104
- version_requirements: *70321466643840
104
+ version_requirements: *70346477107660
105
105
  - !ruby/object:Gem::Dependency
106
106
  name: em-syslog
107
- requirement: &70321466642720 !ruby/object:Gem::Requirement
107
+ requirement: &70346477106820 !ruby/object:Gem::Requirement
108
108
  none: false
109
109
  requirements:
110
110
  - - ~>
@@ -112,10 +112,10 @@ dependencies:
112
112
  version: 0.0.2
113
113
  type: :runtime
114
114
  prerelease: false
115
- version_requirements: *70321466642720
115
+ version_requirements: *70346477106820
116
116
  - !ruby/object:Gem::Dependency
117
117
  name: rspec
118
- requirement: &70321466641880 !ruby/object:Gem::Requirement
118
+ requirement: &70346477106340 !ruby/object:Gem::Requirement
119
119
  none: false
120
120
  requirements:
121
121
  - - ~>
@@ -123,10 +123,10 @@ dependencies:
123
123
  version: 2.9.0
124
124
  type: :development
125
125
  prerelease: false
126
- version_requirements: *70321466641880
126
+ version_requirements: *70346477106340
127
127
  - !ruby/object:Gem::Dependency
128
128
  name: rack-test
129
- requirement: &70321466641380 !ruby/object:Gem::Requirement
129
+ requirement: &70346477105760 !ruby/object:Gem::Requirement
130
130
  none: false
131
131
  requirements:
132
132
  - - ~>
@@ -134,10 +134,10 @@ dependencies:
134
134
  version: 0.6.1
135
135
  type: :development
136
136
  prerelease: false
137
- version_requirements: *70321466641380
137
+ version_requirements: *70346477105760
138
138
  - !ruby/object:Gem::Dependency
139
139
  name: rake
140
- requirement: &70321466640920 !ruby/object:Gem::Requirement
140
+ requirement: &70346477105080 !ruby/object:Gem::Requirement
141
141
  none: false
142
142
  requirements:
143
143
  - - ! '>='
@@ -145,10 +145,10 @@ dependencies:
145
145
  version: '0'
146
146
  type: :development
147
147
  prerelease: false
148
- version_requirements: *70321466640920
148
+ version_requirements: *70346477105080
149
149
  - !ruby/object:Gem::Dependency
150
150
  name: rake_commit
151
- requirement: &70321466640060 !ruby/object:Gem::Requirement
151
+ requirement: &70346477104300 !ruby/object:Gem::Requirement
152
152
  none: false
153
153
  requirements:
154
154
  - - ~>
@@ -156,10 +156,10 @@ dependencies:
156
156
  version: '0.13'
157
157
  type: :development
158
158
  prerelease: false
159
- version_requirements: *70321466640060
159
+ version_requirements: *70346477104300
160
160
  - !ruby/object:Gem::Dependency
161
161
  name: vagrant
162
- requirement: &70321466639400 !ruby/object:Gem::Requirement
162
+ requirement: &70346477103860 !ruby/object:Gem::Requirement
163
163
  none: false
164
164
  requirements:
165
165
  - - ! '>='
@@ -167,7 +167,7 @@ dependencies:
167
167
  version: '0'
168
168
  type: :development
169
169
  prerelease: false
170
- version_requirements: *70321466639400
170
+ version_requirements: *70346477103860
171
171
  description: IPVS backend supervisor
172
172
  email:
173
173
  - code@getbraintree.com
@@ -180,6 +180,7 @@ files:
180
180
  - .gitignore
181
181
  - .rake_commit
182
182
  - .rvmrc
183
+ - .travis.yml
183
184
  - Gemfile
184
185
  - LICENSE
185
186
  - README.md
@@ -193,6 +194,7 @@ files:
193
194
  - lib/big_brother/cli.rb
194
195
  - lib/big_brother/cluster.rb
195
196
  - lib/big_brother/configuration.rb
197
+ - lib/big_brother/health_fetcher.rb
196
198
  - lib/big_brother/ipvs.rb
197
199
  - lib/big_brother/logger.rb
198
200
  - lib/big_brother/node.rb
@@ -207,6 +209,7 @@ files:
207
209
  - spec/big_brother/app_spec.rb
208
210
  - spec/big_brother/cluster_spec.rb
209
211
  - spec/big_brother/configuration_spec.rb
212
+ - spec/big_brother/health_fetcher_spec.rb
210
213
  - spec/big_brother/ipvs_spec.rb
211
214
  - spec/big_brother/node_spec.rb
212
215
  - spec/big_brother/shell_executor_spec.rb
@@ -252,6 +255,7 @@ test_files:
252
255
  - spec/big_brother/app_spec.rb
253
256
  - spec/big_brother/cluster_spec.rb
254
257
  - spec/big_brother/configuration_spec.rb
258
+ - spec/big_brother/health_fetcher_spec.rb
255
259
  - spec/big_brother/ipvs_spec.rb
256
260
  - spec/big_brother/node_spec.rb
257
261
  - spec/big_brother/shell_executor_spec.rb