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
@@ -58,6 +58,8 @@ describe Arborist::Node::Resource do
58
58
 
59
59
  it "can be matched with a category" do
60
60
  expect( node ).to match_criteria( category: 'disk' )
61
+ expect( node ).to match_criteria( category: [ 'chungwatch', 'disk' ] )
62
+ expect( node ).to_not match_criteria( category: [ 'chungwatch', 'snippersnapper' ] )
61
63
  expect( node ).to_not match_criteria( category: 'processes' )
62
64
  end
63
65
  end
@@ -25,5 +25,18 @@ describe Arborist::Node::Root do
25
25
  expect( node.parent ).to be_nil
26
26
  end
27
27
 
28
+
29
+ it "immediately transitions to up when re-enabled" do
30
+ expect( node ).to be_up
31
+
32
+ node.acknowledge(
33
+ message: 'METEOR COMING DISABLE ALERTS THAT WILL BE 100K KPLZTHX',
34
+ sender: 'SunGuard'
35
+ )
36
+ expect( node ).to be_disabled
37
+
38
+ node.unacknowledge
39
+ expect( node ).to be_up
40
+ end
28
41
  end
29
42
 
@@ -121,6 +121,10 @@ describe Arborist::Node::Service do
121
121
  described_class.new( 'ssh', host )
122
122
  end
123
123
 
124
+ let( :node2 ) do
125
+ described_class.new( 'ntp', host ) { protocol 'udp' }
126
+ end
127
+
124
128
 
125
129
  it "inherits its host's addresses" do
126
130
  expect( node ).to match_criteria( address: '192.168.66.12' )
@@ -153,6 +157,7 @@ describe Arborist::Node::Service do
153
157
 
154
158
  it "can be matched with a port" do
155
159
  expect( node ).to match_criteria( port: 22 )
160
+ expect( node ).to match_criteria( port: [ 22, 123 ] )
156
161
  expect( node ).to match_criteria( port: 'ssh' )
157
162
  expect( node ).to_not match_criteria( port: 80 )
158
163
  expect( node ).to_not match_criteria( port: 'www' )
@@ -163,6 +168,7 @@ describe Arborist::Node::Service do
163
168
  it "can be matched with a protocol" do
164
169
  expect( node ).to match_criteria( protocol: 'tcp' )
165
170
  expect( node ).to_not match_criteria( protocol: 'udp' )
171
+ expect( node ).to match_criteria( protocol: [ 'udp', 'tcp' ] )
166
172
  end
167
173
 
168
174
 
@@ -171,6 +177,9 @@ describe Arborist::Node::Service do
171
177
  expect( node ).to match_criteria( app: 'ssh' )
172
178
  expect( node ).to_not match_criteria( app_protocol: 'http' )
173
179
  expect( node ).to_not match_criteria( app: 'http' )
180
+ expect( node ).to match_criteria( app: [ 'ntp', 'ssh' ] )
181
+ expect( node2 ).to match_criteria( app: [ 'ntp', 'ssh' ] )
182
+ expect( node2 ).to_not match_criteria( app: [ 'http' ] )
174
183
  end
175
184
 
176
185
  end
@@ -11,15 +11,42 @@ describe Arborist::Node do
11
11
  before( :all ) do
12
12
  Arborist::Event.load_all
13
13
  end
14
+ before( :each ) do
15
+ @real_derivatives = described_class.derivatives.dup
16
+ end
17
+ after( :each ) do
18
+ described_class.derivatives.replace( @real_derivatives )
19
+ end
14
20
 
15
21
 
16
- let( :concrete_class ) { TestNode }
17
- let( :subnode_class ) { TestSubNode }
22
+ let( :concrete_class ) do
23
+ Class.new( described_class )
24
+ end
18
25
 
19
26
  let( :identifier ) { 'the_identifier' }
20
27
  let( :identifier2 ) { 'the_other_identifier' }
21
28
 
22
29
 
30
+ shared_examples_for "a reachable node" do
31
+
32
+ it "is still 'reachable'" do
33
+ expect( node ).to be_reachable
34
+ expect( node ).to_not be_unreachable
35
+ end
36
+
37
+ end
38
+
39
+
40
+ shared_examples_for "an unreachable node" do
41
+
42
+ it "is not 'reachable'" do
43
+ expect( node ).to_not be_reachable
44
+ expect( node ).to be_unreachable
45
+ end
46
+
47
+ end
48
+
49
+
23
50
  it "can be loaded from a file" do
24
51
  concrete_instance = nil
25
52
  expect( Kernel ).to receive( :load ).with( "a/path/to/a/node.rb" ) do
@@ -81,15 +108,56 @@ describe Arborist::Node do
81
108
  context "subnode classes" do
82
109
 
83
110
  it "can declare the type of node they live under" do
84
- expect( subnode_class.parent_types ).to include( described_class.get_subclass(:test) )
111
+ subnode_class = Class.new( described_class )
112
+ subnode_class.parent_type( concrete_class )
113
+
114
+ expect( subnode_class.parent_types ).to include( concrete_class )
85
115
  end
86
116
 
87
117
 
88
118
  it "can be constructed via a factory method on instances of their parent type" do
119
+ subnode_class = Class.new( described_class ) do
120
+ def self::name; "TestSubNode"; end
121
+ def self::plugin_name; "testsub"; end
122
+ end
123
+ described_class.derivatives['testsub'] = subnode_class
124
+
125
+ subnode_class.parent_type( concrete_class )
89
126
  parent = concrete_class.new( 'branch' )
90
127
  node = parent.testsub( 'leaf' )
