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