arborist 0.2.0.pre20170519125456 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/ChangeLog +670 -1
  5. data/History.md +67 -0
  6. data/Manifest.txt +9 -6
  7. data/README.md +1 -3
  8. data/Rakefile +39 -4
  9. data/TODO.md +22 -31
  10. data/lib/arborist.rb +9 -2
  11. data/lib/arborist/cli.rb +67 -85
  12. data/lib/arborist/client.rb +125 -59
  13. data/lib/arborist/command/ack.rb +86 -0
  14. data/lib/arborist/command/reset.rb +48 -0
  15. data/lib/arborist/command/start.rb +11 -1
  16. data/lib/arborist/command/summary.rb +173 -0
  17. data/lib/arborist/command/tree.rb +215 -0
  18. data/lib/arborist/command/watch.rb +22 -22
  19. data/lib/arborist/dependency.rb +24 -4
  20. data/lib/arborist/event.rb +18 -2
  21. data/lib/arborist/event/node.rb +6 -2
  22. data/lib/arborist/event/node_warn.rb +16 -0
  23. data/lib/arborist/manager.rb +179 -48
  24. data/lib/arborist/mixins.rb +11 -0
  25. data/lib/arborist/monitor.rb +29 -17
  26. data/lib/arborist/monitor/connection_batching.rb +293 -0
  27. data/lib/arborist/monitor/socket.rb +101 -167
  28. data/lib/arborist/monitor_runner.rb +101 -24
  29. data/lib/arborist/node.rb +297 -68
  30. data/lib/arborist/node/ack.rb +1 -1
  31. data/lib/arborist/node/host.rb +26 -5
  32. data/lib/arborist/node/resource.rb +14 -5
  33. data/lib/arborist/node/root.rb +12 -3
  34. data/lib/arborist/node/service.rb +29 -26
  35. data/lib/arborist/node_subscription.rb +65 -0
  36. data/lib/arborist/observer.rb +8 -0
  37. data/lib/arborist/observer/action.rb +6 -0
  38. data/lib/arborist/subscription.rb +22 -16
  39. data/lib/arborist/tree_api.rb +7 -2
  40. data/spec/arborist/client_spec.rb +157 -51
  41. data/spec/arborist/dependency_spec.rb +21 -0
  42. data/spec/arborist/event/node_spec.rb +5 -0
  43. data/spec/arborist/event_spec.rb +3 -3
  44. data/spec/arborist/manager_spec.rb +626 -347
  45. data/spec/arborist/mixins_spec.rb +19 -0
  46. data/spec/arborist/monitor/socket_spec.rb +1 -2
  47. data/spec/arborist/monitor_runner_spec.rb +81 -29
  48. data/spec/arborist/monitor_spec.rb +89 -14
  49. data/spec/arborist/node/host_spec.rb +68 -0
  50. data/spec/arborist/node/resource_spec.rb +2 -0
  51. data/spec/arborist/node/root_spec.rb +13 -0
  52. data/spec/arborist/node/service_spec.rb +9 -0
  53. data/spec/arborist/node_spec.rb +673 -111
  54. data/spec/arborist/node_subscription_spec.rb +54 -0
  55. data/spec/arborist/observer/action_spec.rb +6 -0
  56. data/spec/arborist/observer_runner_spec.rb +8 -1
  57. data/spec/arborist/tree_api_spec.rb +111 -8
  58. data/spec/data/monitors/pings.rb +0 -11
  59. data/spec/data/monitors/port_checks.rb +0 -9
  60. data/spec/data/nodes/sidonie.rb +1 -0
  61. data/spec/data/nodes/vhosts.rb +23 -0
  62. data/spec/data/nodes/yevaud.rb +4 -2
  63. data/spec/spec_helper.rb +71 -1
  64. metadata +91 -28
  65. metadata.gz.sig +0 -0
  66. data/Events.md +0 -35
  67. data/Monitors.md +0 -155
  68. data/Nodes.md +0 -70
  69. data/Observers.md +0 -72
  70. data/Protocol.md +0 -276
  71. data/Tutorial.md +0 -8
