arborist 0.0.1.pre20160106113421

Sign up to get free protection for your applications and to get access to all the features.
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