arborist 0.0.1.pre20160128152542 → 0.0.1.pre20160606141735

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +2 -0
  4. data/ChangeLog +426 -1
  5. data/Manifest.txt +17 -2
  6. data/Nodes.md +70 -0
  7. data/Protocol.md +68 -9
  8. data/README.md +3 -5
  9. data/Rakefile +4 -1
  10. data/TODO.md +52 -20
  11. data/lib/arborist.rb +19 -6
  12. data/lib/arborist/cli.rb +39 -25
  13. data/lib/arborist/client.rb +97 -4
  14. data/lib/arborist/command/client.rb +2 -1
  15. data/lib/arborist/command/start.rb +51 -5
  16. data/lib/arborist/dependency.rb +286 -0
  17. data/lib/arborist/event.rb +7 -2
  18. data/lib/arborist/event/{node_matching.rb → node.rb} +11 -5
  19. data/lib/arborist/event/node_acked.rb +5 -7
  20. data/lib/arborist/event/node_delta.rb +30 -3
  21. data/lib/arborist/event/node_disabled.rb +16 -0
  22. data/lib/arborist/event/node_down.rb +10 -0
  23. data/lib/arborist/event/node_quieted.rb +11 -0
  24. data/lib/arborist/event/node_unknown.rb +10 -0
  25. data/lib/arborist/event/node_up.rb +10 -0
  26. data/lib/arborist/event/node_update.rb +2 -11
  27. data/lib/arborist/event/sys_node_added.rb +10 -0
  28. data/lib/arborist/event/sys_node_removed.rb +10 -0
  29. data/lib/arborist/exceptions.rb +4 -0
  30. data/lib/arborist/manager.rb +188 -18
  31. data/lib/arborist/manager/event_publisher.rb +1 -1
  32. data/lib/arborist/manager/tree_api.rb +92 -13
  33. data/lib/arborist/mixins.rb +17 -0
  34. data/lib/arborist/monitor.rb +10 -1
  35. data/lib/arborist/monitor/socket.rb +123 -2
  36. data/lib/arborist/monitor_runner.rb +6 -5
  37. data/lib/arborist/node.rb +420 -94
  38. data/lib/arborist/node/ack.rb +72 -0
  39. data/lib/arborist/node/host.rb +43 -8
  40. data/lib/arborist/node/resource.rb +73 -0
  41. data/lib/arborist/node/root.rb +6 -0
  42. data/lib/arborist/node/service.rb +89 -22
  43. data/lib/arborist/observer.rb +1 -1
  44. data/lib/arborist/subscription.rb +11 -6
  45. data/spec/arborist/client_spec.rb +93 -5
  46. data/spec/arborist/dependency_spec.rb +375 -0
  47. data/spec/arborist/event/node_delta_spec.rb +66 -0
  48. data/spec/arborist/event/node_down_spec.rb +84 -0
  49. data/spec/arborist/event/node_spec.rb +59 -0
  50. data/spec/arborist/event/node_update_spec.rb +14 -3
  51. data/spec/arborist/event_spec.rb +3 -3
  52. data/spec/arborist/manager/tree_api_spec.rb +295 -3
  53. data/spec/arborist/manager_spec.rb +240 -57
  54. data/spec/arborist/monitor_spec.rb +26 -3
  55. data/spec/arborist/node/ack_spec.rb +74 -0
  56. data/spec/arborist/node/host_spec.rb +79 -0
  57. data/spec/arborist/node/resource_spec.rb +56 -0
  58. data/spec/arborist/node/service_spec.rb +68 -2
  59. data/spec/arborist/node_spec.rb +288 -11
  60. data/spec/arborist/subscription_spec.rb +23 -14
  61. data/spec/arborist_spec.rb +0 -4
  62. data/spec/data/observers/webservices.rb +10 -2
  63. data/spec/spec_helper.rb +8 -0
  64. metadata +58 -15
  65. metadata.gz.sig +0 -0
  66. data/LICENSE +0 -29
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env rspec -cfd
2
+
3
+ require_relative '../../spec_helper'
4
+ require 'arborist/node/ack'
5
+
6
+
7
+ describe Arborist::Node::Ack do
8
+
9
+ it "can be constructed with a sender and a message" do
10
+ result = described_class.new( "a message", "a sender" )
11
+ expect( result ).to be_a( described_class )
12
+ expect( result.message ).to eq( "a message" )
13
+ expect( result.sender ).to eq( "a sender" )
14
+
15
+ expect( result.time ).to be_within( 2 ).of( Time.now )
16
+ expect( result.via ).to be_nil
17
+ end
18
+
19
+
20
+ it "requires a sender" do
21
+ expect {
22
+ described_class.from_hash( message: 'hi!' )
23
+ }.to raise_error( ArgumentError, /missing required ack sender/i )
24
+ end
25
+
26
+ it "requires a message" do
27
+ expect {
28
+ described_class.from_hash( sender: 'slick rick' )
29
+ }.to raise_error( ArgumentError, /missing required ack message/i )
30
+ end
31
+
32
+ it "can be round-tripped to a Hash and back" do
33
+ result = described_class.new( 'boom', 'explosivo' )
34
+ expect( described_class.from_hash(result.to_h) ).to eq( result )
35
+ end
36
+
37
+
38
+ it "can describe itself" do
39
+ ack = described_class.new( "someone else's problem", "Hike Mix" )
40
+ expect( ack.description ).to match( /by hike mix -- someone else's problem/i )
41
+ end
42
+
43
+ it "can describe itself with a via source" do
44
+ ack = described_class.new( "someone else's problem", "Hike Mix", via: "sms" )
45
+ expect( ack.description ).to match( /by hike mix via sms -- someone else's problem/i )
46
+ end
47
+
48
+
49
+ describe "time argument" do
50
+
51
+ it "can be constructed with a Time" do
52
+ result = described_class.
53
+ from_hash( message: 'message', sender: 'sender', time: Time.at(1460569977) )
54
+ expect( result.time.to_i ).to eq( 1460569977 )
55
+ end
56
+
57
+
58
+ it "can be constructed with a numeric time" do
59
+ result = described_class.
60
+ from_hash( message: 'message', sender: 'sender', time: 1460569977 )
61
+ expect( result.time.to_i ).to eq( 1460569977 )
62
+ end
63
+
64
+
65
+ it "can be constructed with a string time" do
66
+ result = described_class.
67
+ from_hash( message: 'message', sender: 'sender', time: Time.at(1460569977).iso8601 )
68
+ expect( result.time.to_i ).to eq( 1460569977 )
69
+ end
70
+
71
+ end
72
+
73
+ end
74
+
@@ -75,6 +75,85 @@ describe Arborist::Node::Host do
75
75
  end
76
76
 
77
77
 
78
+ it "can be created with address attributes" do
79
+ result = described_class.new( 'testhost', addresses: '192.168.118.3' )
80
+ expect( result.addresses ).to include( IPAddr.new('192.168.118.3') )
81
+ end
82
+
83
+
84
+ it "appends block address arguments to addresses in attributes" do
85
+ result = described_class.new( 'testhost', addresses: '192.168.118.3' ) do
86
+ address '127.0.0.1'
87
+ end
88
+
89
+ expect( result.addresses.length ).to eq( 2 )
90
+ expect( result.addresses ).to include(
91
+ IPAddr.new( '192.168.118.3' ),
92
+ IPAddr.new( '127.0.0.1' )
93
+ )
94
+ end
95
+
96
+
97
+ it "replaces its addresses when it's updated via #modify" do
98
+ result = described_class.new( 'testhost' ) do
99
+ address '192.168.118.3'
100
+ end
101
+
102
+ result.modify( addresses: ['192.168.28.2'] )
103
+
104
+ expect( result.addresses ).to include( IPAddr.new('192.168.28.2') )
105
+ expect( result.addresses ).to_not include( IPAddr.new('192.168.118.3') )
106
+ end
107
+
108
+
109
+ it "includes its addresses when turned into a Hash" do
110
+ node = described_class.new( 'testhost' ) do
111
+ address '192.168.118.3'
112
+ end
113
+
114
+ expect( node.to_h ).to include( :addresses )
115
+ expect( node.to_h[:addresses] ).to eq([ '192.168.118.3' ])
116
+ end
117
+
118
+
119
+ it "keeps its addresses when marshalled" do
120
+ node = described_class.new( 'testhost' ) do
121
+ address '192.168.118.3'
122
+ address '192.168.67.2'
123
+ end
124
+ clone = Marshal.load( Marshal.dump(node) )
125
+
126
+ expect( clone.addresses ).to eq( node.addresses )
127
+ end
128
+
129
+
130
+ it "is equal to another host node with the same metadata and addresses" do
131
+ node1 = described_class.new( 'testhost' ) do
132
+ address '192.168.118.3'
133
+ address '192.168.67.2'
134
+ end
135
+ node2 = described_class.new( 'testhost' ) do
136
+ address '192.168.118.3'
137
+ address '192.168.67.2'
138
+ end
139
+
140
+ expect( node1 ).to eq( node2 )
141
+ end
142
+
143
+
144
+ it "is not equal to another host node with the same metadata and different addresses" do
145
+ node1 = described_class.new( 'testhost' ) do
146
+ address '192.168.118.3'
147
+ address '192.168.67.2'
148
+ end
149
+ node2 = described_class.new( 'testhost' ) do
150
+ address '192.168.118.3'
151
+ end
152
+
153
+ expect( node1 ).to_not eq( node2 )
154
+ end
155
+
156
+
78
157
  describe "matching" do
79
158
 
80
159
  let( :node ) do
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env rspec -cfd
2
+
3
+ require_relative '../../spec_helper'
4
+
5
+ require 'arborist/node/resource'
6
+
7
+
8
+ describe Arborist::Node::Resource do
9
+
10
+ let( :host ) do
11
+ Arborist::Node.create( 'host', 'testhost' ) do
12
+ address '192.168.118.3'
13
+ end
14
+ end
15
+
16
+
17
+ it "can be created without reasonable defaults based on its identifier" do
18
+ result = described_class.new( 'disk', host )
19
+ expect( result.identifier ).to eq( "testhost-disk" )
20
+ end
21
+
22
+
23
+ it "raises a sensible error when created without a host" do
24
+ expect {
25
+ described_class.new( 'load', nil )
26
+ }.to raise_error( Arborist::NodeError, /no host/i )
27
+ end
28
+
29
+ describe "matching" do
30
+
31
+ let( :host ) do
32
+ Arborist::Node.create( 'host', 'testhost' ) do
33
+ address '192.168.66.12'
34
+ address '10.1.33.8'
35
+ end
36
+ end
37
+
38
+ let( :node ) do
39
+ described_class.new( 'disk', host )
40
+ end
41
+
42
+
43
+ it "can be matched with one of its host's addresses" do
44
+ expect( node ).to match_criteria( address: '192.168.66.12' )
45
+ expect( node ).to_not match_criteria( address: '127.0.0.1' )
46
+ end
47
+
48
+ it "can be matched with a netblock that includes one of its host's addresses" do
49
+ expect( node ).to match_criteria( address: '192.168.66.0/24' )
50
+ expect( node ).to match_criteria( address: '10.0.0.0/8' )
51
+ expect( node ).to_not match_criteria( address: '192.168.66.64/27' )
52
+ expect( node ).to_not match_criteria( address: '127.0.0.0/8' )
53
+ end
54
+ end
55
+ end
56
+
@@ -9,8 +9,7 @@ describe Arborist::Node::Service do
9
9
 
10
10
  let( :host ) do
11
11
  Arborist::Node.create( 'host', 'testhost' ) do
12
- address '192.168.66.12'
13
- address '10.2.12.68'
12
+ address '192.168.118.3'
14
13
  end
15
14
  end
16
15
 
@@ -49,8 +48,75 @@ describe Arborist::Node::Service do
49
48
  end
50
49
 
51
50
 
51
+ it "raises a sensible error when created without a host" do
52
+ expect {
53
+ described_class.new( 'dnsd', nil )
54
+ }.to raise_error( Arborist::NodeError, /no host/i )
55
+ end
56
+
57
+
58
+ it "includes its service attributes when turned into a Hash" do
59
+ service = described_class.new( 'dnsd', host, port: 53, protocol: 'udp', app_protocol: 'dns' )
60
+
61
+ expect( service.to_h ).to include( :port, :protocol, :app_protocol )
62
+ expect( service.to_h[:port] ).to eq( service.port )
63
+ expect( service.to_h[:protocol] ).to eq( service.protocol )
64
+ expect( service.to_h[:app_protocol] ).to eq( service.app_protocol )
65
+ end
66
+
67
+
68
+ it "keeps its service attributes when marshalled" do
69
+ service = described_class.new( 'dnsd', host, port: 53, protocol: 'udp', app_protocol: 'dns' )
70
+
71
+ expect( service.to_h ).to include( :port, :protocol, :app_protocol )
72
+ expect( service.to_h[:port] ).to eq( service.port )
73
+ expect( service.to_h[:protocol] ).to eq( service.protocol )
74
+ expect( service.to_h[:app_protocol] ).to eq( service.app_protocol )
75
+ end
76
+
77
+
78
+ it "is equal to another service node with the same metadata and service attributes" do
79
+ service1 = described_class.new( 'dnsd', host, port: 53, protocol: 'udp', app_protocol: 'dns' )
80
+ service2 = described_class.new( 'dnsd', host, port: 53, protocol: 'udp', app_protocol: 'dns' )
81
+
82
+ expect( service1 ).to eq( service2 )
83
+ end
84
+
85
+
86
+ it "is not equal to another service node with the same metadata and different service attributes" do
87
+ service1 = described_class.new( 'dnsd', host, port: 53, protocol: 'udp', app_protocol: 'dns' )
88
+ service2 = described_class.new( 'dnsd', host, port: 53, protocol: 'tcp', app_protocol: 'dns' )
89
+
90
+ expect( service1 ).to_not eq( service2 )
91
+ end
92
+
93
+
94
+ it "is not equal to another service node with the same metadata and different port" do
95
+ service1 = described_class.new( 'dnsd', host, port: 53, protocol: 'udp', app_protocol: 'dns' )
96
+ service2 = described_class.new( 'dnsd', host, port: 80, protocol: 'udp', app_protocol: 'dns' )
97
+
98
+ expect( service1 ).to_not eq( service2 )
99
+ end
100
+
101
+
102
+ it "is not equal to another service node with the same metadata and different app protocol" do
103
+ service1 = described_class.new( 'dnsd', host, port: 53, protocol: 'udp', app_protocol: 'dns' )
104
+ service2 = described_class.new( 'dnsd', host, port: 53, protocol: 'udp', app_protocol: 'smtp' )
105
+
106
+ expect( service1 ).to_not eq( service2 )
107
+ end
108
+
109
+
110
+
52
111
  describe "matching" do
53
112
 
113
+ let( :host ) do
114
+ Arborist::Node.create( 'host', 'testhost' ) do
115
+ address '192.168.66.12'
116
+ address '10.1.33.8'
117
+ end
118
+ end
119
+
54
120
  let( :node ) do
55
121
  described_class.new( 'ssh', host )
56
122
  end
@@ -9,10 +9,12 @@ require 'arborist/node'
9
9
  describe Arborist::Node do
10
10
 
11
11
  let( :concrete_class ) { TestNode }
12
+ let( :subnode_class ) { TestSubNode }
12
13
 
13
14
  let( :identifier ) { 'the_identifier' }
14
15
  let( :identifier2 ) { 'the_other_identifier' }
15
16
 
17
+
16
18
  it "can be loaded from a file" do
17
19
  concrete_instance = nil
18
20
  expect( Kernel ).to receive( :load ).with( "a/path/to/a/node.rb" ) do
@@ -26,6 +28,20 @@ describe Arborist::Node do
26
28
  end
27
29
 
28
30
 
31
+ it "can be constructed from a Hash" do
32
+ instance = concrete_class.new( identifier,
33
+ parent: 'branch',
34
+ description: 'A testing node',
35
+ tags: ['internal', 'testing']
36
+ )
37
+
38
+ expect( instance ).to be_a( described_class )
39
+ expect( instance.parent ).to eq( 'branch' )
40
+ expect( instance.description ).to eq( 'A testing node' )
41
+ expect( instance.tags ).to include( 'internal', 'testing' )
42
+ end
43
+
44
+
29
45
  it "can load multiple nodes from a single file" do
30
46
  concrete_instance1 = concrete_instance2 = nil
31
47
  expect( Kernel ).to receive( :load ).with( "a/path/to/a/node.rb" ) do
@@ -57,6 +73,23 @@ describe Arborist::Node do
57
73
  end
58
74
 
59
75
 
76
+ context "subnode classes" do
77
+
78
+ it "can declare the type of node they live under" do
79
+ expect( subnode_class.parent_types ).to include( described_class.get_subclass(:test) )
80
+ end
81
+
82
+
83
+ it "can be constructed via a factory method on instances of their parent type" do
84
+ parent = concrete_class.new( 'branch' )
85
+ node = parent.testsub( 'leaf' )
86
+ expect( node ).to be_an_instance_of( subnode_class )
87
+ expect( node.parent ).to eq( parent.identifier )
88
+ end
89
+
90
+ end
91
+
92
+
60
93
  context "an instance of a concrete subclass" do
61
94
 
62
95
  let( :node ) { concrete_class.new(identifier) }
@@ -145,6 +178,26 @@ describe Arborist::Node do
145
178
  expect( node ).to be_acked
146
179
  end
147
180
 
181
+ it "transitions from `acked` to `up` status if its error is cleared" do
182
+ node.status = 'down'
183
+ node.error = 'Something is wrong | he falls | betraying the trust | "\
184
+ "there is a disaster in his life.'
185
+ node.update( ack: {message: "Leitmotiv", sender: 'ged'} )
186
+ node.update( error: nil )
187
+
188
+ expect( node ).to be_up
189
+ end
190
+
191
+ it "stays `up` if its error is cleared and stays cleared" do
192
+ node.status = 'down'
193
+ node.error = 'stay up damn you!'
194
+ node.update( ack: {message: "Leitmotiv", sender: 'ged'} )
195
+ node.update( error: nil )
196
+ node.update( error: nil )
197
+
198
+ expect( node ).to be_up
199
+ end
200
+
148
201
  it "transitions to `disabled` from `up` status if it's updated with an `ack` property" do
149
202
  node.status = 'up'
150
203
  node.update( ack: {message: "Maintenance", sender: 'mahlon'} )
@@ -298,19 +351,96 @@ describe Arborist::Node do
298
351
  description "The prototypical node"
299
352
  tags :chunker, :hunky, :flippin, :hippo
300
353
 
354
+ depends_on(
355
+ all_of('postgres', 'rabbitmq', 'memcached', on: 'svchost'),
356
+ any_of('webproxy', on: ['fe-host1','fe-host2','fe-host3'])
357
+ )
358
+
301
359
  update( 'song' => 'Around the World', 'artist' => 'Daft Punk', 'length' => '7:09' )
302
360
  end
303
361
  end
304
362
 
305
363
 
364
+ it "can restore saved state from an older copy of the node" do
365
+ old_node = Marshal.load( Marshal.dump(node) )
366
+
367
+ old_node.status = 'down'
368
+ old_node.status_changed = Time.now - 400
369
+ old_node.error = "Host unreachable"
370
+ old_node.update(
371
+ ack: {
372
+ 'time' => Time.now - 200,
373
+ 'message' => "Technician dispatched.",
374
+ 'sender' => 'darby@example.com'
375
+ }
376
+ )
377
+ old_node.properties.replace(
378
+ 'ping' => {
379
+ 'ttl' => 0.23
380
+ }
381
+ )
382
+ old_node.last_contacted = Time.now - 28
383
+ old_node.dependencies.mark_down( 'svchost-postgres' )
384
+
385
+ node.restore( old_node )
386
+
387
+ expect( node.status ).to eq( old_node.status )
388
+ expect( node.status_changed ).to eq( old_node.status_changed )
389
+ expect( node.error ).to eq( old_node.error )
390
+ expect( node.ack ).to eq( old_node.ack )
391
+ expect( node.properties ).to include( old_node.properties )
392
+ expect( node.last_contacted ).to eq( old_node.last_contacted )
393
+ expect( node.dependencies ).to eql( old_node.dependencies )
394
+ end
395
+
396
+
397
+ it "doesn't restore operational attributes from the node file on disk with those from saved state" do
398
+ old_node = Marshal.load( Marshal.dump(node) )
399
+ node_copy = Marshal.load( Marshal.dump(node) )
400
+
401
+ old_node.instance_variable_set( :@parent, 'foo' )
402
+ old_node.instance_variable_set( :@description, 'Some older description' )
403
+ old_node.tags( :bunker, :lucky, :tickle, :trucker )
404
+ old_node.source = '/somewhere/else'
405
+
406
+ node.restore( old_node )
407
+
408
+ expect( node.parent ).to eq( node_copy.parent )
409
+ expect( node.description ).to eq( node_copy.description )
410
+ expect( node.tags ).to eq( node_copy.tags )
411
+ expect( node.source ).to eq( node_copy.source )
412
+ expect( node.dependencies ).to eq( node_copy.dependencies )
413
+ end
414
+
415
+
416
+ it "doesn't replace dependencies if they've changed" do
417
+ old_node = Marshal.load( Marshal.dump(node) )
418
+ old_node.dependencies.mark_down( 'svchost-postgres' )
419
+ old_node.dependencies.mark_down( 'svchost-rabbitmq' )
420
+
421
+ # Drop 'svchost-rabbitmq'
422
+ node.depends_on(
423
+ node.all_of('postgres', 'memcached', on: 'svchost'),
424
+ node.any_of('webproxy', on: ['fe-host1','fe-host2','fe-host3'])
425
+ )
426
+
427
+ node.restore( old_node )
428
+
429
+ expect( node.dependencies ).to_not eql( old_node.dependencies )
430
+ expect( node.dependencies.all_identifiers ).to_not include( 'svchost-rabbitmq' )
431
+ expect( node.dependencies.down_subdeps.length ).to eq( 1 )
432
+ end
433
+
434
+
306
435
  it "can return a Hash of serializable node data" do
307
- result = node.to_hash
436
+ result = node.to_h
308
437
 
309
438
  expect( result ).to be_a( Hash )
310
439
  expect( result ).to include(
311
440
  :identifier,
312
- :parent, :description, :tags, :properties, :status, :ack,
313
- :last_contacted, :status_changed, :error
441
+ :parent, :description, :tags, :properties, :ack, :status,
442
+ :last_contacted, :status_changed, :error, :quieted_reasons,
443
+ :dependencies
314
444
  )
315
445
  expect( result[:identifier] ).to eq( 'foo' )
316
446
  expect( result[:type] ).to eq( 'testnode' )
@@ -318,29 +448,33 @@ describe Arborist::Node do
318
448
  expect( result[:description] ).to eq( node.description )
319
449
  expect( result[:tags] ).to eq( node.tags )
320
450
  expect( result[:properties] ).to eq( node.properties )
321
- expect( result[:status] ).to eq( node.status )
322
451
  expect( result[:ack] ).to be_nil
323
452
  expect( result[:last_contacted] ).to eq( node.last_contacted.iso8601 )
324
453
  expect( result[:status_changed] ).to eq( node.status_changed.iso8601 )
325
454
  expect( result[:error] ).to be_nil
455
+ expect( result[:dependencies] ).to be_a( Hash )
456
+ expect( result[:quieted_reasons] ).to be_a( Hash )
326
457
  end
327
458
 
328
459
 
329
460
  it "can be reconstituted from a serialized Hash of node data" do
330
- hash = node.to_hash
461
+ hash = node.to_h
331
462
  cloned_node = concrete_class.from_hash( hash )
332
463
 
333
464
  expect( cloned_node ).to eq( node )
334
465
  end
335
466
 
336
467
 
337
- it "an ACKed node stays ACKed when reconstituted" do
468
+ it "an ACKed node goes back to ACKed when re-added to the tree" do
469
+
338
470
  node.update( error: "there's a fire" )
339
471
  node.update( ack: {
340
472
  message: 'We know about the fire. It rages on.',
341
473
  sender: '1986 Labyrinth David Bowie'
342
474
  })
343
- cloned_node = concrete_class.from_hash( node.to_hash )
475
+ cloned_node = concrete_class.from_hash( node.to_h )
476
+ node_added_event = Arborist::Event.create( :sys_node_added, cloned_node )
477
+ cloned_node.handle_event( node_added_event )
344
478
 
345
479
  expect( cloned_node ).to be_acked
346
480
  end
@@ -448,7 +582,7 @@ describe Arborist::Node do
448
582
  ack_event = events.find {|ev| ev.type == 'node.acked' }
449
583
 
450
584
  expect( ack_event ).to be_a( Arborist::Event )
451
- expect( ack_event.payload ).to include( sender: 'Seabound' )
585
+ expect( ack_event.payload ).to include( ack: a_hash_including(sender: 'Seabound') )
452
586
  end
453
587
 
454
588
  end
@@ -466,7 +600,7 @@ describe Arborist::Node do
466
600
 
467
601
 
468
602
  it "allows the addition of a Subscription" do
469
- sub = Arborist::Subscription.new( 'test', { type: 'host'} )
603
+ sub = Arborist::Subscription.new {}
470
604
  node.add_subscription( sub )
471
605
  expect( node.subscriptions ).to include( sub.id )
472
606
  expect( node.subscriptions[sub.id] ).to be( sub )
@@ -474,7 +608,7 @@ describe Arborist::Node do
474
608
 
475
609
 
476
610
  it "allows the removal of a Subscription" do
477
- sub = Arborist::Subscription.new( 'test', { type: 'host'} )
611
+ sub = Arborist::Subscription.new {}
478
612
  node.add_subscription( sub )
479
613
  node.remove_subscription( sub.id )
480
614
  expect( node.subscriptions ).to_not include( sub )
@@ -485,7 +619,7 @@ describe Arborist::Node do
485
619
  events = node.update( 'song' => 'Fear', 'artist' => "Mind.in.a.Box" )
486
620
  delta_event = events.find {|ev| ev.type == 'node.delta' }
487
621
 
488
- sub = Arborist::Subscription.new( 'node.delta' )
622
+ sub = Arborist::Subscription.new( 'node.delta' ) {}
489
623
  node.add_subscription( sub )
490
624
 
491
625
  results = node.find_matching_subscriptions( delta_event )
@@ -578,5 +712,148 @@ describe Arborist::Node do
578
712
 
579
713
  end
580
714
 
715
+
716
+ describe "secondary dependencies" do
717
+
718
+ let( :provider_node_parent ) do
719
+ concrete_class.new( 'san' )
720
+ end
721
+
722
+ let( :provider_node ) do
723
+ concrete_class.new( 'san-iscsi' ) do
724
+ parent 'san'
725
+ end
726
+ end
727
+
728
+ let( :node ) do
729
+ concrete_class.new( 'appserver' ) do
730
+ description "An appserver virtual machine"
731
+ end
732
+ end
733
+
734
+ let( :manager ) do
735
+ man = Arborist::Manager.new
736
+ man.load_tree([ node, provider_node, provider_node_parent ])
737
+ man
738
+ end
739
+
740
+
741
+ it "can be declared for a node" do
742
+ node.depends_on( 'san-iscsi' )
743
+ expect( node ).to have_dependencies
744
+ expect( node.dependencies ).to include( 'san-iscsi' )
745
+ end
746
+
747
+
748
+ it "can't be declared for the root node" do
749
+ expect {
750
+ node.depends_on( '_' )
751
+ }.to raise_exception( Arborist::ConfigError, /root node/i )
752
+ end
753
+
754
+
755
+ it "can't be declared for itself" do
756
+ expect {
757
+ node.depends_on( 'appserver' )
758
+ }.to raise_exception( Arborist::ConfigError, /itself/i )
759
+ end
760
+
761
+
762
+ it "can't be declared for any of its ancestors" do
763
+ provider_node.depends_on( 'san' )
764
+
765
+ expect {
766
+ provider_node.register_secondary_dependencies( manager )
767
+ }.to raise_exception( Arborist::ConfigError, /ancestor/i )
768
+ end
769
+
770
+
771
+ it "can't be declared for any of its decendants" do
772
+ provider_node_parent.depends_on( 'san-iscsi' )
773
+
774
+ expect {
775
+ provider_node_parent.register_secondary_dependencies( manager )
776
+ }.to raise_exception( Arborist::ConfigError, /descendant/i )
777
+ end
778
+
779
+
780
+ it "can be declared with a simple identifier" do
781
+ node.depends_on( 'san-iscsi' )
782
+
783
+ expect {
784
+ node.register_secondary_dependencies( manager )
785
+ }.to_not raise_exception
786
+ end
787
+
788
+
789
+ it "can be declared on a service on a host" do
790
+ node.depends_on( 'iscsi', on: 'san' )
791
+ expect( node ).to have_dependencies
792
+ expect( node.dependencies.behavior ).to eq( :all )
793
+ expect( node.dependencies.identifiers ).to include( 'san-iscsi' )
794
+ end
795
+
796
+
797
+ it "can be declared for unrelated identifiers"
798
+ it "can be declared for related identifiers"
799
+
800
+
801
+ it "can be declared for all of a group of identifiers"
802
+ it "can be declared for any of a group of identifiers"
803
+
804
+
805
+ it "cause the node to be quieted when the dependent node goes down" do
806
+ node.depends_on( provider_node.identifier )
807
+ node.register_secondary_dependencies( manager )
808
+
809
+ events = provider_node.update( error: "fatal disk error: offlined" )
810
+ provider_node.publish_events( *events )
811
+
812
+ expect( node ).to be_quieted
813
+ expect( node ).to have_downed_dependencies
814
+ # :TODO: Quieted description?
815
+ end
816
+
817
+ end
818
+
819
+
820
+ describe "operational attribute modification" do
821
+
822
+
823
+ let( :node ) do
824
+ concrete_class.new( 'foo' ) do
825
+ parent 'bar'
826
+ description "The prototypical node"
827
+ tags :chunker, :hunky, :flippin, :hippo
828
+ end
829
+ end
830
+
831
+
832
+ it "can change its parent" do
833
+ node.modify( parent: 'foo' )
834
+ expect( node.parent ).to eq( 'foo' )
835
+ end
836
+
837
+
838
+ it "can change its description" do
839
+ node.modify( description: 'A different node' )
840
+ expect( node.description ).to eq( 'A different node' )
841
+ end
842
+
843
+
844
+ it "can change its tags" do
845
+ node.modify( tags: %w[dew dairy daisy dilettante] )
846
+ expect( node.tags ).to eq( %w[dew dairy daisy dilettante] )
847
+ end
848
+
849
+
850
+ it "arrayifies tags modifications" do
851
+ node.modify( tags: 'single' )
852
+ expect( node.tags ).to eq( %w[single] )
853
+ end
854
+
855
+
856
+ end
857
+
581
858
  end
582
859