@@ -82,6 +82,7 @@ describe Arborist, "mixins" do
82
82
  expect( extended_class ).to respond_to( :foo? )
83
83
  end
84
84
 
85
+
85
86
  it "can declare a class-level predicate method" do
86
87
  extended_class.singleton_predicate_reader :foo
87
88
  expect( extended_class ).to_not respond_to( :foo )
@@ -89,6 +90,24 @@ describe Arborist, "mixins" do
89
90
  expect( extended_class ).to respond_to( :foo? )
90
91
  end
91
92
 
93
+
94
+ it "can declare an instance DSLish accessor" do
95
+ extended_class.dsl_accessor( :foo )
96
+ instance = extended_class.new
97
+
98
+ instance.foo( 13 )
99
+ expect( instance.foo ).to eq( 13 )
100
+ end
101
+
102
+
103
+ it "the instance DSLish accessor works with a `false` argument" do
104
+ extended_class.dsl_accessor( :foo )
105
+ instance = extended_class.new
106
+
107
+ instance.foo( false )
108
+ expect( instance.foo ).to equal( false )
109
+ end
110
+
92
111
  end
93
112
 
94
113
 
@@ -102,9 +102,8 @@ describe Arborist::Monitor::Socket do
102
102
  expect( result ).to be_a( Hash )
103
103
  expect( result ).to include( *service_nodes.map(&:identifier) )
104
104
  expect( result.values ).to all( include(
105
- tcp_socket_connect: a_hash_including(:time, :duration)
105
+ tcp_socket_connect: a_hash_including(:duration)
106
106
  ) )
107
- expect( result.map {|_, res| res[:tcp_socket_connect][:time]} ).to all( be_a(String) )
108
107
  end
109
108
 
110
109
 
@@ -3,7 +3,7 @@
3
3
  require_relative '../spec_helper'
4
4
 
5
5
  require 'arborist/monitor_runner'
6
-
6
+ require 'arborist/node/root'
7
7
 
8
8
  describe Arborist::MonitorRunner do
9
9
 
@@ -35,8 +35,11 @@ describe Arborist::MonitorRunner do
35
35
 
36
36
  before( :each ) do
37
37
  allow( CZTop::Reactor ).to receive( :new ).and_return( reactor )
38
+ allow( Thread ).to receive( :new ).and_yield
38
39
  allow( reactor ).to receive( :register )
39
40
  allow( reactor ).to receive( :unregister )
41
+ allow( reactor ).to receive( :add_periodic_timer ).
42
+ with( described_class::THREAD_CLEANUP_INTERVAL )
40
43
  end
41
44
 
42
45
 
@@ -72,36 +75,39 @@ describe Arborist::MonitorRunner do
72
75
 
73
76
  it "can run a monitor using async ZMQ IO" do
74
77
 
75
- # Queue up the monitor requests and register the socket as wanting to write
78
+ # Set up the monitor's execution block with fixtured data
76
79
  mon1.exec do |nodes|
77
80
  ping_monitor_data
78
81
  end
79
82
 
80
- # Fetch
81
- request = runner.client.make_fetch_request(
82
- mon1.positive_criteria,
83
- include_down: false,
84
- properties: mon1.node_properties
85
- )
86
- response = Arborist::TreeAPI.successful_response( node_tree )
83
+ expect( reactor ).to receive( :event_enabled? ).with( runner.client.tree_api, :write ).
84
+ at_least( :once ).
85
+ and_return( true )
86
+
87
87
 
88
- expect( runner.client.tree_api ).to receive( :send ).with( request )
89
- expect( runner.client.tree_api ).to receive( :recv ).and_return( response )
88
+ search_request = instance_double( CZTop::Message )
89
+ search_response = Arborist::TreeAPI.successful_response( node_tree )
90
+ update_request = instance_double( CZTop::Message )
91
+ update_response = Arborist::TreeAPI.successful_response( nil )
90
92
 
