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
@@ -9,6 +9,13 @@ describe Arborist::Client do
9
9
 
10
10
  let( :client ) { described_class.new }
11
11
 
12
+ it "can return a singleton instance" do
13
+ one = described_class.instance
14
+ two = described_class.instance
15
+ expect( one ).to be_a( described_class )
16
+ expect( one ).to equal( two )
17
+ end
18
+
12
19
  describe "synchronous API", :testing_manager do
13
20
 
14
21
  before( :each ) do
@@ -47,26 +54,52 @@ describe Arborist::Client do
47
54
  let( :manager ) { @manager }
48
55
 
49
56
 
50
- describe "high-level methods" do
57
+ describe "convenience API" do
51
58
 
52
- it "provides a convenience method for acknowledging" do
53
- manager.nodes['sidonie'].update( error: "Clown apocalypse" )
59
+ it "can fetch a single node" do
60
+ res = client.fetch_node( 'duir' )
61
+ expect( res ).to be_a( Hash )
62
+ expect( res['identifier'] ).to eq( 'duir' )
63
+ end
54
64
 
55
- res = client.acknowledge( :sidonie, "I'm on it.", "ged" )
56
65
 
57
- expect( manager.nodes['sidonie'] ).to be_acked
66
+ it "has a convenience method for fetching dependencies" do
67
+ res = client.dependencies_of( 'sidonie' )
68
+ expect( res ).to be_a( Hash ).and include( 'sidonie-postgresql', 'sidonie-ssh' )
58
69
  end
59
70
 
60
71
 
61
- it "provides a convenience method for clearing acknowledgments" do
62
- manager.nodes['sidonie'].update( error: "Clown apocalypse" )
72
+ it "can pivot dependencies on node attributes" do
73
+ res = client.dependencies_of( 'sidonie', partition: 'type' )
74
+ expect( res ).to be_a( Hash )
75
+ expect( res['service'] ).to include(
76
+ a_hash_including( { 'identifier' => 'sidonie-ssh' } ),
77
+ a_hash_including( { 'identifier' => 'sidonie-postgresql' } )
78
+ )
79
+ end
63
80
 
64
- res = client.acknowledge( :sidonie, "I'm on it.", "ged" )
65
- res = client.clear_acknowledgement( :sidonie )
66
81
 
67
- expect( manager.nodes['sidonie'] ).to_not be_acked
82
+ it "can fetch a subset of node dependency attributes" do
83
+ res = client.dependencies_of( 'sidonie', properties: %w[ description type ] )
84
+ expect( res ).to be_a( Hash ).and include(
85
+ 'sandbox01',
86
+ 'sandbox01-canary',
87
+ 'sidonie-couchpotato',
88
+ 'sidonie-demon-http',
89
+ 'sidonie-http',
90
+ 'sidonie-iscsi',
91
+ 'sidonie-pms',
92
+ 'sidonie-postgresql',
93
+ 'sidonie-sabnzbd',
94
+ 'sidonie-sickbeard',
95
+ 'sidonie-smtp',
96
+ 'sidonie-ssh',
97
+ 'vhost01',
98
+ 'yevaud-cozy_frontend'
99
+ )
100
+ expect( res.values ).to all( have_attributes(length: a_value_between(1, 2)) )
101
+ expect( res.values.map(&:keys) ).to all( contain_exactly 'type', 'description' )
68
102
  end
69
-
70
103
  end
71
104
 
72
105
 
@@ -78,52 +111,61 @@ describe Arborist::Client do
78
111
  end
79
112
 
80
113
 
81
- it "can list the nodes of the manager it's connected to" do
82
- res = client.list
114
+ it "can fetch the nodes of the manager it's connected to" do
115
+ res = client.fetch
83
116
  expect( res ).to be_an( Array )
84
117
  expect( res.length ).to eq( manager.nodes.length )
85
118
  end
86
119
 
87
120
 
88
- it "can list a subtree of the nodes of the manager it's connected to" do
89
- res = client.list( from: 'duir' )
121
+ it "can fetch a subtree of the nodes of the manager it's connected to" do
122
+ res = client.fetch( from: 'duir' )
90
123
  expect( res ).to be_an( Array )
91
124
  expect( res.length ).to be < manager.nodes.length
92
125
  end
93
126
 
94
127
 
95
- it "can list a depth-limited subtree of the node of the managed it's connected to" do
96
- res = client.list( depth: 2 )
128
+ it "can fetch a depth-limited subtree of the node of the managed it's connected to" do
129
+ res = client.fetch( depth: 2 )
97
130
  expect( res ).to be_an( Array )
98
- expect( res.length ).to eq( 8 )
131
+ expect( res.length ).to eq( 9 )
99
132
  end
100
133
 
101
134
 
102
- it "can list a depth-limited subtree of the nodes of the manager it's connected to" do
103
- res = client.list( from: 'duir', depth: 1 )
135
+ it "can fetch a depth-limited subtree of the nodes of the manager it's connected to" do
136
+ res = client.fetch( from: 'duir', depth: 1 )
104
137
  expect( res ).to be_an( Array )
105
- expect( res.length ).to eq( 5 )
138
+ expect( res.length ).to eq( 6 )
106
139
  end
107
140
 
108
141
 
109
- it "can fetch all node properties for all 'up' nodes" do
110
- res = client.fetch
142
+ it "can get a Hash of all nodes keyed by identifier" do
143
+ res = client.search
111
144
  expect( res ).to be_a( Hash )
112
145
  expect( res.length ).to be == manager.nodes.length
113
146
  expect( res.values ).to all( be_a(Hash) )
114
147
  end
115
148
 
116
149
 
117
- it "can fetch identifiers for all 'up' nodes" do
118
- res = client.fetch( {}, properties: nil )
150
+ it "includes downed nodes by default in the results of a search" do
151
+ manager.nodes['sidonie'].update( error: 'something happened' )
152
+ res = client.search
153
+ expect( res ).to be_a( Hash )
154
+ expect( res.length ).to be == manager.nodes.length
155
+ expect( res.values ).to all( be_a(Hash) )
156
+ end
157
+
158
+
159
+ it "can get a Hash of all nodes without user properties" do
160
+ res = client.search( options: { properties: nil } )
119
161
  expect( res ).to be_a( Hash )
120
162
  expect( res.length ).to be == manager.nodes.length
121
163
  expect( res.values ).to all( be_empty )
122
164
  end
123
165
 
124
166
 
125
- it "can fetch a subset of properties for all 'up' nodes" do
126
- res = client.fetch( {}, properties: [:addresses, :status] )
167
+ it "can get a Hash of all nodes with a subset of properties" do
168
+ res = client.search( options: { properties: [:addresses, :status] })
127
169
  expect( res ).to be_a( Hash )
128
170
  expect( res.length ).to be == manager.nodes.length
129
171
  expect( res.values ).to all( be_a(Hash) )
@@ -131,16 +173,21 @@ describe Arborist::Client do
131
173
  end
132
174
 
133
175
 
134
- it "can fetch a subset of properties for all 'up' nodes matching specified criteria" do
135
- res = client.fetch( {type: 'host'}, properties: [:addresses, :status] )
176
+ it "can get a Hash of all nodes with a subset of properties that match specified criteria" do
177
+ res = client.search( criteria: {type: 'host'}, options: {properties: [:addresses, :status]} )
136
178
  expect( res ).to be_a( Hash )
137
179
  expect( res.length ).to be == manager.nodes.values.count {|n| n.type == 'host' }
138
180
  expect( res.values ).to all( include('addresses', 'status') )
139
181
  end
140
182
 
141
183
 
142
- it "can fetch all node properties for 'up' nodes that don't match specified criteria" do
143
- res = client.fetch( {}, properties: [:addresses, :status], exclude: {tag: 'testing'} )
184
+ it "can get a Hash of all nodes with a subset of properties that don't match specified criteria" do
185
+ res = client.search(
186
+ options: {
187
+ properties: [:addresses, :status],
188
+ exclude: {tag: 'testing'}
189
+ }
190
+ )
144
191
 
145
192
  testing_nodes = manager.nodes.values.select {|n| n.tags.include?('testing') }
146
193
 
@@ -151,15 +198,14 @@ describe Arborist::Client do
151
198
  end
152
199
 
153
200
 
154
- it "can fetch all properties for all nodes regardless of their status" do
201
+ it "can get a Hash of nodes that exclude nodes that are down" do
155
202
  # Down a node
156
203
  manager.nodes['duir'].update( error: 'something happened' )
157
204
 
158
- res = client.fetch( {type: 'host'}, include_down: true )
205
+ res = client.search( criteria: {type: 'host'}, options: {exclude_down: true} )
159
206
 
160
207
  expect( res ).to be_a( Hash )
161
- expect( res ).to include( 'duir' )
162
- expect( res['duir']['status'] ).to eq( 'down' )
208
+ expect( res ).to_not include( 'duir' )
163
209
  end
164
210
 
165
211
 
@@ -173,6 +219,17 @@ describe Arborist::Client do
173
219
  end
174
220
 
175
221
 
222
+ it "can fetch a list of all nodes which have a dependency on a target node" do
223
+ res = client.deps( identifier: 'sidonie-postgresql' )
224
+
225
+ expected_ids = manager.nodes['sidonie-postgresql'].node_subscribers.to_a
226
+
227
+ expect( res ).to be_a( Hash ).and( include('deps') )
228
+ expect( res['deps'] ).to be_an( Array )
229
+ expect( res['deps'] ).to contain_exactly( *expected_ids )
230
+ end
231
+
232
+
176
233
  it "can subscribe to all events" do
177
234
  sub_id = client.subscribe
178
235
  expect( sub_id ).to be_a( String )
