arborist 0.0.1.pre20160128152542 → 0.0.1.pre20160606141735

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 (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