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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +2 -0
- data/ChangeLog +426 -1
- data/Manifest.txt +17 -2
- data/Nodes.md +70 -0
- data/Protocol.md +68 -9
- data/README.md +3 -5
- data/Rakefile +4 -1
- data/TODO.md +52 -20
- data/lib/arborist.rb +19 -6
- data/lib/arborist/cli.rb +39 -25
- data/lib/arborist/client.rb +97 -4
- data/lib/arborist/command/client.rb +2 -1
- data/lib/arborist/command/start.rb +51 -5
- data/lib/arborist/dependency.rb +286 -0
- data/lib/arborist/event.rb +7 -2
- data/lib/arborist/event/{node_matching.rb → node.rb} +11 -5
- data/lib/arborist/event/node_acked.rb +5 -7
- data/lib/arborist/event/node_delta.rb +30 -3
- data/lib/arborist/event/node_disabled.rb +16 -0
- data/lib/arborist/event/node_down.rb +10 -0
- data/lib/arborist/event/node_quieted.rb +11 -0
- data/lib/arborist/event/node_unknown.rb +10 -0
- data/lib/arborist/event/node_up.rb +10 -0
- data/lib/arborist/event/node_update.rb +2 -11
- data/lib/arborist/event/sys_node_added.rb +10 -0
- data/lib/arborist/event/sys_node_removed.rb +10 -0
- data/lib/arborist/exceptions.rb +4 -0
- data/lib/arborist/manager.rb +188 -18
- data/lib/arborist/manager/event_publisher.rb +1 -1
- data/lib/arborist/manager/tree_api.rb +92 -13
- data/lib/arborist/mixins.rb +17 -0
- data/lib/arborist/monitor.rb +10 -1
- data/lib/arborist/monitor/socket.rb +123 -2
- data/lib/arborist/monitor_runner.rb +6 -5
- data/lib/arborist/node.rb +420 -94
- data/lib/arborist/node/ack.rb +72 -0
- data/lib/arborist/node/host.rb +43 -8
- data/lib/arborist/node/resource.rb +73 -0
- data/lib/arborist/node/root.rb +6 -0
- data/lib/arborist/node/service.rb +89 -22
- data/lib/arborist/observer.rb +1 -1
- data/lib/arborist/subscription.rb +11 -6
- data/spec/arborist/client_spec.rb +93 -5
- data/spec/arborist/dependency_spec.rb +375 -0
- data/spec/arborist/event/node_delta_spec.rb +66 -0
- data/spec/arborist/event/node_down_spec.rb +84 -0
- data/spec/arborist/event/node_spec.rb +59 -0
- data/spec/arborist/event/node_update_spec.rb +14 -3
- data/spec/arborist/event_spec.rb +3 -3
- data/spec/arborist/manager/tree_api_spec.rb +295 -3
- data/spec/arborist/manager_spec.rb +240 -57
- data/spec/arborist/monitor_spec.rb +26 -3
- data/spec/arborist/node/ack_spec.rb +74 -0
- data/spec/arborist/node/host_spec.rb +79 -0
- data/spec/arborist/node/resource_spec.rb +56 -0
- data/spec/arborist/node/service_spec.rb +68 -2
- data/spec/arborist/node_spec.rb +288 -11
- data/spec/arborist/subscription_spec.rb +23 -14
- data/spec/arborist_spec.rb +0 -4
- data/spec/data/observers/webservices.rb +10 -2
- data/spec/spec_helper.rb +8 -0
- metadata +58 -15
- metadata.gz.sig +0 -0
- 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.
|
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
|
data/spec/arborist/node_spec.rb
CHANGED
@@ -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.
|
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, :
|
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.
|
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
|
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.
|
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
|
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
|
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
|
|