@@ -261,7 +318,7 @@ describe Arborist::Client do
261
318
 
262
319
 
263
320
  it "can prune nodes from the tree" do
264
- res = client.prune( 'sidonie-ssh' )
321
+ res = client.prune( identifier: 'sidonie-ssh' )
265
322
 
266
323
  expect( res ).to be_a( Hash )
267
324
  expect( res ).to include( 'identifier' => 'sidonie-ssh' )
@@ -270,13 +327,13 @@ describe Arborist::Client do
270
327
 
271
328
 
272
329
  it "returns nil without error when pruning a node that doesn't exist" do
273
- res = client.prune( 'carrigor' )
330
+ res = client.prune( identifier: 'carrigor' )
274
331
  expect( res ).to be_nil
275
332
  end
276
333
 
277
334
 
278
335
  it "can graft new nodes onto the tree" do
279
- res = client.graft( 'breakfast-burrito', type: 'host' )
336
+ res = client.graft( identifier: 'breakfast-burrito', type: 'host' )
280
337
  expect( res ).to eq({ 'identifier' => 'breakfast-burrito' })
281
338
  expect( manager.nodes ).to include( 'breakfast-burrito' )
282
339
  expect( manager.nodes['breakfast-burrito'] ).to be_a( Arborist::Node::Host )
@@ -285,11 +342,14 @@ describe Arborist::Client do
285
342
 
286
343
 
287
344
  it "can graft nodes with attributes onto the tree" do
