arborist 0.0.1.pre20160128152542 → 0.0.1.pre20160606141735

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +2 -0
  4. data/ChangeLog +426 -1
  5. data/Manifest.txt +17 -2
  6. data/Nodes.md +70 -0
  7. data/Protocol.md +68 -9
  8. data/README.md +3 -5
  9. data/Rakefile +4 -1
  10. data/TODO.md +52 -20
  11. data/lib/arborist.rb +19 -6
  12. data/lib/arborist/cli.rb +39 -25
  13. data/lib/arborist/client.rb +97 -4
  14. data/lib/arborist/command/client.rb +2 -1
  15. data/lib/arborist/command/start.rb +51 -5
  16. data/lib/arborist/dependency.rb +286 -0
  17. data/lib/arborist/event.rb +7 -2
  18. data/lib/arborist/event/{node_matching.rb → node.rb} +11 -5
  19. data/lib/arborist/event/node_acked.rb +5 -7
  20. data/lib/arborist/event/node_delta.rb +30 -3
  21. data/lib/arborist/event/node_disabled.rb +16 -0
  22. data/lib/arborist/event/node_down.rb +10 -0
  23. data/lib/arborist/event/node_quieted.rb +11 -0
  24. data/lib/arborist/event/node_unknown.rb +10 -0
  25. data/lib/arborist/event/node_up.rb +10 -0
  26. data/lib/arborist/event/node_update.rb +2 -11
  27. data/lib/arborist/event/sys_node_added.rb +10 -0
  28. data/lib/arborist/event/sys_node_removed.rb +10 -0
  29. data/lib/arborist/exceptions.rb +4 -0
  30. data/lib/arborist/manager.rb +188 -18
  31. data/lib/arborist/manager/event_publisher.rb +1 -1
  32. data/lib/arborist/manager/tree_api.rb +92 -13
  33. data/lib/arborist/mixins.rb +17 -0
  34. data/lib/arborist/monitor.rb +10 -1
  35. data/lib/arborist/monitor/socket.rb +123 -2
  36. data/lib/arborist/monitor_runner.rb +6 -5
  37. data/lib/arborist/node.rb +420 -94
  38. data/lib/arborist/node/ack.rb +72 -0
  39. data/lib/arborist/node/host.rb +43 -8
  40. data/lib/arborist/node/resource.rb +73 -0
  41. data/lib/arborist/node/root.rb +6 -0
  42. data/lib/arborist/node/service.rb +89 -22
  43. data/lib/arborist/observer.rb +1 -1
  44. data/lib/arborist/subscription.rb +11 -6
  45. data/spec/arborist/client_spec.rb +93 -5
  46. data/spec/arborist/dependency_spec.rb +375 -0
  47. data/spec/arborist/event/node_delta_spec.rb +66 -0
  48. data/spec/arborist/event/node_down_spec.rb +84 -0
  49. data/spec/arborist/event/node_spec.rb +59 -0
  50. data/spec/arborist/event/node_update_spec.rb +14 -3
  51. data/spec/arborist/event_spec.rb +3 -3
  52. data/spec/arborist/manager/tree_api_spec.rb +295 -3
  53. data/spec/arborist/manager_spec.rb +240 -57
  54. data/spec/arborist/monitor_spec.rb +26 -3
  55. data/spec/arborist/node/ack_spec.rb +74 -0
  56. data/spec/arborist/node/host_spec.rb +79 -0
  57. data/spec/arborist/node/resource_spec.rb +56 -0
  58. data/spec/arborist/node/service_spec.rb +68 -2
  59. data/spec/arborist/node_spec.rb +288 -11
  60. data/spec/arborist/subscription_spec.rb +23 -14
  61. data/spec/arborist_spec.rb +0 -4
  62. data/spec/data/observers/webservices.rb +10 -2
  63. data/spec/spec_helper.rb +8 -0
  64. metadata +58 -15
  65. metadata.gz.sig +0 -0
  66. data/LICENSE +0 -29
@@ -4,10 +4,16 @@ require_relative '../spec_helper'
4
4
 
5
5
  require 'timecop'
6
6
  require 'arborist/manager'