128
+
129
+ expect( node ).to be_an_instance_of( subnode_class )
130
+ expect( node.identifier ).to eq( 'leaf' )
131
+ expect( node.parent ).to eq( 'branch' )
132
+ end
133
+
134
+
135
+ it "can pre-process the factory method arguments" do
136
+ subnode_class = Class.new( described_class ) do
137
+ def self::name; "TestSubNode"; end
138
+ def self::plugin_name; "testsub"; end
139
+ def args( new_args=nil )
140
+ @args = new_args if new_args
141
+ return @args
142
+ end
143
+ def modify( attributes )
144
+ attributes = stringify_keys( attributes )
145
+ super
146
+ self.args( attributes['args'] )
147
+ end
148
+ end
149
+ described_class.derivatives['testsub'] = subnode_class
150
+
151
+ subnode_class.parent_type( concrete_class ) do |arg1, id, *args|
152
+ [ id, {args: [arg1] + args} ]
153
+ end
154
+
155
+ parent = concrete_class.new( 'branch' )
156
+ node = parent.testsub( :arg1, 'leaf', :arg2, :arg3 )
157
+
91
158
  expect( node ).to be_an_instance_of( subnode_class )
92
159
  expect( node.parent ).to eq( parent.identifier )
160
+ expect( node.args ).to eq([ :arg1, :arg2, :arg3 ])
93
161
  end
94
162
 
95
163
  end
@@ -97,187 +165,452 @@ describe Arborist::Node do
97
165
 
98
166
  context "an instance of a concrete subclass" do
99
167
 
100
- let( :node ) { concrete_class.new(identifier) }
101
- let( :child_node ) do
102
- concrete_class.new(identifier2) do
168
+ let( :parent_node ) { concrete_class.new(identifier) }
169
+ let( :sibling_node ) do
170
+ concrete_class.new( 'sibling' ) do
171
+ parent 'the_identifier'
172
+ end
173
+ end
174
+ let( :node ) do
175
+ concrete_class.new( identifier2 ) do
103
176
  parent 'the_identifier'
104
177
  end
105
178
  end
106
179
 
107
180
 
108
181
  it "can declare what its parent is by identifier" do
109
- expect( child_node.parent ).to eq( identifier )
182
+ expect( node.parent ).to eq( identifier )
110
183
  end
111
184
 
112
185
 
113
186
  it "can have child nodes added to it" do
114
- node.add_child( child_node )
115
- expect( node.children ).to include( child_node.identifier )
187
+ parent_node.add_child( node )
188
+ expect( parent_node.children ).to include( node.identifier )
116
189
  end
117
190
 
118
191
 
119
192
  it "can have child nodes appended to it" do
120
- node << child_node
121
- expect( node.children ).to include( child_node.identifier )
193
+ parent_node << node
194
+ expect( parent_node.children ).to include( node.identifier )
122
195
  end
123
196
 
124
197
 
125
198
  it "raises an error if a node which specifies a different parent is added to it" do
126
- not_child_node = concrete_class.new(identifier2) do
199
+ stranger_node = concrete_class.new( identifier2 ) do
127
200
  parent 'youre_not_my_mother'
128
201
  end
129
202
  expect {
130
- node.add_child( not_child_node )
203
+ parent_node.add_child( stranger_node )
131
204
  }.to raise_error( /not a child of/i )
132
205
  end
133
206
 
134
207
 
135
208
  it "doesn't add the same child more than once" do
136
- node.add_child( child_node )
137
- node.add_child( child_node )
138
- expect( node.children.size ).to eq( 1 )
209
+ parent_node.add_child( node )
210
+ parent_node.add_child( node )
211
+ expect( parent_node.children.size ).to eq( 1 )
139
212
  end
140
213
 
141
214
 
142
215
  it "knows it doesn't have any children if it's empty" do
143
- expect( node ).to_not have_children
216
+ expect( parent_node ).to_not have_children
144
217
  end
145
218
 
146
219
 
147
220
  it "knows it has children if subnodes have been added" do
148
- node.add_child( child_node )
149
- expect( node ).to have_children
221
+ parent_node.add_child( node )
222
+ expect( parent_node ).to have_children
150
223
  end
151
224
 
152
225
 
153
226
  it "knows how to remove one of its children" do