288
- res = client.graft( 'breakfast-burrito',
345
+ res = client.graft(
346
+ identifier: 'breakfast-burrito',
289
347
  type: 'service',
290
348
  parent: 'duir',
291
- port: 9999,
292
- tags: ['yusss']
349
+ attributes: {
350
+ port: 9999,
351
+ tags: ['yusss']
352
+ }
293
353
  )
294
354
  expect( res ).to eq({ 'identifier' => 'duir-breakfast-burrito' })
295
355
  expect( manager.nodes ).to include( 'duir-breakfast-burrito' )
@@ -301,11 +361,36 @@ describe Arborist::Client do
301
361
 
302
362
 
303
363
  it "can modify operational attributes of a node" do
304
- res = client.modify( "duir", tags: 'girlrobot' )
364
+ res = client.modify( identifier: "duir", attributes: { tags: 'girlrobot' })
305
365
  expect( res ).to be_truthy
306
366
  expect( manager.nodes['duir'].tags ).to eq( ['girlrobot'] )
307
367
  end
308
368
 
369
+
370
+ it "can acknowledge a node" do
371
+ manager.nodes['sidonie'].update( error: "Clown apocalypse" )
372
+
373
+ res = client.acknowledge( identifier: 'sidonie', message: "I'm on it.", sender: "ged" )
374
+
375
+ expect( manager.nodes['sidonie'] ).to be_acked
376
+ end
377
+
378
+
379
+ it "can clear a node's acknowledgment" do
380
+ manager.nodes['sidonie'].update( error: "Clown apocalypse" )
381
+
382
+ res = client.acknowledge( identifier: 'sidonie', message: "I'm on it.", sender: "ged" )
383
+ res = client.clear_acknowledgement( identifier: 'sidonie' )
384
+
385
+ expect( manager.nodes['sidonie'] ).to_not be_acked
386
+ end
387
+
388
+
389
+ it "acking raises an appropriate error when it's missing arguments" do
390
+ expect {
391
+ client.acknowledge( identifier: 'sidonie', message: "I'm on it." )
392
+ }.to raise_error( ArgumentError, /missing keyword: sender/ )
393
+ end
309
394
  end
310
395
 
311
396
  end
@@ -326,8 +411,8 @@ describe Arborist::Client do
326
411
  end
327
412
 
328
413
 
329
- it "can make a raw list request" do
330
- req = client.make_list_request
414
+ it "can make a raw fetch request" do
415
+ req = client.make_fetch_request
331
416
  expect( req ).to be_a( CZTop::Message )
332
417
 
333
418
  header, body = Arborist::TreeAPI.decode( req )
@@ -336,12 +421,12 @@ describe Arborist::Client do
336
421
  expect( header ).to include( 'version', 'action' )
337
422
  expect( header ).to_not include( 'from' )
338
423
  expect( header['version'] ).to eq( Arborist::Client::API_VERSION )
339
- expect( header['action'] ).to eq( 'list' )
424
+ expect( header['action'] ).to eq( 'fetch' )
340
425
  end
341
426
 
342
427
 
343
- it "can make a raw fetch request" do
344
- req = client.make_fetch_request( {} )
428
+ it "can make a raw search request" do
429
+ req = client.make_search_request( {} )
345
430
  expect( req ).to be_a( CZTop::Message )
346
431
 
347
432
  header, body = Arborist::TreeAPI.decode( req )
@@ -349,14 +434,14 @@ describe Arborist::Client do
349
434
  expect( header ).to be_a( Hash )
350
435
  expect( header ).to include( 'version', 'action' )
351
436
  expect( header['version'] ).to eq( Arborist::Client::API_VERSION )
352
- expect( header['action'] ).to eq( 'fetch' )
437
+ expect( header['action'] ).to eq( 'search' )
353
438
 
354
439
  expect( body ).to eq([ {}, {} ])
355
440
  end
356
441
 
357
442
 
358
- it "can make a raw fetch request with criteria" do
359
- req = client.make_fetch_request( {type: 'host'} )
443
+ it "can make a raw search request with criteria" do
444
+ req = client.make_search_request( {type: 'host'} )
360
445
  expect( req ).to be_a( CZTop::Message )
361
446
 
362
447
  header, body = Arborist::TreeAPI.decode( req )
@@ -364,7 +449,7 @@ describe Arborist::Client do
364
449
  expect( header ).to be_a( Hash )
365
450
  expect( header ).to include( 'version', 'action' )
366
451
  expect( header['version'] ).to eq( Arborist::Client::API_VERSION )
367
- expect( header['action'] ).to eq( 'fetch' )
452
+ expect( header['action'] ).to eq( 'search' )
368
453
 
369
454
  body = body
370
455
  expect( body.first ).to be_a( Hash )
@@ -389,6 +474,27 @@ describe Arborist::Client do
389
474
  expect( body['duir'] ).to eq( 'error' => 'Something happened.' )
390
475
  end
391
476
 
477
+
478
+ it "can make a raw update request with headers" do
479
+ req = client.make_update_request(
480
+ {duir: {error: "Something happened."}},
481
+ {monitor_key: 'foom'}
482
+ )
483
+ expect( req ).to be_a( CZTop::Message )
484
+
485
+ header, body = Arborist::TreeAPI.decode( req )
486
+
487
+ expect( header ).to be_a( Hash )
488
+ expect( header ).to include( 'version', 'action', 'monitor_key' )
489
+ expect( header['version'] ).to eq( Arborist::Client::API_VERSION )
490
+ expect( header['action'] ).to eq( 'update' )
491
+ expect( header['monitor_key'] ).to eq( 'foom' )
492
+
493
+ expect( body ).to be_a( Hash )
494
+ expect( body ).to include( 'duir' )
495
+ expect( body['duir'] ).to eq( 'error' => 'Something happened.' )
496
+ end
497
+
392
498
  end
393
499
 
394
500
 
@@ -332,6 +332,27 @@ describe Arborist::Dependency do
332
332
  expect( dep.down_reason ).to match( /node(1|2) \(and 1 other\) are unavailable as of/i )
333
333
  end
334
334
 
335
+
336
+ it "can describe the reason if nodes in subdepedencies are down" do
337
+ dep.subdeps << described_class.on( :any, 'node4', 'node5' )
338
+
339
+ dep.mark_down( 'node1' )
340
+ dep.mark_down( 'node4' )
341
+ dep.mark_down( 'node5' )
342
+
343
+ expect( dep.down_reason ).to match( /node1.*node4.*node5/i )
344
+ end
345
+
346
+
347
+ it "can describe the reason if only nodes in subdepedencies are down" do
348
+ dep.subdeps << described_class.on( :any, 'node4', 'node5' )
349
+
350
+ dep.mark_down( 'node4' )
351
+ dep.mark_down( 'node5' )
352
+
353
+ expect( dep.down_reason ).to match( /node4.*node5/i )
354
+ end
355
+
335
356
  end
336
357
 
337
358
 
@@ -26,6 +26,11 @@ describe Arborist::Event::Node do
26
26
  let( :event ) { described_class.new(node) }
27
27
 
28
28
 
29
+ it "serializes with useful metadata attached" do
30
+ expect( event.to_h ).to include( :identifier, :parent, :nodetype )
31
+ expect( event.to_h[:nodetype] ).to eq( 'testnode' )
32
+ end
33
+
29
34
 
30
35
  it "matches match-anything subscriptions" do
31
36
  sub = Arborist::Subscription.new {}
@@ -51,10 +51,10 @@ describe Arborist::Event do
51
51
 
52
52
  result = ev.to_h
53
53
 
54
- expect( result ).to include( 'type', 'data' )
54
+ expect( result ).to include( :type, :data )
55
55
 
56
- expect( result['type'] ).to eq( 'test.event' )
57
- expect( result['data'] ).to eq( payload )
56
+ expect( result[:type] ).to eq( 'test.event' )
57
+ expect( result[:data] ).to eq( payload )
58
58
  end
59
59
 
60
60
 
@@ -5,7 +5,12 @@ require_relative '../spec_helper'
5
5
  require 'tmpdir'
6
6
  require 'timecop'
7
7
  require 'arborist/manager'
8
+ require 'arborist/mixins'
8
9
  require 'arborist/node/host'
10
+ require 'arborist/event/node_update'
11
+
12
+ using Arborist::TimeRefinements
13
+
9
14
 
10
15
  describe Arborist::Manager do
11
16
 
@@ -21,7 +26,7 @@ describe Arborist::Manager do
21
26
 
22
27
 
23
28
  let( :manager ) { described_class.new }
24
-
29
+ let( :tmpfile_pathname ) { Pathname(Dir::Tmpname.create(['arb', 'tree']) {}) }
25
30
 
26
31
 
27
32
  #
@@ -164,7 +169,7 @@ describe Arborist::Manager do
164
169
 
165
170
 
166
171
  it "checkpoints the state file periodically if an interval is configured" do
167
- statefile = Pathname( Dir.tmpdir ) + Dir::Tmpname.make_tmpname( 'arb', 'tree' )
172
+ statefile = tmpfile_pathname()
168
173
  described_class.configure( checkpoint_frequency: 20_000, state_file: statefile )
169
174
 
170
175
  manager = described_class.new
@@ -212,7 +217,38 @@ describe Arborist::Manager do
212
217
  end
213
218
 
214
219
 
215
- it "is sent at the configured interval"
220
+ it "is sent at the configured interval" do
221
+ described_class.configure( heartbeat_frequency: 11 )
222
+ expect( manager.reactor ).to receive( :add_periodic_timer ).with( 11 )
223
+
224
+ manager.register_heartbeat_timer
225
+ end
226
+
227
+
228
+ it "doesn't try to publish the heartbeat if it's not been started" do
229
+ manager.start_time = nil
230
+
231
+ manager.publish_heartbeat_event
232
+ expect( manager.event_queue ).to be_empty
233
+ end
234
+
235
+
236
+ it "contains runtime information about the manager" do
237
+ time = Time.now
238
+ manager.start_time = time
239
+
240
+ manager.publish_heartbeat_event
241
+ event = manager.event_queue.shift
242
+
243
+ expect( event ).to be_a( CZTop::Message )
244
+ decoded = Arborist::EventAPI.decode( event )
245
+
246
+ expect( decoded ).to include(
247
+ 'run_id' => manager.run_id,
248
+ 'start_time' => time.iso8601,
249
+ 'version' => Arborist::VERSION
250
+ )
251
+ end
216
252
 
217
253
  end
218
254
 
@@ -257,17 +293,13 @@ describe Arborist::Manager do
257
293
  end
258
294
 
259
295
 
260
- it "can replace an existing node" do
296
+ it "complains if adding a node that already exists" do
261
297
  manager.add_node( node )
262
298
  another_node = testing_node( 'italian_lessons' )
263
- manager.add_node( another_node )
264
-
265
- expect( manager.nodes ).to include( 'italian_lessons' )
266
- expect( manager.nodes['italian_lessons'] ).to_not be( node )
267
- expect( manager.nodes['italian_lessons'] ).to be( another_node )
268
299
 
269
- expect( manager.nodecount ).to eq( 2 )
270
- expect( manager.nodelist ).to include( '_', 'italian_lessons' )
300
+ expect {
301
+ manager.add_node( another_node )
302
+ }.to raise_error( Arborist::NodeError, /already present/ )
271
303
  end
272
304
 
273
305
 
@@ -343,17 +375,6 @@ describe Arborist::Manager do
343
375
  end
344
376
 
345
377
 
346
- it "replaces a node in the tree when a node with an existing identifier is added" do
347
- updated_node = testing_node( 'leaf' ) do
348
- parent 'trunk'
349
- end
350
-
351
- manager.add_node( updated_node )
352
- expect( manager.nodes['branch'].children ).to_not include( 'leaf' => leaf_node )
353
- expect( manager.nodes['trunk'].children ).to include( 'leaf' => updated_node )
354
- end
355
-
356
-
357
378
  it "rebuilds the tree when a node is removed from it" do
358
379
  manager.remove_node( 'branch' )
359
380
 
@@ -365,7 +386,7 @@ describe Arborist::Manager do
365
386
  end
366
387
 
367
388
 
368
- xdescribe "tree API", :testing_manager do
389
+ describe "tree API", :testing_manager do
369
390
 
370
391
  before( :each ) do
371
392
  @manager = nil
@@ -401,14 +422,23 @@ describe Arborist::Manager do
401
422
  end
402
423
 
403
424
 
404
- describe "status" do
425
+ let( :manager ) { @manager }
426
+
427
+ let( :sock ) do
428
+ sock = CZTop::Socket::REQ.new
429
+ sock.options.linger = 0
430
+ sock.connect( TESTING_API_SOCK )
431
+ sock
432
+ end
405
433
 
406
434
 
435
+ describe "status" do
436
+
407
437
  it "returns a Map describing the manager and its state" do
408
- msg = Arborist::TreeAPI.encode( :status )
438
+ msg = Arborist::TreeAPI.request( :status )
409
439
 
410
- sock.send( msg )
411
- resmsg = sock.recv
440
+ msg.send_to( sock )
441
+ resmsg = sock.receive
412
442
 
413
443
  hdr, body = Arborist::TreeAPI.decode( resmsg )
414
444
  expect( hdr ).to include( 'success' => true )
@@ -419,13 +449,13 @@ describe Arborist::Manager do
419
449
  end
420
450
 
421
451
 
422
- describe "fetch" do
452
+ describe "search" do
423
453
 
424
454
  it "returns an array of full state maps for nodes matching specified criteria" do
425
- msg = Arborist::TreeAPI.encode( :fetch, type: 'service', port: 22 )
455
+ msg = Arborist::TreeAPI.request( :search, type: 'service', port: 22 )
426
456
 
427
- sock.send( msg )
428
- resmsg = sock.recv
457
+ msg.send_to( sock )
458
+ resmsg = sock.receive
429
459
 
430
460
  hdr, body = Arborist::TreeAPI.decode( resmsg )
431
461
  expect( hdr ).to include( 'success' => true )
@@ -439,10 +469,10 @@ describe Arborist::Manager do
439
469
 
440
470
 
441
471
  it "returns an array of full state maps for nodes not matching specified negative criteria" do
442
- msg = Arborist::TreeAPI.encode( :fetch, [ {}, {type: 'service', port: 22} ] )
472
+ msg = Arborist::TreeAPI.request( :search, [ {}, {type: 'service', port: 22} ] )
443
473
 
444
- sock.send( msg )
445
- resmsg = sock.recv
474
+ msg.send_to( sock )
475
+ resmsg = sock.receive
446
476
 
447
477
  hdr, body = Arborist::TreeAPI.decode( resmsg )
448
478
  expect( hdr ).to include( 'success' => true )
@@ -456,28 +486,28 @@ describe Arborist::Manager do
456
486
 
457
487
 
458
488
  it "returns an array of full state maps for nodes combining positive and negative criteria" do
459
- msg = Arborist::TreeAPI.encode( :fetch, [ {type: 'service'}, {port: 22} ] )
489
+ msg = Arborist::TreeAPI.request( :search, [ {type: 'service'}, {port: 22} ] )
460
490
 
461
- sock.send( msg )
462
- resmsg = sock.recv
491
+ msg.send_to( sock )
492
+ resmsg = sock.receive
463
493
 
464
494
  hdr, body = Arborist::TreeAPI.decode( resmsg )
465
495
  expect( hdr ).to include( 'success' => true )
466
496
 
467
497
  expect( body ).to be_a( Hash )
468
- expect( body.length ).to eq( 16 )
498
+ expect( body.length ).to eq( 18 )
469
499
 
470
500
  expect( body.values ).to all( be_a(Hash) )
471
501
  expect( body.values ).to all( include('status', 'type') )
472
502
  end
473
503
 
474
504
 
475
- it "doesn't return nodes beneath downed nodes by default" do
505
+ it "omits nodes beneath downed nodes if asked to" do
476
506
  manager.nodes['sidonie'].update( error: 'sunspots' )
477
- msg = Arborist::TreeAPI.encode( :fetch, type: 'service', port: 22 )
507
+ msg = Arborist::TreeAPI.request( :search, {exclude_down: true}, type: 'service', port: 22 )
478
508
 
479
- sock.send( msg )
480
- resmsg = sock.recv
509
+ msg.send_to( sock )
510
+ resmsg = sock.receive
481
511
 
482
512
  hdr, body = Arborist::TreeAPI.decode( resmsg )
483
513
  expect( hdr ).to include( 'success' => true )
@@ -487,12 +517,12 @@ describe Arborist::Manager do
487
517
  end
488
518
 
489
519
 
490
- it "does return nodes beneath downed nodes if asked to" do
520
+ it "include nodes beneath downed nodes by default" do
491
521
  manager.nodes['sidonie'].update( error: 'plague of locusts' )
492
- msg = Arborist::TreeAPI.encode( :fetch, {include_down: true}, type: 'service', port: 22 )
522
+ msg = Arborist::TreeAPI.request( :search, type: 'service', port: 22 )
493
523
 
494
- sock.send( msg )
495
- resmsg = sock.recv
524
+ msg.send_to( sock )
525
+ resmsg = sock.receive
496
526
 
497
527
  hdr, body = Arborist::TreeAPI.decode( resmsg )
498
528
  expect( hdr ).to include( 'success' => true )
@@ -503,10 +533,10 @@ describe Arborist::Manager do
503
533
 
504
534
 
505
535
  it "returns only identifiers if the `return` header is set to `nil`" do
506
- msg = Arborist::TreeAPI.encode( :fetch, {return: nil}, type: 'service', port: 22 )
536
+ msg = Arborist::TreeAPI.request( :search, {return: nil}, type: 'service', port: 22 )
507
537
 
508
- sock.send( msg )
509
- resmsg = sock.recv
538
+ msg.send_to( sock )
539
+ resmsg = sock.receive
510
540
 
511
541
  hdr, body = Arborist::TreeAPI.decode( resmsg )
512
542
  expect( hdr ).to include( 'success' => true )
@@ -518,11 +548,11 @@ describe Arborist::Manager do
518
548
 
519
549
 
520
550
  it "returns only specified state if the `return` header is set to an Array of keys" do
521
- msg = Arborist::TreeAPI.encode( :fetch, {return: %w[status tags addresses]},
551
+ msg = Arborist::TreeAPI.request( :search, {return: %w[status tags addresses]},
522
552
  type: 'service', port: 22 )
523
553
 
524
- sock.send( msg )
525
- resmsg = sock.recv
554
+ msg.send_to( sock )
555
+ resmsg = sock.receive
526
556
 
527
557
  hdr, body = Arborist::TreeAPI.decode( resmsg )
528
558
  expect( hdr ).to include( 'success' => true )
@@ -535,12 +565,12 @@ describe Arborist::Manager do
535
565
  end
536
566
 
537
567
 
538
- describe "list" do
568
+ describe "fetch" do
539
569
 
540
570
  it "returns an array of node state" do
541
- msg = Arborist::TreeAPI.encode( :list )
542
- sock.send( msg )
543
- resmsg = sock.recv
571
+ msg = Arborist::TreeAPI.request( :fetch )
572
+ msg.send_to( sock )
573
+ resmsg = sock.receive
544
574
 
545
575
  hdr, body = Arborist::TreeAPI.decode( resmsg )
546
576
  expect( hdr ).to include( 'success' => true )
@@ -554,10 +584,44 @@ describe Arborist::Manager do
554
584
  expect( body ).to include( hash_including('identifier' => 'yevaud') )
555
585
  end
556
586
 
587
+
588
+ it "can start at a node other than the root" do
589
+ msg = Arborist::TreeAPI.request( :fetch, {from: 'sidonie'}, nil )
590
+ msg.send_to( sock )
591
+ resmsg = sock.receive
592
+
593
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
594
+ expect( hdr ).to include( 'success' => true )
595
+ expect( body.length ).to eq( manager.nodes.keys.count {|id| id.include?('sidonie')} )
596
+ expect( body ).to all( be_a(Hash) )
597
+ expect( body ).to_not include( hash_including('identifier' => '_') )
598
+ expect( body ).to_not include( hash_including('identifier' => 'duir') )
599
+ expect( body ).to include( hash_including('identifier' => 'sidonie') )
600
+ expect( body ).to include( hash_including('identifier' => 'sidonie-ssh') )
601
+ expect( body ).to include( hash_including('identifier' => 'sidonie-demon-http') )
602
+ expect( body ).to_not include( hash_including('identifier' => 'yevaud') )
603
+ end
604
+
605
+
606
+ it "can be fetched as a tree" do
607
+ msg = Arborist::TreeAPI.request( :fetch, {tree: true}, nil )
608
+ msg.send_to( sock )
609
+ resmsg = sock.receive
610
+
611
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
612
+ expect( hdr ).to include( 'success' => true )
613
+ expect( body.length ).to eq( 1 )
614
+ expect( body.first ).to be_a( Hash )
615
+ expect( body.first ).to include( 'children' )
616
+ expect( body.first['identifier'] ).to eq( '_' )
617
+ expect( body.first['children'].keys ).to include( 'duir', 'localhost' )
618
+ end
619
+
620
+
557
621
  it "can be limited by depth" do
558
- msg = Arborist::TreeAPI.encode( :list, {depth: 1}, nil )
559
- sock.send( msg )
560
- resmsg = sock.recv
622
+ msg = Arborist::TreeAPI.request( :fetch, {depth: 1}, nil )
623
+ msg.send_to( sock )
624
+ resmsg = sock.receive
561
625
 
562
626
  hdr, body = Arborist::TreeAPI.decode( resmsg )
563
627
  expect( hdr ).to include( 'success' => true )
@@ -567,6 +631,18 @@ describe Arborist::Manager do
567
631
  expect( body ).to include( hash_including('identifier' => 'duir') )
568
632
  expect( body ).to_not include( hash_including('identifier' => 'duir-ssh') )
569
633
  end
634
+
635
+
636
+ it "errors when fetching from a nonexistent node" do
637
+ msg = Arborist::TreeAPI.request( :fetch, {from: "nope-nope-nope"}, nil )
638
+ msg.send_to( sock )
639
+ resmsg = sock.receive
640
+
641
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
642
+ expect( hdr ).to include( 'success' => false )
643
+ expect( hdr ).to include( "reason" => "No such node nope-nope-nope." )
644
+ expect( body ).to be_nil
645
+ end
570
646
  end
571
647
 
572
648
 
@@ -590,9 +666,9 @@ describe Arborist::Manager do
590
666
  }
591
667
  }
