arborist 0.0.1.pre20160106113421

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/.document +4 -0
  3. data/.simplecov +9 -0
  4. data/ChangeLog +417 -0
  5. data/Events.md +20 -0
  6. data/History.md +4 -0
  7. data/LICENSE +29 -0
  8. data/Manifest.txt +72 -0
  9. data/Monitors.md +141 -0
  10. data/Nodes.md +0 -0
  11. data/Observers.md +72 -0
  12. data/Protocol.md +214 -0
  13. data/README.md +75 -0
  14. data/Rakefile +81 -0
  15. data/TODO.md +24 -0
  16. data/bin/amanagerd +10 -0
  17. data/bin/amonitord +12 -0
  18. data/bin/aobserverd +12 -0
  19. data/lib/arborist.rb +182 -0
  20. data/lib/arborist/client.rb +191 -0
  21. data/lib/arborist/event.rb +61 -0
  22. data/lib/arborist/event/node_acked.rb +18 -0
  23. data/lib/arborist/event/node_delta.rb +20 -0
  24. data/lib/arborist/event/node_matching.rb +34 -0
  25. data/lib/arborist/event/node_update.rb +19 -0
  26. data/lib/arborist/event/sys_reloaded.rb +15 -0
  27. data/lib/arborist/exceptions.rb +21 -0
  28. data/lib/arborist/manager.rb +508 -0
  29. data/lib/arborist/manager/event_publisher.rb +97 -0
  30. data/lib/arborist/manager/tree_api.rb +207 -0
  31. data/lib/arborist/mixins.rb +363 -0
  32. data/lib/arborist/monitor.rb +377 -0
  33. data/lib/arborist/monitor/socket.rb +163 -0
  34. data/lib/arborist/monitor_runner.rb +217 -0
  35. data/lib/arborist/node.rb +700 -0
  36. data/lib/arborist/node/host.rb +87 -0
  37. data/lib/arborist/node/root.rb +60 -0
  38. data/lib/arborist/node/service.rb +112 -0
  39. data/lib/arborist/observer.rb +176 -0
  40. data/lib/arborist/observer/action.rb +125 -0
  41. data/lib/arborist/observer/summarize.rb +105 -0
  42. data/lib/arborist/observer_runner.rb +181 -0
  43. data/lib/arborist/subscription.rb +82 -0
  44. data/spec/arborist/client_spec.rb +282 -0
  45. data/spec/arborist/event/node_update_spec.rb +71 -0
  46. data/spec/arborist/event_spec.rb +64 -0
  47. data/spec/arborist/manager/event_publisher_spec.rb +66 -0
  48. data/spec/arborist/manager/tree_api_spec.rb +458 -0
  49. data/spec/arborist/manager_spec.rb +442 -0
  50. data/spec/arborist/mixins_spec.rb +195 -0
  51. data/spec/arborist/monitor/socket_spec.rb +195 -0
  52. data/spec/arborist/monitor_runner_spec.rb +152 -0
  53. data/spec/arborist/monitor_spec.rb +251 -0
  54. data/spec/arborist/node/host_spec.rb +104 -0
  55. data/spec/arborist/node/root_spec.rb +29 -0
  56. data/spec/arborist/node/service_spec.rb +98 -0
  57. data/spec/arborist/node_spec.rb +552 -0
  58. data/spec/arborist/observer/action_spec.rb +205 -0
  59. data/spec/arborist/observer/summarize_spec.rb +294 -0
  60. data/spec/arborist/observer_spec.rb +146 -0
  61. data/spec/arborist/subscription_spec.rb +71 -0
  62. data/spec/arborist_spec.rb +146 -0
  63. data/spec/data/monitors/pings.rb +80 -0
  64. data/spec/data/monitors/port_checks.rb +27 -0
  65. data/spec/data/monitors/system_resources.rb +30 -0
  66. data/spec/data/monitors/web_services.rb +17 -0
  67. data/spec/data/nodes/duir.rb +20 -0
  68. data/spec/data/nodes/localhost.rb +15 -0
  69. data/spec/data/nodes/sidonie.rb +29 -0
  70. data/spec/data/nodes/yevaud.rb +26 -0
  71. data/spec/data/observers/auditor.rb +23 -0
  72. data/spec/data/observers/webservices.rb +18 -0
  73. data/spec/spec_helper.rb +117 -0
  74. metadata +368 -0
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env rspec -cfd
2
+
3
+ require_relative '../../spec_helper'
4
+
5
+ require 'arborist'
6
+ require 'arborist/node/host'
7
+ require 'arborist/node/service'
8
+ require 'arborist/monitor/socket'
9
+
10
+
11
+ describe Arborist::Monitor::Socket do
12
+
13
+ describe 'TCP' do
14
+
15
+ let( :described_class ) { Arborist::Monitor::Socket::TCP }
16
+
17
+ let( :host_node ) do
18
+ Arborist::Node.create( 'host', 'test' ) do
19
+ description "Test host node with a few TCP services"
20
+ address '192.168.26.1'
21
+
22
+ tags :testing
23
+ end
24
+ end
25
+
26
+ let( :default_timeout ) { described_class::DEFAULT_OPTIONS[:timeout] }
27
+
28
+ let( :www_service_node ) { host_node.service('www') }
29
+ let( :ssh_service_node ) { host_node.service('ssh') }
30
+ let( :nat_pmp_service_node ) { host_node.service('nat-pmp', port: 5351) }
31
+
32
+ let( :service_nodes ) {[ www_service_node, ssh_service_node, nat_pmp_service_node ]}
33
+ let( :service_nodes_hash ) do
34
+ service_nodes.each_with_object({}) do |node, accum|
35
+ accum[ node.identifier ] = node.fetch_values
36
+ end
37
+ end
38
+
39
+
40
+ # it_behaves_like "an Arborist Monitor"
41
+
42
+
43
+ def sockaddr_for( node )
44
+ return Socket.sockaddr_in( node.port, node.addresses.first.to_s )
45
+ end
46
+
47
+
48
+ def make_successful_mock_socket( node )
49
+ address = Addrinfo.tcp( node.addresses.first.to_s, node.port )
50
+ socket = instance_double( Socket, "#{node.identifier} socket", remote_address: address )
51
+ errors = [ IO::EINPROGRESSWaitWritable, Errno::EISCONN ]
52
+
53
+ expect( socket ).to receive( :connect_nonblock ) do |addr|
54
+ expect( addr ).to eq( sockaddr_for(node) )
55
+ raise errors.shift
56
+ end.at_least( :once )
57
+
58
+ return socket
59
+ end
60
+
61
+
62
+ def make_initial_error_mock_socket( node, error_class, message )
63
+ address = Addrinfo.tcp( node.addresses.first.to_s, node.port )
64
+ socket = instance_double( Socket, "#{node.identifier} socket", remote_address: address )
65
+
66
+ expect( socket ).to receive( :connect_nonblock ).
67
+ with( sockaddr_for(node) ).
68
+ and_raise( error_class.new(message) )
69
+
70
+ return socket
71
+ end
72
+
73
+
74
+ def make_wait_error_mock_socket( node, error_class, message )
75
+ address = Addrinfo.tcp( node.addresses.first.to_s, node.port )
76
+ socket = instance_double( Socket, "#{node.identifier} socket", remote_address: address )
77
+ errors = [ IO::EINPROGRESSWaitWritable, error_class.new(message) ]
78
+
79
+ expect( socket ).to receive( :connect_nonblock ) do |addr|
80
+ expect( addr ).to eq( sockaddr_for(node) )
81
+ raise errors.shift
82
+ end.at_least( :once )
83
+
84
+ return socket
85
+ end
86
+
87
+
88
+ it "opens TCP connections to the ports of the nodes" do
89
+ fake_sockets = service_nodes.map do |node|
90
+ make_successful_mock_socket( node )
91
+ end
92
+
93
+ expect( Socket ).to receive( :new ).and_return( *fake_sockets )
94
+ expect( IO ).to receive( :select ).
95
+ with( nil, fake_sockets, nil, kind_of(Numeric) ).
96
+ and_return( [nil, fake_sockets, nil] )
97
+
98
+ expect( fake_sockets ).to all( receive( :close ) )
99
+
100
+ result = described_class.run( service_nodes_hash )
101
+
102
+ expect( result ).to be_a( Hash )
103
+ expect( result ).to include( *service_nodes.map(&:identifier) )
104
+ expect( result.values ).to all( include(
105
+ tcp_socket_connect: a_hash_including(:time, :duration)
106
+ ) )
107
+ expect( result.map {|_, res| res[:tcp_socket_connect][:time]} ).to all( be_a(String) )
108
+ end
109
+
110
+
111
+ it "updates nodes with an error on a SocketError" do
112
+ socket = make_initial_error_mock_socket( www_service_node, SocketError,
113
+ "getaddrinfo: nodename nor servname provided, or not known" )
114
+ allow( Socket ).to receive( :new ).and_return( socket )
115
+
116
+ result = described_class.run( 'test-www' => www_service_node.fetch_values )
117
+
118
+ expect( result ).to be_a( Hash )
119
+ expect( result ).to include( 'test-www' )
120
+ expect(
121
+ result['test-www']
122
+ ).to include( error: 'getaddrinfo: nodename nor servname provided, or not known' )
123
+ end
124
+
125
+
126
+ it "updates nodes with an error if the connection times out" do
127
+ socket = make_successful_mock_socket( www_service_node )
128
+ allow( Socket ).to receive( :new ).and_return( socket )
129
+ allow( socket ).to receive( :close )
130
+ allow( IO ).to receive( :select ) do
131
+ sleep 0.2
132
+ [nil, nil, nil]
133
+ end
134
+
135
+ result = described_class.new( timeout: 0.1 ).
136
+ run( 'test-www' => www_service_node.fetch_values )
137
+
138
+ expect( result ).to be_a( Hash )
139
+ expect( result ).to include( 'test-www' )
140
+ expect( result['test-www'] ).to include( error: 'Timeout after 0.100s' )
141
+ end
142
+
143
+
144
+ it "updates nodes with an error on a 'connection refused' error" do
145
+ socket = make_initial_error_mock_socket( www_service_node, Errno::ECONNREFUSED,
146
+ "the message" )
147
+ allow( Socket ).to receive( :new ).and_return( socket )
148
+
149
+ result = described_class.run( 'test-www' => www_service_node.fetch_values )
150
+
151
+ expect( result ).to be_a( Hash )
152
+ expect( result ).to include( 'test-www' )
153
+ expect( result['test-www'] ).to include( error: 'Connection refused - the message' )
154
+ end
155
+
156
+
157
+ it "updates nodes with an error on a 'host unreachable' error" do
158
+ socket = make_initial_error_mock_socket( www_service_node, Errno::EHOSTUNREACH,
159
+ "the message" )
160
+ allow( Socket ).to receive( :new ).and_return( socket )
161
+
162
+ result = described_class.run( 'test-www' => www_service_node.fetch_values )
163
+
164
+ expect( result ).to be_a( Hash )
165
+ expect( result ).to include( 'test-www' )
166
+ expect( result['test-www'] ).to include( error: 'No route to host - the message' )
167
+ end
168
+
169
+
170
+ it "updates nodes with an error on a 'getpeername' error" do
171
+ socket = make_wait_error_mock_socket( www_service_node, Errno::EINVAL, "getpeername(2)" )
172
+ allow( Socket ).to receive( :new ).and_return( socket )
173
+ allow( IO ).to receive( :select ).
174
+ with( nil, [socket], nil, kind_of(Numeric) ).
175
+ and_return( [nil, [socket], nil] )
176
+ allow( socket ).to receive( :close )
177
+
178
+ result = described_class.run( 'test-www' => www_service_node.fetch_values )
179
+
180
+ expect( result ).to be_a( Hash )
181
+ expect( result ).to include( 'test-www' )
182
+ expect( result['test-www'] ).to include( error: 'Invalid argument - getpeername(2)' )
183
+ end
184
+
185
+
186
+ it "can be instantiated to run with a different timeout" do
187
+ mon = described_class.new.with_timeout( 30 )
188
+ expect( mon.timeout ).to eq( 30 )
189
+ end
190
+
191
+ end
192
+
193
+ end
194
+
195
+
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env rspec -cfd
2
+
3
+ require_relative '../spec_helper'
4
+
5
+ require 'arborist/monitor_runner'
6
+
7
+
8
+ describe Arborist::MonitorRunner do
9
+
10
+ let( :zmq_loop ) { instance_double(ZMQ::Loop) }
11
+ let( :req_socket ) { instance_double(ZMQ::Socket::Req) }
12
+ let( :pollitem ) { instance_double(ZMQ::Pollitem) }
13
+
14
+ let( :runner ) do
15
+ obj = described_class.new
16
+ obj.reactor = zmq_loop
17
+ obj
18
+ end
19
+
20
+ let( :monitor_class ) { Class.new(Arborist::Monitor) }
21
+
22
+ let( :mon1 ) { monitor_class.new("testing monitor1") }
23
+ let( :mon2 ) { monitor_class.new("testing monitor2") { splay 10 } }
24
+ let( :mon3 ) { monitor_class.new("testing monitor3") }
25
+ let( :monitors ) {[ mon1, mon2, mon3 ]}
26
+
27
+
28
+ it "can load monitors from an enumerator that yields Arborist::Monitors" do
29
+ runner.load_monitors([ mon1, mon2, mon3 ])
30
+ expect( runner.monitors ).to include( mon1, mon2, mon3 )
31
+ end
32
+
33
+
34
+ describe "a runner with loaded monitors" do
35
+
36
+ before( :each ) do
37
+ allow( zmq_loop ).to receive( :register ).with( an_instance_of(ZMQ::Pollitem) )
38
+ end
39
+
40
+
41
+ it "registers its monitors to run on an interval and starts the ZMQ loop when run" do
42
+ runner.monitors.replace([ mon1 ])
43
+
44
+ interval_timer = instance_double( ZMQ::Timer )
45
+ expect( ZMQ::Timer ).to receive( :new ) do |i_delay, i_repeat, &i_block|
46
+ expect( i_delay ).to eq( mon1.interval )
47
+ expect( i_repeat ).to eq( 0 )
48
+
49
+ expect( runner.handler ).to receive( :run_monitor ).with( mon1 )
50
+
51
+ i_block.call
52
+ interval_timer
53
+ end
54
+
55
+ expect( zmq_loop ).to receive( :register_timer ).with( interval_timer )
56
+ expect( zmq_loop ).to receive( :start )
57
+
58
+ runner.run
59
+ end
60
+
61
+
62
+ it "delays registration of its interval timer if a monitor has a splay" do
63
+ runner.monitors.replace([ mon2 ])
64
+
65
+ interval_timer = instance_double( ZMQ::Timer )
66
+ expect( ZMQ::Timer ).to receive( :new ).with( mon2.interval, 0 ).
67
+ and_return( interval_timer )
68
+
69
+ timer = instance_double( ZMQ::Timer )
70
+ expect( ZMQ::Timer ).to receive( :new ) do |delay, repeat, &block|
71
+ expect( delay ).to be >= 0
72
+ expect( delay ).to be <= mon2.splay
73
+ expect( repeat ).to eq( 1 )
74
+
75
+ block.call
76
+ timer
77
+ end
78
+
79
+ expect( zmq_loop ).to receive( :register_timer ).with( interval_timer )
80
+ expect( zmq_loop ).to receive( :register_timer ).with( timer )
81
+ expect( zmq_loop ).to receive( :start )
82
+
83
+ runner.run
84
+ end
85
+
86
+ end
87
+
88
+
89
+ describe Arborist::MonitorRunner::Handler do
90
+
91
+ let( :tree_api_handler ) { Arborist::Manager::TreeAPI.new(:pollable, :manager) }
92
+
93
+ let( :handler ) { described_class.new( zmq_loop ) }
94
+
95
+ let( :node_tree ) {{
96
+ 'router' => {
97
+ 'addresses' => ['10.2.1.2', '1.2.3.4']
98
+ },
99
+ 'server' => {
100
+ 'addresses' => ['10.2.1.118']
101
+ }
102
+ }}
103
+ let( :ping_monitor_data ) {{
104
+ 'router' => {'ping' => { 'rtt' => 22 }},
105
+ 'server' => {'ping' => { 'rtt' => 8 }},
106
+ }}
107
+
108
+
109
+ it "can run a monitor using async ZMQ IO" do
110
+ expect( zmq_loop ).to receive( :register ).with( handler.pollitem )
111
+
112
+ # Queue up the monitor requests and register the socket as wanting to write
113
+ mon1.exec do |nodes|
114
+ ping_monitor_data
115
+ end
116
+ expect {
117
+ handler.run_monitor( mon1 )
118
+ }.to change { handler.registered? }.from( false ).to( true )
119
+
120
+ # Fetch
121
+ request = handler.client.make_fetch_request(
122
+ mon1.positive_criteria,
123
+ include_down: false,
124
+ properties: mon1.node_properties
125
+ )
126
+ response = tree_api_handler.successful_response( node_tree )
127
+
128
+ expect( handler.client.tree_api ).to receive( :send ).with( request )
129
+ expect( handler.client.tree_api ).to receive( :recv ).and_return( response )
130
+
131
+ expect {
132
+ handler.on_writable
133
+ }.to_not change { handler.registered? }
134
+
135
+ # Update
136
+ request = handler.client.make_update_request( ping_monitor_data )
137
+ response = tree_api_handler.successful_response( nil )
138
+ expect( handler.client.tree_api ).to receive( :send ).with( request )
139
+ expect( handler.client.tree_api ).to receive( :recv ).and_return( response )
140
+
141
+ # Unregister
142
+ expect( zmq_loop ).to receive( :remove ).with( handler.pollitem )
143
+ expect {
144
+ handler.on_writable
145
+ }.to change { handler.registered? }.from( true ).to( false )
146
+
147
+ end
148
+
149
+ end
150
+
151
+ end
152
+
@@ -0,0 +1,251 @@
1
+ #!/usr/bin/env rspec -cfd
2
+
3
+ require_relative '../spec_helper'
4
+
5
+ require 'arborist/monitor'
6
+
7
+
8
+ describe Arborist::Monitor do
9
+
10
+
11
+ let( :trunk_node ) do
12
+ testing_node( 'trunk' ) do
13
+ properties['pork'] = 'nope'
14
+ end
15
+ end
16
+ let( :branch_node ) do
17
+ testing_node( 'branch', 'trunk' ) do
18
+ properties['pork'] = 'yes'
19
+ end
20
+ end
21
+ let( :leaf_node ) do
22
+ testing_node( 'leaf', 'branch' ) do
23
+ properties['pork'] = 'twice'
24
+ end
25
+ end
26
+
27
+
28
+ let( :testing_nodes ) {[ trunk_node, branch_node, leaf_node ]}
29
+
30
+
31
+ it "can be created with just a description" do
32
+ mon = described_class.new( "the description" )
33
+ expect( mon ).to be_a( described_class )
34
+ expect( mon.description ).to eq( "the description" )
35
+ expect( mon.include_down? ).to be_falsey
36
+ expect( mon.interval ).to eq( Arborist::Monitor::DEFAULT_INTERVAL )
37
+ expect( mon.splay ).to eq( 0 )
38
+ expect( mon.positive_criteria ).to be_empty
39
+ expect( mon.negative_criteria ).to be_empty
40
+ expect( mon.node_properties ).to be_empty
41
+ end
42
+
43
+
44
+ it "yields itself to the provided block for the DSL" do
45
+ block_self = nil
46
+ mon = described_class.new( "testing monitor" ) do
47
+ block_self = self
48
+ end
49
+
50
+ expect( block_self ).to be( mon )
51
+ end
52
+
53
+
54
+ it "can specify an interval" do
55
+ mon = described_class.new( "testing monitor" ) do
56
+ every 30
57
+ end
58
+
59
+ expect( mon.interval ).to eq( 30 )
60
+ end
61
+
62
+
63
+ it "can specify a splay" do
64
+ mon = described_class.new( "testing monitor" ) do
65
+ splay 15
66
+ end
67
+
68
+ expect( mon.splay ).to eq( 15 )
69
+ end
70
+
71
+
72
+ it "can specify criteria for matching nodes to monitor" do
73
+ mon = described_class.new( "testing monitor" ) do
74
+ match type: 'host'
75
+ end
76
+
77
+ expect( mon.positive_criteria ).to include( type: 'host' )
78
+ end
79
+
80
+
81
+ it "can specify criteria for matching nodes not to monitor" do
82
+ mon = described_class.new( "testing monitor" ) do
83
+ exclude tag: 'laptop'
84
+ end
85
+
86
+ expect( mon.negative_criteria ).to include( tag: 'laptop' )
87
+ end
88
+
89
+
90
+ it "can specify that it will include hosts marked as 'down'" do
91
+ mon = described_class.new( "testing monitor" ) do
92
+ include_down true
93
+ end
94
+
95
+ expect( mon.include_down? ).to be_truthy
96
+ end
97
+
98
+
99
+ it "can specify one or more properties to include in the input to the monitor" do
100
+ mon = described_class.new( "testing monitor" ) do
101
+ use :address, :tags
102
+ end
103
+
104
+ expect( mon.node_properties ).to include( :address, :tags )
105
+ end
106
+
107
+
108
+ it "can specify a command to exec to do the monitor's work" do
109
+ mon = described_class.new( "the description" ) do
110
+ exec 'cat'
111
+ end
112
+
113
+ output = mon.run( testing_nodes )
114
+ expect( output ).to be_a( Hash )
115
+ expect( output ).to include( *(testing_nodes.map(&:identifier)) )
116
+ end
117
+
118
+
119
+ it "can specify a block to call to do the monitor's work" do
120
+ block_was_run = false
121
+
122
+ mon = described_class.new( "the description" )
123
+ mon.exec do |nodes|
124
+ block_was_run = true
125
+ end
126
+
127
+ mon.run( testing_nodes )
128
+
129
+ expect( block_was_run ).to be_truthy
130
+ end
131
+
132
+
133
+ it "can specify a runnable object to do the monitor's work" do
134
+ mod = Module.new do
135
+ class << self; attr_accessor :was_run ; end
136
+ @was_run = false
137
+
138
+ def self::run( nodes )
139
+ self.was_run = true
140
+ end
141
+ end
142
+
143
+
144
+ mon = described_class.new( "the description" )
145
+ mon.exec( mod )
146
+
147
+ mon.run( testing_nodes )
148
+
149
+ expect( mod.was_run ).to be_truthy
150
+ end
151
+
152
+
153
+ it "can provide a function for building arguments for its command" do
154
+ mon = described_class.new( "the description" ) do
155
+
156
+ exec 'the_command'
157
+
158
+ handle_results {|*| }
159
+ exec_input {|*| }
160
+ exec_arguments do |nodes|
161
+ Loggability[ Arborist ].debug "In the argument-builder."
162
+ nodes.map {|n| n.identifier }
163
+ end
164
+ end
165
+
166
+ expect( Process ).to receive( :spawn ) do |*args|
167
+ options = args.pop
168
+
169
+ expect( args ).to eq([ 'the_command', 'trunk', 'branch', 'leaf' ])
170
+ expect( options ).to be_a( Hash )
171
+ expect( options ).to include( :in, :out, :err )
172
+
173
+ nil
174
+ end
175
+
176
+ mon.run( testing_nodes )
177
+ end
178
+
179
+
180
+ it "can provide a function for providing input to its command" do
181
+ mon = described_class.new( "the description" ) do
182
+
183
+ exec 'cat'
184
+
185
+ exec_input do |nodes, writer|
186
+ writer.puts( nodes.map(&:identifier) )
187
+ end
188
+ handle_results do |pid, out, err|
189
+ return out.readlines.map( &:chomp )
190
+ end
191
+ end
192
+
193
+ results = mon.run( testing_nodes )
194
+
195
+ expect( results ).to eq( testing_nodes.map(&:identifier) )
196
+ end
197
+
198
+
199
+ it "can provide a function for parsing its command's output" do
200
+ mon = described_class.new( "the description" ) do
201
+
202
+ exec 'cat'
203
+
204
+ exec_arguments {|*| }
205
+ exec_input do |nodes, writer|
206
+ writer.puts( nodes.map(&:identifier) )
207
+ end
208
+ handle_results do |pid, out, err|
209
+ out.readlines.map( &:chomp ).map( &:upcase )
210
+ end
211
+ end
212
+
213
+ results = mon.run( testing_nodes )
214
+
215
+ expect( results ).to eq( testing_nodes.map(&:identifier).map(&:upcase) )
216
+ end
217
+
218
+
219
+ it "can provide a Module that implements its exec callbacks" do
220
+ the_module = Module.new do
221
+
222
+ def exec_input( nodes, writer )
223
+ writer.puts( nodes.map {|n| n.identifier } )
224
+ end
225
+
226
+ def handle_results( pid, out, err )
227
+ err.flush
228
+ return out.each_line.with_object({}) do |line, accum|
229
+ accum[ line.chomp ] = { echoed: 'yep' }
230
+ end
231
+ end
232
+
233
+ end
234
+
235
+ mon = described_class.new( "the description" ) do
236
+ exec 'cat'
237
+ exec_callbacks( the_module )
238
+ end
239
+
240
+ results = mon.run( testing_nodes )
241
+
242
+ expect( results ).to be_a( Hash )
243
+ expect( results.size ).to eq( 3 )
244
+ expect( results ).to include( *testing_nodes.map(&:identifier) )
245
+ expect( results['trunk'] ).to eq({ echoed: 'yep' })
246
+ expect( results['branch'] ).to eq({ echoed: 'yep' })
247
+ expect( results['leaf'] ).to eq({ echoed: 'yep' })
248
+ end
249
+
250
+ end
251
+