7
-
7
+ require 'arborist/node/host'
8
8
 
9
9
  describe Arborist::Manager do
10
10
 
11
+ after( :all ) do
12
+ Arborist::Manager.state_file = nil
13
+ end
14
+ before( :each ) do
15
+ Arborist::Manager.configure
16
+ end
11
17
  after( :each ) do
12
18
  Arborist::Node::Root.reset
13
19
  end
@@ -49,6 +55,148 @@ describe Arborist::Manager do
49
55
  end
50
56
 
51
57
 
58
+ describe "state-saving" do
59
+
60
+ before( :each ) do
61
+ Arborist::Manager.state_file = nil
62
+ end
63
+
64
+ let( :router_node ) { Arborist::Host('router') }
65
+ let( :host_node ) { Arborist::Host( 'host-a', router_node ) }
66
+ let( :tree ) {[ router_node, host_node ]}
67
+
68
+ let( :manager ) do
69
+ instance = described_class.new
70
+ instance.load_tree( tree )
71
+ instance
72
+ end
73
+
74
+
75
+ it "saves the state of its node tree if the state file is configured" do
76
+ statefile = Pathname( './arborist.tree' )
77
+ Arborist::Manager.state_file = statefile
78
+
79
+ tempfile = instance_double( Tempfile,
80
+ path: './arborist20160224-31449-zevoz2.tree', unlink: nil )
81
+
82
+ expect( Tempfile ).to receive( :create ).
83
+ with( ['arborist', '.tree'], '.', encoding: 'binary' ).
84
+ and_return( tempfile )
85
+ expect( Marshal ).to receive( :dump ).with( manager.nodes, tempfile )
86
+ expect( tempfile ).to receive( :close )
87
+ expect( File ).to receive( :rename ).
88
+ with( './arborist20160224-31449-zevoz2.tree', './arborist.tree' )
89
+
90
+ manager.save_node_states
91
+ end
92
+
93
+
94
+ it "cleans up the tempfile created by checkpointing if renaming the file fails" do
95
+ statefile = Pathname( './arborist.tree' )
96
+ Arborist::Manager.state_file = statefile
97
+
98
+ tempfile = instance_double( Tempfile, path: './arborist20160224-31449-zevoz2.tree' )
99
+
100
+ expect( Tempfile ).to receive( :create ).
101
+ with( ['arborist', '.tree'], '.', encoding: 'binary' ).
102
+ and_return( tempfile )
103
+ expect( Marshal ).to receive( :dump ).with( manager.nodes, tempfile )
104
+ expect( tempfile ).to receive( :close )
105
+ expect( File ).to receive( :rename ).
106
+ and_raise( Errno::ENOENT.new("no such file or directory") )
107
+ expect( File ).to receive( :exist? ).with( tempfile.path ).and_return( true )
108
+ expect( File ).to receive( :unlink ).with( tempfile.path )
109
+
110
+ manager.save_node_states
111
+ end
112
+
113
+
114
+ it "doesn't try to save state if the state file is not configured" do
115
+ Arborist::Manager.state_file = nil
116
+
117
+ expect( Tempfile ).to_not receive( :create )
118
+ expect( Marshal ).to_not receive( :dump )
119
+ expect( File ).to_not receive( :rename )
120
+
121
+ manager.save_node_states
122
+ end
123
+
124
+
125
+ it "restores the state of loaded nodes if the state file is configured" do
126
+ _ = manager
127
+
128
+ statefile = Pathname( './arborist.tree' )
129
+ Arborist::Manager.state_file = statefile
130
+ state_file_io = instance_double( File )
131
+
132
+ saved_router_node = Marshal.load( Marshal.dump(router_node) )
133
+ saved_router_node.instance_variable_set( :@status, 'up' )
134
+ saved_host_node = Marshal.load( Marshal.dump(host_node) )
135
+ saved_host_node.instance_variable_set( :@status, 'down' )
136
+ saved_host_node.error = 'Stuff happened and it was not good.'
137
+
138
+ expect( statefile ).to receive( :readable? ).and_return( true )
139
+ expect( statefile ).to receive( :open ).with( 'r:binary' ).
140
+ and_return( state_file_io )
141
+ expect( Marshal ).to receive( :load ).with( state_file_io ).
142
+ and_return({ 'router' => saved_router_node, 'host-a' => saved_host_node })
143
+
144
+ expect( manager.restore_node_states ).to be_truthy
145
+
146
+ expect( manager.nodes['router'].status ).to eq( 'up' )
147
+ expect( manager.nodes['host-a'].status ).to eq( 'down' )
148
+ expect( manager.nodes['host-a'].error ).to eq( 'Stuff happened and it was not good.' )
149
+
150
+ end
151
+
152
+
153
+ it "doesn't error if the configured state file isn't readable" do
154
+ _ = manager
155
+
156
+ statefile = Pathname( './arborist.tree' )
157
+ Arborist::Manager.state_file = statefile
158
+
159
+ expect( statefile ).to receive( :readable? ).and_return( false )
160
+ expect( statefile ).to_not receive( :open )
161
+
162
+ expect( manager.restore_node_states ).to be_falsey
163
+ end
164
+
165
+
166
+ it "checkpoints the state file periodically if an interval is configured" do
167
+ described_class.configure( manager: {checkpoint_frequency: 20, state_file: 'arb.tree'} )
168
+
169
+ timer = instance_double( ZMQ::Timer, "checkpoint timer" )
170
+ expect( ZMQ::Timer ).to receive( :new ).with( 20, 0 ).and_return( timer )
171
+
172
+ expect( manager.start_state_checkpointing ).to eq( timer )
173
+ end
174
+
175
+
176
+ it "doesn't checkpoint if no interval is configured" do
177
+ described_class.configure( manager: {checkpoint_frequency: nil, state_file: 'arb.tree'} )
178
+
179
+ expect( ZMQ::Timer ).to_not receive( :new )
180
+
181
+ expect( manager.start_state_checkpointing ).to be_nil
182
+ end
183
+
184
+
185
+ it "doesn't checkpoint if no state file is configured" do
186
+ described_class.configure( manager: {checkpoint_frequency: 20, state_file: nil} )
187
+
188
+ expect( ZMQ::Timer ).to_not receive( :new )
189
+
190
+ expect( manager.start_state_checkpointing ).to be_nil
191
+ end
192
+
193
+
194
+ it "writes a checkpoint if it receives a SIGUSR1"
195
+
196
+
197
+ end
198
+
199
+
52
200
  context "a new empty manager" do