592
668
  }
593
- msg = Arborist::TreeAPI.encode( :update, update_data )
594
- sock.send( msg )
595
- resmsg = sock.recv
669
+ msg = Arborist::TreeAPI.request( :update, update_data )
670
+ msg.send_to( sock )
671
+ resmsg = sock.receive
596
672
 
597
673
  hdr, body = Arborist::TreeAPI.decode( resmsg )
598
674
  expect( hdr ).to include( 'success' => true )
@@ -605,18 +681,18 @@ describe Arborist::Manager do
605
681
 
606
682
 
607
683
  it "ignores unknown identifiers" do
608
- msg = Arborist::TreeAPI.encode( :update, charlie_humperton: {ping: { rtt: 8 }} )
609
- sock.send( msg )
610
- resmsg = sock.recv
684
+ msg = Arborist::TreeAPI.request( :update, charlie_humperton: {ping: { rtt: 8 }} )
685
+ msg.send_to( sock )
686
+ resmsg = sock.receive
611
687
 
612
688
  hdr, body = Arborist::TreeAPI.decode( resmsg )
613
689
  expect( hdr ).to include( 'success' => true )
614
690
  end
615
691
 
616
692
  it "fails with a client error if the body is invalid" do
617
- msg = Arborist::TreeAPI.encode( :update, nil )
618
- sock.send( msg )
619
- resmsg = sock.recv
693
+ msg = Arborist::TreeAPI.request( :update, nil )
694
+ msg.send_to( sock )
695
+ resmsg = sock.receive
620
696
 
621
697
  hdr, body = Arborist::TreeAPI.decode( resmsg )
622
698
  expect( hdr ).to include( 'success' => false )
@@ -628,12 +704,12 @@ describe Arborist::Manager do
628
704
  describe "subscribe" do
629
705
 
630
706
  it "adds a subscription for all event types to the root node by default" do
631
- msg = Arborist::TreeAPI.encode( :subscribe, [{}, {}] )
707
+ msg = Arborist::TreeAPI.request( :subscribe, [{}, {}] )
632
708
 
633
709
  resmsg = nil
634
710
  expect {
635
- sock.send( msg )
636
- resmsg = sock.recv
711
+ msg.send_to( sock )
712
+ resmsg = sock.receive
637
713
  }.to change { manager.subscriptions.length }.by( 1 ).and(
638
714
  change { manager.root.subscriptions.length }.by( 1 )
639
715
  )
@@ -642,42 +718,44 @@ describe Arborist::Manager do
642
718
  sub_id = manager.subscriptions.keys.first
643
719
 
644
720
  expect( hdr ).to include( 'success' => true )
645
- expect( body ).to eq([ sub_id ])
721
+ expect( body ).to be_a( Hash )
722
+ expect( body ).to include( 'id' )
723
+ expect( manager.subscriptions.keys ).to include( body['id'] )
646
724
  end
647
725
 
648
726
 
649
727
  it "adds a subscription to the specified node if an identifier is specified" do
650
- msg = Arborist::TreeAPI.encode( :subscribe, {identifier: 'sidonie'}, [{}, {}] )
728
+ msg = Arborist::TreeAPI.request( :subscribe, {identifier: 'sidonie'}, [{}, {}] )
651
729
 
652
730
  resmsg = nil
653
731
  expect {
654
- sock.send( msg )
655
- resmsg = sock.recv
732
+ msg.send_to( sock )
733
+ resmsg = sock.receive
656
734
  }.to change { manager.subscriptions.length }.by( 1 ).and(
657
735
  change { manager.nodes['sidonie'].subscriptions.length }.by( 1 )
658
736
  )
659
737
  hdr, body = Arborist::TreeAPI.decode( resmsg )
660
738
 
661
- sub_id = manager.subscriptions.keys.first
662
-
663
739
  expect( hdr ).to include( 'success' => true )
664
- expect( body ).to eq([ sub_id ])
740
+ expect( body ).to be_a( Hash )
741
+ expect( body ).to include( 'id' )
742
+ expect( manager.subscriptions.keys ).to include( body['id'] )
665
743
  end
666
744
 
667
745
 
668
746
  it "adds a subscription for particular event types if one is specified" do
669
- msg = Arborist::TreeAPI.encode( :subscribe, {event_type: 'node.acked'}, [{}, {}] )
747
+ msg = Arborist::TreeAPI.request( :subscribe, {event_type: 'node.acked'}, [{}, {}] )
670
748
 
671
749
  resmsg = nil
672
750
  expect {
673
- sock.send( msg )
674
- resmsg = sock.recv
751
+ msg.send_to( sock )
752
+ resmsg = sock.receive
675
753
  }.to change { manager.subscriptions.length }.by( 1 ).and(
676
754
  change { manager.root.subscriptions.length }.by( 1 )
677
755
  )
