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,217 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
#encoding: utf-8
|
3
|
+
|
4
|
+
require 'rbczmq'
|
5
|
+
require 'loggability'
|
6
|
+
|
7
|
+
require 'arborist' unless defined?( Arborist )
|
8
|
+
require 'arborist/client'
|
9
|
+
|
10
|
+
|
11
|
+
# Undo the useless scoping
|
12
|
+
class ZMQ::Loop
|
13
|
+
public_class_method :instance
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
# An event-driven runner for Arborist::Monitors.
|
18
|
+
class Arborist::MonitorRunner
|
19
|
+
extend Loggability
|
20
|
+
|
21
|
+
log_to :arborist
|
22
|
+
|
23
|
+
|
24
|
+
# A ZMQ::Handler object for managing IO for all running monitors.
|
25
|
+
class Handler < ZMQ::Handler
|
26
|
+
extend Loggability,
|
27
|
+
Arborist::MethodUtilities
|
28
|
+
|
29
|
+
log_to :arborist
|
30
|
+
|
31
|
+
### Create a ZMQ::Handler that acts as the agent that runs the specified
|
32
|
+
### +monitor+.
|
33
|
+
def initialize( reactor )
|
34
|
+
@reactor = reactor
|
35
|
+
@client = Arborist::Client.new
|
36
|
+
@pollitem = ZMQ::Pollitem.new( @client.tree_api, ZMQ::POLLOUT )
|
37
|
+
@pollitem.handler = self
|
38
|
+
|
39
|
+
@request_queue = {}
|
40
|
+
@registered = false
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
######
|
45
|
+
public
|
46
|
+
######
|
47
|
+
|
48
|
+
# The ZMQ::Loop that this runner is registered with
|
49
|
+
attr_reader :reactor
|
50
|
+
|
51
|
+
# The Queue of pending requests, keyed by the callback that should be called with the
|
52
|
+
# results.
|
53
|
+
attr_reader :request_queue
|
54
|
+
|
55
|
+
# The Arborist::Client that will provide the message packing and unpacking
|
56
|
+
attr_reader :client
|
57
|
+
|
58
|
+
##
|
59
|
+
# True if the Handler is registered to write one or more requests
|
60
|
+
attr_predicate :registered
|
61
|
+
|
62
|
+
|
63
|
+
### Run the specified +monitor+ and update nodes with the results.
|
64
|
+
def run_monitor( monitor )
|
65
|
+
criteria = monitor.positive_criteria
|
66
|
+
include_down = monitor.include_down?
|
67
|
+
props = monitor.node_properties
|
68
|
+
|
69
|
+
self.fetch( criteria, include_down, props ) do |nodes|
|
70
|
+
# :FIXME: Doesn't apply negative criteria
|
71
|
+
results = monitor.run( nodes )
|
72
|
+
self.update( results ) do
|
73
|
+
self.log.debug "Updated %d via the '%s' monitor" %
|
74
|
+
[ results.length, monitor.description ]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
### Create a fetch request using the runner's client, then queue the request up
|
81
|
+
### with the specified +block+ as the callback.
|
82
|
+
def fetch( criteria, include_down, properties, &block )
|
83
|
+
fetch = self.client.make_fetch_request( criteria,
|
84
|
+
include_down: include_down,
|
85
|
+
properties: properties
|
86
|
+
)
|
87
|
+
self.queue_request( fetch, &block )
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
### Create an update request using the runner's client, then queue the request up
|
92
|
+
### with the specified +block+ as the callback.
|
93
|
+
def update( nodemap, &block )
|
94
|
+
update = self.client.make_update_request( nodemap )
|
95
|
+
self.queue_request( update, &block )
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
### Add the specified +event+ to the queue to be published to the console event
|
100
|
+
### socket
|
101
|
+
def queue_request( request, &callback )
|
102
|
+
self.request_queue[ callback ] = request
|
103
|
+
self.register
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
### Register the handler's pollitem as being ready to write if it isn't already.
|
108
|
+
def register
|
109
|
+
# self.log.debug "Registering for writing."
|
110
|
+
self.reactor.register( self.pollitem ) unless @registered
|
111
|
+
@registered = true
|
112
|
+
end
|
113
|
+
|
114
|
+
|
115
|
+
### Unregister the handler's pollitem from the reactor when there's nothing ready
|
116
|
+
### to write.
|
117
|
+
def unregister
|
118
|
+
# self.log.debug "Unregistering for writing."
|
119
|
+
self.reactor.remove( self.pollitem ) if @registered
|
120
|
+
@registered = false
|
121
|
+
end
|
122
|
+
|
123
|
+
|
124
|
+
### Write commands from the queue
|
125
|
+
def on_writable
|
126
|
+
if (( pair = self.request_queue.shift ))
|
127
|
+
callback, request = *pair
|
128
|
+
res = self.client.send_tree_api_request( request )
|
129
|
+
callback.call( res )
|
130
|
+
end
|
131
|
+
|
132
|
+
self.unregister if self.request_queue.empty?
|
133
|
+
return true
|
134
|
+
end
|
135
|
+
|
136
|
+
end # class Handler
|
137
|
+
|
138
|
+
|
139
|
+
### Create a new Arborist::MonitorRunner
|
140
|
+
def initialize
|
141
|
+
@monitors = []
|
142
|
+
@handler = nil
|
143
|
+
@reactor = ZMQ::Loop.new
|
144
|
+
end
|
145
|
+
|
146
|
+
|
147
|
+
######
|
148
|
+
public
|
149
|
+
######
|
150
|
+
|
151
|
+
# The Array of loaded Arborist::Monitors the runner should run.
|
152
|
+
attr_reader :monitors
|
153
|
+
|
154
|
+
# The ZMQ::Handler subclass that handles all async IO
|
155
|
+
attr_accessor :handler
|
156
|
+
|
157
|
+
# The reactor (a ZMQ::Loop) the runner uses to drive everything
|
158
|
+
attr_accessor :reactor
|
159
|
+
|
160
|
+
|
161
|
+
### Load monitors from the specified +enumerator+.
|
162
|
+
def load_monitors( enumerator )
|
163
|
+
@monitors += enumerator.to_a
|
164
|
+
end
|
165
|
+
|
166
|
+
|
167
|
+
### Run the specified +monitors+
|
168
|
+
def run
|
169
|
+
self.handler = Arborist::MonitorRunner::Handler.new( self.reactor )
|
170
|
+
|
171
|
+
self.monitors.each do |mon|
|
172
|
+
self.add_timer_for( mon )
|
173
|
+
end
|
174
|
+
|
175
|
+
self.reactor.start
|
176
|
+
end
|
177
|
+
|
178
|
+
|
179
|
+
### Register a timer for the specified +monitor+.
|
180
|
+
def add_timer_for( monitor )
|
181
|
+
interval = monitor.interval
|
182
|
+
|
183
|
+
timer = if monitor.splay.nonzero?
|
184
|
+
self.splay_timer_for( monitor )
|
185
|
+
else
|
186
|
+
self.interval_timer_for( monitor )
|
187
|
+
end
|
188
|
+
|
189
|
+
self.reactor.register_timer( timer )
|
190
|
+
end
|
191
|
+
|
192
|
+
|
193
|
+
### Create a repeating ZMQ::Timer that will run the specified monitor on its interval.
|
194
|
+
def interval_timer_for( monitor )
|
195
|
+
interval = monitor.interval
|
196
|
+
self.log.info "Creating timer for %s monitor to run every %ds" % [ monitor, interval ]
|
197
|
+
|
198
|
+
return ZMQ::Timer.new( interval, 0 ) do
|
199
|
+
self.handler.run_monitor( monitor )
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
|
204
|
+
### Create a one-shot ZMQ::Timer that will register the interval timer for the specified
|
205
|
+
### +monitor+ after a random number of seconds no greater than its splay.
|
206
|
+
def splay_timer_for( monitor )
|
207
|
+
delay = rand( monitor.splay )
|
208
|
+
self.log.info "Splaying registration of %s monitor for %ds" % [ monitor, delay ]
|
209
|
+
|
210
|
+
return ZMQ::Timer.new( delay, 1 ) do
|
211
|
+
interval_timer = self.interval_timer_for( monitor )
|
212
|
+
self.reactor.register_timer( interval_timer )
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
end # class Arborist::MonitorRunner
|
217
|
+
|
@@ -0,0 +1,700 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
#encoding: utf-8
|
3
|
+
|
4
|
+
require 'set'
|
5
|
+
require 'uri'
|
6
|
+
require 'time'
|
7
|
+
require 'pathname'
|
8
|
+
require 'state_machines'
|
9
|
+
|
10
|
+
require 'loggability'
|
11
|
+
require 'pluggability'
|
12
|
+
require 'arborist' unless defined?( Arborist )
|
13
|
+
require 'arborist/mixins'
|
14
|
+
|
15
|
+
using Arborist::TimeRefinements
|
16
|
+
|
17
|
+
|
18
|
+
# The basic node class for an Arborist tree
|
19
|
+
class Arborist::Node
|
20
|
+
include Enumerable,
|
21
|
+
Arborist::HashUtilities
|
22
|
+
extend Loggability,
|
23
|
+
Pluggability,
|
24
|
+
Arborist::MethodUtilities
|
25
|
+
|
26
|
+
|
27
|
+
##
|
28
|
+
# The key for the thread local that is used to track instances as they're
|
29
|
+
# loaded.
|
30
|
+
LOADED_INSTANCE_KEY = :loaded_node_instances
|
31
|
+
|
32
|
+
##
|
33
|
+
# The glob pattern to use for searching for node
|
34
|
+
NODE_FILE_PATTERN = '**/*.rb'
|
35
|
+
|
36
|
+
|
37
|
+
##
|
38
|
+
# The struct for the 'ack' operational property
|
39
|
+
ACK = Struct.new( 'ArboristNodeACK', :message, :via, :sender, :time )
|
40
|
+
|
41
|
+
##
|
42
|
+
# The keys required to be set for an ACK
|
43
|
+
ACK_REQUIRED_PROPERTIES = %w[ message sender ]
|
44
|
+
|
45
|
+
|
46
|
+
##
|
47
|
+
# Log via the Arborist logger
|
48
|
+
log_to :arborist
|
49
|
+
|
50
|
+
##
|
51
|
+
# Search for plugins in lib/arborist/node directories in loaded gems
|
52
|
+
plugin_prefixes 'arborist/node'
|
53
|
+
|
54
|
+
|
55
|
+
state_machine( :status, initial: :unknown ) do
|
56
|
+
|
57
|
+
state :unknown,
|
58
|
+
:up,
|
59
|
+
:down,
|
60
|
+
:acked
|
61
|
+
|
62
|
+
event :update do
|
63
|
+
transition any - [:acked] => :acked, if: :ack_set?
|
64
|
+
transition any - [:up] => :up, if: :last_contact_successful?
|
65
|
+
transition any - [:down, :acked] => :down, unless: :last_contact_successful?
|
66
|
+
end
|
67
|
+
|
68
|
+
after_transition any => :acked, do: :on_ack
|
69
|
+
after_transition :acked => :up, do: :on_ack_cleared
|
70
|
+
after_transition :down => :up, do: :on_node_up
|
71
|
+
after_transition [:unknown, :up] => :down, do: :on_node_down
|
72
|
+
|
73
|
+
after_transition do: :add_status_to_update_delta
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
### Return a curried Proc for the ::create method for the specified +type+.
|
78
|
+
def self::curried_create( type )
|
79
|
+
return self.method( :create ).to_proc.curry( 2 )[ type ]
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
### Overridden to track instances of created nodes for the DSL.
|
84
|
+
def self::new( * )
|
85
|
+
new_instance = super
|
86
|
+
Arborist::Node.add_loaded_instance( new_instance )
|
87
|
+
return new_instance
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
### Create a new node with its state read from the specified +hash+.
|
92
|
+
def self::from_hash( hash )
|
93
|
+
return self.new( hash[:identifier] ) do
|
94
|
+
self.marshal_load( hash )
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
### Record a new loaded instance if the Thread-local variable is set up to track
|
100
|
+
### them.
|
101
|
+
def self::add_loaded_instance( new_instance )
|
102
|
+
instances = Thread.current[ LOADED_INSTANCE_KEY ] or return
|
103
|
+
instances << new_instance
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
### Inheritance hook -- add a DSL declarative function for the given +subclass+.
|
108
|
+
def self::inherited( subclass )
|
109
|
+
super
|
110
|
+
|
111
|
+
if name = subclass.name
|
112
|
+
name.sub!( /.*::/, '' )
|
113
|
+
body = self.curried_create( subclass )
|
114
|
+
Arborist.add_dsl_constructor( name, &body )
|
115
|
+
else
|
116
|
+
self.log.info "Skipping DSL constructor for anonymous class."
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
### Load the specified +file+ and return any new Nodes created as a result.
|
123
|
+
def self::load( file )
|
124
|
+
self.log.info "Loading node file %s..." % [ file ]
|
125
|
+
Thread.current[ LOADED_INSTANCE_KEY ] = []
|
126
|
+
|
127
|
+
begin
|
128
|
+
Kernel.load( file )
|
129
|
+
rescue => err
|
130
|
+
self.log.error "%p while loading %s: %s" % [ err.class, file, err.message ]
|
131
|
+
raise
|
132
|
+
end
|
133
|
+
|
134
|
+
return Thread.current[ LOADED_INSTANCE_KEY ]
|
135
|
+
ensure
|
136
|
+
Thread.current[ LOADED_INSTANCE_KEY ] = nil
|
137
|
+
end
|
138
|
+
|
139
|
+
|
140
|
+
### Return an iterator for all the node files in the specified +directory+.
|
141
|
+
def self::each_in( directory )
|
142
|
+
path = Pathname( directory )
|
143
|
+
paths = if path.directory?
|
144
|
+
Pathname.glob( directory + NODE_FILE_PATTERN )
|
145
|
+
else
|
146
|
+
[ path ]
|
147
|
+
end
|
148
|
+
|
149
|
+
return paths.flat_map do |file|
|
150
|
+
file_url = "file://%s" % [ file.expand_path ]
|
151
|
+
nodes = self.load( file )
|
152
|
+
self.log.debug "Loaded nodes %p..." % [ nodes ]
|
153
|
+
nodes.each do |node|
|
154
|
+
node.source = file_url
|
155
|
+
end
|
156
|
+
nodes
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
|
161
|
+
### Create a new Node with the specified +identifier+, which must be unique to the
|
162
|
+
### loaded tree.
|
163
|
+
def initialize( identifier, &block )
|
164
|
+
raise "Invalid identifier %p" % [identifier] unless
|
165
|
+
identifier =~ /^\w[\w\-]*$/
|
166
|
+
|
167
|
+
@identifier = identifier
|
168
|
+
@parent = '_'
|
169
|
+
@description = nil
|
170
|
+
@tags = Set.new
|
171
|
+
@source = nil
|
172
|
+
@children = {}
|
173
|
+
|
174
|
+
@status = 'unknown'
|
175
|
+
@status_changed = Time.at( 0 )
|
176
|
+
|
177
|
+
@error = nil
|
178
|
+
@ack = nil
|
179
|
+
@properties = {}
|
180
|
+
@last_contacted = Time.at( 0 )
|
181
|
+
|
182
|
+
@update_delta = Hash.new do |h,k|
|
183
|
+
h[ k ] = Hash.new( &h.default_proc )
|
184
|
+
end
|
185
|
+
@pending_update_events = []
|
186
|
+
@subscriptions = {}
|
187
|
+
|
188
|
+
self.instance_eval( &block ) if block
|
189
|
+
end
|
190
|
+
|
191
|
+
|
192
|
+
######
|
193
|
+
public
|
194
|
+
######
|
195
|
+
|
196
|
+
##
|
197
|
+
# The node's identifier
|
198
|
+
attr_reader :identifier
|
199
|
+
|
200
|
+
##
|
201
|
+
# The URI of the source the object was read from
|
202
|
+
attr_reader :source
|
203
|
+
|
204
|
+
##
|
205
|
+
# The Hash of nodes which are children of this node, keyed by identifier
|
206
|
+
attr_reader :children
|
207
|
+
|
208
|
+
##
|
209
|
+
# Arbitrary attributes attached to this node via the manager API
|
210
|
+
attr_reader :properties
|
211
|
+
|
212
|
+
##
|
213
|
+
# The Time the node was last contacted
|
214
|
+
attr_accessor :last_contacted
|
215
|
+
|
216
|
+
##
|
217
|
+
# The Time the node's status last changed.
|
218
|
+
attr_accessor :status_changed
|
219
|
+
|
220
|
+
##
|
221
|
+
# The last error encountered by a monitor attempting to update this node.
|
222
|
+
attr_accessor :error
|
223
|
+
|
224
|
+
##
|
225
|
+
# The acknowledgement currently in effect. Should be an instance of Arborist::Node::ACK
|
226
|
+
attr_accessor :ack
|
227
|
+
|
228
|
+
##
|
229
|
+
# The Hash of changes tracked during an #update.
|
230
|
+
attr_reader :update_delta
|
231
|
+
|
232
|
+
##
|
233
|
+
# The Array of events generated by the current update event
|
234
|
+
attr_reader :pending_update_events
|
235
|
+
|
236
|
+
##
|
237
|
+
# The Hash of Subscription objects observing this node and its children, keyed by
|
238
|
+
# subscription ID.
|
239
|
+
attr_reader :subscriptions
|
240
|
+
|
241
|
+
|
242
|
+
### Set the source of the node to +source+, which should be a valid URI.
|
243
|
+
def source=( source )
|
244
|
+
@source = URI( source )
|
245
|
+
end
|
246
|
+
|
247
|
+
|
248
|
+
#
|
249
|
+
# :section: DSLish declaration methods
|
250
|
+
# These methods are both getter and setter for a node's attributes, used
|
251
|
+
# in the node source.
|
252
|
+
#
|
253
|
+
|
254
|
+
### Get/set the node's parent node, which should either be an identifier or an object
|
255
|
+
### that responds to #identifier with one.
|
256
|
+
def parent( new_parent=nil )
|
257
|
+
return @parent if new_parent.nil?
|
258
|
+
|
259
|
+
@parent = if new_parent.respond_to?( :identifier )
|
260
|
+
new_parent.identifier.to_s
|
261
|
+
else
|
262
|
+
@parent = new_parent.to_s
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
|
267
|
+
### Get/set the node's description.
|
268
|
+
def description( new_description=nil )
|
269
|
+
return @description unless new_description
|
270
|
+
@description = new_description.to_s
|
271
|
+
end
|
272
|
+
|
273
|
+
|
274
|
+
### Declare one or more +tags+ for this node.
|
275
|
+
def tags( *tags )
|
276
|
+
@tags.merge( tags.map(&:to_s) ) unless tags.empty?
|
277
|
+
return @tags.to_a
|
278
|
+
end
|
279
|
+
|
280
|
+
|
281
|
+
#
|
282
|
+
# :section: Manager API
|
283
|
+
# Methods used by the manager to manage its nodes.
|
284
|
+
#
|
285
|
+
|
286
|
+
|
287
|
+
### Return the simple type of this node (e.g., Arborist::Node::Host => 'host')
|
288
|
+
def type
|
289
|
+
return 'anonymous' unless self.class.name
|
290
|
+
return self.class.name.sub( /.*::/, '' ).downcase
|
291
|
+
end
|
292
|
+
|
293
|
+
|
294
|
+
### Add the specified +subscription+ (an Arborist::Subscription) to the node.
|
295
|
+
def add_subscription( subscription )
|
296
|
+
self.subscriptions[ subscription.id ] = subscription
|
297
|
+
end
|
298
|
+
|
299
|
+
|
300
|
+
### Remove the specified +subscription+ (an Arborist::Subscription) from the node.
|
301
|
+
def remove_subscription( subscription_id )
|
302
|
+
return self.subscriptions.delete( subscription_id )
|
303
|
+
end
|
304
|
+
|
305
|
+
|
306
|
+
### Return subscriptions matching the specified +event+ on the receiving node.
|
307
|
+
def find_matching_subscriptions( event )
|
308
|
+
return self.subscriptions.values.find_all {|sub| event =~ sub }
|
309
|
+
end
|
310
|
+
|
311
|
+
|
312
|
+
### Publish the specified +events+ to any subscriptions the node has which match them.
|
313
|
+
def publish_events( *events )
|
314
|
+
self.subscriptions.each_value do |sub|
|
315
|
+
sub.on_events( *events )
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
|
320
|
+
### Update specified +properties+ for the node.
|
321
|
+
def update( new_properties )
|
322
|
+
new_properties = stringify_keys( new_properties )
|
323
|
+
self.log.debug "Updated: %p" % [ new_properties ]
|
324
|
+
|
325
|
+
self.last_contacted = Time.now
|
326
|
+
self.error = new_properties.delete( 'error' )
|
327
|
+
self.ack = new_properties.delete( 'ack' ) if new_properties.key?( 'ack' )
|
328
|
+
|
329
|
+
self.properties.merge!( new_properties, &self.method(:merge_and_record_delta) )
|
330
|
+
compact_hash( self.properties )
|
331
|
+
|
332
|
+
# Super to the state machine event method
|
333
|
+
super
|
334
|
+
|
335
|
+
events = self.pending_update_events.clone
|
336
|
+
events << self.make_update_event
|
337
|
+
events << self.make_delta_event unless self.update_delta.empty?
|
338
|
+
|
339
|
+
return events
|
340
|
+
ensure
|
341
|
+
self.update_delta.clear
|
342
|
+
self.pending_update_events.clear
|
343
|
+
end
|
344
|
+
|
345
|
+
|
346
|
+
### Merge the specified +new_properties+ into the node's properties, recording
|
347
|
+
### each change in the node's #update_delta.
|
348
|
+
def merge_and_record_delta( key, oldval, newval, prefixes=[] )
|
349
|
+
self.log.debug "Merging property %s: %p -> %p" % [
|
350
|
+
(prefixes + [key]).join('.'),
|
351
|
+
oldval,
|
352
|
+
newval
|
353
|
+
]
|
354
|
+
|
355
|
+
# Merge them (recursively) if they're both merge-able
|
356
|
+
if oldval.respond_to?( :merge! ) && newval.respond_to?( :merge! )
|
357
|
+
return oldval.merge( newval ) do |ikey, ioldval, inewval|
|
358
|
+
self.merge_and_record_delta( ikey, ioldval, inewval, prefixes + [key] )
|
359
|
+
end
|
360
|
+
|
361
|
+
# Otherwise just directly compare them and record any changes
|
362
|
+
else
|
363
|
+
unless oldval == newval
|
364
|
+
prefixed_delta = prefixes.inject( self.update_delta ) do |hash, key|
|
365
|
+
hash[ key ]
|
366
|
+
end
|
367
|
+
prefixed_delta[ key ] = [ oldval, newval ]
|
368
|
+
end
|
369
|
+
|
370
|
+
return newval
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
|
375
|
+
### Return the node's state in an Arborist::Event of type 'node.update'.
|
376
|
+
def make_update_event
|
377
|
+
return Arborist::Event.create( 'node_update', self )
|
378
|
+
end
|
379
|
+
|
380
|
+
|
381
|
+
### Return an Event generated from the node's state changes.
|
382
|
+
def make_delta_event
|
383
|
+
self.log.debug "Making node.delta event: %p" % [ self.update_delta ]
|
384
|
+
return Arborist::Event.create( 'node_delta', self, self.update_delta )
|
385
|
+
end
|
386
|
+
|
387
|
+
|
388
|
+
### Returns +true+ if the node's state has changed since the last time
|
389
|
+
### #snapshot_state was called.
|
390
|
+
def state_has_changed?
|
391
|
+
return ! self.update_delta.empty?
|
392
|
+
end
|
393
|
+
|
394
|
+
|
395
|
+
### Returns +true+ if the specified search +criteria+ all match this node.
|
396
|
+
def matches?( criteria )
|
397
|
+
self.log.debug "Matching %p against criteria: %p" % [ self, criteria ]
|
398
|
+
return criteria.all? do |key, val|
|
399
|
+
self.match_criteria?( key, val )
|
400
|
+
end.tap {|match| self.log.debug " node %s match." % [ match ? "DID" : "did not"] }
|
401
|
+
end
|
402
|
+
|
403
|
+
|
404
|
+
### Returns +true+ if the node matches the specified +key+ and +val+ criteria.
|
405
|
+
def match_criteria?( key, val )
|
406
|
+
return case key
|
407
|
+
when 'status'
|
408
|
+
self.status == val
|
409
|
+
when 'type'
|
410
|
+
self.log.debug "Checking node type %p against %p" % [ self.type, val ]
|
411
|
+
self.type == val
|
412
|
+
when 'tag' then @tags.include?( val.to_s )
|
413
|
+
when 'tags' then Array(val).all? {|tag| @tags.include?(tag) }
|
414
|
+
when 'identifier' then @identifier == val
|
415
|
+
else
|
416
|
+
hash_matches( @properties, key, val )
|
417
|
+
end
|
418
|
+
end
|
419
|
+
|
420
|
+
|
421
|
+
### Return a Hash of node state values that match the specified +value_spec+.
|
422
|
+
def fetch_values( value_spec=nil )
|
423
|
+
state = self.properties.merge( self.operational_values )
|
424
|
+
state = stringify_keys( state )
|
425
|
+
|
426
|
+
if value_spec
|
427
|
+
self.log.debug "Eliminating all values except: %p (from keys: %p)" %
|
428
|
+
[ value_spec, state.keys ]
|
429
|
+
state.delete_if {|key, _| !value_spec.include?(key) }
|
430
|
+
end
|
431
|
+
|
432
|
+
return state
|
433
|
+
end
|
434
|
+
|
435
|
+
|
436
|
+
### Return a Hash of the operational values that are included with the node's
|
437
|
+
### monitor state.
|
438
|
+
def operational_values
|
439
|
+
values = {
|
440
|
+
type: self.type,
|
441
|
+
status: self.status,
|
442
|
+
tags: self.tags
|
443
|
+
}
|
444
|
+
values[:ack] = self.ack.to_h if self.ack
|
445
|
+
|
446
|
+
return values
|
447
|
+
end
|
448
|
+
|
449
|
+
|
450
|
+
#
|
451
|
+
# :section: Hierarchy API
|
452
|
+
#
|
453
|
+
|
454
|
+
### Enumerable API -- iterate over the children of this node.
|
455
|
+
def each( &block )
|
456
|
+
return self.children.values.each( &block )
|
457
|
+
end
|
458
|
+
|
459
|
+
|
460
|
+
### Returns +true+ if the node has one or more child nodes.
|
461
|
+
def has_children?
|
462
|
+
return !self.children.empty?
|
463
|
+
end
|
464
|
+
|
465
|
+
|
466
|
+
### Returns +true+ if the node is considered operational.
|
467
|
+
def operational?
|
468
|
+
return self.identifier.start_with?( '_' )
|
469
|
+
end
|
470
|
+
|
471
|
+
|
472
|
+
### Register the specified +node+ as a child of this node, replacing any existing
|
473
|
+
### node with the same identifier.
|
474
|
+
def add_child( node )
|
475
|
+
self.log.debug "Adding node %p as a child. Parent = %p" % [ node, node.parent ]
|
476
|
+
raise "%p is not a child of %p" % [ node, self ] if
|
477
|
+
node.parent && node.parent != self.identifier
|
478
|
+
self.children[ node.identifier ] = node
|
479
|
+
end
|
480
|
+
|
481
|
+
|
482
|
+
### Append operator -- add the specified +node+ as a child and return +self+.
|
483
|
+
def <<( node )
|
484
|
+
self.add_child( node )
|
485
|
+
return self
|
486
|
+
end
|
487
|
+
|
488
|
+
|
489
|
+
### Unregister the specified +node+ as a child of this node.
|
490
|
+
def remove_child( node )
|
491
|
+
self.log.debug "Removing node %p from children" % [ node ]
|
492
|
+
return self.children.delete( node.identifier )
|
493
|
+
end
|
494
|
+
|
495
|
+
|
496
|
+
#
|
497
|
+
# :section: Utility methods
|
498
|
+
#
|
499
|
+
|
500
|
+
### Return a string describing the node's status.
|
501
|
+
def status_description
|
502
|
+
case self.status
|
503
|
+
when 'up', 'down'
|
504
|
+
return "%s as of %s" % [ self.status.upcase, self.last_contacted ]
|
505
|
+
when 'acked'
|
506
|
+
return "ACKed by %s %s" % [ self.ack.sender, self.ack.time.as_delta ]
|
507
|
+
else
|
508
|
+
return "in an unknown state"
|
509
|
+
end
|
510
|
+
end
|
511
|
+
|
512
|
+
|
513
|
+
### Return a string describing node details; returns +nil+ for the base class. Subclasses
|
514
|
+
### may override this to add to the output of #inspect.
|
515
|
+
def node_description
|
516
|
+
return nil
|
517
|
+
end
|
518
|
+
|
519
|
+
|
520
|
+
### Return a String representation of the object suitable for debugging.
|
521
|
+
def inspect
|
522
|
+
return "#<%p:%#x [%s] -> %s %p %s%s, %d children, %s>" % [
|
523
|
+
self.class,
|
524
|
+
self.object_id * 2,
|
525
|
+
self.identifier,
|
526
|
+
self.parent || 'root',
|
527
|
+
self.description || "(no description)",
|
528
|
+
self.node_description.to_s,
|
529
|
+
self.source,
|
530
|
+
self.children.length,
|
531
|
+
self.status_description,
|
532
|
+
]
|
533
|
+
end
|
534
|
+
|
535
|
+
|
536
|
+
#
|
537
|
+
# :section: Serialization API
|
538
|
+
#
|
539
|
+
|
540
|
+
### Return a Hash of the node's state.
|
541
|
+
def to_hash
|
542
|
+
return {
|
543
|
+
identifier: self.identifier,
|
544
|
+
type: self.class.name.to_s.sub( /.+::/, '' ).downcase,
|
545
|
+
parent: self.parent,
|
546
|
+
description: self.description,
|
547
|
+
tags: self.tags,
|
548
|
+
properties: self.properties.dup,
|
549
|
+
status: self.status,
|
550
|
+
ack: self.ack ? self.ack.to_h : nil,
|
551
|
+
last_contacted: self.last_contacted ? self.last_contacted.iso8601 : nil,
|
552
|
+
status_changed: self.status_changed ? self.status_changed.iso8601 : nil,
|
553
|
+
error: self.error,
|
554
|
+
}
|
555
|
+
end
|
556
|
+
|
557
|
+
|
558
|
+
### Marshal API -- return the node as an object suitable for marshalling.
|
559
|
+
def marshal_dump
|
560
|
+
return self.to_hash
|
561
|
+
end
|
562
|
+
|
563
|
+
|
564
|
+
### Marshal API -- set up the object's state using the +hash+ from a
|
565
|
+
### previously-marshalled node.
|
566
|
+
def marshal_load( hash )
|
567
|
+
@identifier = hash[:identifier]
|
568
|
+
@properties = hash[:properties]
|
569
|
+
|
570
|
+
@parent = hash[:parent]
|
571
|
+
@description = hash[:description]
|
572
|
+
@tags = Set.new( hash[:tags] )
|
573
|
+
@children = {}
|
574
|
+
|
575
|
+
@status = hash[:status]
|
576
|
+
@status_changed = Time.parse( hash[:status_changed] )
|
577
|
+
|
578
|
+
@error = hash[:error]
|
579
|
+
@properties = hash[:properties]
|
580
|
+
@last_contacted = Time.parse( hash[:last_contacted] )
|
581
|
+
|
582
|
+
if hash[:ack]
|
583
|
+
ack_values = hash[:ack].values_at( *Arborist::Node::ACK.members )
|
584
|
+
@ack = Arborist::Node::ACK.new( *ack_values )
|
585
|
+
end
|
586
|
+
end
|
587
|
+
|
588
|
+
|
589
|
+
### Equality operator -- returns +true+ if +other_node+ has the same identifier, parent, and
|
590
|
+
### state as the receiving one.
|
591
|
+
def ==( other_node )
|
592
|
+
return \
|
593
|
+
other_node.identifier == self.identifier &&
|
594
|
+
other_node.parent == self.parent &&
|
595
|
+
other_node.description == self.description &&
|
596
|
+
other_node.tags == self.tags &&
|
597
|
+
other_node.properties == self.properties &&
|
598
|
+
other_node.status == self.status &&
|
599
|
+
other_node.ack == self.ack &&
|
600
|
+
other_node.error == self.error
|
601
|
+
end
|
602
|
+
|
603
|
+
|
604
|
+
#########
|
605
|
+
protected
|
606
|
+
#########
|
607
|
+
|
608
|
+
### Ack the node with the specified +ack_data+, which should contain
|
609
|
+
def ack=( ack_data )
|
610
|
+
self.log.debug "ACKed with data: %p" % [ ack_data ]
|
611
|
+
|
612
|
+
ack_data['time'] ||= Time.now
|
613
|
+
ack_values = ack_data.values_at( *Arborist::Node::ACK.members.map(&:to_s) )
|
614
|
+
new_ack = Arborist::Node::ACK.new( *ack_values )
|
615
|
+
|
616
|
+
if missing = ACK_REQUIRED_PROPERTIES.find {|prop| new_ack[prop].nil? }
|
617
|
+
raise "Missing required ACK attribute %s" % [ missing ]
|
618
|
+
end
|
619
|
+
|
620
|
+
@ack = new_ack
|
621
|
+
end
|
622
|
+
|
623
|
+
|
624
|
+
### State machine guard predicate -- Returns +true+ if the node has an ACK status set.
|
625
|
+
def ack_set?
|
626
|
+
self.log.debug "Checking to see if this node has been ACKed (it %s)" %
|
627
|
+
[ @ack ? "has" : "has not" ]
|
628
|
+
return @ack ? true : false
|
629
|
+
end
|
630
|
+
|
631
|
+
|
632
|
+
### State machine guard predicate -- Returns +true+ if the last time the node
|
633
|
+
### was monitored resulted in an update.
|
634
|
+
def last_contact_successful?
|
635
|
+
self.log.debug "Checking to see if last contact was successful (it %s)" %
|
636
|
+
[ self.error ? "wasn't" : "was" ]
|
637
|
+
return !self.error
|
638
|
+
end
|
639
|
+
|
640
|
+
|
641
|
+
#
|
642
|
+
# :section: State Callbacks
|
643
|
+
#
|
644
|
+
|
645
|
+
### Callback for when an acknowledgement is set.
|
646
|
+
def on_ack( transition )
|
647
|
+
self.log.warn "ACKed: %s" % [ self.status_description ]
|
648
|
+
self.pending_update_events <<
|
649
|
+
Arborist::Event.create( 'node_acked', self.fetch_values, self.ack.to_h )
|
650
|
+
end
|
651
|
+
|
652
|
+
|
653
|
+
### Callback for when an acknowledgement is cleared.
|
654
|
+
def on_ack_cleared( transition )
|
655
|
+
self.log.warn "ACK cleared for %s" % [ self.identifier ]
|
656
|
+
end
|
657
|
+
|
658
|
+
|
659
|
+
### Callback for when a node goes from down to up
|
660
|
+
def on_node_up( transition )
|
661
|
+
self.log.warn "%s is %s" % [ self.identifier, self.status_description ]
|
662
|
+
end
|
663
|
+
|
664
|
+
|
665
|
+
### Callback for when a node goes from up to down
|
666
|
+
def on_node_down( transition )
|
667
|
+
self.log.error "%s is %s" % [ self.identifier, self.status_description ]
|
668
|
+
self.update_delta[ 'error' ] = [ nil, self.error ]
|
669
|
+
end
|
670
|
+
|
671
|
+
|
672
|
+
### Add the transition from one state to another to the data used to build
|
673
|
+
### deltas for the #update event.
|
674
|
+
def add_status_to_update_delta( transition )
|
675
|
+
self.update_delta[ 'status' ] = [ transition.from, transition.to ]
|
676
|
+
end
|
677
|
+
|
678
|
+
|
679
|
+
#######
|
680
|
+
private
|
681
|
+
#######
|
682
|
+
|
683
|
+
### Returns true if the specified +hash+ includes the specified +key+, and the value
|
684
|
+
### associated with the +key+ either includes +val+ if it is a Hash, or equals +val+ if it's
|
685
|
+
### anything but a Hash.
|
686
|
+
def hash_matches( hash, key, val )
|
687
|
+
actual = hash[ key ] or return false
|
688
|
+
|
689
|
+
if actual.is_a?( Hash )
|
690
|
+
if val.is_a?( Hash )
|
691
|
+
return val.all? {|subkey, subval| hash_matches(actual, subkey, subval) }
|
692
|
+
else
|
693
|
+
return false
|
694
|
+
end
|
695
|
+
else
|
696
|
+
return actual == val
|
697
|
+
end
|
698
|
+
end
|
699
|
+
|
700
|
+
end # class Arborist::Node
|