154
- node.add_child( child_node )
155
- node.remove_child( child_node )
156
- expect( node ).to_not have_children
227
+ parent_node.add_child( node )
228
+ parent_node.remove_child( node )
229
+ expect( parent_node ).to_not have_children
230
+ end
231
+
232
+
233
+ it "starts out in `unknown` status" do
234
+ expect( parent_node ).to be_unknown
235
+ end
236
+
237
+
238
+ it "remembers status time changes" do
239
+ expect( node.status_changed ).to eq( Time.at(0) )
240
+
241
+ time = Time.at( 1523900910 )
242
+ allow( Time ).to receive( :now ).and_return( time )
243
+
244
+ node.update( { error: 'boom' } )
245
+ expect( node ).to be_down
246
+ expect( node.status_changed ).to eq( time )
247
+ expect( node.status_last_changed ).to eq( Time.at(0) )
248
+
249
+
250
+ node.update( {} )
251
+ expect( node ).to be_up
252
+ expect( node.status_last_changed ).to eq( time )
253
+ end
254
+
255
+
256
+ it "groups errors from separate monitors by their key" do
257
+ expect( node ).to be_unknown
258
+
259
+ node.update( {error: 'ded'}, 'MonitorTron2000' )
260
+ node.update( {error: 'moar ded'}, 'MonitorTron5000' )
261
+ expect( node ).to be_down
262
+
263
+ expect( node.errors.length ).to eq( 2 )
264
+ node.update( {}, 'MonitorTron5000' )
265
+
266
+ expect( node ).to be_down
267
+ expect( node.errors.length ).to eq( 1 )
268
+
269
+ node.update( {}, 'MonitorTron2000' )
270
+ expect( node ).to be_up
271
+ end
272
+
273
+
274
+ it "sets a default monitor key" do
275
+ node.update( error: 'ded' )
276
+ expect( node ).to be_down
277
+ expect( node.errors ).to eq({ '_' => 'ded' })
278
+ end
279
+
280
+
281
+ describe "in `unknown` status" do
282
+
283
+ let( :node ) do
284
+ obj = super()
285
+ obj.status = 'unknown'
286
+ obj
287
+ end
288
+
289
+
290
+ it_behaves_like "a reachable node"
291
+
292
+
293
+ it "transitions to `up` status if doesn't have any errors after an update" do
294
+ expect {
295
+ node.update( tested: true )
296
+ }.to change { node.status }.from( 'unknown' ).to( 'up' )
297
+ end
298
+
299
+
300
+ it "transitions to `down` status if its state is updated with an `error` property" do
301
+ expect {
302
+ node.update( error: "Couldn't talk to it!" )
303
+ }.to change { node.status }.from( 'unknown' ).to( 'down' )
304
+ end
305
+
306
+
307
+ it "transitions to `warn` status if its state is updated with a `warning` property" do
308
+ expect {
309
+ node.update( warning: "Things are starting to look bad!" )
310
+ }.to change { node.status }.from( 'unknown' ).to( 'warn' )
311
+ end
312
+
313
+
314
+ it "transitions to `disabled` if it's acknowledged" do
315
+ expect {
316
+ node.acknowledge( message: "Maintenance", sender: 'mahlon' )
317
+ }.to change { node.status }.from( 'unknown' ).to( 'disabled' )
318
+ end
319
+
157
320
  end
158
321
 
159
- describe "status" do
160
322
 
161
- it "starts out in `unknown` status" do
162
- expect( node ).to be_unknown
323
+ describe "in `up` status" do
324
+
325
+ let( :node ) do
326
+ obj = super()
327
+ obj.status = 'up'
328
+ obj
163
329
  end
164
330
 
165
331
 
166
- it "transitions to `up` status if its state is updated with no `error` property" do
167
- node.update( tested: true )
168
- expect( node ).to be_up
332
+ it_behaves_like "a reachable node"
333
+
334
+
335
+ it "stays in `up` status if doesn't have any errors after an update" do
336
+ expect {
337
+ node.update( tested: true )
338
+ }.to_not change { node.status }.from( 'up' )
169
339
  end
170
340
 
171
341
 
172
342
  it "transitions to `down` status if its state is updated with an `error` property" do
173
- node.update( error: "Couldn't talk to it!" )
174
- expect( node ).to be_down
343
+ expect {
344
+ node.update( error: "Couldn't talk to it!" )
345
+ }.to change { node.status }.from( 'up' ).to( 'down' )
346
+ end
347
+
348
+
349
+ it "transitions to `down` status if it's updated with both an `error` and `warning` property" do
350
+ expect {
351
+ node.update( error: "Couldn't talk to it!", warning: "Above configured levels!" )
352
+ }.to change { node.status }.from( 'up' ).to( 'down' )
353
+ end
354
+
355
+
356
+ it "transitions to `warn` status if its state is updated with a `warning` property" do
357
+ expect {
358
+ node.update( warning: "Things are starting to look bad!" )
359
+ }.to change { node.status }.from( 'up' ).to( 'warn' )
360
+ end
361
+
362
+
363
+ it "transitions to `disabled` if it's acknowledged" do
364
+ expect {
365
+ node.acknowledge( message: "Maintenance", sender: 'mahlon' )
366
+ }.to change { node.status }.from( 'up' ).to( 'disabled' )
367
+ end
368
+
369
+
370
+ it "transitions to `quieted` if it's notified that its parent has gone down" do
371
+ down_event = Arborist::Event.create( :node_down, parent_node )
372
+ expect {
373
+ node.handle_event( down_event )
374
+ }.to change { node.status }.from( 'up' ).to( 'quieted' )
175
375
  end
176
376
 
177
- it "transitions from `down` to `acked` status if it's updated with an `ack` property" do
178
- node.status = 'down'
179
- node.errors = 'Something is wrong | he falls | betraying the trust | "\
377
+ end
378
+
379
+
380
+ describe "in `down` status" do
381
+
382
+ let( :node ) do
383
+ obj = super()
384
+ obj.status = 'down'
385
+ obj.errors['moldovia'] = 'Something is wrong | he falls | betraying the trust | "\
180
386
  "there is a disaster in his life.'
181
- node.update( ack: {message: "Leitmotiv", sender: 'ged'} )
182
- expect( node ).to be_acked
387
+ obj
388
+ end
389
+
390
+
391
+ it_behaves_like "an unreachable node"
392
+
393
+
394
+ it "transitions to `acked` status if it's acknowledged" do
395
+ expect {
396
+ node.acknowledge( message: "Leitmotiv", sender: 'ged' )
397
+ }.to change { node.status }.from( 'down' ).to( 'acked' )
398
+ end
399
+
400
+
401
+ it "transitions to `up` status if all of its errors are cleared" do
402
+ expect {
403
+ node.update( {error: nil}, 'moldovia' )
404
+ }.to change { node.status }.from( 'down' ).to( 'up' )
405
+ end
406
+
407
+
408
+ it "transitions to `warn` status if errors are cleared but warnings remain" do
409
+ expect {
410
+ node.update( {error: nil, warning: 'squirt!'}, 'moldovia' )
411
+ }.to change { node.status }.from( 'down' ).to( 'warn' )
412
+ end
413
+
414
+ end
415
+
416
+
417
+ describe "in `warn` status" do
418
+
419
+ let( :node ) do
420
+ obj = super()
421
+ obj.status = 'warn'
422
+ obj.warnings = { 'beach' => 'Sweaty but functional servers.' }
423
+ obj
183
424
  end