678
756
  hdr, body = Arborist::TreeAPI.decode( resmsg )
679
- node = manager.subscriptions[ body.first ]
680
- sub = node.subscriptions[ body.first ]
757
+ node = manager.subscriptions[ body['id'] ]
758
+ sub = node.subscriptions[ body['id'] ]
681
759
 
682
760
  expect( sub.event_type ).to eq( 'node.acked' )
683
761
  end
@@ -686,18 +764,19 @@ describe Arborist::Manager do
686
764
  it "adds a subscription for events which match a pattern if one is specified" do
687
765
  criteria = { type: 'host' }
688
766
 
689
- msg = Arborist::TreeAPI.encode( :subscribe, [criteria, {}] )
767
+ msg = Arborist::TreeAPI.request( :subscribe, [criteria, {}] )
690
768
 
691
769
  resmsg = nil
692
770
  expect {
693
- sock.send( msg )
694
- resmsg = sock.recv
771
+ msg.send_to( sock )
772
+ resmsg = sock.receive
695
773
  }.to change { manager.subscriptions.length }.by( 1 ).and(
696
774
  change { manager.root.subscriptions.length }.by( 1 )
697
775
  )
698
776
  hdr, body = Arborist::TreeAPI.decode( resmsg )
699
- node = manager.subscriptions[ body.first ]
700
- sub = node.subscriptions[ body.first ]
777
+ expect( body ).to be_a( Hash ).and( include('id') )
778
+ node = manager.subscriptions[ body['id'] ]
779
+ sub = node.subscriptions[ body['id'] ]
701
780
 
702
781
  expect( sub.event_type ).to be_nil
703
782
  expect( sub.criteria ).to eq({ 'type' => 'host' })
@@ -707,18 +786,19 @@ describe Arborist::Manager do
707
786
  it "adds a subscription for events which don't match a pattern if an exclusion pattern is given" do
708
787
  criteria = { type: 'host' }
709
788
 
710
- msg = Arborist::TreeAPI.encode( :subscribe, [{}, criteria] )
789
+ msg = Arborist::TreeAPI.request( :subscribe, [{}, criteria] )
711
790
 
712
791
  resmsg = nil
713
792
  expect {
714
- sock.send( msg )
715
- resmsg = sock.recv
793
+ msg.send_to( sock )
794
+ resmsg = sock.receive
716
795
  }.to change { manager.subscriptions.length }.by( 1 ).and(
717
796
  change { manager.root.subscriptions.length }.by( 1 )
718
797
  )
719
798
  hdr, body = Arborist::TreeAPI.decode( resmsg )
720
- node = manager.subscriptions[ body.first ]
721
- sub = node.subscriptions[ body.first ]
799
+ expect( body ).to be_a( Hash ).and( include('id') )
800
+ node = manager.subscriptions[ body['id'] ]
801
+ sub = node.subscriptions[ body['id'] ]
722
802
 
723
803
  expect( sub.event_type ).to be_nil
724
804
  expect( sub.negative_criteria ).to eq({ 'type' => 'host' })
@@ -735,12 +815,12 @@ describe Arborist::Manager do
735
815
 
736
816
 
737
817
  it "removes the subscription with the specified ID" do
738
- msg = Arborist::TreeAPI.encode( :unsubscribe, {subscription_id: subscription.id}, nil )
818
+ msg = Arborist::TreeAPI.request( :unsubscribe, {subscription_id: subscription.id}, nil )
739
819
 
740
820
  resmsg = nil
741
821
  expect {
742
- sock.send( msg )
743
- resmsg = sock.recv
822
+ msg.send_to( sock )
823
+ resmsg = sock.receive
744
824
  }.to change { manager.subscriptions.length }.by( -1 ).and(
745
825
  change { manager.root.subscriptions.length }.by( -1 )
746
826
  )
@@ -751,12 +831,12 @@ describe Arborist::Manager do
751
831
 
752
832
 
753
833
  it "ignores unsubscription of a non-existant ID" do
754
- msg = Arborist::TreeAPI.encode( :unsubscribe, {subscription_id: 'the bears!'}, nil )
834
+ msg = Arborist::TreeAPI.request( :unsubscribe, {subscription_id: 'the bears!'}, nil )
755
835
 
756
836
  resmsg = nil
757
837
  expect {
758
- sock.send( msg )
759
- resmsg = sock.recv
838
+ msg.send_to( sock )
839
+ resmsg = sock.receive
760
840
  }.to_not change { manager.subscriptions.length }
761
841
  hdr, body = Arborist::TreeAPI.decode( resmsg )
762
842
 
@@ -769,21 +849,22 @@ describe Arborist::Manager do
769
849
  describe "prune" do
770
850
 
771
851
  it "removes a single node" do
772
- msg = Arborist::TreeAPI.encode( :prune, {identifier: 'duir-ssh'}, nil )
773
- sock.send( msg )
774
- resmsg = sock.recv
852
+ msg = Arborist::TreeAPI.request( :prune, {identifier: 'duir-ssh'}, nil )
853
+ msg.send_to( sock )
854
+ resmsg = sock.receive
775
855
 
776
856
  hdr, body = Arborist::TreeAPI.decode( resmsg )
777
857
  expect( hdr ).to include( 'success' => true )
778
- expect( body ).to eq( true )
858
+ expect( body ).to be_a( Hash )
859
+ expect( body ).to include( 'identifier' => 'duir-ssh' )
779
860
  expect( manager.nodes ).to_not include( 'duir-ssh' )
780
861
  end
781
862
 
782
863
 
783
864
  it "returns Nil without error if the node to prune didn't exist" do
784
- msg = Arborist::TreeAPI.encode( :prune, {identifier: 'shemp-ssh'}, nil )
785
- sock.send( msg )
786
- resmsg = sock.recv
865
+ msg = Arborist::TreeAPI.request( :prune, {identifier: 'shemp-ssh'}, nil )
866
+ msg.send_to( sock )
867
+ resmsg = sock.receive
787
868
 
788
869
  hdr, body = Arborist::TreeAPI.decode( resmsg )
789
870
  expect( hdr ).to include( 'success' => true )
@@ -792,22 +873,23 @@ describe Arborist::Manager do
792
873
 
793
874
 
794
875
  it "removes children nodes along with the parent" do
795
- msg = Arborist::TreeAPI.encode( :prune, {identifier: 'duir'}, nil )
796
- sock.send( msg )
797
- resmsg = sock.recv
876
+ msg = Arborist::TreeAPI.request( :prune, {identifier: 'duir'}, nil )
877
+ msg.send_to( sock )
878
+ resmsg = sock.receive
798
879
 
799
880
  hdr, body = Arborist::TreeAPI.decode( resmsg )
800
881
  expect( hdr ).to include( 'success' => true )
801
- expect( body ).to eq( true )
882
+ expect( body ).to be_a( Hash )
883
+ expect( body ).to include( 'identifier' => 'duir' )
802
884
  expect( manager.nodes ).to_not include( 'duir' )
803
885
  expect( manager.nodes ).to_not include( 'duir-ssh' )
804
886
  end
805
887
 
806
888
 
807
889
  it "returns an error to the client when missing required attributes" do
808
- msg = Arborist::TreeAPI.encode( :prune )
809
- sock.send( msg )
810
- resmsg = sock.recv
890
+ msg = Arborist::TreeAPI.request( :prune )
891
+ msg.send_to( sock )
892
+ resmsg = sock.receive
811
893
 
812
894
  hdr, body = Arborist::TreeAPI.decode( resmsg )
813
895
  expect( hdr ).to include( 'success' => false )
@@ -828,14 +910,14 @@ describe Arborist::Manager do
828
910
  addresses: ['10.2.66.8'],
829
911
  tags: ['internal', 'football']
830
912
  }
831
- msg = Arborist::TreeAPI.encode( :graft, header, attributes )
913
+ msg = Arborist::TreeAPI.request( :graft, header, attributes )
832
914
 
833
- sock.send( msg )
834
- resmsg = sock.recv
915
+ msg.send_to( sock )
916
+ resmsg = sock.receive
835
917
 
836
918
  hdr, body = Arborist::TreeAPI.decode( resmsg )
837
919
  expect( hdr ).to include( 'success' => true )
838
- expect( body ).to eq( 'guenter' )
920
+ expect( body ).to include( 'identifier' => 'guenter' )
839
921
 
840
922
  new_node = manager.nodes[ 'guenter' ]
841
923
  expect( new_node ).to be_a( Arborist::Node::Host )
@@ -857,14 +939,14 @@ describe Arborist::Manager do
857
939
  addresses: ['192.168.22.8'],
858
940
  tags: ['evil', 'space', 'entity']
859
941
  }
860
- msg = Arborist::TreeAPI.encode( :graft, header, attributes )
942
+ msg = Arborist::TreeAPI.request( :graft, header, attributes )
861
943
 
862
- sock.send( msg )
863
- resmsg = sock.recv
944
+ msg.send_to( sock )
945
+ resmsg = sock.receive
864
946
 
865
947
  hdr, body = Arborist::TreeAPI.decode( resmsg )
866
948
  expect( hdr ).to include( 'success' => true )
867
- expect( body ).to eq( 'orgalorg' )
949
+ expect( body ).to include( 'identifier' => 'orgalorg' )
868
950
 
869
951
  new_node = manager.nodes[ 'orgalorg' ]
870
952
  expect( new_node ).to be_a( Arborist::Node::Host )
@@ -885,14 +967,14 @@ describe Arborist::Manager do
885
967
  attributes = {
886
968
  description: 'Mmmmm AppleTalk.'
887
969
  }
888
- msg = Arborist::TreeAPI.encode( :graft, header, attributes )
970
+ msg = Arborist::TreeAPI.request( :graft, header, attributes )
889
971
 
