arborist 0.0.1.pre20160106113421

Sign up to get free protection for your applications and to get access to all the features.
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