53
201
 
54
202
  let( :node ) do
@@ -206,16 +354,21 @@ describe Arborist::Manager do
206
354
 
207
355
  [
208
356
  testing_node( 'router' ),
209
- testing_node( 'host_a', 'router' ),
210
- testing_node( 'host_a_www', 'host_a' ),
211
- testing_node( 'host_a_smtp', 'host_a' ),
212
- testing_node( 'host_a_imap', 'host_a' ),
213
- testing_node( 'host_b', 'router' ),
214
- testing_node( 'host_b_www', 'host_b' ),
215
- testing_node( 'host_b_nfs', 'host_b' ),
216
- testing_node( 'host_b_ssh', 'host_b' ),
217
- testing_node( 'host_c', 'router' ),
218
- testing_node( 'host_c_www', 'host_c' ),
357
+ testing_node( 'host-a', 'router' ),
358
+ testing_node( 'host-a-www', 'host-a' ),
359
+ testing_node( 'host-a-smtp', 'host-a' ),
360
+ testing_node( 'host-a-imap', 'host-a' ),
361
+ testing_node( 'host-b', 'router' ),
362
+ testing_node( 'host-b-www', 'host-b' ),
363
+ testing_node( 'host-b-nfs', 'host-b' ),
364
+ testing_node( 'host-b-ssh', 'host-b' ),
365
+ testing_node( 'host-c', 'router' ),
366
+ testing_node( 'host-c-www', 'host-c' ),
367
+ testing_node( 'host-d', 'router' ),
368
+ testing_node( 'host-d-ssh', 'host-d' ),
369
+ testing_node( 'host-d-amqp', 'host-d' ),
370
+ testing_node( 'host-d-database', 'host-d' ),
371
+ testing_node( 'host-d-memcached', 'host-d' ),
219
372
  ]