890
- sock.send( msg )
891
- resmsg = sock.recv
972
+ msg.send_to( sock )
973
+ resmsg = sock.receive
892
974
 
893
975
  hdr, body = Arborist::TreeAPI.decode( resmsg )
894
976
  expect( hdr ).to include( 'success' => true )
895
- expect( body ).to eq( 'duir-echo' )
977
+ expect( body ).to eq( 'identifier' => 'duir-echo' )
896
978
 
897
979
  new_node = manager.nodes[ 'duir-echo' ]
898
980
  expect( new_node ).to be_a( Arborist::Node::Service )
@@ -913,16 +995,31 @@ describe Arborist::Manager do
913
995
  attributes = {
914
996
  description: 'Mmmmm AppleTalk.'
915
997
  }
916
- msg = Arborist::TreeAPI.encode( :graft, header, attributes )
998
+ msg = Arborist::TreeAPI.request( :graft, header, attributes )
917
999
 
918
- sock.send( msg )
919
- resmsg = sock.recv
1000
+ msg.send_to( sock )
1001
+ resmsg = sock.receive
920
1002
 
921
1003
  hdr, body = Arborist::TreeAPI.decode( resmsg )
922
1004
  expect( hdr ).to include( 'success' => false )
923
1005
  expect( hdr['reason'] ).to match( /no host given/i )
924
1006
  end
925
1007
 
1008
+
1009
+ it "errors if adding a node that already exists" do
1010
+ header = {
1011
+ identifier: 'duir',
1012
+ type: 'host',
1013
+ }
1014
+ msg = Arborist::TreeAPI.request( :graft, header, {} )
1015
+
1016
+ msg.send_to( sock )
1017
+ resmsg = sock.receive
1018
+
1019
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
1020
+ expect( hdr ).to include( 'success' => false )
1021
+ expect( hdr['reason'] ).to match( /exists/i )
1022
+ end
926
1023
  end
927
1024
 
928
1025
 
@@ -936,10 +1033,10 @@ describe Arborist::Manager do
936
1033
  parent: '_',
937
1034
  addresses: ['192.168.32.32', '10.2.2.28']
938
1035
  }
939
- msg = Arborist::TreeAPI.encode( :modify, header, attributes )
1036
+ msg = Arborist::TreeAPI.request( :modify, header, attributes )
940
1037
 
941
- sock.send( msg )
942
- resmsg = sock.recv
1038
+ msg.send_to( sock )
1039
+ resmsg = sock.receive
943
1040
 
944
1041
  hdr, body = Arborist::TreeAPI.decode( resmsg )
945
1042
  expect( hdr ).to include( 'success' => true )
@@ -959,10 +1056,10 @@ describe Arborist::Manager do
959
1056
  attributes = {
960
1057
  identifier: 'somethingelse'
961
1058
  }
962
- msg = Arborist::TreeAPI.encode( :modify, header, attributes )
1059
+ msg = Arborist::TreeAPI.request( :modify, header, attributes )
963
1060
 
964
- sock.send( msg )
965
- resmsg = sock.recv
1061
+ msg.send_to( sock )
1062
+ resmsg = sock.receive
966
1063
 
967
1064
  hdr, body = Arborist::TreeAPI.decode( resmsg )
968
1065
  expect( hdr ).to include( 'success' => true )
@@ -979,10 +1076,10 @@ describe Arborist::Manager do
979
1076
  attributes = {
980
1077
  identifier: 'somethingelse'
981
1078
  }
982
- msg = Arborist::TreeAPI.encode( :modify, header, attributes )
1079
+ msg = Arborist::TreeAPI.request( :modify, header, attributes )
983
1080
 
984
- sock.send( msg )
985
- resmsg = sock.recv
1081
+ msg.send_to( sock )
1082
+ resmsg = sock.receive
986
1083
 
987
1084
  hdr, body = Arborist::TreeAPI.decode( resmsg )
988
1085
  expect( hdr ).to include( 'success' => false )
@@ -997,16 +1094,396 @@ describe Arborist::Manager do
997
1094
  attributes = {
998
1095
  identifier: 'somethingelse'
999
1096
  }
1000
- msg = Arborist::TreeAPI.encode( :modify, header, attributes )
1097
+ msg = Arborist::TreeAPI.request( :modify, header, attributes )
1098
+
1099
+ msg.send_to( sock )
1100
+ resmsg = sock.receive
1101
+
1102
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
1103
+ expect( hdr ).to include( 'success' => false )
1104
+ end
1105
+
1106
+
1107
+ it "reparents a node whose parent is altered" do
1108
+ header = {
1109
+ identifier: 'sidonie'
1110
+ }
1111
+ attributes = {
1112
+ parent: 'yevaud'
1113
+ }
1114
+
1115
+ msg = Arborist::TreeAPI.request( :modify, header, attributes )
1116
+
1117
+ node = manager.nodes[ 'sidonie' ]
1118
+ old_parent = manager.nodes[ 'duir' ]
1119
+ expect( node.parent ).to eq( 'duir' )
1120
+
1121
+ msg.send_to( sock )
1122
+ resmsg = sock.receive
1123
+
1124
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
1125
+ expect( hdr ).to include( 'success' => true )
1126
+
1127
+ new_parent = manager.nodes[ 'yevaud' ]
1128
+ expect( node.parent ).to eq( 'yevaud' )
1129
+ expect( old_parent.children ).to_not include( 'sidonie' )
1130
+ expect( new_parent.children ).to include( 'sidonie' )
1131
+ end
1132
+
1133
+ end
1134
+
1001
1135
 
1002
- sock.send( msg )
1003
- resmsg = sock.recv
1136
+ describe "deps" do
1137
+
1138
+ it "returns a list of the identifiers of nodes that depend on it" do
1139
+ msg = Arborist::TreeAPI.request( :deps, {from: 'sidonie'}, nil )
1140
+ msg.send_to( sock )
1141
+ resmsg = sock.receive
1142
+
1143
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
1144
+ expect( hdr ).to include( 'success' => true )
1145
+ expect( body ).to be_a( Hash )
1146
+ expect( body ).to include( 'deps' )
1147
+ expect( body['deps'] ).to_not include( 'sidonie' )
1148
+ expect( body['deps'] ).to be_an( Array ).
1149
+ and( include('sidonie-ssh', 'yevaud-cozy_frontend', 'sandbox01-canary') )
1150
+ end
1151
+
1152
+ end
1153
+
1154
+
1155
+ describe "ack" do
1156
+
1157
+ it "acknowledges a single node" do
1158
+ msg = Arborist::TreeAPI.request( :ack,
1159
+ {identifier: 'sidonie'},
1160
+ {message: "Planned maintenance", sender: 'xerces'}
1161
+ )
1162
+ msg.send_to( sock )
1163
+ resmsg = sock.receive
1164
+
1165
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
1166
+ expect( hdr ).to include( 'success' => true )
1167
+ expect( body ).to be_nil
1168
+
1169
+ expect( manager.nodes['sidonie'] ).to be_disabled
1170
+ end
1171
+
1172
+
1173
+ it "returns an error if the node to ack doesn't exist" do
1174
+ msg = Arborist::TreeAPI.request( :ack,
1175
+ {identifier: 'shemp-ssh'},
1176
+ {message: "Planned maintenance", sender: 'xerces'}
1177
+ )
1178
+ msg.send_to( sock )
1179
+ resmsg = sock.receive
1180
+
1181
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
1182
+ expect( hdr ).to include( 'success' => false )
1183
+ expect( hdr['reason'] ).to match( /no such node/i )
1184
+ end
1185
+
1186
+
1187
+ it "returns an error if the node to ack isn't specified" do
1188
+ msg = Arborist::TreeAPI.request( :ack, {}, {message: "", sender: ''} )
1189
+ msg.send_to( sock )
1190
+ resmsg = sock.receive
1191
+
1192
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
1193
+ expect( hdr ).to include( 'success' => false )
1194
+ expect( hdr['reason'] ).to match( /no identifier/i )
1195
+ end
1196
+
1197
+
1198
+ it "returns an error if one of the required arguments isn't provided" do
1199
+ msg = Arborist::TreeAPI.request( :ack,
1200
+ {identifier: 'sidonie'},
1201
+ {message: "Planned maintenance"}
1202
+ )
1203
+ msg.send_to( sock )
1204
+ resmsg = sock.receive
1004
1205
 
1005
1206
  hdr, body = Arborist::TreeAPI.decode( resmsg )
1006
1207
  expect( hdr ).to include( 'success' => false )
1208
+ expect( hdr['reason'] ).to match( /missing required ack sender/i )
1209
+ end
1210
+
1211
+
1212
+ it "propagates events from an acknowledgement up the node tree" do
1213
+ expect( manager.root ).to receive( :publish_events ).
1214
+ at_least( :once ).
1215
+ and_call_original
1216
+ expect( manager.nodes['duir'] ).to receive( :publish_events ).
1217
+ at_least( :once ).
1218
+ and_call_original
1219
+
1220
+ manager.handle_ack_request( { 'identifier' => 'sidonie' }, {
1221
+ 'message' => 'There will also be corn served.',
1222
+ 'sender' => 'Smoove-B'
1223
+ })
1007
1224
  end
1008
1225
  end
1009
1226
 
