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.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/.document +4 -0
  3. data/.simplecov +9 -0
  4. data/ChangeLog +417 -0
  5. data/Events.md +20 -0
  6. data/History.md +4 -0
  7. data/LICENSE +29 -0
  8. data/Manifest.txt +72 -0
  9. data/Monitors.md +141 -0
  10. data/Nodes.md +0 -0
  11. data/Observers.md +72 -0
  12. data/Protocol.md +214 -0
  13. data/README.md +75 -0
  14. data/Rakefile +81 -0
  15. data/TODO.md +24 -0
  16. data/bin/amanagerd +10 -0
  17. data/bin/amonitord +12 -0
  18. data/bin/aobserverd +12 -0
  19. data/lib/arborist.rb +182 -0
  20. data/lib/arborist/client.rb +191 -0
  21. data/lib/arborist/event.rb +61 -0
  22. data/lib/arborist/event/node_acked.rb +18 -0
  23. data/lib/arborist/event/node_delta.rb +20 -0
  24. data/lib/arborist/event/node_matching.rb +34 -0
  25. data/lib/arborist/event/node_update.rb +19 -0
  26. data/lib/arborist/event/sys_reloaded.rb +15 -0
  27. data/lib/arborist/exceptions.rb +21 -0
  28. data/lib/arborist/manager.rb +508 -0
  29. data/lib/arborist/manager/event_publisher.rb +97 -0
  30. data/lib/arborist/manager/tree_api.rb +207 -0
  31. data/lib/arborist/mixins.rb +363 -0
  32. data/lib/arborist/monitor.rb +377 -0
  33. data/lib/arborist/monitor/socket.rb +163 -0
  34. data/lib/arborist/monitor_runner.rb +217 -0
  35. data/lib/arborist/node.rb +700 -0
  36. data/lib/arborist/node/host.rb +87 -0
  37. data/lib/arborist/node/root.rb +60 -0
  38. data/lib/arborist/node/service.rb +112 -0
  39. data/lib/arborist/observer.rb +176 -0
  40. data/lib/arborist/observer/action.rb +125 -0
  41. data/lib/arborist/observer/summarize.rb +105 -0
  42. data/lib/arborist/observer_runner.rb +181 -0
  43. data/lib/arborist/subscription.rb +82 -0
  44. data/spec/arborist/client_spec.rb +282 -0
  45. data/spec/arborist/event/node_update_spec.rb +71 -0
  46. data/spec/arborist/event_spec.rb +64 -0
  47. data/spec/arborist/manager/event_publisher_spec.rb +66 -0
  48. data/spec/arborist/manager/tree_api_spec.rb +458 -0
  49. data/spec/arborist/manager_spec.rb +442 -0
  50. data/spec/arborist/mixins_spec.rb +195 -0
  51. data/spec/arborist/monitor/socket_spec.rb +195 -0
  52. data/spec/arborist/monitor_runner_spec.rb +152 -0
  53. data/spec/arborist/monitor_spec.rb +251 -0
  54. data/spec/arborist/node/host_spec.rb +104 -0
  55. data/spec/arborist/node/root_spec.rb +29 -0
  56. data/spec/arborist/node/service_spec.rb +98 -0
  57. data/spec/arborist/node_spec.rb +552 -0
  58. data/spec/arborist/observer/action_spec.rb +205 -0
  59. data/spec/arborist/observer/summarize_spec.rb +294 -0
  60. data/spec/arborist/observer_spec.rb +146 -0
  61. data/spec/arborist/subscription_spec.rb +71 -0
  62. data/spec/arborist_spec.rb +146 -0
  63. data/spec/data/monitors/pings.rb +80 -0
  64. data/spec/data/monitors/port_checks.rb +27 -0
  65. data/spec/data/monitors/system_resources.rb +30 -0
  66. data/spec/data/monitors/web_services.rb +17 -0
  67. data/spec/data/nodes/duir.rb +20 -0
  68. data/spec/data/nodes/localhost.rb +15 -0
  69. data/spec/data/nodes/sidonie.rb +29 -0
  70. data/spec/data/nodes/yevaud.rb +26 -0
  71. data/spec/data/observers/auditor.rb +23 -0
  72. data/spec/data/observers/webservices.rb +18 -0
  73. data/spec/spec_helper.rb +117 -0
  74. 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