arborist 0.0.1.pre20160106113421

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 (74) hide show
  1. checksums.yaml +7 -0
  2. data/.document +4 -0
  3. data/.simplecov +9 -0
  4. data/ChangeLog +417 -0
  5. data/Events.md +20 -0
  6. data/History.md +4 -0
  7. data/LICENSE +29 -0
  8. data/Manifest.txt +72 -0
  9. data/Monitors.md +141 -0
  10. data/Nodes.md +0 -0
  11. data/Observers.md +72 -0
  12. data/Protocol.md +214 -0
  13. data/README.md +75 -0
  14. data/Rakefile +81 -0
  15. data/TODO.md +24 -0
  16. data/bin/amanagerd +10 -0
  17. data/bin/amonitord +12 -0
  18. data/bin/aobserverd +12 -0
  19. data/lib/arborist.rb +182 -0
  20. data/lib/arborist/client.rb +191 -0
  21. data/lib/arborist/event.rb +61 -0
  22. data/lib/arborist/event/node_acked.rb +18 -0
  23. data/lib/arborist/event/node_delta.rb +20 -0
  24. data/lib/arborist/event/node_matching.rb +34 -0
  25. data/lib/arborist/event/node_update.rb +19 -0
  26. data/lib/arborist/event/sys_reloaded.rb +15 -0
  27. data/lib/arborist/exceptions.rb +21 -0
  28. data/lib/arborist/manager.rb +508 -0
  29. data/lib/arborist/manager/event_publisher.rb +97 -0
  30. data/lib/arborist/manager/tree_api.rb +207 -0
  31. data/lib/arborist/mixins.rb +363 -0
  32. data/lib/arborist/monitor.rb +377 -0
  33. data/lib/arborist/monitor/socket.rb +163 -0
  34. data/lib/arborist/monitor_runner.rb +217 -0
  35. data/lib/arborist/node.rb +700 -0
  36. data/lib/arborist/node/host.rb +87 -0
  37. data/lib/arborist/node/root.rb +60 -0
  38. data/lib/arborist/node/service.rb +112 -0
  39. data/lib/arborist/observer.rb +176 -0
  40. data/lib/arborist/observer/action.rb +125 -0
  41. data/lib/arborist/observer/summarize.rb +105 -0
  42. data/lib/arborist/observer_runner.rb +181 -0
  43. data/lib/arborist/subscription.rb +82 -0
  44. data/spec/arborist/client_spec.rb +282 -0
  45. data/spec/arborist/event/node_update_spec.rb +71 -0
  46. data/spec/arborist/event_spec.rb +64 -0
  47. data/spec/arborist/manager/event_publisher_spec.rb +66 -0
  48. data/spec/arborist/manager/tree_api_spec.rb +458 -0
  49. data/spec/arborist/manager_spec.rb +442 -0
  50. data/spec/arborist/mixins_spec.rb +195 -0
  51. data/spec/arborist/monitor/socket_spec.rb +195 -0
  52. data/spec/arborist/monitor_runner_spec.rb +152 -0
  53. data/spec/arborist/monitor_spec.rb +251 -0
  54. data/spec/arborist/node/host_spec.rb +104 -0
  55. data/spec/arborist/node/root_spec.rb +29 -0
  56. data/spec/arborist/node/service_spec.rb +98 -0
  57. data/spec/arborist/node_spec.rb +552 -0
  58. data/spec/arborist/observer/action_spec.rb +205 -0
  59. data/spec/arborist/observer/summarize_spec.rb +294 -0
  60. data/spec/arborist/observer_spec.rb +146 -0
  61. data/spec/arborist/subscription_spec.rb +71 -0
  62. data/spec/arborist_spec.rb +146 -0
  63. data/spec/data/monitors/pings.rb +80 -0
  64. data/spec/data/monitors/port_checks.rb +27 -0
  65. data/spec/data/monitors/system_resources.rb +30 -0
  66. data/spec/data/monitors/web_services.rb +17 -0
  67. data/spec/data/nodes/duir.rb +20 -0
  68. data/spec/data/nodes/localhost.rb +15 -0
  69. data/spec/data/nodes/sidonie.rb +29 -0
  70. data/spec/data/nodes/yevaud.rb +26 -0
  71. data/spec/data/observers/auditor.rb +23 -0
  72. data/spec/data/observers/webservices.rb +18 -0
  73. data/spec/spec_helper.rb +117 -0
  74. metadata +368 -0
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env rspec -cfd
2
+
3
+ require_relative '../../spec_helper'
4
+
5
+ require 'arborist/event/node_update'
6
+
7
+
8
+ describe Arborist::Event::NodeUpdate do
9
+
10
+ class TestNode < Arborist::Node; end
11
+
12
+
13
+ let( :node ) do
14
+ TestNode.new( 'foo' ) do
15
+ parent 'bar'
16
+ description "The prototypical node"
17
+ tags :chunker, :hunky, :flippin, :hippo
18
+
19
+ update(
20
+ 'song' => 'Around the World',
21
+ 'artist' => 'Daft Punk',
22
+ 'length' => '7:09',
23
+ 'cider' => {
24
+ 'description' => 'tasty',
25
+ 'size' => '16oz',
26
+ },
27
+ 'sausage' => {
28
+ 'description' => 'pork',
29
+ 'size' => 'monsterous',
30
+ 'price' => {
31
+ 'units' => 1200,
32
+ 'currency' => 'usd'
33
+ }
34
+ },
35
+ 'music' => '80s'
36
+ )
37
+ end
38
+ end
39
+
40
+
41
+ describe "subscription support" do
42
+
43
+ it "matches a subscription with only an event type if the type is the same" do
44
+ sub = Arborist::Subscription.new( :publisher, 'node.update' )
45
+ event = described_class.new( node )
46
+
47
+ expect( event ).to match( sub )
48
+ end
49
+
50
+
51
+ it "matches a subscription with a matching event type and matching criteria" do
52
+ sub = Arborist::Subscription.new( :publisher, 'node.update', 'tag' => 'chunker' )
53
+ event = described_class.new( node )
54
+
55
+ expect( event ).to match( sub )
56
+ end
57
+
58
+
59
+ it "doesn't match a subscription with a matching event type if the criteria don't match" do
60
+ sub = Arborist::Subscription.new( :publisher, 'node.update', 'tag' => 'looper' )
61
+ event = described_class.new( node )
62
+
63
+ expect( event ).to_not match( sub )
64
+ end
65
+
66
+
67
+ end
68
+
69
+
70
+ end
71
+
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env rspec -cfd
2
+
3
+ require_relative '../spec_helper'
4
+
5
+ require 'arborist/event'
6
+
7
+
8
+ describe Arborist::Event do
9
+
10
+ it "derives its type name from its class" do
11
+ payload = { 'status' => ['up', 'down'] }
12
+ expect( TestEvent.new(payload).type ).to eq( 'test.event' )
13
+ end
14
+
15
+
16
+ it "copies the payload it's constructed with" do
17
+ payload = { 'status' => ['up', 'down'] }
18
+
19
+ ev = TestEvent.create( TestEvent, payload )
20
+ payload.clear
21
+
22
+ expect( ev.payload ).to include( 'status' )
23
+ end
24
+
25
+
26
+ describe "subscription support" do
27
+
28
+ it "matches a subscription with only an event type if the type is the same" do
29
+ sub = Arborist::Subscription.new( :publisher, 'test.event' )
30
+ event = described_class.create( TestEvent, [] )
31
+
32
+ expect( event ).to match( sub )
33
+ end
34
+
35
+
36
+ it "always matches a subscription with a nil event type" do
37
+ sub = Arborist::Subscription.new( :publisher )
38
+ event = described_class.create( TestEvent, [] )
39
+
40
+ expect( event ).to match( sub )
41
+ end
42
+
43
+ end
44
+
45
+
46
+ describe "serialization support" do
47
+
48
+ it "can represent itself as a Hash" do
49
+ payload = { 'status' => ['up', 'down'] }
50
+ ev = TestEvent.create( TestEvent, payload )
51
+
52
+ result = ev.to_hash
53
+
54
+ expect( result ).to include( 'type', 'data' )
55
+
56
+ expect( result['type'] ).to eq( 'test.event' )
57
+ expect( result['data'] ).to eq( payload )
58
+ end
59
+
60
+
61
+ end
62
+
63
+ end
64
+
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env rspec -cfd
2
+
3
+ require_relative '../../spec_helper'
4
+
5
+ require 'arborist/manager/event_publisher'
6
+
7
+
8
+ describe Arborist::Manager::EventPublisher do
9
+
10
+ let( :socket ) { instance_double( ZMQ::Socket::Pub ) }
11
+ let( :pollitem ) { instance_double( ZMQ::Pollitem, pollable: socket ) }
12
+ let( :zloop ) { instance_double( ZMQ::Loop ) }
13
+
14
+ let( :manager ) { Arborist::Manager.new }
15
+ let( :event ) { Arborist::Event.create(TestEvent, 'stuff') }
16
+
17
+ let( :publisher ) { described_class.new(pollitem, manager, zloop) }
18
+
19
+
20
+ it "starts out registered for writing" do
21
+ expect( publisher ).to be_registered
22
+ end
23
+
24
+
25
+ it "unregisters itself if told to write with an empty event queue" do
26
+ expect( zloop ).to receive( :remove ).with( pollitem )
27
+ expect {
28
+ publisher.on_writable
29
+ }.to change { publisher.registered? }.to( false )
30
+ end
31
+
32
+
33
+ it "registers itself if it's not already when an event is appended" do
34
+ # Cause the socket to become unregistered
35
+ allow( zloop ).to receive( :remove )
36
+ publisher.on_writable
37
+
38
+ expect( zloop ).to receive( :register ).with( pollitem )
39
+
40
+ expect {
41
+ publisher.publish( 'identifier-00aa', event )
42
+ }.to change { publisher.registered? }.to( true )
43
+ end
44
+
45
+
46
+ it "publishes events with their identifier" do
47
+ identifier = '65b2430b-6855-4961-ab46-d742cf4456a1'
48
+
49
+ expect( socket ).to receive( :sendm ).with( identifier )
50
+ expect( socket ).to receive( :send ) do |raw_data|
51
+ ev = MessagePack.unpack( raw_data )
52
+ expect( ev ).to include( 'type', 'data' )
53
+
54
+ expect( ev['type'] ).to eq( 'test.event' )
55
+ expect( ev['data'] ).to eq( 'stuff' )
56
+ end
57
+ expect( zloop ).to receive( :remove ).with( pollitem )
58
+
59
+ publisher.publish( identifier, event )
60
+ publisher.on_writable
61
+ end
62
+
63
+
64
+ end
65
+
66
+
@@ -0,0 +1,458 @@
1
+ #!/usr/bin/env rspec -cfd
2
+
3
+ require_relative '../../spec_helper'
4
+
5
+
6
+ describe Arborist::Manager::TreeAPI, :testing_manager do
7
+
8
+ before( :each ) do
9
+ @manager = make_testing_manager()
10
+ @manager_thread = Thread.new do
11
+ Thread.current.abort_on_exception = true
12
+ manager.run
13
+ Loggability[ Arborist ].info "Stopped the test manager"
14
+ end
15
+
16
+ count = 0
17
+ until manager.running? || count > 30
18
+ sleep 0.1
19
+ count += 1
20
+ end
21
+ raise "Manager didn't start up" unless manager.running?
22
+ end
23
+
24
+ after( :each ) do
25
+ @manager.stop
26
+ unless @manager_thread.join( 5 )
27
+ $stderr.puts "Manager thread didn't exit on its own; killing it."
28
+ @manager_thread.kill
29
+ end
30
+
31
+ count = 0
32
+ while @manager.zmq_loop.running? || count > 30
33
+ sleep 0.1
34
+ Loggability[ Arborist ].info "ZMQ loop still running"
35
+ count += 1
36
+ end
37
+ raise "ZMQ Loop didn't stop" if @manager.zmq_loop.running?
38
+ end
39
+
40
+
41
+ let( :manager ) { @manager }
42
+
43
+ let!( :sock ) do
44
+ sock = Arborist.zmq_context.socket( :REQ )
45
+ sock.linger = 0
46
+ sock.connect( TESTING_API_SOCK )
47
+ sock
48
+ end
49
+
50
+ let( :api_handler ) { described_class.new( rep_sock, manager ) }
51
+
52
+
53
+ describe "malformed requests" do
54
+
55
+ it "send an error response if the request can't be deserialized" do
56
+ sock.send( "whatevs, dude!" )
57
+ resmsg = sock.recv
58
+
59
+ hdr, body = unpack_message( resmsg )
60
+ expect( hdr ).to include(
61
+ 'success' => false,
62
+ 'reason' => /invalid request/i,
63
+ 'category' => 'client'
64
+ )
65
+ expect( body ).to be_nil
66
+ end
67
+
68
+
69
+ it "send an error response if the request isn't a tuple" do
70
+ sock.send( MessagePack.pack({ version: 1, action: 'list' }) )
71
+ resmsg = sock.recv
72
+
73
+ hdr, body = unpack_message( resmsg )
74
+ expect( hdr ).to include(
75
+ 'success' => false,
76
+ 'reason' => /invalid request.*not a tuple/i,
77
+ 'category' => 'client'
78
+ )
79
+ expect( body ).to be_nil
80
+ end
81
+
82
+
83
+ it "send an error response if the request is empty" do
84
+ sock.send( MessagePack.pack([]) )
85
+ resmsg = sock.recv
86
+
87
+ hdr, body = unpack_message( resmsg )
88
+ expect( hdr ).to include(
89
+ 'success' => false,
90
+ 'reason' => /invalid request.*incorrect length/i,
91
+ 'category' => 'client'
92
+ )
93
+ expect( body ).to be_nil
94
+ end
95
+
96
+
97
+ it "send an error response if the request is an incorrect length" do
98
+ sock.send( MessagePack.pack([{}, {}, {}]) )
99
+ resmsg = sock.recv
100
+
101
+ hdr, body = unpack_message( resmsg )
102
+ expect( hdr ).to include(
103
+ 'success' => false,
104
+ 'reason' => /invalid request.*incorrect length/i,
105
+ 'category' => 'client'
106
+ )
107
+ expect( body ).to be_nil
108
+ end
109
+
110
+
111
+ it "send an error response if the request's header is not a Map" do
112
+ sock.send( MessagePack.pack([nil, {}]) )
113
+ resmsg = sock.recv
114
+
115
+ hdr, body = unpack_message( resmsg )
116
+ expect( hdr ).to include(
117
+ 'success' => false,
118
+ 'reason' => /invalid request.*header is not a map/i,
119
+ 'category' => 'client'
120
+ )
121
+ expect( body ).to be_nil
122
+ end
123
+
124
+
125
+ it "send an error response if the request's body is not a Map or Nil" do
126
+ sock.send( MessagePack.pack([{version: 1, action: 'list'}, 18]) )
127
+ resmsg = sock.recv
128
+
129
+ hdr, body = unpack_message( resmsg )
130
+ expect( hdr ).to include(
131
+ 'success' => false,
132
+ 'reason' => /invalid request.*body must be a map or nil/i,
133
+ 'category' => 'client'
134
+ )
135
+ expect( body ).to be_nil
136
+ end
137
+
138
+
139
+ it "send an error response if missing a version" do
140
+ sock.send( MessagePack.pack([{action: 'list'}]) )
141
+ resmsg = sock.recv
142
+
143
+ hdr, body = unpack_message( resmsg )
144
+ expect( hdr ).to include(
145
+ 'success' => false,
146
+ 'reason' => /invalid request.*missing required header 'version'/i,
147
+ 'category' => 'client'
148
+ )
149
+ expect( body ).to be_nil
150
+ end
151
+
152
+
153
+ it "send an error response if missing an action" do
154
+ sock.send( MessagePack.pack([{version: 1}]) )
155
+ resmsg = sock.recv
156
+
157
+ hdr, body = unpack_message( resmsg )
158
+ expect( hdr ).to include(
159
+ 'success' => false,
160
+ 'reason' => /invalid request.*missing required header 'action'/i,
161
+ 'category' => 'client'
162
+ )
163
+ expect( body ).to be_nil
164
+ end
165
+
166
+
167
+ it "send an error response for unknown actions" do
168
+ badmsg = pack_message( :slap )
169
+ sock.send( badmsg )
170
+ resmsg = sock.recv
171
+
172
+ hdr, body = unpack_message( resmsg )
173
+ expect( hdr ).to include(
174
+ 'success' => false,
175
+ 'reason' => /invalid request.*no such action 'slap'/i,
176
+ 'category' => 'client'
177
+ )
178
+ expect( body ).to be_nil
179
+ end
180
+ end
181
+
182
+
183
+ describe "status" do
184
+
185
+
186
+ it "returns a Map describing the manager and its state" do
187
+ msg = pack_message( :status )
188
+
189
+ sock.send( msg )
190
+ resmsg = sock.recv
191
+
192
+ hdr, body = unpack_message( resmsg )
193
+ expect( hdr ).to include( 'success' => true )
194
+ expect( body.length ).to eq( 4 )
195
+ expect( body ).to include( 'server_version', 'state', 'uptime', 'nodecount' )
196
+ end
197
+
198
+ end
199
+
200
+
201
+ describe "fetch" do
202
+
203
+ it "returns an array of full state maps for nodes matching specified criteria" do
204
+ msg = pack_message( :fetch, type: 'service', port: 22 )
205
+
206
+ sock.send( msg )
207
+ resmsg = sock.recv
208
+
209
+ hdr, body = unpack_message( resmsg )
210
+ expect( hdr ).to include( 'success' => true )
211
+
212
+ expect( body ).to be_a( Hash )
213
+ expect( body.length ).to eq( 3 )
214
+
215
+ expect( body.values ).to all( be_a(Hash) )
216
+ expect( body.values ).to all( include('status', 'type') )
217
+ end
218
+
219
+
220
+ it "doesn't return nodes beneath downed nodes by default" do
221
+ manager.nodes['sidonie'].update( error: 'sunspots' )
222
+ msg = pack_message( :fetch, type: 'service', port: 22 )
223
+
224
+ sock.send( msg )
225
+ resmsg = sock.recv
226
+
227
+ hdr, body = unpack_message( resmsg )
228
+ expect( hdr ).to include( 'success' => true )
229
+ expect( body ).to be_a( Hash )
230
+ expect( body.length ).to eq( 2 )
231
+ expect( body ).to include( 'duir-ssh', 'yevaud-ssh' )
232
+ end
233
+
234
+
235
+ it "does return nodes beneath downed nodes if asked to" do
236
+ manager.nodes['sidonie'].update( error: 'plague of locusts' )
237
+ msg = pack_message( :fetch, {include_down: true}, type: 'service', port: 22 )
238
+
239
+ sock.send( msg )
240
+ resmsg = sock.recv
241
+
242
+ hdr, body = unpack_message( resmsg )
243
+ expect( hdr ).to include( 'success' => true )
244
+ expect( body ).to be_a( Hash )
245
+ expect( body.length ).to eq( 3 )
246
+ expect( body ).to include( 'duir-ssh', 'yevaud-ssh', 'sidonie-ssh' )
247
+ end
248
+
249
+
250
+ it "returns only identifiers if the `return` header is set to `nil`" do
251
+ msg = pack_message( :fetch, {return: nil}, type: 'service', port: 22 )
252
+
253
+ sock.send( msg )
254
+ resmsg = sock.recv
255
+
256
+ hdr, body = unpack_message( resmsg )
257
+ expect( hdr ).to include( 'success' => true )
258
+ expect( body ).to be_a( Hash )
259
+ expect( body.length ).to eq( 3 )
260
+ expect( body ).to include( 'duir-ssh', 'yevaud-ssh', 'sidonie-ssh' )
261
+ expect( body.values ).to all( be_empty )
262
+ end
263
+
264
+
265
+ it "returns only specified state if the `return` header is set to an Array of keys" do
266
+ msg = pack_message( :fetch, {return: %w[status tags addresses]},
267
+ type: 'service', port: 22 )
268
+
269
+ sock.send( msg )
270
+ resmsg = sock.recv
271
+
272
+ hdr, body = unpack_message( resmsg )
273
+ expect( hdr ).to include( 'success' => true )
274
+ expect( body.length ).to eq( 3 )
275
+ expect( body ).to include( 'duir-ssh', 'yevaud-ssh', 'sidonie-ssh' )
276
+ expect( body.values.map(&:keys) ).to all( contain_exactly('status', 'tags', 'addresses') )
277
+ end
278
+
279
+ end
280
+
281
+
282
+ describe "list" do
283
+
284
+ it "returns an array of node state" do
285
+ msg = pack_message( :list )
286
+ sock.send( msg )
287
+ resmsg = sock.recv
288
+
289
+ hdr, body = unpack_message( resmsg )
290
+ expect( hdr ).to include( 'success' => true )
291
+ expect( body.length ).to eq( manager.nodes.length )
292
+ expect( body ).to all( be_a(Hash) )
293
+ expect( body ).to include( hash_including('identifier' => '_') )
294
+ expect( body ).to include( hash_including('identifier' => 'duir') )
295
+ expect( body ).to include( hash_including('identifier' => 'sidonie-ssh') )
296
+ expect( body ).to include( hash_including('identifier' => 'sidonie-demon-http') )
297
+ expect( body ).to include( hash_including('identifier' => 'yevaud') )
298
+ end
299
+
300
+ end
301
+
302
+
303
+ describe "update" do
304
+
305
+ it "merges the properties sent with those of the targeted nodes" do
306
+ update_data = {
307
+ duir: {
308
+ ping: {
309
+ rtt: 254
310
+ }
311
+ },
312
+ sidonie: {
313
+ ping: {
314
+ rtt: 1208
315
+ }
316
+ },
317
+ yevaud: {
318
+ ping: {
319
+ rtt: 843
320
+ }
321
+ }
322
+ }
323
+ msg = pack_message( :update, update_data )
324
+ sock.send( msg )
325
+ resmsg = sock.recv
326
+
327
+ hdr, body = unpack_message( resmsg )
328
+ expect( hdr ).to include( 'success' => true )
329
+ expect( body ).to be_nil
330
+
331
+ expect( manager.nodes['duir'].properties['ping'] ).to include( 'rtt' => 254 )
332
+ expect( manager.nodes['sidonie'].properties['ping'] ).to include( 'rtt' => 1208 )
333
+ expect( manager.nodes['yevaud'].properties['ping'] ).to include( 'rtt' => 843 )
334
+ end
335
+
336
+
337
+ it "ignores unknown identifiers" do
338
+ msg = pack_message( :update, charlie_humperton: {ping: { rtt: 8 }} )
339
+ sock.send( msg )
340
+ resmsg = sock.recv
341
+
342
+ hdr, body = unpack_message( resmsg )
343
+ expect( hdr ).to include( 'success' => true )
344
+ end
345
+
346
+ end
347
+
348
+
349
+ describe "subscribe" do
350
+
351
+ it "adds a subscription for all event types to the root node by default" do
352
+ criteria = {
353
+ type: 'host'
354
+ }
355
+
356
+ msg = pack_message( :subscribe, criteria )
357
+
358
+ resmsg = nil
359
+ expect {
360
+ sock.send( msg )
361
+ resmsg = sock.recv
362
+ }.to change { manager.subscriptions.length }.by( 1 ).and(
363
+ change { manager.root.subscriptions.length }.by( 1 )
364
+ )
365
+ hdr, body = unpack_message( resmsg )
366
+
367
+ sub_id = manager.subscriptions.keys.first
368
+
369
+ expect( hdr ).to include( 'success' => true )
370
+ expect( body ).to eq([ sub_id ])
371
+ end
372
+
373
+
374
+ it "adds a subscription to the specified node if an identifier is specified" do
375
+ criteria = {
376
+ type: 'host'
377
+ }
378
+
379
+ msg = pack_message( :subscribe, {identifier: 'sidonie'}, criteria )
380
+
381
+ resmsg = nil
382
+ expect {
383
+ sock.send( msg )
384
+ resmsg = sock.recv
385
+ }.to change { manager.subscriptions.length }.by( 1 ).and(
386
+ change { manager.nodes['sidonie'].subscriptions.length }.by( 1 )
387
+ )
388
+ hdr, body = unpack_message( resmsg )
389
+
390
+ sub_id = manager.subscriptions.keys.first
391
+
392
+ expect( hdr ).to include( 'success' => true )
393
+ expect( body ).to eq([ sub_id ])
394
+ end
395
+
396
+
397
+ it "adds a subscription for node types matching a pattern if one is specified" do
398
+ criteria = {
399
+ type: 'host'
400
+ }
401
+
402
+ msg = pack_message( :subscribe, {event_type: 'node.ack'}, criteria )
403
+
404
+ resmsg = nil
405
+ expect {
406
+ sock.send( msg )
407
+ resmsg = sock.recv
408
+ }.to change { manager.subscriptions.length }.by( 1 ).and(
409
+ change { manager.root.subscriptions.length }.by( 1 )
410
+ )
411
+ hdr, body = unpack_message( resmsg )
412
+ node = manager.subscriptions[ body.first ]
413
+ sub = node.subscriptions[ body.first ]
414
+
415
+ expect( sub.event_type ).to eq( 'node.ack' )
416
+ end
417
+
418
+ end
419
+
420
+
421
+ describe "unsubscribe" do
422
+
423
+ let( :subscription ) do
424
+ manager.create_subscription( nil, 'node.delta', {type: 'host'} )
425
+ end
426
+
427
+ it "removes the subscription with the specified ID" do
428
+ msg = pack_message( :unsubscribe, {subscription_id: subscription.id}, nil )
429
+
430
+ resmsg = nil
431
+ expect {
432
+ sock.send( msg )
433
+ resmsg = sock.recv
434
+ }.to change { manager.subscriptions.length }.by( -1 ).and(
435
+ change { manager.root.subscriptions.length }.by( -1 )
436
+ )
437
+ hdr, body = unpack_message( resmsg )
438
+
439
+ expect( body ).to include( 'event_type' => 'node.delta', 'criteria' => {'type' => 'host'} )
440
+ end
441
+
442
+
443
+ it "ignores unsubscription of a non-existant ID" do
444
+ msg = pack_message( :unsubscribe, {subscription_id: 'the bears!'}, nil )
445
+
446
+ resmsg = nil
447
+ expect {
448
+ sock.send( msg )
449
+ resmsg = sock.recv
450
+ }.to_not change { manager.subscriptions.length }
451
+ hdr, body = unpack_message( resmsg )
452
+
453
+ expect( body ).to be_nil
454
+ end
455
+
456
+ end
457
+
458
+ end