184
425
 
185
- it "transitions from `acked` to `up` status if its error is cleared" do
186
- node.status = 'down'
187
- node.errors = { '_' => 'Something is wrong | he falls | betraying the trust | "\
188
- "there is a disaster in his life.' }
189
- node.update( ack: {message: "Leitmotiv", sender: 'ged'} )
190
- node.update( error: nil )
191
426
 
192
- expect( node ).to be_up
427
+ it_behaves_like "a reachable node"
428
+
429
+
430
+ it "transitions to `up` if its warnings are cleared" do
431
+ expect {
432
+ node.update( {warning: nil}, 'beach' )
433
+ }.to change { node.status }.from( 'warn' ).to( 'up' )
193
434
  end
194
435
 
195
- it "stays `up` if its error is cleared and stays cleared" do
196
- node.status = 'down'
197
- node.errors = { '_' => 'stay up damn you!' }
198
- node.update( ack: {message: "Leitmotiv", sender: 'ged'} )
199
- node.update( error: nil )
200
- node.update( error: nil )
201
436
 
202
- expect( node ).to be_up
437
+ it "transitions to `down` if has an error set" do
438
+ expect {
439
+ node.update( {error: "Shark warning."}, 'beach' )
440
+ }.to change { node.status }.from( 'warn' ).to( 'down' )
203
441
  end
204
442
 
205
- it "transitions to `disabled` from `up` status if it's updated with an `ack` property" do
206
- node.status = 'up'
207
- node.update( ack: {message: "Maintenance", sender: 'mahlon'} )
208
443
 
209
- expect( node ).to be_disabled
444
+ it "transitions to `disabled` if it's acknowledged" do
445
+ expect {
446
+ node.acknowledge( message: "Chill", sender: 'ged' )
447
+ }.to change { node.status }.from( 'warn' ).to( 'disabled' )
210
448
  end
211
449
 
212
- it "transitions to `disabled` from `unknown` status if it's updated with an `ack` property" do
213
- node.status = 'unknown'
214
- node.update( ack: {message: "Maintenance", sender: 'mahlon'} )
450
+ end
451
+
215
452
 
216
- expect( node ).to be_disabled
453
+ describe "in `acked` status" do
454
+
455
+ let( :node ) do
456
+ obj = super()
457
+ obj.status = 'acked'
458
+ obj.errors['moldovia'] = 'Something is wrong | he falls | betraying the trust | "\
459
+ "there is a disaster in his life.'
460
+ obj.acknowledge( message: "Leitmotiv", sender: 'ged' )
461
+ obj
217
462
  end
218
463
 
464
+
465
+ it_behaves_like "a reachable node"
466
+
467
+
468
+ it "transitions to `up` status if its error is cleared" do
469
+ expect {
470
+ node.update( {error: nil}, 'moldovia' )
471
+ }.to change { node.status }.from( 'acked' ).to( 'up' )
472
+ end
473
+
474
+
475
+ it "stays `up` if it is updated twice with an error key" do
476
+ node.update( {error: nil}, 'moldovia' )
477
+
478
+ expect {
479
+ node.update( {error: nil}, 'moldovia' ) # make sure it stays cleared
480
+ }.to_not change { node.status }.from( 'up' )
481
+ end
482
+
483
+ end
484
+
485
+
486
+ describe "in `disabled` status" do
487
+
488
+ let( :node ) do
489
+ obj = super()
490
+ obj.acknowledge( message: "Bikini models", sender: 'ged' )
491
+ obj
492
+ end
493
+
494
+
495
+ it_behaves_like "an unreachable node"
496
+
497
+
219
498
  it "stays `disabled` if it gets an error" do
220
- node.status = 'up'
221
- node.update( ack: {message: "Maintenance", sender: 'mahlon'} )
222
- node.update( error: "take me to the virus hospital" )
499
+ expect {
500
+ node.update( error: "take me to the virus hospital" )
501
+ }.to_not change { node.status }.from( 'disabled' )
502
+
503
+ expect( node.ack ).to_not be_nil
504
+ end
505
+
506
+
507
+ it "stays `disabled` if it gets a warning" do
508
+ expect {
509
+ node.update( warning: "heartbone" )
510
+ }.to_not change { node.status }.from( 'disabled' )
223
511
 
224
- expect( node ).to be_disabled
225
512
  expect( node.ack ).to_not be_nil
226
513
  end
227
514
 
515
+
228
516
  it "stays `disabled` if it gets a successful update" do
229
- node.status = 'up'
230
- node.update( ack: {message: "Maintenance", sender: 'mahlon'} )
231
- node.update( ping: {time: 0.02} )
517
+ expect {
518
+ node.update( ping: {time: 0.02} )
519
+ }.to_not change { node.status }.from( 'disabled' )
232
520
 
233
- expect( node ).to be_disabled
234
521
  expect( node.ack ).to_not be_nil
235
522
  end
236
523
 
237
- it "transitions to `unknown` from `disabled` status if its ack is cleared" do
238
- node.status = 'up'
239
- node.update( ack: {message: "Maintenance", sender: 'mahlon'} )
240
- node.update( ack: nil )
241
524
 
242
- expect( node ).to_not be_disabled
243
- expect( node ).to be_unknown
525
+ it "transitions to `unknown` if its acknowledgment is cleared" do
526
+ expect {
527
+ node.unacknowledge
528
+ }.to change { node.status }.from( 'disabled' ).to( 'unknown' )
529
+
244
530
  expect( node.ack ).to be_nil
245
531
  end
246
532
 
247
- it "knows if it's status deems it 'reachable'" do
248
- node.update( error: nil )
249
- expect( node ).to be_reachable
250
- expect( node ).to_not be_unreachable
533
+ end
534
+
535
+
536
+ describe "in `quieted` status because its parent is down" do
537
+
538
+ let( :down_event ) { Arborist::Event.create(:node_down, parent_node) }
539
+ let( :up_event ) { Arborist::Event.create(:node_up, parent_node) }
540
+
541
+ let( :node ) do
542
+ obj = super()
543
+ obj.handle_event( down_event )
544
+ obj
251
545
  end
252
546
 
253
- it "knows if it's status deems it 'unreachable'" do
254
- node.update( error: 'ded' )
255
- expect( node ).to be_unreachable
256
- expect( node ).to_not be_reachable
547
+
548
+ it_behaves_like "an unreachable node"
549
+
550
+
551
+ it "remains `quieted` even if updated with an error" do
552
+ expect {
553
+ node.update( {error: "Internal error"}, 'webservice' )
554
+ }.to_not change { node.status }.from( 'quieted' )
257
555
  end
258
556
 
259
- it "groups errors from separate monitor by their key" do
260
- expect( node ).to be_unknown
261
557
 
262
- node.update( _monitor_key: 'MonitorTron2000', error: 'ded' )
263
- node.update( _monitor_key: 'MonitorTron5000', error: 'moar ded' )
264
- expect( node ).to be_down
558
+ it "transitions to `unknown` if its parent transitions to up" do
559
+ up_event = Arborist::Event.create( :node_up, parent_node )
560
+
561
+ expect {
562
+ node.handle_event( up_event )
563
+ }.to change { node.status }.from( 'quieted' ).to( 'unknown' )
564
+ end
265
565
 
266
- expect( node.errors.length ).to eq( 2 )
267
- node.update( _monitor_key: 'MonitorTron5000' )
268
566
 
269
- expect( node ).to be_down
270
- expect( node.errors.length ).to eq( 1 )
567
+ it "transitions to `unknown` if its parent transitions to warn" do
568
+ warn_event = Arborist::Event.create( :node_warn, parent_node )
271
569
 
272
- node.update( _monitor_key: 'MonitorTron2000' )
273
- expect( node ).to be_up
570
+ expect {
571
+ node.handle_event( warn_event )
572
+ }.to change { node.status }.from( 'quieted' ).to( 'unknown' )
274
573
  end
275
574
 
276
- it "sets a default monitor key" do
277
- node.update( error: 'ded' )
278
- expect( node ).to be_down
279
- expect( node.errors ).to eq({ '_' => 'ded' })
575
+
576
+ it "transitions to `disabled` if it's acknowledged" do
577
+ expect {
578
+ node.acknowledge( message: 'Turning this off for now.', sender: 'ged' )
579
+ }.to change { node.status }.from( 'quieted' ).to( 'disabled' )
280
580
  end
581
+
582
+ end
583
+
584
+
585
+ describe "in `quieted` status because one of its dependencies is down" do
586
+
587
+ let( :down_event ) { Arborist::Event.create(:node_down, sibling_node) }
588
+ let( :up_event ) { Arborist::Event.create(:node_up, sibling_node) }
589
+
590
+ let( :node ) do
591
+ obj = super()
592
+ obj.depends_on( 'sibling' )
593
+ obj.handle_event( down_event )
594
+ obj
595
+ end
596
+
597
+
598
+ it_behaves_like "an unreachable node"
599
+
600
+
601
+ it "transitions to `unknown` if its reasons for being quieted are cleared" do
602
+ expect {
603
+ node.handle_event( up_event )
604
+ }.to change { node.status }.from( 'quieted' ).to( 'unknown' )
605
+ end
606
+
607
+
608
+ it "transitions to `disabled` if it's acknowledged" do
609
+ expect {
610
+ node.acknowledge( message: 'Turning this off for now.', sender: 'ged' )
611
+ }.to change { node.status }.from( 'quieted' ).to( 'disabled' )
612
+ end
613
+
281
614
  end
282
615
 
283
616
 
@@ -376,13 +709,13 @@ describe Arborist::Node do
376
709
  describe "Enumeration" do
377
710
 
378
711
  it "iterates over its children for #each" do
379
- parent = node
712
+ parent = parent_node
380
713
  parent <<
381
714
  concrete_class.new('child1') { parent 'the_identifier' } <<
382
715
  concrete_class.new('child2') { parent 'the_identifier' } <<
383
716
  concrete_class.new('child3') { parent 'the_identifier' }
384
717
 
385
- expect( parent.map(&:identifier) ).to eq([ 'child1', 'child2', 'child3' ])
718
+ expect( parent_node.map(&:identifier) ).to eq([ 'child1', 'child2', 'child3' ])
386
719
  end
387
720
 
388
721
  end
@@ -390,6 +723,8 @@ describe Arborist::Node do
390
723
 
391
724
  describe "Serialization" do
392
725
 
726
+ # From spec_helper.rb
727
+ let( :concrete_class ) { TestNode }
393
728
  let( :node ) do
394
729
  concrete_class.new( 'foo' ) do
395
730
  parent 'bar'
@@ -407,12 +742,37 @@ describe Arborist::Node do
407
742
  end
408
743
  end
409
744
 
745
+ let( :tree ) do
746
+ node_hierarchy( node,
747
+ node_hierarchy( 'host-a',
748
+ testing_node( 'host-a-www' ),
749
+ testing_node( 'host-a-smtp' ),
750
+ testing_node( 'host-a-imap' )
751
+ ),
752
+ node_hierarchy( 'host-b',
753
+ testing_node( 'host-b-www' ),
754
+ testing_node( 'host-b-nfs' ),
755
+ testing_node( 'host-b-ssh' )
756
+ ),
757
+ node_hierarchy( 'host-c',
758
+ testing_node( 'host-c-www' )
759
+ ),
760
+ node_hierarchy( 'host-d',
761
+ testing_node( 'host-d-ssh' ),
762
+ testing_node( 'host-d-amqp' ),
763
+ testing_node( 'host-d-database' ),
764
+ testing_node( 'host-d-memcached' )
765
+ )
766
+ )
767
+ end
768
+
410
769
 
411
770
  it "can restore saved state from an older copy of the node" do
412
771
  old_node = Marshal.load( Marshal.dump(node) )
413
772
 
414
773
  old_node.status = 'down'
415
774
  old_node.status_changed = Time.now - 400
775
+ old_node.status_last_changed = Time.now - 800
416
776
  old_node.errors = "Host unreachable"
417
777
  old_node.update(
418
778
  ack: {
@@ -433,6 +793,7 @@ describe Arborist::Node do
433
793
 
434
794
  expect( node.status ).to eq( old_node.status )
435
795
  expect( node.status_changed ).to eq( old_node.status_changed )
796
+ expect( node.status_last_changed ).to eq( old_node.status_last_changed )
436
797
  expect( node.errors ).to eq( old_node.errors )
437
798
  expect( node.ack ).to eq( old_node.ack )
438
799
  expect( node.properties ).to include( old_node.properties )
@@ -482,14 +843,14 @@ describe Arborist::Node do
482
843
 
483
844
 
484
845
  it "can return a Hash of serializable node data" do
485
- result = node.to_h
846
+ result = tree.to_h
486
847
 
487
848
  expect( result ).to be_a( Hash )
488
849
  expect( result ).to include(
489
850
  :identifier,
490
851
  :parent, :description, :tags, :properties, :ack, :status,
491
852
  :last_contacted, :status_changed, :errors, :quieted_reasons,
492
- :dependencies
853
+ :dependencies, :status_last_changed
493
854
  )
494
855
  expect( result[:identifier] ).to eq( 'foo' )
495
856
  expect( result[:type] ).to eq( 'testnode' )
@@ -500,13 +861,45 @@ describe Arborist::Node do
500
861
  expect( result[:ack] ).to be_nil
501
862
  expect( result[:last_contacted] ).to eq( node.last_contacted.iso8601 )
502
863
  expect( result[:status_changed] ).to eq( node.status_changed.iso8601 )
864
+ expect( result[:status_last_changed] ).to eq( node.status_last_changed.iso8601 )
503
865
  expect( result[:errors] ).to be_a( Hash )
504
866
  expect( result[:errors] ).to be_empty
505
867
  expect( result[:dependencies] ).to be_a( Hash )
506
868
  expect( result[:quieted_reasons] ).to be_a( Hash )
869
+
870
+ expect( result[:children] ).to be_empty
507
871
  end
508
872
 
509
873
 
874
+ it "can include all of its serialized children" do
875
+ result = tree.to_h( depth: -1 )
876
+
877
+ expect( result ).to be_a( Hash )
878
+ expect( result ).to include(
879
+ :identifier,
880
+ :parent, :description, :tags, :properties, :ack, :status,
881
+ :last_contacted, :status_changed, :errors, :quieted_reasons,
882
+ :dependencies
883
+ )
884
+
885
+ expect( result[:children] ).to be_a( Hash )
886
+ expect( result[:children].length ).to eq( 4 )
887
+
888
+ host_a = result[:children]['host-a']
889
+ expect( host_a ).to be_a( Hash )
890
+ expect( host_a ).to include(
891
+ :identifier,
892
+ :parent, :description, :tags, :properties, :ack, :status,
893
+ :last_contacted, :status_changed, :errors, :quieted_reasons,
894
+ :dependencies
895
+ )
896
+ expect( host_a[:children].length ).to eq( 3 )
897
+ end
898
+
899
+
900
+ it "can include a specific depth of its children"
901
+
902
+
510
903
  it "can be reconstituted from a serialized Hash of node data" do
511
904
  hash = node.to_h
512
905
  cloned_node = concrete_class.from_hash( hash )
@@ -525,10 +918,10 @@ describe Arborist::Node do
525
918
 
526
919
  it "an ACKed node stays ACKed when serialized and restored" do
527
920
  node.update( error: "there's a fire" )
528
- node.update( ack: {
921
+ node.acknowledge(
529
922
  message: 'We know about the fire. It rages on.',
530
923
  sender: '1986 Labyrinth David Bowie'
531
- })
924
+ )
532
925
  expect( node ).to be_acked
533
926
 
534
927
  restored_node = Marshal.load( Marshal.dump(node) )
@@ -620,20 +1013,78 @@ describe Arborist::Node do
620
1013
  end
621
1014
 
622
1015
 
623
- it "generates a node.acked event when a node is acked" do
624
- node.update( error: 'ping failed ')
625
- events = node.update(ack: {
1016
+ it "includes the original ack in delta events" do
1017
+ events = node.acknowledge(
626
1018
  message: "I have a poisonous friend. She's living in the house.",
627
1019
  sender: 'Seabound'
628
- })
1020
+ )
1021
+ delta_event = events.find {|ev| ev.type == 'node.delta' }
1022
+ expect( delta_event.payload ).to include( 'status' => ['up', 'disabled'] )
1023
+ expect( delta_event.payload ).to include( 'ack' => [ nil, a_hash_including(sender: 'Seabound') ] )
1024
+
1025
+ events = node.unacknowledge
1026
+ delta_event = events.find {|ev| ev.type == 'node.delta' }
1027
+
1028
+ expect( delta_event.payload ).to include( 'status' => ['disabled', 'unknown'] )
1029
+ expect( delta_event.payload ).to include( 'ack' => [ a_hash_including(sender: 'Seabound'), nil ] )
1030
+ end
1031
+
1032
+
1033
+ it "generates a node.delta event when a node ack is updated" do
1034
+ node.update( error: 'ping failed ')
1035
+ node.acknowledge(
1036
+ message: "The last one was dead. This one is on her way.",
1037
+ sender: 'Average Trigram'
1038
+ )
1039
+
1040
+ events = node.acknowledge(
1041
+ message: "000100101011111",
1042
+ sender: 'Robots'
1043
+ )
1044
+ expect( events.size ).to eq( 2 )
629
1045
 
630
- expect( events.size ).to eq( 3 )
631
- ack_event = events.find {|ev| ev.type == 'node.acked' }
1046
+ delta = events.last
1047
+ expect( delta ).to be_a( Arborist::Event::NodeDelta )
632
1048
 
633
- expect( ack_event ).to be_a( Arborist::Event )
634
- expect( ack_event.payload ).to include( ack: a_hash_including(sender: 'Seabound') )
1049
+ expect( delta.payload ).
1050
+ to include( 'ack' => [
1051
+ a_hash_including(sender: 'Average Trigram'), a_hash_including(sender: 'Robots')
1052
+ ]
1053
+ )
1054
+ end
1055
+
1056
+
1057
+ it "generates a node.acked and node.delta event when a node is acked" do
1058
+ node.update( error: 'ping failed ')
1059
+ events = node.acknowledge(
1060
+ message: "The last one was dead. This one is on her way.",
1061
+ sender: 'Average Trigram'
1062
+ )
1063
+
1064
+ expect( events.size ).to eq( 2 )
1065
+
1066
+ expect( events.first ).to be_a( Arborist::Event::NodeAcked )
1067
+ expect( events.last ).to be_a( Arborist::Event::NodeDelta )
1068
+ expect( events.first.payload ).
1069
+ to include( ack: a_hash_including(sender: 'Average Trigram') )
1070
+ expect( events.last.payload ).
1071
+ to include( 'ack' => [ nil, a_hash_including(sender: 'Average Trigram') ])
1072
+ expect( events.last.payload ).to include( 'status' => ['down', 'acked'] )
635
1073
  end
636
1074
 
1075
+
1076
+ it "generates a node.down and node.delta event when a node is unacked" do
1077
+ node.update( error: 'ping failed ')
1078
+ node.acknowledge(
1079
+ message: "The humans are dead. I poked one. It's dead.",
1080
+ sender: 'Jermaine and Brit'
1081
+ )
1082
+
1083
+ events = node.unacknowledge
1084
+ expect( events.last.payload ).
1085
+ to include( 'ack' => [ a_hash_including(sender: 'Jermaine and Brit'), nil ])
1086
+ expect( events.last.payload ).to include( 'status' => ['acked', 'down'] )
1087
+ end
637
1088
  end
638
1089
 
639
1090
 
@@ -648,6 +1099,7 @@ describe Arborist::Node do
648
1099
  end
649
1100
 
650
1101
 
1102
+
651
1103
  it "allows the addition of a Subscription" do
652
1104
  sub = Arborist::Subscription.new {}
653
1105
  node.add_subscription( sub )
@@ -655,7 +1107,6 @@ describe Arborist::Node do
655
1107
  expect( node.subscriptions[sub.id] ).to be( sub )
656
1108
  end
657
1109
 
658
-
659
1110
  it "allows the removal of a Subscription" do
660
1111
  sub = Arborist::Subscription.new {}
661
1112
  node.add_subscription( sub )
@@ -678,11 +1129,23 @@ describe Arborist::Node do
678
1129
  expect( results.first ).to be( sub )
679
1130
  end
680
1131
 
1132
+
1133
+ it "can return the identifiers of all other nodes that subscribe to it" do
1134
+
1135
+ end
1136
+
681
1137
  end
682
1138
 
683
1139
 
684
1140
  describe "matching" do
685
1141
 
1142
+ let( :concrete_class ) do
1143
+ cls = Class.new( described_class ) do
1144
+ def self::name; "TestNode"; end
1145
+ end
1146
+ end
1147
+
1148
+
686
1149
  let( :node ) do
687
1150
  concrete_class.new( 'foo' ) do
688
1151
  parent 'bar'
@@ -717,6 +1180,12 @@ describe Arborist::Node do
717
1180
  expect( node ).to_not match_criteria( status: 'down' )
718
1181
  end
719
1182
 
1183
+ it "can be matched with multiple statuses" do
1184
+ expect( node ).to match_criteria( status: ['up','warn'] )
1185
+ expect( node ).to_not match_criteria( status: 'down' )
1186
+ expect( node ).to match_criteria( status: 'up' )
1187
+ end
1188
+
720
1189
 
721
1190
  it "can be matched with its type" do
722
1191
  expect( node ).to match_criteria( type: 'testnode' )
@@ -726,7 +1195,9 @@ describe Arborist::Node do
726
1195
 
727
1196
  it "can be matched with its parent" do
728
1197
  expect( node ).to match_criteria( parent: 'bar' )
1198
+ expect( node ).to match_criteria( parent: [ 'bar', 'hooowat' ] )
729
1199
  expect( node ).to_not match_criteria( parent: 'hooowat' )
1200
+ expect( node ).to_not match_criteria( parent: [ 'hooowat', 'wathoooo' ] )
730
1201
  end
731
1202
 
732
1203
 
@@ -902,7 +1373,7 @@ describe Arborist::Node do
902
1373
  mgr.load_tree([ vmhost01, vm01, memcache ])
903
1374
 
904
1375
  events = vmhost01.
905
- update( ack: {message: "Imma gonna f up yo' sash", sender: "GOD"} )
1376
+ acknowledge( message: "Imma gonna f up yo' sash", sender: "GOD" )
906
1377
  vmhost01.publish_events( *events )
907
1378
 
908
1379
  expect( memcache ).to be_quieted
@@ -913,10 +1384,10 @@ describe Arborist::Node do
913
1384
 
914
1385
  describe "operational attribute modification" do
915
1386
 
916
-
917
1387
  let( :node ) do
918
1388
  concrete_class.new( 'foo' ) do
919
1389
  parent 'bar'
1390
+ config boop: false
920
1391
  description "The prototypical node"
921
1392
  tags :chunker, :hunky, :flippin, :hippo
922
1393
  end
@@ -935,6 +1406,12 @@ describe Arborist::Node do
935
1406
  end
936
1407
 
937
1408
 
1409
+ it "can change any custom configuration values" do
1410
+ node.modify( config: { boop: true } )
1411
+ expect( node.config ).to eq({ 'boop' => true })
1412
+ end
1413
+
1414
+
938
1415
  it "can change its tags" do
939
1416
  node.modify( tags: %w[dew dairy daisy dilettante] )
940
1417
  expect( node.tags ).to eq( %w[dew dairy daisy dilettante] )
@@ -945,6 +1422,91 @@ describe Arborist::Node do
945
1422
  node.modify( tags: 'single' )
946
1423
  expect( node.tags ).to eq( %w[single] )
947
1424
  end
1425
+ end
1426
+
1427
+
1428
+ describe "reparenting" do
1429
+
1430
+ before( :each ) do
1431
+ @old_parent = concrete_class.new( 'router1' ) do
1432
+ description "The first router"
1433
+ end
1434
+ @new_parent = concrete_class.new( 'router2' ) do
1435
+ description "The second router"
1436
+ end
1437
+ @node = concrete_class.new( 'foo' ) do
1438
+ parent 'router1'
1439
+ description "The prototypical node"
1440
+ end
1441
+
1442
+ @old_parent.add_child( @node )
1443
+ end
1444
+
1445
+ let( :node ) { @node }
1446
+ let( :old_parent ) { @old_parent }
1447
+ let( :new_parent ) { @new_parent }
1448
+
1449
+
1450
+ it "moves itself to the new node and removes itself from its old parent" do
1451
+ expect( old_parent.children ).to include( node.identifier )
1452
+ expect( new_parent.children ).to_not include( node.identifier )
1453
+
1454
+ node.reparent( old_parent, new_parent )
1455
+
1456
+ expect( old_parent.children ).to_not include( node.identifier )
1457
+ expect( new_parent.children ).to include( node.identifier )
1458
+ end
1459
+
1460
+
1461
+ it "sets its state to unknown if it was down prior to the move" do
1462
+ node.update( error: 'Rock and Roll McDonalds' )
1463
+
1464
+ node.reparent( old_parent, new_parent )
1465
+
1466
+ expect( node ).to be_unknown
1467
+ end
1468
+
1469
+
1470
+ it "sets its state to unknown if it was quieted by its parent prior to the move" do
1471
+ node.quieted_reasons[ :primary ] = "Timex takes a licking and... well, broke, it looks like."
1472
+ node.status = 'quieted'
1473
+
1474
+ node.reparent( old_parent, new_parent )
1475
+
1476
+ expect( node ).to be_unknown
1477
+ end
1478
+
1479
+
1480
+ it "keeps its quieted state if it was quieted by secondary dependency prior to the move" do
1481
+ node.quieted_reasons[ :primary ] = "Timex takes a licking and... well, broke, it looks like."
1482
+ node.quieted_reasons[ :secondary ] = "Western Union: The fastest way to send money"
1483
+ node.status = 'quieted'
1484
+
1485
+ node.reparent( old_parent, new_parent )
1486
+
1487
+ expect( node ).to be_quieted
1488
+ end
1489
+
1490
+
1491
+ it "keeps its disabled state" do
1492
+ node.acknowledge( message: 'Moving the machine', sender: 'Me' )
1493
+ expect( node ).to be_disabled
1494
+
1495
+ node.reparent( old_parent, new_parent )
1496
+
1497
+ expect( node ).to be_disabled
1498
+ end
1499
+
1500
+
1501
+ it "keeps its acked state" do
1502
+ node.update( {error: 'Batman whooped my ass.'}, 'gotham' )
1503
+ node.acknowledge( message: 'Moving the machine', sender: 'Me' )
1504
+ expect( node ).to be_acked
1505
+
1506
+ node.reparent( old_parent, new_parent )
1507
+
1508
+ expect( node ).to be_acked
1509
+ end
948
1510
 
949
1511
  end
950
1512