91
- runner.reactor.poll_once
93
+ expect( CZTop::Message ).to receive( :new ).and_return( search_request, update_request )
94
+ expect( search_request ).to receive( :send_to ).with( runner.client.tree_api )
95
+ expect( update_request ).to receive( :send_to ).with( runner.client.tree_api )
96
+ expect( CZTop::Message ).to receive( :receive_from ).with( runner.client.tree_api ).
97
+ and_return( search_response, update_response )
98
+ expect( reactor ).to receive( :disable_events ).with( runner.client.tree_api, :write )
92
99
 
93
- # Update
94
- request = runner.client.make_update_request( ping_monitor_data )
95
- response = Arborist::TreeAPI.successful_response( nil )
96
- expect( runner.client.tree_api ).to receive( :send ).with( request )
97
- expect( runner.client.tree_api ).to receive( :recv ).and_return( response )
100
+ runner.run_monitor( mon1 )
98
101
 
99
- # Unregister
100
- expect( zmq_loop ).to receive( :remove ).with( runner.pollitem )
101
- expect {
102
- runner.on_writable
103
- }.to change { runner.registered? }.from( true ).to( false )
102
+ # trigger the search request
103
+ search_event = instance_double( CZTop::Reactor::Event,
104
+ writable?: true, socket: runner.client.tree_api )
105
+ runner.handle_io_event( search_event )
104
106
 
107
+ # trigger the update request
108
+ update_event = instance_double( CZTop::Reactor::Event,
109
+ writable?: true, socket: runner.client.tree_api )
110
+ runner.handle_io_event( update_event )
105
111
  end
106
112
 
107
113
 
@@ -117,25 +123,71 @@ describe Arborist::MonitorRunner do
117
123
  nodes = { 'test1' => {}, 'test2' => {} }
118
124
  monitor_results = { 'test1' => {ping: {rtt: 1}}, 'test2' => {ping: {rtt: 8}} }
119
125
 
120
- expect( runner ).to receive( :fetch ).
126
+ expect( runner ).to receive( :search ).
121
127
  with( {type: 'host'}, false, [:addresses], {} ).
122
128
  and_yield( nodes )
123
129
 
124
130
  expect( monitor ).to receive( :run ).with( nodes ).
125
131
  and_return( monitor_results )
126
132
 
127
- expect( runner ).to receive( :update ).
128
- with({
129
- "test1"=>{:ping=>{:rtt=>1}, "_monitor_key"=>:test},
130
- "test2"=>{:ping=>{:rtt=>8}, "_monitor_key"=>:test}
131
- })
133
+ expect( runner ).to receive( :update ).with(
134
+ {
135
+ "test1"=>{:ping=>{:rtt=>1}},
136
+ "test2"=>{:ping=>{:rtt=>8}}
137
+ },
138
+ :test
139
+ )
132
140
 
133
141
  runner.run_monitor( monitor )
134
142
  end
135
143
 
136
- end
137
144
 
145
+ it "sets an error condition if the monitor raises an exception" do
146
+ monitor = Arborist::Monitor.new do
147
+ description 'test monitor'
148
+ key :test
149
+ every 20
150
+ match type: 'host'
151
+ use :addresses
152
+ exec do |nodes|
153
+ raise "boom!"
154
+ end
155
+ end
156
+ nodes = { 'test' => {} }
157
+
158
+ expect( runner ).to receive( :search ).
159
+ with( {type: 'host'}, false, [:addresses], {} ).
160
+ and_yield( nodes )
138
161
 
162
+ expect( runner ).to receive( :update ).with(
163
+ {"test" => {error: 'Exception while running "test monitor" monitor: RuntimeError: boom!'}},
164
+ :test
165
+ )
166
+
167
+ runner.run_monitor( monitor )
168
+ end
139
169
 