1227
+
1228
+ describe "unack" do
1229
+
1230
+ it "removes an acknowledgement from a single node" do
1231
+ msg = Arborist::TreeAPI.request( :unack, {identifier: 'sidonie'}, nil )
1232
+ msg.send_to( sock )
1233
+ resmsg = sock.receive
1234
+
1235
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
1236
+ expect( hdr ).to include( 'success' => true )
1237
+ expect( body ).to be_nil
1238
+
1239
+ expect( manager.nodes['sidonie'] ).to be_unknown
1240
+ end
1241
+
1242
+
1243
+ it "returns an error if the node to unack doesn't exist" do
1244
+ msg = Arborist::TreeAPI.request( :unack, {identifier: 'shemp-ssh'}, nil )
1245
+ msg.send_to( sock )
1246
+ resmsg = sock.receive
1247
+
1248
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
1249
+ expect( hdr ).to include( 'success' => false )
1250
+ expect( hdr['reason'] ).to match( /no such/i )
1251
+ end
1252
+
1253
+
1254
+ it "returns an error if the node to unack isn't specified" do
1255
+ msg = Arborist::TreeAPI.request( :unack, {}, nil )
1256
+ msg.send_to( sock )
1257
+ resmsg = sock.receive
1258
+
1259
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
1260
+ expect( hdr ).to include( 'success' => false )
1261
+ expect( hdr['reason'] ).to match( /no identifier/i )
1262
+ end
1263
+
1264
+
1265
+ it "propagates events from an unack up the node tree" do
1266
+ expect( manager.root ).to receive( :publish_events ).
1267
+ at_least( :once ).
1268
+ and_call_original
1269
+ expect( manager.nodes['duir'] ).to receive( :publish_events ).
1270
+ at_least( :once ).
1271
+ and_call_original
1272
+ manager.handle_unack_request( { 'identifier' => 'sidonie' }, nil )
1273
+ end
1274
+ end
1275
+
1276
+
1277
+
1278
+ describe "malformed requests" do
1279
+
1280
+ it "send an error response if the request can't be deserialized" do
1281
+ sock << "whatevs, dude!"
1282
+ resmsg = sock.receive
1283
+
1284
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
1285
+ expect( hdr ).to include(
1286
+ 'success' => false,
1287
+ 'reason' => /invalid message/i,
1288
+ 'category' => 'client'
1289
+ )
1290
+ expect( body ).to be_nil
1291
+ end
1292
+
1293
+
1294
+ it "send an error response if the request isn't a tuple" do
1295
+ sock << MessagePack.pack({ version: 1, action: 'fetch' })
1296
+ resmsg = sock.receive
1297
+
1298
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
1299
+ expect( hdr ).to include(
1300
+ 'success' => false,
1301
+ 'reason' => /invalid message.*not an array/i,
1302
+ 'category' => 'client'
1303
+ )
1304
+ expect( body ).to be_nil
1305
+ end
1306
+
1307
+
1308
+ it "send an error response if the request is empty" do
1309
+ sock << MessagePack.pack([])
1310
+ resmsg = sock.receive
1311
+
1312
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
1313
+ expect( hdr ).to include(
1314
+ 'success' => false,
1315
+ 'reason' => /invalid message.*expected 1-2 parts, got 0/i,
1316
+ 'category' => 'client'
1317
+ )
1318
+ expect( body ).to be_nil
1319
+ end
1320
+
1321
+
1322
+ it "send an error response if the request is an incorrect length" do
1323
+ sock << MessagePack.pack( [{}, {}, {}] )
1324
+ resmsg = sock.receive
1325
+
1326
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
1327
+ expect( hdr ).to include(
1328
+ 'success' => false,
1329
+ 'reason' => /expected 1-2 parts, got 3/i,
1330
+ 'category' => 'client'
1331
+ )
1332
+ expect( body ).to be_nil
1333
+ end
1334
+
1335
+
1336
+ it "send an error response if the request's header is not a Map" do
1337
+ sock << MessagePack.pack( [nil, {}] )
1338
+ resmsg = sock.receive
1339
+
1340
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
1341
+ expect( hdr ).to include(
1342
+ 'success' => false,
1343
+ 'reason' => /no header/i,
1344
+ 'category' => 'client'
1345
+ )
1346
+ expect( body ).to be_nil
1347
+ end
1348
+
1349
+
1350
+ it "send an error response if the request's body is not Nil, a Map, or an Array of Maps" do
1351
+ sock << MessagePack.pack( [{version: 1, action: 'fetch'}, 18] )
1352
+ resmsg = sock.receive
1353
+
1354
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
1355
+ expect( hdr ).to include(
1356
+ 'success' => false,
1357
+ 'reason' => /invalid message.*body must be nil, a map, or an array of maps/i,
1358
+ 'category' => 'client'
1359
+ )
1360
+ expect( body ).to be_nil
1361
+ end
1362
+
1363
+
1364
+ it "send an error response if missing a version" do
1365
+ sock << MessagePack.pack( [{action: 'fetch'}] )
1366
+ resmsg = sock.receive
1367
+
1368
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
1369
+ expect( hdr ).to include(
1370
+ 'success' => false,
1371
+ 'reason' => /invalid message.*missing required header 'version'/i,
1372
+ 'category' => 'client'
1373
+ )
1374
+ expect( body ).to be_nil
1375
+ end
1376
+
1377
+
1378
+ it "send an error response if missing an action" do
1379
+ sock << MessagePack.pack( [{version: 1}] )
1380
+ resmsg = sock.receive
1381
+
1382
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
1383
+ expect( hdr ).to include(
1384
+ 'success' => false,
1385
+ 'reason' => /invalid message.*missing required header 'action'/i,
1386
+ 'category' => 'client'
1387
+ )
1388
+ expect( body ).to be_nil
1389
+ end
1390
+
1391
+
1392
+ it "send an error response for unknown actions" do
1393
+ badmsg = Arborist::TreeAPI.request( :slap )
1394
+ badmsg.send_to( sock )
1395
+ resmsg = sock.receive
1396
+
1397
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
1398
+ expect( hdr ).to include(
1399
+ 'success' => false,
1400
+ 'reason' => /invalid message.*no such action 'slap'/i,
1401
+ 'category' => 'client'
1402
+ )
1403
+ expect( body ).to be_nil
1404
+ end
1405
+
1406
+
1407
+ it "send an error response for the `tree` action" do
1408
+ badmsg = Arborist::TreeAPI.request( :tree )
1409
+ badmsg.send_to( sock )
1410
+ resmsg = sock.receive
1411
+
1412
+ hdr, body = Arborist::TreeAPI.decode( resmsg )
1413
+ expect( hdr ).to include(
1414
+ 'success' => false,
1415
+ 'reason' => /invalid message.*no such action 'tree'/i,
1416
+ 'category' => 'client'
1417
+ )
1418
+ expect( body ).to be_nil
1419
+ end
1420
+
1421
+ end
1422
+
1423
+ end
1424
+
1425
+
1426
+ describe "event API" do
1427
+
1428
+ before( :each ) do
1429
+ @manager = nil
1430
+ @manager_thread = Thread.new do
1431
+ @manager = make_testing_manager()
1432
+ Thread.current.abort_on_exception = true
1433
+ @manager.run
1434
+ Loggability[ Arborist ].info "Stopped the test manager"
1435
+ end
1436
+
1437
+ count = 0
1438
+ until (@manager && @manager.running?) || count > 30
1439
+ sleep 0.1
1440
+ count += 1
1441
+ end
1442
+ raise "Manager didn't start up" unless @manager.running?
1443
+ end
1444
+
1445
+ after( :each ) do
1446
+ @manager.simulate_signal( :TERM )
1447
+ unless @manager_thread.join( 5 )
1448
+ $stderr.puts "Manager thread didn't exit on its own; killing it."
1449
+ @manager_thread.kill
1450
+ end
1451
+
1452
+ count = 0
1453
+ while @manager.running? || count > 30
1454
+ sleep 0.1
1455
+ Loggability[ Arborist ].info "Manager still running"
1456
+ count += 1
1457
+ end
1458
+ raise "Manager didn't stop" if @manager.running?
1459
+ end
1460
+
1461
+
1462
+ let( :manager ) { @manager }
1463
+
1464
+ let!( :sock ) do
1465
+ sock = CZTop::Socket::SUB.new
1466
+ sock.options.linger = 0
1467
+ sock.subscribe( '' )
1468
+ event_uri = manager.event_socket.last_endpoint
1469
+ sock.connect( event_uri )
1470
+ Loggability[ Arborist ].info "Connected subscribed socket to %p" % [ event_uri ]
1471
+ sock
1472
+ end
1473
+
1474
+
1475
+ it "publishes messages via the event socket" do
1476
+ node = Arborist::Node.create( :root )
1477
+ event = Arborist::Event.create( :node_update, node )
1478
+ manager.publish( 'identifier-00aa', event )
1479
+
1480
+ msg = nil
1481
+ wait( 1.second ).for { msg = sock.receive }.to be_a( CZTop::Message )
1482
+
1483
+ expect( msg.frames.first.to_s ).to eq( 'identifier-00aa' )
1484
+ expect( msg.frames.last.to_s ).to be_a_messagepacked( Hash )
1485
+ end
1486
+
1010
1487
  end
1011
1488
 
1012
1489
 
@@ -1096,7 +1573,7 @@ describe Arborist::Manager do
1096
1573
  end
1097
1574
 
1098
1575
 
1099
- describe "node updates and events" do
1576
+ describe "node updates" do
1100
1577
 
1101
1578
  let( :tree ) do
1102
1579
  # router
@@ -1125,8 +1602,8 @@ describe Arborist::Manager do
1125
1602
  end
1126
1603
 
1127
1604
 
1128
- it "can fetch a Hash of node states" do
1129
- states = manager.fetch_matching_node_states( {}, [] )
1605
+ it "can search a Hash of node states" do
1606
+ states = manager.find_matching_node_states( {}, [] )
1130
1607
  expect( states.size ).to eq( manager.nodes.size )