220
373
  end
221
374
 
@@ -234,15 +387,11 @@ describe Arborist::Manager do
234
387
 
235
388
 
236
389
  it "can traverse all nodes whose status is 'up'" do
237
- manager.nodes.each {|_, node| node.status = :up }
238
- manager.nodes[ 'host_a' ].update( error: "ping failed" )
239
- expect( manager.nodes[ 'host_a' ] ).to be_down
240
- manager.nodes[ 'host_c' ].update( error: "gamma rays" )
241
- expect( manager.nodes[ 'host_c' ] ).to be_down
242
- manager.nodes[ 'host_b_nfs' ].
243
- update( ack: {sender: 'nancy_kerrigan', message: 'bad case of disk rot'} )
244
- expect( manager.nodes[ 'host_b_nfs' ] ).to be_disabled
245
- expect( manager.nodes[ 'host_b_nfs' ] ).to_not be_down
390
+ manager.nodes.each {|_, node| node.status = 'up' }
391
+ manager.nodes[ 'host-a' ].status = 'down'
392
+ manager.nodes[ 'host-c' ].status = 'down'
393
+ manager.nodes[ 'host-b-nfs' ].status = 'disabled'
394
+ manager.nodes[ 'host-b-www' ].status = 'quieted'
246
395
 
247
396
  iter = manager.reachable_nodes
248
397
 
@@ -252,23 +401,32 @@ describe Arborist::Manager do
252
401
  expect( nodes ).to include(
253
402
  "_",
254
403
  "router",
255
- "host_b",
256
- "host_b_www",
257
- "host_b_ssh"
404
+ "host-b",
405
+ "host-b-ssh",
406
+ "host-d",
407
+ "host-d-ssh",
408
+ "host-d-amqp",
409
+ "host-d-database",
410
+ "host-d-memcached",
258
411
  )
259
412
  expect( nodes ).to_not include(
260
- "host_b_nfs",
261
- "host_c",
262
- "host_c_www",
263
- "host_a",
264
- 'host_a_www',
265
- 'host_a_smtp',
266
- 'host_a_imap'
413
+ "host-b-www",
414
+ "host-b-nfs",
415
+ "host-c",
416
+ "host-c-www",
417
+ "host-a",
418
+ "host-a-www",
419
+ "host-a-smtp",
420
+ "host-a-imap",
267
421
  )
268
422
  end
269
423
 
270
424
 
271
- it "can create an Enumerator for all of a node's parents from leaf to root"
425
+ it "can create an Enumerator for all of its children to a specified depth" do
426
+ nodes = manager.depth_limited_enumerator_for( manager.nodes['_'], 2 ).to_a
427
+ expect( nodes.length ).to eq( 6 )
428
+ expect( nodes.map(&:identifier) ).to eq( %w[_ router host-a host-b host-c host-d] )
429
+ end
272
430
 
273
431
  end
274
432
 
@@ -282,16 +440,16 @@ describe Arborist::Manager do
282
440
 
283
441
  [
284
442
  testing_node( 'router' ),
285
- testing_node( 'host_a', 'router' ),
286
- testing_node( 'host_a_www', 'host_a' ),
287
- testing_node( 'host_a_smtp', 'host_a' ),
288
- testing_node( 'host_a_imap', 'host_a' ),
289
- testing_node( 'host_b', 'router' ),
290
- testing_node( 'host_b_www', 'host_b' ),
291
- testing_node( 'host_b_nfs', 'host_b' ),
292
- testing_node( 'host_b_ssh', 'host_b' ),
293
- testing_node( 'host_c', 'router' ),
294
- testing_node( 'host_c_www', 'host_c' ),
443
+ testing_node( 'host-a', 'router' ),
444
+ testing_node( 'host-a-www', 'host-a' ) { tags :home, :church },
445
+ testing_node( 'host-a-smtp', 'host-a' ) { tags :home },
446
+ testing_node( 'host-a-imap', 'host-a' ),
447
+ testing_node( 'host-b', 'router' ),
448
+ testing_node( 'host-b-www', 'host-b' ) { tags :church },
449
+ testing_node( 'host-b-nfs', 'host-b' ),
450
+ testing_node( 'host-b-ssh', 'host-b' ) { tags :work },
451
+ testing_node( 'host-c', 'router' ),
452
+ testing_node( 'host-c-www', 'host-c' ) { tags :work, :home },
295
453
  ]