170
+
171
+ it "skips the monitor execution if no nodes were returned in the search" do
172
+ monitor = Arborist::Monitor.new do
173
+ description 'test monitor'
174
+ key :test
175
+ every 20
176
+ match type: 'host'
177
+ use :addresses
178
+ exec 'fping', '-e', '-t', '150'
179
+ end
180
+ nodes = {}
181
+
182
+ expect( runner ).to receive( :search ).
183
+ with( {type: 'host'}, false, [:addresses], {} ).
184
+ and_yield( nodes )
185
+
186
+ expect( runner ).to_not receive( :update )
187
+ runner.run_monitor( monitor )
188
+ end
189
+
190
+
191
+ end
140
192
  end
141
193
 
@@ -20,6 +20,7 @@ describe Arborist::Monitor do
20
20
  end
21
21
  let( :leaf_node ) do
22
22
  testing_node( 'leaf', 'branch' ) do
23
+ tags :one, :two
23
24
  properties['pork'] = 'twice'
24
25
  end
25
26
  end
@@ -37,7 +38,7 @@ describe Arborist::Monitor do
37
38
  expect( mon ).to be_a( described_class )
38
39
  expect( mon.description ).to eq( "the description" )
39
40
  expect( mon.key ).to eq( :key )
40
- expect( mon.include_down? ).to be_falsey
41
+ expect( mon.exclude_down? ).to be_falsey
41
42
  expect( mon.interval ).to eq( Arborist::Monitor::DEFAULT_INTERVAL )
42
43
  expect( mon.splay ).to eq( 0 )
43
44
  expect( mon.positive_criteria ).to be_empty
@@ -55,7 +56,7 @@ describe Arborist::Monitor do
55
56
  expect( mon ).to be_a( described_class )
56
57
  expect( mon.description ).to eq( "the description" )
57
58
  expect( mon.key ).to eq( :key )
58
- expect( mon.include_down? ).to be_falsey
59
+ expect( mon.exclude_down? ).to be_falsey
59
60
  expect( mon.interval ).to eq( Arborist::Monitor::DEFAULT_INTERVAL )
60
61
  expect( mon.splay ).to eq( 0 )
61
62
  expect( mon.positive_criteria ).to be_empty
@@ -72,7 +73,7 @@ describe Arborist::Monitor do
72
73
  expect( mon ).to be_a( described_class )
73
74
  expect( mon.description ).to eq( "the description" )
74
75
  expect( mon.key ).to eq( :key )
75
- expect( mon.include_down? ).to be_falsey
76
+ expect( mon.exclude_down? ).to be_falsey
76
77
  expect( mon.interval ).to eq( Arborist::Monitor::DEFAULT_INTERVAL )
77
78
  expect( mon.splay ).to eq( 0 )
78
79
  expect( mon.positive_criteria ).to be_empty
@@ -87,7 +88,7 @@ describe Arborist::Monitor do
87
88
  expect( mon ).to be_a( described_class )
88
89
  expect( mon.description ).to eq( "the description" )
89
90
  expect( mon.key ).to eq( :the_key )
90
- expect( mon.include_down? ).to be_falsey
91
+ expect( mon.exclude_down? ).to be_falsey
91
92
  expect( mon.interval ).to eq( Arborist::Monitor::DEFAULT_INTERVAL )
92
93
  expect( mon.splay ).to eq( 0 )
93
94
  expect( mon.positive_criteria ).to be_empty
@@ -96,12 +97,12 @@ describe Arborist::Monitor do
96
97
  end
97
98
 
98
99
 
99
- it "raises a ConfigError if constructed without a description" do
100
- expect {
101
- described_class.new do
102
- key :key
103
- end
104
- }.to raise_error( Arborist::ConfigError, /no description/i )
100
+ it "uses a default description if constructed without one" do
101
+ mon = described_class.new do
102
+ key :key
103
+ end
104
+
105
+ expect( mon.description ).to_not be_empty
105
106
  end
106
107
 
107
108
 
@@ -163,16 +164,16 @@ describe Arborist::Monitor do
163
164
  match status: 'down'
164
165
  end
165
166
 
166
- expect( mon.include_down? ).to be_truthy
167
+ expect( mon.exclude_down? ).to be_falsey
167
168
  end