1131
1608
  expect( states ).to include( 'host-b-nfs', 'host-c', 'router' )
1132
1609
  expect( states['host-b-nfs'] ).to be_a( Hash )
@@ -1135,26 +1612,26 @@ describe Arborist::Manager do
1135
1612
  end
1136
1613
 
1137
1614
 
1138
- it "can fetch a Hash of node states for nodes which match specified criteria" do
1139
- states = manager.fetch_matching_node_states( {'identifier' => 'host-c'}, [] )
1615
+ it "can search a Hash of node states for nodes which match specified criteria" do
1616
+ states = manager.find_matching_node_states( {'identifier' => 'host-c'}, [] )
1140
1617
  expect( states.size ).to eq( 1 )
1141
1618
  expect( states.keys.first ).to eq( 'host-c' )
1142
1619
  expect( states['host-c'] ).to be_a( Hash )
1143
1620
  end
1144
1621
 
1145
1622
 
1146
- it "can fetch a Hash of node states for nodes which don't match specified negative criteria" do
1147
- states = manager.fetch_matching_node_states( {}, [], false, {'identifier' => 'host-c'} )
1623
+ it "can search a Hash of node states for nodes which don't match specified negative criteria" do
1624
+ states = manager.find_matching_node_states( {}, [], false, {'identifier' => 'host-c'} )
1148
1625
  expect( states.size ).to eq( manager.nodes.size - 1 )
1149
1626
  expect( states ).to_not include( 'host-c' )
1150
1627
  end
1151
1628
 
1152
1629
 
1153
- it "can fetch a Hash of node states for nodes combining positive and negative criteria" do
1630
+ it "can search a Hash of node states for nodes combining positive and negative criteria" do
1154
1631
  positive = {'tag' => 'home'}
1155
1632
  negative = {'identifier' => 'host-a-www'}
1156
1633
 
1157
- states = manager.fetch_matching_node_states( positive, [], false, negative )
1634
+ states = manager.find_matching_node_states( positive, [], false, negative )
1158
1635
 
1159
1636
  expect( states.size ).to eq( 2 )
1160
1637
  expect( states ).to_not include( 'host-a-www' )
@@ -1241,201 +1718,3 @@ describe Arborist::Manager do
1241
1718
  end
1242
1719
 
1243
1720
 
1244
- __END__
1245
-
1246
- let( :socket ) { instance_double( ZMQ::Socket::Pub ) }
1247
- let( :pollitem ) { instance_double( ZMQ::Pollitem, pollable: socket ) }
1248
- let( :zloop ) { instance_double( ZMQ::Loop ) }
1249
-
1250
- let( :manager ) { Arborist::Manager.new }
1251
- let( :event ) { Arborist::Event.create(TestEvent, 'stuff') }
1252
-
1253
- let( :publisher ) { described_class.new(pollitem, manager, zloop) }
1254
-
1255
-
1256
- it "starts out registered for writing" do
1257
- expect( publisher ).to be_registered
1258
- end
1259
-
1260
-
1261
- it "unregisters itself if told to write with an empty event queue" do
1262
- expect( zloop ).to receive( :remove ).with( pollitem )
1263
- expect {
1264
- publisher.on_writable
1265
- }.to change { publisher.registered? }.to( false )
1266
- end
1267
-
1268
-
1269
- it "registers itself if it's not already when an event is appended" do
1270
- # Cause the socket to become unregistered
1271
- allow( zloop ).to receive( :remove )
1272
- publisher.on_writable
1273
-
1274
- expect( zloop ).to receive( :register ).with( pollitem )
1275
-
1276
- expect {
1277
- publisher.publish( 'identifier-00aa', event )
1278
- }.to change { publisher.registered? }.to( true )
1279
- end
1280
-
1281
-
1282
- it "publishes events with their identifier" do
1283
- identifier = '65b2430b-6855-4961-ab46-d742cf4456a1'
1284
-
1285
- expect( socket ).to receive( :sendm ).with( identifier )
1286
- expect( socket ).to receive( :send ) do |raw_data|
1287
- ev = MessagePack.unpack( raw_data )
1288
- expect( ev ).to include( 'type', 'data' )
1289
-
1290
- expect( ev['type'] ).to eq( 'test.event' )
1291
- expect( ev['data'] ).to eq( 'stuff' )
1292
- end
1293
- expect( zloop ).to receive( :remove ).with( pollitem )
1294
-
1295
- publisher.publish( identifier, event )
1296
- publisher.on_writable
1297
- end
1298
-
1299
-
1300
-
1301
- let( :manager ) { @manager }
1302
-
1303
- let!( :sock ) do
1304
- sock = Arborist.zmq_context.socket( :REQ )
1305
- sock.linger = 0
1306
- sock.connect( TESTING_API_SOCK )
1307
- sock
1308
- end
1309
-
1310
- let( :api_handler ) { described_class.new( rep_sock, manager ) }
1311
-
1312
-
1313
- describe "malformed requests" do
1314
-
1315
- it "send an error response if the request can't be deserialized" do
1316
- sock.send( "whatevs, dude!" )
1317
- resmsg = sock.recv
1318
-
1319
- hdr, body = Arborist::TreeAPI.decode( resmsg )
1320
- expect( hdr ).to include(
1321
- 'success' => false,
1322
- 'reason' => /invalid request/i,
1323
- 'category' => 'client'
1324
- )
1325
- expect( body ).to be_nil
1326
- end
1327
-
1328
-
1329
- it "send an error response if the request isn't a tuple" do
1330
- sock.send( MessagePack.pack({ version: 1, action: 'list' }) )
1331
- resmsg = sock.recv
1332
-
1333
- hdr, body = Arborist::TreeAPI.decode( resmsg )
1334
- expect( hdr ).to include(
1335
- 'success' => false,
1336
- 'reason' => /invalid request.*not a tuple/i,
1337
- 'category' => 'client'
1338
- )
1339
- expect( body ).to be_nil
1340
- end
1341
-
1342
-
1343
- it "send an error response if the request is empty" do
1344
- sock.send( MessagePack.pack([]) )
1345
- resmsg = sock.recv
1346
-
1347
- hdr, body = Arborist::TreeAPI.decode( resmsg )
1348
- expect( hdr ).to include(
1349
- 'success' => false,
1350
- 'reason' => /invalid request.*incorrect length/i,
1351
- 'category' => 'client'
1352
- )
1353
- expect( body ).to be_nil
1354
- end
1355
-
1356
-
1357
- it "send an error response if the request is an incorrect length" do
1358
- sock.send( MessagePack.pack([{}, {}, {}]) )
1359
- resmsg = sock.recv
1360
-
1361
- hdr, body = Arborist::TreeAPI.decode( resmsg )
1362
- expect( hdr ).to include(
1363
- 'success' => false,
1364
- 'reason' => /invalid request.*incorrect length/i,
1365
- 'category' => 'client'
1366
- )
1367
- expect( body ).to be_nil
1368
- end
1369
-
1370
-
1371
- it "send an error response if the request's header is not a Map" do
1372
- sock.send( MessagePack.pack([nil, {}]) )
1373
- resmsg = sock.recv
1374
-
1375
- hdr, body = Arborist::TreeAPI.decode( resmsg )
1376
- expect( hdr ).to include(
1377
- 'success' => false,
1378
- 'reason' => /invalid request.*header is not a map/i,
1379
- 'category' => 'client'
1380
- )
1381
- expect( body ).to be_nil
1382
- end
1383
-
1384
-
1385
- it "send an error response if the request's body is not Nil, a Map, or an Array of Maps" do
1386
- sock.send( MessagePack.pack([{version: 1, action: 'list'}, 18]) )
1387
- resmsg = sock.recv
1388
-
1389
- hdr, body = Arborist::TreeAPI.decode( resmsg )
1390
- expect( hdr ).to include(
1391
- 'success' => false,
1392
- 'reason' => /invalid request.*body must be nil, a map, or an array of maps/i,
1393
- 'category' => 'client'
1394
- )
1395
- expect( body ).to be_nil
1396
- end
1397
-
1398
-
1399
- it "send an error response if missing a version" do
1400
- sock.send( MessagePack.pack([{action: 'list'}]) )
1401
- resmsg = sock.recv
1402
-
1403
- hdr, body = Arborist::TreeAPI.decode( resmsg )
1404
- expect( hdr ).to include(
1405
- 'success' => false,
1406
- 'reason' => /invalid request.*missing required header 'version'/i,
1407
- 'category' => 'client'
1408
- )
1409
- expect( body ).to be_nil
1410
- end
1411
-
1412
-
1413
- it "send an error response if missing an action" do
1414
- sock.send( MessagePack.pack([{version: 1}]) )
1415
- resmsg = sock.recv
1416
-
1417
- hdr, body = Arborist::TreeAPI.decode( resmsg )
1418
- expect( hdr ).to include(
1419
- 'success' => false,
1420
- 'reason' => /invalid request.*missing required header 'action'/i,
1421
- 'category' => 'client'
1422
- )
1423
- expect( body ).to be_nil
1424
- end
1425
-
1426
-
1427
- it "send an error response for unknown actions" do
1428
- badmsg = Arborist::TreeAPI.encode( :slap )
1429
- sock.send( badmsg )
1430
- resmsg = sock.recv
1431
-
1432
- hdr, body = Arborist::TreeAPI.decode( resmsg )
1433
- expect( hdr ).to include(
1434
- 'success' => false,
1435
- 'reason' => /invalid request.*no such action 'slap'/i,
1436
- 'category' => 'client'
1437
- )
1438
- expect( body ).to be_nil
1439
- end
1440
- end
1441
-