296
454
  end
297
455
 
@@ -305,24 +463,50 @@ describe Arborist::Manager do
305
463
  it "can fetch a Hash of node states" do
306
464
  states = manager.fetch_matching_node_states( {}, [] )
307
465
  expect( states.size ).to eq( manager.nodes.size )
308
- expect( states ).to include( 'host_b_nfs', 'host_c', 'router' )
309
- expect( states['host_b_nfs'] ).to be_a( Hash )
310
- expect( states['host_c'] ).to be_a( Hash )
466
+ expect( states ).to include( 'host-b-nfs', 'host-c', 'router' )
467
+ expect( states['host-b-nfs'] ).to be_a( Hash )
468
+ expect( states['host-c'] ).to be_a( Hash )
311
469
  expect( states['router'] ).to be_a( Hash )
312
470
  end
313
471
 
314
472
 
473
+ it "can fetch a Hash of node states for nodes which match specified criteria" do
474
+ states = manager.fetch_matching_node_states( {'identifier' => 'host-c'}, [] )
475
+ expect( states.size ).to eq( 1 )
476
+ expect( states.keys.first ).to eq( 'host-c' )
477
+ expect( states['host-c'] ).to be_a( Hash )
478
+ end
479
+
480
+
481
+ it "can fetch a Hash of node states for nodes which don't match specified negative criteria" do
482
+ states = manager.fetch_matching_node_states( {}, [], false, {'identifier' => 'host-c'} )
483
+ expect( states.size ).to eq( manager.nodes.size - 1 )
484
+ expect( states ).to_not include( 'host-c' )
485
+ end
486
+
487
+
488
+ it "can fetch a Hash of node states for nodes combining positive and negative criteria" do
489
+ positive = {'tag' => 'home'}
490
+ negative = {'identifier' => 'host-a-www'}
491
+
492
+ states = manager.fetch_matching_node_states( positive, [], false, negative )
493
+
494
+ expect( states.size ).to eq( 2 )
495
+ expect( states ).to_not include( 'host-a-www' )
496
+ end
497
+
498
+
315
499
  it "can update an event by identifier" do
316
- manager.update_node( 'host_b_www', http: { status: 200 } )
500
+ manager.update_node( 'host-b-www', http: { status: 200 } )
317
501
  expect(
318
- manager.nodes['host_b_www'].properties
502
+ manager.nodes['host-b-www'].properties
319
503
  ).to include( 'http' => { 'status' => 200 } )
320
504
  end
321
505
 
322
506
 
323
507
  it "ignores updates to an identifier that is not (any longer) in the tree" do
324
508
  expect {
325
- manager.update_node( 'host_y', asset_tag: '2by-n86y7t' )
509
+ manager.update_node( 'host-y', asset_tag: '2by-n86y7t' )
326
510
  }.to_not raise_error
327
511
  end
328
512
 
@@ -331,10 +515,10 @@ describe Arborist::Manager do
331
515
  expect( manager.root ).to receive( :publish_events ).
332
516
  at_least( :once ).
333
517
  and_call_original
334
- expect( manager.nodes['host_c'] ).to receive( :publish_events ).
518
+ expect( manager.nodes['host-c'] ).to receive( :publish_events ).
335
519
  at_least( :once ).
336
520
  and_call_original
337
- manager.update_node( 'host_c_www', response_status: 504, error: 'Timeout talking to web service.' )
521
+ manager.update_node( 'host-c-www', response_status: 504, error: 'Timeout talking to web service.' )
338
522
  end
339
523
 
340
524
 
@@ -342,9 +526,9 @@ describe Arborist::Manager do
342
526
  expect( manager.root ).to receive( :publish_events ).
343
527
  at_least( :once ).
344
528
  and_call_original