168
169
 
169
170
 
170
- it "can specify that it will include hosts marked as 'down'" do
171
+ it "can specify that it will exclude hosts marked as 'down'" do
171
172
  mon = described_class.new( "testing monitor", :testing ) do
172
- include_down true
173
+ exclude_down true
173
174
  end
174
175
 
175
- expect( mon.include_down? ).to be_truthy
176
+ expect( mon.exclude_down? ).to be_truthy
176
177
  end
177
178
 
178
179
 
@@ -230,6 +231,27 @@ describe Arborist::Monitor do
230
231
  end
231
232
 
232
233
 
234
+ it "uses node properties specified by the runnable object if it provides them" do
235
+ mod = Module.new do
236
+ class << self; attr_accessor :was_run ; end
237
+ @was_run = false
238
+
239
+ def self::run( nodes )
240
+ self.was_run = true
241
+ end
242
+
243
+ def self::node_properties
244
+ %i[ uri http_method body mimetype ]
245
+ end
246
+ end
247
+
248
+ mon = described_class.new( "the description", :testing )
249
+ mon.exec( mod )
250
+
251
+ expect( mon.node_properties ).to include( :uri, :http_method, :body, :mimetype )
252
+ end
253
+
254
+
233
255
  it "can provide a function for building arguments for its command" do
234
256
  mon = described_class.new( "the description", :testing ) do
235
257
 
@@ -257,6 +279,30 @@ describe Arborist::Monitor do
257
279
  end
258
280
 
259
281
 
282
+ it "stringifies any Array properties with the default exec_input context" do
283
+ mon = described_class.new( "the description", :testing ) do
284
+ exec 'the_command'
285
+ handle_results {|*| }
286
+ end
287
+
288
+ child_stdin, parent_writer = IO.pipe
289
+ parent_reader, child_stdout = IO.pipe
290
+ parent_err_reader, child_stderr = IO.pipe
291
+
292
+ expect( IO ).to receive( :pipe ).and_return(
293
+ [ child_stdin, parent_writer ],
294
+ [ parent_reader, child_stdout ],
295
+ [ parent_err_reader, child_stderr ]
296
+ )
297
+
298
+ expect( parent_writer ).to receive( :puts ).with match( 'tags=one,two' )
299
+ expect( Process ).to receive( :spawn ).
300
+ with( 'the_command', out: child_stdout, in: child_stdin, err: child_stderr )
301
+
302
+ mon.run({ leaf: leaf_node.to_h })
303
+ end
304
+
305
+
260
306
  it "handles system call errors while running the monitor command" do
261
307
  mon = described_class.new( "the description", :testing ) do
262
308
 
@@ -350,5 +396,34 @@ describe Arborist::Monitor do
350
396
  expect( results['leaf'] ).to eq({ echoed: 'yep' })
351
397
  end
352
398
 
399
+
400
+ it "uses node properties specified by the exec_callbacks module if it provides them" do
401
+ the_module = Module.new do
402
+
403
+ def self::node_properties
404
+ %i[ uri http_method body mimetype ]
405
+ end
406
+
407
+ def exec_input( nodes, writer )
408
+ writer.puts( nodes.keys )
409
+ end
410
+
411
+ def handle_results( pid, out, err )
412
+ err.flush
413
+ return out.each_line.with_object({}) do |line, accum|
414
+ accum[ line.chomp ] = { echoed: 'yep' }
415
+ end
416
+ end
417
+
418
+ end
419
+
420
+ mon = described_class.new( "the description", :testing ) do
421
+ exec 'cat'
422
+ exec_callbacks( the_module )
423
+ end
424
+
425
+ expect( mon.node_properties ).to include( :uri, :http_method, :body, :mimetype )
426
+ end
427
+
353
428
  end
354
429
 
@@ -81,6 +81,35 @@ describe Arborist::Node::Host do
81
81
  end
82
82
 
83
83
 
