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,104 @@
1
+ #!/usr/bin/env rspec -cfd
2
+
3
+ require_relative '../../spec_helper'
4
+
5
+ require 'arborist/node/host'
6
+
7
+
8
+ describe Arborist::Node::Host do
9
+
10
+ it "can be created without any addresses" do
11
+ result = described_class.new( 'testhost' )
12
+ expect( result.addresses ).to be_empty
13
+ end
14
+
15
+
16
+ it "can be created with a single IPv4 address" do
17
+ result = described_class.new( 'testhost' ) do
18
+ address '192.168.118.3'
19
+ end
20
+
21
+ expect( result.addresses ).to eq([ IPAddr.new('192.168.118.3') ])
22
+ end
23
+
24
+
25
+ it "can be created with an IPAddr object" do
26
+ result = described_class.new( 'testhost' ) do
27
+ address IPAddr.new( '192.168.118.3' )
28
+ end
29
+
30
+ expect( result.addresses ).to eq([ IPAddr.new('192.168.118.3') ])
31
+ end
32
+
33
+
34
+ it "can be created with a single hostname with an IPv4 address" do
35
+ expect( TCPSocket ).to receive( :gethostbyname ).with( 'arbori.st' ).
36
+ and_return(['arbori.st', [], Socket::AF_INET, '198.145.180.85'])
37
+
38
+ result = described_class.new( 'arborist' ) do
39
+ address 'arbori.st'
40
+ end
41
+
42
+ expect( result.addresses.size ).to eq( 1 )
43
+ expect( result.addresses ).to include( IPAddr.new('198.145.180.85') )
44
+ end
45
+
46
+
47
+ it "can be created with a single hostname with both an IPv4 and an IPv6 address" do
48
+ expect( TCPSocket ).to receive( :gethostbyname ).with( 'google.com' ).
49
+ and_return(["google.com", [], 2, "216.58.216.174", "2607:f8b0:400a:807::200e"])
50
+
51
+ result = described_class.new( 'google' ) do
52
+ address 'google.com'
53
+ end
54
+
55
+ expect( result.addresses.size ).to eq( 2 )
56
+ expect( result.addresses ).to include( IPAddr.new('216.58.216.174') )
57
+ expect( result.addresses ).to include( IPAddr.new('2607:f8b0:400a:807::200e') )
58
+ end
59
+
60
+
61
+ it "can be created with multiple hostnames with IPv4 addresses" do
62
+ expect( TCPSocket ).to receive( :gethostbyname ).with( 'arbori.st' ).
63
+ and_return(['arbori.st', [], Socket::AF_INET, '198.145.180.85'])
64
+ expect( TCPSocket ).to receive( :gethostbyname ).with( 'faeriemud.org' ).
65
+ and_return(['faeriemud.org', [], Socket::AF_INET, '198.145.180.86'])
66
+
67
+ result = described_class.new( 'arborist' ) do
68
+ address 'arbori.st'
69
+ address 'faeriemud.org'
70
+ end
71
+
72
+ expect( result.addresses.size ).to eq( 2 )
73
+ expect( result.addresses ).to include( IPAddr.new('198.145.180.85') )
74
+ expect( result.addresses ).to include( IPAddr.new('198.145.180.86') )
75
+ end
76
+
77
+
78
+ describe "matching" do
79
+
80
+ let( :node ) do
81
+ described_class.new( 'testhost' ) do
82
+ address '192.168.66.12'
83
+ address '10.2.12.68'
84
+ end
85
+ end
86
+
87
+
88
+ it "can be matched with one of its addresses" do
89
+ expect( node ).to match_criteria( address: '192.168.66.12' )
90
+ expect( node ).to_not match_criteria( address: '127.0.0.1' )
91
+ end
92
+
93
+
94
+ it "can be matched with a netblock that includes one of its addresses" do
95
+ expect( node ).to match_criteria( address: '192.168.66.0/24' )
96
+ expect( node ).to match_criteria( address: '10.0.0.0/8' )
97
+ expect( node ).to_not match_criteria( address: '192.168.66.64/27' )
98
+ expect( node ).to_not match_criteria( address: '127.0.0.0/8' )
99
+ end
100
+
101
+ end
102
+
103
+ end
104
+
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env rspec -cfd
2
+
3
+ require_relative '../../spec_helper'
4
+
5
+ require 'arborist/node/root'
6
+
7
+
8
+ describe Arborist::Node::Root do
9
+
10
+ let( :node ) { described_class.instance }
11
+
12
+
13
+ it "is a singleton" do
14
+ expect( described_class.new ).to be( described_class.new )
15
+ end
16
+
17
+
18
+ it "doesn't have a parent node" do
19
+ expect( node.parent ).to be_nil
20
+ end
21
+
22
+
23
+ it "doesn't allow a parent to be set on it" do
24
+ node.parent( 'supernode' )
25
+ expect( node.parent ).to be_nil
26
+ end
27
+
28
+ end
29
+
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env rspec -cfd
2
+
3
+ require_relative '../../spec_helper'
4
+
5
+ require 'arborist/node/service'
6
+
7
+
8
+ describe Arborist::Node::Service do
9
+
10
+ let( :host ) do
11
+ Arborist::Node.create( 'host', 'testhost' ) do
12
+ address '192.168.66.12'
13
+ address '10.2.12.68'
14
+ end
15
+ end
16
+
17
+
18
+ it "can be created without reasonable defaults based on its identifier" do
19
+ result = described_class.new( 'ssh', host )
20
+ expect( result.port ).to eq( 22 )
21
+ expect( result.protocol ).to eq( 'tcp' )
22
+ end
23
+
24
+
25
+ it "can be created with an explicit port" do
26
+ result = described_class.new( 'ssh', host, port: 2222 )
27
+ expect( result.port ).to eq( 2222 )
28
+ expect( result.protocol ).to eq( 'tcp' )
29
+ end
30
+
31
+
32
+ it "can be created with an explicit port" do
33
+ result = described_class.new( 'rsspk', host, port: 1801, protocol: 'udp' )
34
+ expect( result.port ).to eq( 1801 )
35
+ expect( result.protocol ).to eq( 'udp' )
36
+ end
37
+
38
+
39
+ it "uses the identifier as the application protocol if none is specified" do
40
+ result = described_class.new( 'rsspk', host, port: 1801 )
41
+ expect( result.port ).to eq( 1801 )
42
+ expect( result.app_protocol ).to eq( 'rsspk' )
43
+ end
44
+
45
+
46
+ it "can specify an explicit application protocol" do
47
+ result = described_class.new( 'dnsd', host, port: 53, protocol: 'udp', app_protocol: 'dns' )
48
+ expect( result.app_protocol ).to eq( 'dns' )
49
+ end
50
+
51
+
52
+ describe "matching" do
53
+
54
+ let( :node ) do
55
+ described_class.new( 'ssh', host )
56
+ end
57
+
58
+
59
+ it "can be matched with one of its host's addresses" do
60
+ expect( node ).to match_criteria( address: '192.168.66.12' )
61
+ expect( node ).to_not match_criteria( address: '127.0.0.1' )
62
+ end
63
+
64
+
65
+ it "can be matched with a netblock that includes one of its host's addresses" do
66
+ expect( node ).to match_criteria( address: '192.168.66.0/24' )
67
+ expect( node ).to match_criteria( address: '10.0.0.0/8' )
68
+ expect( node ).to_not match_criteria( address: '192.168.66.64/27' )
69
+ expect( node ).to_not match_criteria( address: '127.0.0.0/8' )
70
+ end
71
+
72
+
73
+ it "can be matched with a port" do
74
+ expect( node ).to match_criteria( port: 22 )
75
+ expect( node ).to match_criteria( port: 'ssh' )
76
+ expect( node ).to_not match_criteria( port: 80 )
77
+ expect( node ).to_not match_criteria( port: 'www' )
78
+ expect( node ).to_not match_criteria( port: 'chungwatch' )
79
+ end
80
+
81
+
82
+ it "can be matched with a protocol" do
83
+ expect( node ).to match_criteria( protocol: 'tcp' )
84
+ expect( node ).to_not match_criteria( protocol: 'udp' )
85
+ end
86
+
87
+
88
+ it "can be matched with an application protocol" do
89
+ expect( node ).to match_criteria( app_protocol: 'ssh' )
90
+ expect( node ).to match_criteria( app: 'ssh' )
91
+ expect( node ).to_not match_criteria( app_protocol: 'http' )
92
+ expect( node ).to_not match_criteria( app: 'http' )
93
+ end
94
+
95
+ end
96
+
97
+ end
98
+
@@ -0,0 +1,552 @@
1
+ #!/usr/bin/env rspec -cfd
2
+
3
+ require_relative '../spec_helper'
4
+
5
+ require 'time'
6
+ require 'arborist/node'
7
+
8
+
9
+ describe Arborist::Node do
10
+
11
+ let( :concrete_class ) { TestNode }
12
+
13
+ let( :identifier ) { 'the_identifier' }
14
+ let( :identifier2 ) { 'the_other_identifier' }
15
+
16
+ it "can be loaded from a file" do
17
+ concrete_instance = nil
18
+ expect( Kernel ).to receive( :load ).with( "a/path/to/a/node.rb" ) do
19
+ concrete_instance = concrete_class.new( identifier )
20
+ end
21
+
22
+ result = described_class.load( "a/path/to/a/node.rb" )
23
+ expect( result ).to be_an( Array )
24
+ expect( result.length ).to eq( 1 )
25
+ expect( result ).to include( concrete_instance )
26
+ end
27
+
28
+
29
+ it "can load multiple nodes from a single file" do
30
+ concrete_instance1 = concrete_instance2 = nil
31
+ expect( Kernel ).to receive( :load ).with( "a/path/to/a/node.rb" ) do
32
+ concrete_instance1 = concrete_class.new( identifier )
33
+ concrete_instance2 = concrete_class.new( identifier2 )
34
+ end
35
+
36
+ result = described_class.load( "a/path/to/a/node.rb" )
37
+ expect( result ).to be_an( Array )
38
+ expect( result.length ).to eq( 2 )
39
+ expect( result ).to include( concrete_instance1, concrete_instance2 )
40
+ end
41
+
42
+
43
+ it "knows what its identifier is" do
44
+ expect( described_class.new('good_identifier').identifier ).to eq( 'good_identifier' )
45
+ end
46
+
47
+
48
+ it "accepts identifiers with hyphens" do
49
+ expect( described_class.new('router_nat-pmp').identifier ).to eq( 'router_nat-pmp' )
50
+ end
51
+
52
+
53
+ it "raises an error if the node identifier is invalid" do
54
+ expect {
55
+ described_class.new 'bad identifier'
56
+ }.to raise_error( RuntimeError, /identifier/i )
57
+ end
58
+
59
+
60
+ context "an instance of a concrete subclass" do
61
+
62
+ let( :node ) { concrete_class.new(identifier) }
63
+ let( :child_node ) do
64
+ concrete_class.new(identifier2) do
65
+ parent 'the_identifier'
66
+ end
67
+ end
68
+
69
+
70
+ it "can declare what its parent is by identifier" do
71
+ expect( child_node.parent ).to eq( identifier )
72
+ end
73
+
74
+
75
+ it "can have child nodes added to it" do
76
+ node.add_child( child_node )
77
+ expect( node.children ).to include( child_node.identifier )
78
+ end
79
+
80
+
81
+ it "can have child nodes appended to it" do
82
+ node << child_node
83
+ expect( node.children ).to include( child_node.identifier )
84
+ end
85
+
86
+
87
+ it "raises an error if a node which specifies a different parent is added to it" do
88
+ not_child_node = concrete_class.new(identifier2) do
89
+ parent 'youre_not_my_mother'
90
+ end
91
+ expect {
92
+ node.add_child( not_child_node )
93
+ }.to raise_error( /not a child of/i )
94
+ end
95
+
96
+
97
+ it "doesn't add the same child more than once" do
98
+ node.add_child( child_node )
99
+ node.add_child( child_node )
100
+ expect( node.children.size ).to eq( 1 )
101
+ end
102
+
103
+
104
+ it "knows it doesn't have any children if it's empty" do
105
+ expect( node ).to_not have_children
106
+ end
107
+
108
+
109
+ it "knows it has children if subnodes have been added" do
110
+ node.add_child( child_node )
111
+ expect( node ).to have_children
112
+ end
113
+
114
+
115
+ it "knows how to remove one of its children" do
116
+ node.add_child( child_node )
117
+ node.remove_child( child_node )
118
+ expect( node ).to_not have_children
119
+ end
120
+
121
+
122
+ describe "status" do
123
+
124
+ it "starts out in `unknown` status" do
125
+ expect( node ).to be_unknown
126
+ end
127
+
128
+
129
+ it "transitions to `up` status if its state is updated with no `error` property" do
130
+ node.update( tested: true )
131
+ expect( node ).to be_up
132
+ end
133
+
134
+
135
+ it "transitions to `down` status if its state is updated with an `error` property" do
136
+ node.update( error: "Couldn't talk to it!" )
137
+ expect( node ).to be_down
138
+ end
139
+
140
+ it "transitions from `down` to `acked` status if it's updated with an `ack` property" do
141
+ node.status = 'down'
142
+ node.error = 'Something is wrong | he falls | betraying the trust | "\
143
+ "there is a disaster in his life.'
144
+ node.update( ack: {message: "Leitmotiv", sender: 'ged'} )
145
+ expect( node ).to be_acked
146
+ end
147
+
148
+ it "transitions to `up` from `acked` status if it's updated with an `ack` property" do
149
+ node.update( ack: {message: "Maintenance", sender: 'mahlon'}, error: "Offlined" )
150
+ node.update( ping_time: 0.02 )
151
+
152
+ expect( node ).to be_up
153
+ end
154
+
155
+ end
156
+
157
+
158
+ describe "Properties API" do
159
+
160
+ it "is initialized with an empty set" do
161
+ expect( node.properties ).to be_empty
162
+ end
163
+
164
+ it "can attach arbitrary values to the node" do
165
+ node.update( 'cider' => 'tasty' )
166
+ expect( node.properties['cider'] ).to eq( 'tasty' )
167
+ end
168
+
169
+ it "replaces existing values on update" do
170
+ node.properties.replace({
171
+ 'cider' => 'tasty',
172
+ 'cider_size' => '16oz',
173
+ })
174
+ node.update( 'cider_size' => '8oz' )
175
+
176
+ expect( node.properties ).to include(
177
+ 'cider' => 'tasty',
178
+ 'cider_size' => '8oz'
179
+ )
180
+ end
181
+
182
+ it "replaces nested values on update" do
183
+ node.properties.replace({
184
+ 'cider' => {
185
+ 'description' => 'tasty',
186
+ 'size' => '16oz',
187
+ },
188
+ 'sausage' => {
189
+ 'description' => 'pork',
190
+ 'size' => 'huge',
191
+ },
192
+ 'music' => '80s'
193
+ })
194
+ node.update(
195
+ 'cider' => {'size' => '8oz'},
196
+ 'sausage' => 'Linguiça',
197
+ 'music' => {
198
+ 'genre' => '80s',
199
+ 'artist' => 'The Smiths'
200
+ }
201
+ )
202
+
203
+ expect( node.properties ).to eq(
204
+ 'cider' => {
205
+ 'description' => 'tasty',
206
+ 'size' => '8oz',
207
+ },
208
+ 'sausage' => 'Linguiça',
209
+ 'music' => {
210
+ 'genre' => '80s',
211
+ 'artist' => 'The Smiths'
212
+ }
213
+ )
214
+ end
215
+
216
+ it "removes pairs whose value is nil" do
217
+ node.properties.replace({
218
+ 'cider' => {
219
+ 'description' => 'tasty',
220
+ 'size' => '16oz',
221
+ },
222
+ 'sausage' => {
223
+ 'description' => 'pork',
224
+ 'size' => 'huge',
225
+ },
226
+ 'music' => '80s'
227
+ })
228
+ node.update(
229
+ 'cider' => {'size' => nil},
230
+ 'sausage' => nil,
231
+ 'music' => {
232
+ 'genre' => '80s',
233
+ 'artist' => 'The Smiths'
234
+ }
235
+ )
236
+
237
+ expect( node.properties ).to eq(
238
+ 'cider' => {
239
+ 'description' => 'tasty',
240
+ },
241
+ 'music' => {
242
+ 'genre' => '80s',
243
+ 'artist' => 'The Smiths'
244
+ }
245
+ )
246
+ end
247
+ end
248
+
249
+
250
+ describe "Enumeration" do
251
+
252
+ it "iterates over its children for #each" do
253
+ parent = node
254
+ parent <<
255
+ concrete_class.new('child1') { parent 'the_identifier' } <<
256
+ concrete_class.new('child2') { parent 'the_identifier' } <<
257
+ concrete_class.new('child3') { parent 'the_identifier' }
258
+
259
+ expect( parent.map(&:identifier) ).to eq([ 'child1', 'child2', 'child3' ])
260
+ end
261
+
262
+ end
263
+
264
+
265
+ describe "Serialization" do
266
+
267
+ let( :node ) do
268
+ concrete_class.new( 'foo' ) do
269
+ parent 'bar'
270
+ description "The prototypical node"
271
+ tags :chunker, :hunky, :flippin, :hippo
272
+
273
+ update( 'song' => 'Around the World', 'artist' => 'Daft Punk', 'length' => '7:09' )
274
+ end
275
+ end
276
+
277
+
278
+ it "can return a Hash of serializable node data" do
279
+ result = node.to_hash
280
+
281
+ expect( result ).to be_a( Hash )
282
+ expect( result ).to include(
283
+ :identifier,
284
+ :parent, :description, :tags, :properties, :status, :ack,
285
+ :last_contacted, :status_changed, :error
286
+ )
287
+ expect( result[:identifier] ).to eq( 'foo' )
288
+ expect( result[:type] ).to eq( 'testnode' )
289
+ expect( result[:parent] ).to eq( 'bar' )
290
+ expect( result[:description] ).to eq( node.description )
291
+ expect( result[:tags] ).to eq( node.tags )
292
+ expect( result[:properties] ).to eq( node.properties )
293
+ expect( result[:status] ).to eq( node.status )
294
+ expect( result[:ack] ).to be_nil
295
+ expect( result[:last_contacted] ).to eq( node.last_contacted.iso8601 )
296
+ expect( result[:status_changed] ).to eq( node.status_changed.iso8601 )
297
+ expect( result[:error] ).to be_nil
298
+ end
299
+
300
+
301
+ it "can be reconstituted from a serialized Hash of node data" do
302
+ hash = node.to_hash
303
+ cloned_node = concrete_class.from_hash( hash )
304
+
305
+ expect( cloned_node ).to eq( node )
306
+ end
307
+
308
+
309
+ it "an ACKed node stays ACKed when reconstituted" do
310
+ node.update(ack: {
311
+ message: 'We know about the fire. It rages on.',
312
+ sender: '1986 Labyrinth David Bowie'
313
+ })
314
+ cloned_node = concrete_class.from_hash( node.to_hash )
315
+
316
+ expect( cloned_node ).to be_acked
317
+ end
318
+
319
+
320
+ it "can be marshalled" do
321
+ data = Marshal.dump( node )
322
+ cloned_node = Marshal.load( data )
323
+
324
+ expect( cloned_node ).to eq( node )
325
+ end
326
+
327
+
328
+ end
329
+
330
+ end
331
+
332
+
333
+ describe "event system" do
334
+
335
+ let( :node ) do
336
+ concrete_class.new( 'foo' ) do
337
+ parent 'bar'
338
+ description "The prototypical node"
339
+ tags :chunker, :hunky, :flippin, :hippo
340
+
341
+ update(
342
+ 'song' => 'Around the World',
343
+ 'artist' => 'Daft Punk',
344
+ 'length' => '7:09',
345
+ 'cider' => {
346
+ 'description' => 'tasty',
347
+ 'size' => '16oz',
348
+ },
349
+ 'sausage' => {
350
+ 'description' => 'pork',
351
+ 'size' => 'monsterous',
352
+ 'price' => {
353
+ 'units' => 1200,
354
+ 'currency' => 'usd'
355
+ }
356
+ },
357
+ 'music' => '80s'
358
+ )
359
+ end
360
+ end
361
+
362
+
363
+ it "generates a node.update event on update" do
364
+ events = node.update( 'song' => "Around the World" )
365
+
366
+ expect( events ).to be_an( Array )
367
+ expect( events ).to all( be_a(Arborist::Event) )
368
+ expect( events.size ).to eq( 1 )
369
+ expect( events.first.type ).to eq( 'node.update' )
370
+ expect( events.first.node ).to be( node )
371
+ end
372
+
373
+
374
+ it "generates a node.delta event when an update changes a value" do
375
+ events = node.update(
376
+ 'song' => "Motherboard",
377
+ 'artist' => 'Daft Punk',
378
+ 'sausage' => {
379
+ 'price' => {
380
+ 'currency' => 'eur'
381
+ }
382
+ }
383
+ )
384
+
385
+ expect( events ).to be_an( Array )
386
+ expect( events ).to all( be_a(Arborist::Event) )
387
+ expect( events.size ).to eq( 2 )
388
+
389
+ delta_event = events.find {|ev| ev.type == 'node.delta' }
390
+
391
+ expect( delta_event.node ).to be( node )
392
+ expect( delta_event.payload ).to eq({
393
+ 'song' => ['Around the World' , 'Motherboard'],
394
+ 'sausage' => {
395
+ 'price' => {
396
+ 'currency' => ['usd', 'eur']
397
+ }
398
+ }
399
+ })
400
+ end
401
+
402
+
403
+ it "includes status changes in delta events" do
404
+ events = node.update( error: "Couldn't talk to it!" )
405
+ delta_event = events.find {|ev| ev.type == 'node.delta' }
406
+
407
+ expect( delta_event.payload ).to include( 'status' => ['up', 'down'] )
408
+ end
409
+
410
+
411
+ it "generates a node.acked event when a node is acked" do
412
+ events = node.update(ack: {
413
+ message: "I have a poisonous friend. She's living in the house.",
414
+ sender: 'Seabound'
415
+ })
416
+
417
+ expect( events.size ).to eq( 3 )
418
+ ack_event = events.find {|ev| ev.type == 'node.acked' }
419
+
420
+ expect( ack_event ).to be_a( Arborist::Event )
421
+ expect( ack_event.payload ).to include( sender: 'Seabound' )
422
+ end
423
+
424
+ end
425
+
426
+
427
+ describe "subscriptions" do
428
+
429
+ let( :node ) do
430
+ concrete_class.new( 'foo' ) do
431
+ parent 'bar'
432
+ description "The prototypical node"
433
+ tags :chunker, :hunky, :flippin, :hippo
434
+ end
435
+ end
436
+
437
+
438
+ it "allows the addition of a Subscription" do
439
+ sub = Arborist::Subscription.new( 'test', { type: 'host'} )
440
+ node.add_subscription( sub )
441
+ expect( node.subscriptions ).to include( sub.id )
442
+ expect( node.subscriptions[sub.id] ).to be( sub )
443
+ end
444
+
445
+
446
+ it "allows the removal of a Subscription" do
447
+ sub = Arborist::Subscription.new( 'test', { type: 'host'} )
448
+ node.add_subscription( sub )
449
+ node.remove_subscription( sub.id )
450
+ expect( node.subscriptions ).to_not include( sub )
451
+ end
452
+
453
+
454
+ it "can find subscriptions that match a given event" do
455
+ events = node.update( 'song' => 'Fear', 'artist' => "Mind.in.a.Box" )
456
+ delta_event = events.find {|ev| ev.type == 'node.delta' }
457
+
458
+ sub = Arborist::Subscription.new( 'node.delta' )
459
+ node.add_subscription( sub )
460
+
461
+ results = node.find_matching_subscriptions( delta_event )
462
+
463
+ expect( results.size ).to eq( 1 )
464
+ expect( results ).to all( be_a(Arborist::Subscription) )
465
+ expect( results.first ).to be( sub )
466
+ end
467
+
468
+ end
469
+
470
+
471
+ describe "matching" do
472
+
473
+ let( :node ) do
474
+ concrete_class.new( 'foo' ) do
475
+ parent 'bar'
476
+ description "The prototypical node"
477
+ tags :chunker, :hunky, :flippin, :hippo
478
+
479
+ update(
480
+ 'song' => 'Around the World',
481
+ 'artist' => 'Daft Punk',
482
+ 'length' => '7:09',
483
+ 'cider' => {
484
+ 'description' => 'tasty',
485
+ 'size' => '16oz',
486
+ },
487
+ 'sausage' => {
488
+ 'description' => 'pork',
489
+ 'size' => 'monsterous',
490
+ 'price' => {
491
+ 'units' => 1200,
492
+ 'currency' => 'usd'
493
+ }
494
+ },
495
+ 'music' => '80s'
496
+ )
497
+ end
498
+ end
499
+
500
+
501
+ it "can be matched with its status" do
502
+ expect( node ).to match_criteria( status: 'up' )
503
+ expect( node ).to_not match_criteria( status: 'down' )
504
+ end
505
+
506
+
507
+ it "can be matched with its type" do
508
+ expect( node ).to match_criteria( type: 'testnode' )
509
+ expect( node ).to_not match_criteria( type: 'service' )
510
+ end
511
+
512
+
513
+ it "can be matched with a single tag" do
514
+ expect( node ).to match_criteria( tag: 'hunky' )
515
+ expect( node ).to_not match_criteria( tag: 'plucky' )
516
+ end
517
+
518
+
519
+ it "can be matched with multiple tags" do
520
+ expect( node ).to match_criteria( tags: ['hunky', 'hippo'] )
521
+ expect( node ).to_not match_criteria( tags: ['hunky', 'hippo', 'haggis'] )
522
+ end
523
+
524
+
525
+ it "can be matched with its identifier" do
526
+ expect( node ).to match_criteria( identifier: 'foo' )
527
+ expect( node ).to_not match_criteria( identifier: 'bar' )
528
+ end
529
+
530
+
531
+ it "can be matched with its user properties" do
532
+ expect( node ).to match_criteria( song: 'Around the World' )
533
+ expect( node ).to match_criteria( artist: 'Daft Punk' )
534
+ expect( node ).to match_criteria(
535
+ sausage: {size: 'monsterous', price: {currency: 'usd'}},
536
+ cider: { description: 'tasty'}
537
+ )
538
+
539
+ expect( node ).to_not match_criteria( length: '8:01' )
540
+ expect( node ).to_not match_criteria(
541
+ sausage: {size: 'lunch', price: {currency: 'usd'}},
542
+ cider: { description: 'tasty' }
543
+ )
544
+ expect( node ).to_not match_criteria( sausage: {size: 'lunch'} )
545
+ expect( node ).to_not match_criteria( other: 'key' )
546
+ expect( node ).to_not match_criteria( sausage: 'weißwürst' )
547
+ end
548
+
549
+ end
550
+
551
+ end
552
+