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