84
+ it "can be created with a hostname attribute" do
85
+ result = described_class.new( 'testhost', hostname: 'example.com' )
86
+ expect( result.hostname ).to eq( 'example.com')
87
+ end
88
+
89
+
90
+ it "sets a hostname if unset, and the address was discovered via DNS" do
91
+ expect( TCPSocket ).to receive( :gethostbyname ).with( 'example.com' ).
92
+ and_return(['example.com', [], Socket::AF_INET, '1.1.1.1'])
93
+ result = described_class.new( 'testhost' ) do
94
+ address 'example.com'
95
+ end
96
+ expect( result.addresses ).to include( IPAddr.new('1.1.1.1') )
97
+ expect( result.hostname ).to eq( 'example.com')
98
+ end
99
+
100
+
101
+ it "leaves the hostname untouched if already set" do
102
+ expect( TCPSocket ).to receive( :gethostbyname ).with( 'example.com' ).
103
+ and_return(['example.com', [], Socket::AF_INET, '1.1.1.1'])
104
+ result = described_class.new( 'testhost' ) do
105
+ hostname 'www.example.com'
106
+ address 'example.com'
107
+ end
108
+ expect( result.addresses ).to include( IPAddr.new('1.1.1.1') )
109
+ expect( result.hostname ).to_not eq( 'example.com')
110
+ end
111
+
112
+
84
113
  it "appends block address arguments to addresses in attributes" do
85
114
  result = described_class.new( 'testhost', addresses: '192.168.118.3' ) do
86
115
  address '127.0.0.1'
@@ -116,6 +145,16 @@ describe Arborist::Node::Host do
116
145
  end
117
146
 
118
147
 
148
+ it "includes its hostname when turned into a Hash" do
149
+ node = described_class.new( 'testhost' ) do
150
+ hostname 'example.com'
151
+ end
152
+
153
+ expect( node.to_h ).to include( :hostname )
154
+ expect( node.to_h[:hostname] ).to eq( 'example.com' )
155
+ end
156
+
157
+
119
158
  it "keeps its addresses when marshalled" do
120
159
  node = described_class.new( 'testhost' ) do
121
160
  address '192.168.118.3'
@@ -127,6 +166,16 @@ describe Arborist::Node::Host do
127
166
  end
128
167
 
129
168
 
169
+ it "keeps its hostname when marshalled" do
170
+ node = described_class.new( 'testhost' ) do
171
+ hostname 'example.com'
172
+ end
173
+ clone = Marshal.load( Marshal.dump(node) )
174
+
175
+ expect( clone.hostname ).to eq( node.hostname )
176
+ end
177
+
178
+
130
179
  it "is equal to another host node with the same metadata and addresses" do
131
180
  node1 = described_class.new( 'testhost' ) do
132
181
  address '192.168.118.3'
@@ -154,12 +203,26 @@ describe Arborist::Node::Host do
154
203
  end
155
204
 
156
205
 
206
+ it "is not equal to another host node with differing hostnames" do
207
+ node1 = described_class.new( 'testhost' ) do
208
+ hostname 'example.com'
209
+ end
210
+ node2 = described_class.new( 'testhost' ) do
211
+ hostname 'pets.com'
212
+ end
213
+
214
+ expect( node1 ).to_not eq( node2 )
215
+ end
216
+
217
+
218
+
157
219
  describe "matching" do
158
220
 
159
221
  let( :node ) do
160
222
  described_class.new( 'testhost' ) do
161
223
  address '192.168.66.12'
162
224
  address '10.2.12.68'
225
+ hostname 'example.com'
163
226
  end
164
227
  end
165
228
 
@@ -170,6 +233,11 @@ describe Arborist::Node::Host do
170
233
  end
171
234
 
172
235
 
236
+ it "can be matched on its hostname" do
237
+ expect( node ).to match_criteria( hostname: 'example.com' )
238
+ end
239
+
240
+
173
241
  it "can be matched with a netblock that includes one of its addresses" do
174
242
  expect( node ).to match_criteria( address: '192.168.66.0/24' )
175
243
  expect( node ).to match_criteria( address: '10.0.0.0/8' )