345
- expect( manager.nodes['host_c'] ).to_not receive( :publish_events )
529
+ expect( manager.nodes['host-c'] ).to_not receive( :publish_events )
346
530
 
347
- manager.update_node( 'host_b_www', response_status: 504, error: 'Timeout talking to web service.' )
531
+ manager.update_node( 'host-b-www', response_status: 504, error: 'Timeout talking to web service.' )
348
532
  end
349
533
 
350
534
  end
@@ -352,7 +536,7 @@ describe Arborist::Manager do
352
536
 
353
537
  describe "subscriptions" do
354
538
 
355
- let( :tree ) {[ testing_node('host_c') ]}
539
+ let( :tree ) {[ testing_node('host-c') ]}
356
540
  let( :manager ) do
357
541
  instance = described_class.new
358
542
  instance.load_tree( tree )
@@ -363,24 +547,24 @@ describe Arborist::Manager do
363
547
  it "can attach subscriptions to a node by its identifier" do
364
548
  sub = subid = nil
365
549
  expect {
366
- sub = manager.create_subscription( 'host_c', 'node.update', type: 'host' )
550
+ sub = manager.create_subscription( 'host-c', 'node.update', type: 'host' )
367
551
  }.to change { manager.subscriptions.size }.by( 1 )
368
552
 
369
553
  node = manager.subscriptions[ sub.id ]
370
554
 
371
555
  expect( sub ).to be_a( Arborist::Subscription )
372
- expect( node ).to be( manager.nodes['host_c'] )
556
+ expect( node ).to be( manager.nodes['host-c'] )
373
557
  end
374
558
 
375
559
 
376
560
  it "can detach subscriptions from a node given the subscription ID" do
377
- sub = manager.create_subscription( 'host_c', 'node.ack', type: 'service' )
561
+ sub = manager.create_subscription( 'host-c', 'node.ack', type: 'service' )
378
562
  rval = nil
379
563
 
380
564
  expect {
381
565
  rval = manager.remove_subscription( sub.id )
382
566
  }.to change { manager.subscriptions.size }.by( -1 ).and(
383
- change { manager.nodes['host_c'].subscriptions.size }.by( -1 )
567
+ change { manager.nodes['host-c'].subscriptions.size }.by( -1 )
384
568
  )
385
569
 
386
570
  expect( rval ).to be( sub )
@@ -415,7 +599,6 @@ describe Arborist::Manager do
415
599
  end
416
600
 
417
601
 
418
-
419
602
  it "sets up its sockets with handlers and starts the ZMQ loop when started" do
420
603
  expect( tree_sock ).to receive( :bind ).with( Arborist.tree_api_url )
421
604
  expect( tree_sock ).to receive( :linger= ).with( 0 )
@@ -26,9 +26,9 @@ describe Arborist::Monitor do
26
26
 
27
27
 
28
28
  let( :testing_nodes ) {{
29
- 'trunk' => trunk_node.to_hash,
30
- 'branch' => branch_node.to_hash,
31
- 'leaf' => leaf_node.to_hash
29
+ 'trunk' => trunk_node.to_h,
30
+ 'branch' => branch_node.to_h,
31
+ 'leaf' => leaf_node.to_h
32
32
  }}
33
33
 
34
34
 
@@ -181,6 +181,29 @@ describe Arborist::Monitor do
181
181
  end
182
182
 
183
183
 
184
+ it "handles system call errors while running the monitor command" do
185
+ mon = described_class.new( "the description" ) do
186
+
187
+ exec 'the_command'
188
+
189
+ handle_results {|*| }
190
+ exec_input {|*| }
191
+ exec_arguments do |nodes|
192
+ Loggability[ Arborist ].debug "In the argument-builder."
193
+ nodes.keys
194
+ end
195
+ end
196
+
197
+ expect( Process ).to receive( :spawn ) do |*args|
198
+ raise Errno::EPIPE, "broken pipe"
199
+ end
200
+
201
+ expect {
202
+ mon.run( testing_nodes )
203
+ }.to_not raise_error
204
+ end
205
+
206
+
184
207
  it "can provide a function for providing input to its command" do
185
208
  mon = described_class.new( "the description" ) do
186
209