Class: Arborist::Node

Inherits:
Object
  • Object
show all
Extended by:
MethodUtilities, Configurability, Loggability, Pluggability
Includes:
HashUtilities, Enumerable
Defined in:
lib/arborist/node.rb

Overview

The basic node class for an Arborist tree

Direct Known Subclasses

Host, Resource, Root, Service

Defined Under Namespace

Classes: Ack, Host, Resource, Root, Service

Constant Summary collapse

LOADED_INSTANCE_KEY =

The key for the thread local that is used to track instances as they’re loaded.

:loaded_node_instances
VALID_IDENTIFIER =

Regex to match a valid identifier

/^\w[\w\-]*$/
OPERATIONAL_ATTRIBUTES =

The attributes of a node which are used in the operation of the system

%i[
	type
	family
	status
	tags
	parent
	description
	dependencies
	status_changed
	status_last_changed
	last_contacted
	ack
	errors
	warnings
	quieted_reasons
	config
]
UNREACHABLE_STATES =

Node states that are unreachable by default.

%w[
	down
	disabled
	quieted
]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from MethodUtilities

attr_predicate, attr_predicate_accessor, dsl_accessor, singleton_attr_accessor, singleton_attr_reader, singleton_attr_writer, singleton_method_alias, singleton_predicate_accessor, singleton_predicate_reader

Methods included from HashUtilities

compact_hash, hash_matches, merge_recursively, stringify_keys, symbolify_keys

Constructor Details

#initialize(identifier, *args, &block) ⇒ Node

Create a new Node with the specified identifier, which must be unique to the loaded tree.



313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
# File 'lib/arborist/node.rb', line 313

def initialize( identifier, *args, &block )
	attributes  = args.last.is_a?( Hash ) ? args.pop : {}
	parent_node = args.pop

	raise "Invalid identifier %p" % [identifier] unless
		identifier =~ VALID_IDENTIFIER

	# Attributes of the target
	@identifier      = identifier
	@parent          = parent_node ? parent_node.identifier : '_'
	@description     = nil
	@tags            = Set.new
	@properties      = {}
	@config          = {}
	@source          = nil
	@children        = {}
	@dependencies    = Arborist::Dependency.new( :all )
	@flap_threshold  = nil
	@flapping        = false
	@status_history_size = nil

	# Primary state
	@status          = 'unknown'
	@status_changed  = Time.at( 0 )
	@status_last_changed = Time.at( 0 )
	@status_history  = []

	# Attributes that govern state
	@errors          = {}
	@warnings        = {}
	@ack             = nil
	@previous_ack    = nil
	@last_contacted  = Time.at( 0 )
	@quieted_reasons = {}

	# Event-handling
	@update_delta    = Hash.new do |h,k|
		h[ k ] = Hash.new( &h.default_proc )
	end
	@pending_change_events = []
	@subscriptions  = {}

	self.modify( attributes )
	self.instance_eval( &block ) if block
end

Instance Attribute Details

#ackObject

The acknowledgement currently in effect. Should be an instance of Arborist::Node::ACK



405
406
407
# File 'lib/arborist/node.rb', line 405

def ack
  @ack
end

#childrenObject (readonly)

The Hash of nodes which are children of this node, keyed by identifier



374
375
376
# File 'lib/arborist/node.rb', line 374

def children
  @children
end

#dependenciesObject

The node’s secondary dependencies, expressed as an Arborist::Node::Sexp



426
427
428
# File 'lib/arborist/node.rb', line 426

def dependencies
  @dependencies
end

#errorsObject

The Hash of last errors encountered by a monitor attempting to update this node, keyed by the monitor’s ‘key`.



396
397
398
# File 'lib/arborist/node.rb', line 396

def errors
  @errors
end

#identifierObject (readonly)

The node’s identifier



366
367
368
# File 'lib/arborist/node.rb', line 366

def identifier
  @identifier
end

#last_contactedObject

The Time the node was last contacted



382
383
384
# File 'lib/arborist/node.rb', line 382

def last_contacted
  @last_contacted
end

#pending_change_eventsObject (readonly)

The Array of events generated by the current update event



417
418
419
# File 'lib/arborist/node.rb', line 417

def pending_change_events
  @pending_change_events
end

#previous_ackObject

The acknowledgement previously in effect (if any).



409
410
411
# File 'lib/arborist/node.rb', line 409

def previous_ack
  @previous_ack
end

#propertiesObject (readonly)

Arbitrary attributes attached to this node via the manager API



378
379
380
# File 'lib/arborist/node.rb', line 378

def properties
  @properties
end

#quieted_reasonsObject (readonly)

The reasons this node was quieted. This is a Hash of text descriptions keyed by the type of dependency it came from (either :primary or :secondary).



431
432
433
# File 'lib/arborist/node.rb', line 431

def quieted_reasons
  @quieted_reasons
end

#sourceObject

The URI of the source the object was read from



370
371
372
# File 'lib/arborist/node.rb', line 370

def source
  @source
end

#status_changedObject

The Time the node’s status last changed.



386
387
388
# File 'lib/arborist/node.rb', line 386

def status_changed
  @status_changed
end

#status_historyObject (readonly)

An array of statuses, retained after an update.



435
436
437
# File 'lib/arborist/node.rb', line 435

def status_history
  @status_history
end

#status_last_changedObject

The previous Time the node’s status changed, for duration calculations between states.



391
392
393
# File 'lib/arborist/node.rb', line 391

def status_last_changed
  @status_last_changed
end

#subscriptionsObject (readonly)

The Hash of Subscription objects observing this node and its children, keyed by subscription ID.



422
423
424
# File 'lib/arborist/node.rb', line 422

def subscriptions
  @subscriptions
end

#update_deltaObject (readonly)

The Hash of changes tracked during an #update.



413
414
415
# File 'lib/arborist/node.rb', line 413

def update_delta
  @update_delta
end

#warningsObject

The Hash of last warnings encountered by a monitor attempting to update this node, keyed by the monitor’s ‘key`.



401
402
403
# File 'lib/arborist/node.rb', line 401

def warnings
  @warnings
end

Class Method Details

.add_loaded_instance(new_instance) ⇒ Object

Record a new loaded instance if the Thread-local variable is set up to track them.



219
220
221
222
223
# File 'lib/arborist/node.rb', line 219

def self::add_loaded_instance( new_instance )
	instances = Thread.current[ LOADED_INSTANCE_KEY ] or return
	# self.log.debug "Adding new instance %p to node tree" % [ new_instance ]
	instances << new_instance
end

.add_subnode_factory_method(subnode_type, &dsl_block) ⇒ Object

Add a factory method that can be used to create subnodes of the specified subnode_type on instances of the receiving class.



261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
# File 'lib/arborist/node.rb', line 261

def self::add_subnode_factory_method( subnode_type, &dsl_block )
	if subnode_type.name
		name = subnode_type.plugin_name
		# self.log.debug "Adding factory constructor for %s nodes to %p" % [ name, self ]
		body = lambda do |*args, &constructor_block|
			if dsl_block
				# self.log.debug "Using DSL block to split args: %p" % [ dsl_block ]
				identifier, attributes = dsl_block.call( *args )
			else
				# self.log.debug "Splitting args the default way: %p" % [ args ]
				identifier, attributes = *args
			end
			attributes ||= {}
			# self.log.debug "Identifier: %p, attributes: %p, self: %p" %
			# 	[ identifier, attributes, self ]

			return Arborist::Node.create( name, identifier, self, attributes, &constructor_block )
		end

		define_method( name, &body )
	else
		self.log.info "Skipping factory constructor for anonymous subnode class."
	end
end

.curried_create(type) ⇒ Object

Return a curried Proc for the ::create method for the specified type.



192
193
194
195
196
197
198
# File 'lib/arborist/node.rb', line 192

def self::curried_create( type )
	if type.subnode_type?
		return self.method( :create ).to_proc.curry( 3 )[ type ]
	else
		return self.method( :create ).to_proc.curry( 2 )[ type ]
	end
end

.each_in(loader) ⇒ Object

Return an iterator for all the nodes supplied by the specified loader.



306
307
308
# File 'lib/arborist/node.rb', line 306

def self::each_in( loader )
	return loader.nodes
end

.from_hash(hash) ⇒ Object

Create a new node with its state read from the specified hash.



210
211
212
213
214
# File 'lib/arborist/node.rb', line 210

def self::from_hash( hash )
	return self.new( hash[:identifier] ) do
		self.marshal_load( hash )
	end
end

.inherited(subclass) ⇒ Object

Inheritance hook – add a DSL declarative function for the given subclass.



227
228
229
230
231
232
# File 'lib/arborist/node.rb', line 227

def self::inherited( subclass )
	super

	body = self.curried_create( subclass )
	Arborist.add_dsl_constructor( subclass, &body )
end

.load(file) ⇒ Object

Load the specified file and return any new Nodes created as a result.



288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'lib/arborist/node.rb', line 288

def self::load( file )
	self.log.info "Loading node file %s..." % [ file ]
	Thread.current[ LOADED_INSTANCE_KEY ] = []

	begin
		Kernel.load( file )
	rescue => err
		self.log.error "%p while loading %s: %s" % [ err.class, file, err.message ]
		raise
	end

	return Thread.current[ LOADED_INSTANCE_KEY ]
ensure
	Thread.current[ LOADED_INSTANCE_KEY ] = nil
end

.newObject

Overridden to track instances of created nodes for the DSL.



202
203
204
205
206
# File 'lib/arborist/node.rb', line 202

def self::new( * )
	new_instance = super
	Arborist::Node.add_loaded_instance( new_instance )
	return new_instance
end

.parent_types(*types, &block) ⇒ Object

Get/set the node type instances of the class live under. If no parent_type is set, it is a top-level node type. If a block is given, it can be used to pre-process the arguments into the (identifier, attributes, block) arguments used to create the node instances.



239
240
241
242
243
244
245
246
247
248
249
# File 'lib/arborist/node.rb', line 239

def self::parent_types( *types, &block )
	@parent_types ||= []

	types.each do |new_type|
		subclass = Arborist::Node.get_subclass( new_type )
		@parent_types << subclass
		subclass.add_subnode_factory_method( self, &block )
	end

	return @parent_types
end

.subnode_type?Boolean

Returns true if the receiver must be created under a specific node type.

Returns:

  • (Boolean)


254
255
256
# File 'lib/arborist/node.rb', line 254

def self::subnode_type?
	return ! self.parent_types.empty?
end

Instance Method Details

#<<(node) ⇒ Object

Append operator – add the specified node as a child and return self.



1046
1047
1048
1049
# File 'lib/arborist/node.rb', line 1046

def <<( node )
	self.add_child( node )
	return self
end

#==(other_node) ⇒ Object

Equality operator – returns true if other_node has the same identifier, parent, and state as the receiving one.



1227
1228
1229
1230
1231
1232
1233
# File 'lib/arborist/node.rb', line 1227

def ==( other_node )
	return \
		other_node.identifier == self.identifier &&
		other_node.parent == self.parent &&
		other_node.description == self.description &&
		other_node.tags == self.tags
end

#acked_descriptionObject

Return a description of the ack if it’s set, or a generic string otherwise.



1065
1066
1067
1068
# File 'lib/arborist/node.rb', line 1065

def acked_description
	return self.ack.description if self.ack
	return "(unset)"
end

#acknowledge(**args) ⇒ Object

Acknowledge any current or future abnormal status for this node.



697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
# File 'lib/arborist/node.rb', line 697

def acknowledge( **args )
	super()

	self.ack = args

	events = self.pending_change_events.clone
	events << self.make_delta_event unless self.update_delta.empty?
	results = self.broadcast_events( *events )
	self.log.debug ">>> Results from broadcast: %p" % [ results ]
	events.concat( results )

	return events
ensure
	self.clear_transition_temp_vars
end

#add_child(node) ⇒ Object

Register the specified node as a child of this node, replacing any existing node with the same identifier.



1037
1038
1039
1040
1041
1042
# File 'lib/arborist/node.rb', line 1037

def add_child( node )
	self.log.debug "Adding node %p as a child. Parent = %p" % [ node, node.parent ]
	raise "%p is not a child of %p" % [ node, self ] if
		node.parent && node.parent != self.identifier
	self.children[ node.identifier ] = node
end

#add_subscription(subscription) ⇒ Object

Add the specified subscription (an Arborist::Subscription) to the node.



572
573
574
# File 'lib/arborist/node.rb', line 572

def add_subscription( subscription )
	self.subscriptions[ subscription.id ] = subscription
end

#all_of(*identifiers, on: nil) ⇒ Object

Group identifiers together in an ‘all of’ dependency.



514
515
516
# File 'lib/arborist/node.rb', line 514

def all_of( *identifiers, on: nil )
	return Arborist::Dependency.on( :all, *identifiers, prefixes: on )
end

#any_of(*identifiers, on: nil) ⇒ Object

Group identifiers together in an ‘any of’ dependency.



508
509
510
# File 'lib/arborist/node.rb', line 508

def any_of( *identifiers, on: nil )
	return Arborist::Dependency.on( :any, *identifiers, prefixes: on )
end

#broadcast_events(*events) ⇒ Object

Send an event to this node’s immediate children.



855
856
857
858
859
860
861
862
863
864
865
# File 'lib/arborist/node.rb', line 855

def broadcast_events( *events )
	events.flatten!
	results = self.children.flat_map do |identifier, child|
		self.log.debug "Broadcasting events to %p: %p" % [ identifier, events ]
		events.flat_map do |event|
			child.handle_event( event )
		end
	end

	return results
end

#clear_transition_temp_varsObject

Clear out the state used during a transition to track changes.



733
734
735
736
737
# File 'lib/arborist/node.rb', line 733

def clear_transition_temp_vars
	self.previous_ack = nil
	self.update_delta.clear
	self.pending_change_events.clear
end

#config(new_config = nil) ⇒ Object

Get or set the node’s configuration hash. This can be used to pass per-node information to systems using the tree (e.g., monitors, subscribers).



536
537
538
539
# File 'lib/arborist/node.rb', line 536

def config( new_config=nil )
	@config.merge!( stringify_keys( new_config ) ) if new_config
	return @config
end

#dependencies_down?Boolean Also known as: has_downed_dependencies?

Returns true if this node’s dependencies are not met.

Returns:

  • (Boolean)


912
913
914
# File 'lib/arborist/node.rb', line 912

def dependencies_down?
	return self.dependencies.down?
end

#dependencies_up?Boolean

Returns true if this node’s dependencies are met.

Returns:

  • (Boolean)


919
920
921
# File 'lib/arborist/node.rb', line 919

def dependencies_up?
	return !self.dependencies_down?
end

#depends_on(*dependencies, on: nil) ⇒ Object

Add secondary dependencies to the receiving node.



520
521
522
523
524
525
# File 'lib/arborist/node.rb', line 520

def depends_on( *dependencies, on: nil )
	dependencies = self.all_of( *dependencies, on: on )

	self.log.debug "Setting secondary dependencies to: %p" % [ dependencies ]
	self.dependencies = check_dependencies( dependencies )
end

#description(new_description = nil) ⇒ Object

Get/set the node’s description.



493
494
495
496
# File 'lib/arborist/node.rb', line 493

def description( new_description=nil )
	return @description unless new_description
	@description = new_description.to_s
end

#each(&block) ⇒ Object

Enumerable API – iterate over the children of this node.



1003
1004
1005
# File 'lib/arborist/node.rb', line 1003

def each( &block )
	return self.children.values.each( &block )
end

#familyObject

Return the node family, so observers can know ancestry after serialization for custom node types that inherit from this class.



450
451
452
# File 'lib/arborist/node.rb', line 450

def family
	return :node
end

#fetch_values(value_spec = nil) ⇒ Object

Return a Hash of node state values that match the specified value_spec.



802
803
804
805
806
807
808
809
810
811
812
813
814
# File 'lib/arborist/node.rb', line 802

def fetch_values( value_spec=nil )
	state = self.properties.merge( self.operational_values )
	state = stringify_keys( state )
	state = make_serializable( state )

	if value_spec
		self.log.debug "Eliminating all values except: %p (from keys: %p)" %
			[ value_spec, state.keys ]
		state.delete_if {|key, _| !value_spec.include?(key) }
	end

	return state
end

#find_matching_subscriptions(event) ⇒ Object

Return subscriptions matching the specified event on the receiving node.



584
585
586
# File 'lib/arborist/node.rb', line 584

def find_matching_subscriptions( event )
	return self.subscriptions.values.find_all {|sub| event =~ sub }
end

#flap_threshold(new_count = nil) ⇒ Object

Get or set the number of transitions in the status history to determine if a node is considering ‘flapping’.



552
553
554
555
# File 'lib/arborist/node.rb', line 552

def flap_threshold( new_count=nil )
	@flap_threshold = new_count if new_count
	return @flap_threshold || Arborist::Node.flap_threshold || 0
end

#flappingObject

The current flapping state of this node.



439
# File 'lib/arborist/node.rb', line 439

attr_predicate_accessor :flapping

#handle_event(event) ⇒ Object

Handle the specified event, delivered either via broadcast or secondary dependency subscription.



870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
# File 'lib/arborist/node.rb', line 870

def handle_event( event )
	self.log.debug "Handling event %p" % [ event ]
	handler_name = "handle_%s_event" % [ event.type.gsub('.', '_') ]

	if self.respond_to?( handler_name )
		self.log.debug "Handling a %s event." % [ event.type ]
		self.method( handler_name ).call( event )
	else
		self.log.debug "No handler for a %s event!" % [ event.type ]
	end

	# Don't transition on informational events
	return [] if event.informational?
	super # to state-machine

	results = self.pending_change_events.clone
	self.log.debug ">>> Pending change events after: %p" % [ results ]
	results << self.make_delta_event unless self.update_delta.empty?

	child_results = self.broadcast_events( *results )
	results.concat( child_results )

	self.publish_events( *results ) unless results.empty?

	return results
ensure
	self.clear_transition_temp_vars
end

#handle_node_disabled_event(event) ⇒ Object

Handle a ‘node.disabled’ event received via broadcast.



948
949
950
951
952
953
954
955
956
957
958
959
960
# File 'lib/arborist/node.rb', line 948

def handle_node_disabled_event( event )
	self.log.debug "Got a node.disabled event: %p" % [ event ]
	self.dependencies.mark_down( event.node.identifier )

	if self.dependencies_down?
		self.quieted_reasons[ :secondary ] = "Secondary dependencies not met: %s" %
			[ self.dependencies.down_reason ]
	end

	if event.node.identifier == self.parent
		self.quieted_reasons[ :primary ] = "Parent disabled: %s" % [ self.parent ]
	end
end

#handle_node_down_event(event) ⇒ Object

Handle a ‘node.down’ event received via broadcast.



932
933
934
935
936
937
938
939
940
941
942
943
944
# File 'lib/arborist/node.rb', line 932

def handle_node_down_event( event )
	self.log.debug "Got a node.down event: %p" % [ event ]
	self.dependencies.mark_down( event.node.identifier )

	if self.dependencies_down?
		self.quieted_reasons[ :secondary ] = "Secondary dependencies not met: %s" %
			[ self.dependencies.down_reason ]
	end

	if event.node.identifier == self.parent
		self.quieted_reasons[ :primary ] = "Parent down: %s" % [ self.parent ] # :TODO: backtrace?
	end
end

#handle_node_quieted_event(event) ⇒ Object

Handle a ‘node.quieted’ event received via broadcast.



964
965
966
967
968
969
970
971
972
973
974
975
976
# File 'lib/arborist/node.rb', line 964

def handle_node_quieted_event( event )
	self.log.debug "Got a node.quieted event: %p" % [ event ]
	self.dependencies.mark_down( event.node.identifier )

	if self.dependencies_down?
		self.quieted_reasons[ :secondary ] = "Secondary dependencies not met: %s" %
			[ self.dependencies.down_reason ]
	end

	if event.node.identifier == self.parent
		self.quieted_reasons[ :primary ] = "Parent quieted: %s" % [ self.parent ] # :TODO: backtrace?
	end
end

#handle_node_up_event(event) ⇒ Object Also known as: handle_node_warn_event

Handle a ‘node.up’ event received via broadcast.



980
981
982
983
984
985
986
987
988
989
990
991
992
993
# File 'lib/arborist/node.rb', line 980

def handle_node_up_event( event )
	self.log.debug "Got a node.%s event: %p" % [ event.type, event ]

	self.dependencies.mark_up( event.node.identifier )
	self.quieted_reasons.delete( :secondary ) if self.dependencies_up?

	if event.node.identifier == self.parent
		self.log.info "Parent of %s (%s) came back up." % [
			self.identifier,
			self.parent
		]
		self.quieted_reasons.delete( :primary )
	end
end

#has_children?Boolean

Returns true if the node has one or more child nodes.

Returns:

  • (Boolean)


1009
1010
1011
# File 'lib/arborist/node.rb', line 1009

def has_children?
	return !self.children.empty?
end

#has_dependencies?Boolean

Returns true if the node has one or more secondary dependencies.

Returns:

  • (Boolean)


529
530
531
# File 'lib/arborist/node.rb', line 529

def has_dependencies?
	return !self.dependencies.empty?
end

#has_quieted_reason?Boolean

Returns true if any reasons have been set as to why the node has been quieted. Guard condition for transition to and from ‘quieted` state.

Returns:

  • (Boolean)


926
927
928
# File 'lib/arborist/node.rb', line 926

def has_quieted_reason?
	return !self.quieted_reasons.empty?
end

#inspectObject

Return a String representation of the object suitable for debugging.



1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
# File 'lib/arborist/node.rb', line 1099

def inspect
	return "#<%p:%#x [%s] -> %s %p %s %s, %d children, %s>" % [
		self.class,
		self.object_id * 2,
		self.identifier,
		self.parent || 'root',
		self.description || "(no description)",
		self.node_description.to_s,
		self.source,
		self.children.length,
		self.status_description,
	]
end

#make_delta_eventObject

Return an Event generated from the node’s state changes.



747
748
749
750
# File 'lib/arborist/node.rb', line 747

def make_delta_event
	self.log.debug "Making node.delta event: %p" % [ self.update_delta ]
	return Arborist::Event.create( 'node_delta', self, self.update_delta )
end

#make_update_eventObject

Return the node’s state in an Arborist::Event of type ‘node.update’.



741
742
743
# File 'lib/arborist/node.rb', line 741

def make_update_event
	return Arborist::Event.create( 'node_update', self )
end

#marshal_dumpObject

Marshal API – return the node as an object suitable for marshalling.



1181
1182
1183
# File 'lib/arborist/node.rb', line 1181

def marshal_dump
	return self.to_h.merge( dependencies: self.dependencies )
end

#marshal_load(hash) ⇒ Object

Marshal API – set up the object’s state using the hash from a previously-marshalled node.



1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
# File 'lib/arborist/node.rb', line 1188

def marshal_load( hash )
	self.log.debug "Restoring from serialized hash: %p" % [ hash ]
	@identifier          = hash[:identifier]
	@properties          = hash[:properties]

	@parent              = hash[:parent]
	@description         = hash[:description]
	@tags                = Set.new( hash[:tags] )
	@config              = hash[:config]
	@children            = {}

	@status              = hash[:status]
	@status_changed      = Time.parse( hash[:status_changed] )
	@status_last_changed = Time.parse( hash[:status_last_changed] )
	@status_history      = hash[:status_history]
	@flapping            = hash[:flapping]
	@status_history_size = nil
	@ack                 = Arborist::Node::Ack.from_hash( hash[:ack] ) if hash[:ack]

	@errors              = hash[:errors]
	@warnings            = hash[:warnings]
	@properties          = hash[:properties] || {}
	@last_contacted      = Time.parse( hash[:last_contacted] )
	@quieted_reasons     = hash[:quieted_reasons] || {}
	self.log.debug "Deps are: %p" % [ hash[:dependencies] ]
	@dependencies        = hash[:dependencies]

	@update_delta        = Hash.new do |h,k|
		h[ k ] = Hash.new( &h.default_proc )
	end

	@pending_change_events = []
	@subscriptions         = {}

end

#match_criteria?(key, val) ⇒ Boolean

Returns true if the node matches the specified key and val criteria.

Returns:

  • (Boolean)


778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
# File 'lib/arborist/node.rb', line 778

def match_criteria?( key, val )
	array_val = Array( val )
	return case key
		when 'status'
			array_val.include?( self.status )
		when 'type'
			array_val.include?( self.type )
		when 'family'
			array_val.include?( self.family.to_s )
		when 'parent'
			array_val.include?( self.parent )
		when 'tag' then @tags.include?( val.to_s )
		when 'tags' then array_val.all? {|tag| @tags.include?(tag) }
		when 'identifier'
			array_val.include?( self.identifier )
		when 'config'
			val.all? {|ikey, ival| hash_matches(@config, ikey, ival) }
		else
			hash_matches( @properties, key, val )
		end
end

#matches?(criteria, if_empty: true) ⇒ Boolean

Returns true if the specified search criteria all match this node.

Returns:

  • (Boolean)


761
762
763
764
765
766
767
768
769
770
771
772
773
774
# File 'lib/arborist/node.rb', line 761

def matches?( criteria, if_empty: true )

	# Omit 'delta' criteria from matches; delta matching is done separately.
	criteria = criteria.dup
	criteria.delete( 'delta' )

	self.log.debug "Node matching %p (%p if empty)" % [ criteria, if_empty ]
	return if_empty if criteria.empty?

	self.log.debug "Matching %p against criteria: %p" % [ self, criteria ]
	return criteria.all? do |key, val|
		self.match_criteria?( key, val )
	end.tap {|match| self.log.debug "  node %s match." % [ match ? "DID" : "did not"] }
end

#merge_and_record_delta(properties, new_properties) ⇒ Object

Merge the specified newval into the node’s properties at the given key, recording each change in the node’s #update_delta if the oldval is different.



652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
# File 'lib/arborist/node.rb', line 652

def merge_and_record_delta( properties, new_properties )
	delta = {}
	new_properties.each_key do |key|
		newval = new_properties[ key ]
		oldval = properties[ key ]
		subdelta = nil

		# Merge them (recursively) if they're both merge-able
		if oldval.respond_to?( :merge! ) && newval.respond_to?( :merge! )
			newval, subdelta = self.merge_and_record_delta( oldval, newval )

		# Otherwise just directly compare them and record any changes
		elsif oldval != newval
			subdelta = [ oldval, newval ]
		end

		properties[ key ] = newval
		delta[ key ] = subdelta if subdelta
	end

	return properties, delta
end

#merge_new_properties(new_properties) ⇒ Object

Merge the specified Hash of new_properties with the node’s current property Hash.



639
640
641
642
643
644
645
646
647
# File 'lib/arborist/node.rb', line 639

def merge_new_properties( new_properties )
	props = self.properties.dup
	updated_properties, properties_delta = self.merge_and_record_delta( props, new_properties )

	compact_hash( updated_properties )
	self.properties.replace( updated_properties )

	self.update_delta['properties'] = properties_delta unless properties_delta.empty?
end

#modify(attributes) ⇒ Object

Set one or more node attributes. This should be overridden by subclasses which wish to allow their operational attributes to be set/updated via the Tree API (modify and graft). Supported attributes are: parent, description, tags, and config.



459
460
461
462
463
464
465
466
467
468
469
470
# File 'lib/arborist/node.rb', line 459

def modify( attributes )
	attributes = stringify_keys( attributes )

	self.parent( attributes['parent'] )
	self.description( attributes['description'] )
	self.config( attributes['config'] )

	if attributes['tags']
		@tags.clear
		self.tags( attributes['tags'] )
	end
end

#node_descriptionObject

Return a string describing node details; returns nil for the base class. Subclasses may override this to add to the output of #inspect.



1093
1094
1095
# File 'lib/arborist/node.rb', line 1093

def node_description
	return nil
end

#node_subscribersObject

Return the Set of identifier of nodes that are secondary dependencies of this node.



590
591
592
593
594
595
596
597
598
599
# File 'lib/arborist/node.rb', line 590

def node_subscribers
	self.log.debug "Finding node subscribers among %d subscriptions" % [ self.subscriptions.length ]
	return self.subscriptions.each_with_object( Set.new ) do |(identifier, sub), set|
		if sub.respond_to?( :node_identifier )
			set.add( sub.node_identifier )
		else
			self.log.debug "Skipping %p: not a node subscription" % [ sub ]
		end
	end
end

#operational?Boolean

Returns true if the node is considered operational.

Returns:

  • (Boolean)


1015
1016
1017
# File 'lib/arborist/node.rb', line 1015

def operational?
	return self.identifier.start_with?( '_' )
end

#operational_valuesObject

Return a Hash of the operational values that are included with the node’s monitor state.



819
820
821
822
823
824
825
# File 'lib/arborist/node.rb', line 819

def operational_values
	values = OPERATIONAL_ATTRIBUTES.each_with_object( {} ) do |key, hash|
		hash[ key ] = self.send( key )
	end

	return values
end

#parent(new_parent = nil) ⇒ Object

Get/set the node’s parent node, which should either be an identifier or an object that responds to #identifier with one.



481
482
483
484
485
486
487
488
489
# File 'lib/arborist/node.rb', line 481

def parent( new_parent=nil )
	return @parent if new_parent.nil?

	@parent = if new_parent.respond_to?( :identifier )
			new_parent.identifier.to_s
		else
			@parent = new_parent.to_s
		end
end

#publish_events(*events) ⇒ Object

Publish the specified events to any subscriptions the node has which match them.



846
847
848
849
850
851
# File 'lib/arborist/node.rb', line 846

def publish_events( *events )
	self.log.debug "Got events to publish: %p" % [ events ]
	self.subscriptions.each_value do |sub|
		sub.on_events( *events )
	end
end

#reachable?Boolean

Returns true if the node’s status indicates it is included by default when traversing nodes.

Returns:

  • (Boolean)


1030
1031
1032
# File 'lib/arborist/node.rb', line 1030

def reachable?
	return !self.unreachable?
end

#register_secondary_dependencies(manager) ⇒ Object

Register subscriptions for secondary dependencies on the receiving node with the given manager.



830
831
832
833
834
835
836
837
838
839
840
841
842
# File 'lib/arborist/node.rb', line 830

def register_secondary_dependencies( manager )
	self.dependencies.all_identifiers.each do |identifier|
		# Check to be sure the identifier isn't a descendant or ancestor
		if manager.ancestors_for( self ).any? {|node| node.identifier == identifier}
			raise Arborist::ConfigError, "Can't depend on ancestor node %p." % [ identifier ]
		elsif manager.descendants_for( self ).any? {|node| node.identifier == identifier }
			raise Arborist::ConfigError, "Can't depend on descendant node %p." % [ identifier ]
		end

		sub = Arborist::NodeSubscription.new( self )
		manager.subscribe( identifier, sub )
	end
end

#remove_child(node) ⇒ Object

Unregister the specified node as a child of this node.



1053
1054
1055
1056
# File 'lib/arborist/node.rb', line 1053

def remove_child( node )
	self.log.debug "Removing node %p from children" % [ node ]
	return self.children.delete( node.identifier )
end

#remove_subscription(subscription_id) ⇒ Object

Remove the specified subscription (an Arborist::Subscription) from the node.



578
579
580
# File 'lib/arborist/node.rb', line 578

def remove_subscription( subscription_id )
	return self.subscriptions.delete( subscription_id )
end

#reparent(old_parent, new_parent) ⇒ Object

Move a node from old_parent to new_parent.



901
902
903
904
905
906
907
908
# File 'lib/arborist/node.rb', line 901

def reparent( old_parent, new_parent )
	old_parent.remove_child( self )
	self.parent( new_parent.identifier )
	new_parent.add_child( self )

	self.quieted_reasons.delete( :primary )
	super
end

#restore(old_node) ⇒ Object

Restore any saved state from the old_node loaded from the state file. This is used to overlay selective bits of the saved node tree to the equivalent nodes loaded from node definitions.



1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
# File 'lib/arborist/node.rb', line 1121

def restore( old_node )
	@status              = old_node.status
	@properties          = old_node.properties.dup
	@ack                 = old_node.ack.dup if old_node.ack
	@last_contacted      = old_node.last_contacted
	@status_changed      = old_node.status_changed
	@status_history      = old_node.status_history
	@flapping            = old_node.flapping?
	@errors              = old_node.errors
	@warnings            = old_node.warnings
	@quieted_reasons     = old_node.quieted_reasons
	@status_last_changed = old_node.status_last_changed

	# Only merge in downed dependencies.
	old_node.dependencies.each_downed do |identifier, time|
		@dependencies.mark_down( identifier, time )
	end
end

#state_has_changed?Boolean

Returns true if the node’s state has changed since the last time #snapshot_state was called.

Returns:

  • (Boolean)


755
756
757
# File 'lib/arborist/node.rb', line 755

def state_has_changed?
	return ! self.update_delta.empty?
end

#statusObject

:method: status? :call-seq:

status?( status_name )

Returns true if the node’s status is status_name.



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/arborist/node.rb', line 131

state_machine( :status, initial: :unknown ) do

	state :unknown,
		:up,
		:down,
		:warn,
		:acked,
		:disabled,
		:quieted

	event :update do
		transition [:down, :warn, :unknown, :acked] => :up, unless: :has_errors_or_warnings?
		transition [:up, :warn, :unknown] => :down, if: :has_errors?
		transition [:up, :down, :unknown] => :warn, if: :has_only_warnings?
	end

	event :acknowledge do
		transition any - [:down] => :disabled
		transition :down => :acked
	end

	event :unacknowledge do
		transition [:acked, :disabled] => :warn, if: :has_warnings?
		transition [:acked, :disabled] => :down, if: :has_errors?
		transition [:acked, :disabled] => :unknown
	end

	event :handle_event do
		transition any - [:disabled, :quieted, :acked] => :quieted, if: :has_quieted_reason?
		transition :quieted => :unknown, unless: :has_quieted_reason?
	end

	event :reparent do
		transition any - [:disabled, :quieted, :acked] => :unknown
		transition :quieted => :unknown, unless: :has_quieted_reason?
	end

	before_transition [:acked, :disabled] => any, do: :save_previous_ack

	after_transition any => :acked, do: :on_ack
	after_transition :acked => :up, do: :on_ack_cleared
	after_transition :down => :up, do: :on_node_up
	after_transition :up => :warn, do: :on_node_warn
	after_transition [:unknown, :warn, :up] => :down, do: :on_node_down
	after_transition [:acked, :unknown, :warn, :up] => :disabled, do: :on_node_disabled
	after_transition any => :quieted, do: :on_node_quieted
	after_transition :disabled => :unknown, do: :on_node_enabled
	after_transition :quieted => :unknown, do: :on_node_unquieted

	after_transition any => any, do: :log_transition
	after_transition any => any, do: :make_transition_event
	after_transition any => any, do: :update_status_changed

	after_transition do: :add_status_to_update_delta

	after_transition do: :record_status_history
	after_failure do: :record_status_history
end

#status_descriptionObject

Return a string describing the node’s status.



1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
# File 'lib/arborist/node.rb', line 1072

def status_description
	case self.status
	when 'up', 'down', 'warn'
		return "%s as of %s" % [ self.status.upcase, self.last_contacted ]
	when 'acked'
		return "ACKed %s" % [ self.acked_description ]
	when 'disabled'
		return "disabled %s" % [ self.acked_description ]
	when 'quieted'
		reasons = self.quieted_reasons.values.join( ',' )
		return "quieted: %s" % [ reasons ]
	when 'unknown'
		return "in an 'unknown' state"
	else
		return "in an unhandled state"
	end
end

#status_history_size(new_size = nil) ⇒ Object

Get or set the number of entries to store for the status history.



544
545
546
547
# File 'lib/arborist/node.rb', line 544

def status_history_size( new_size=nil )
	@status_history_size = new_size if new_size
	return @status_history_size || Arborist::Node.status_history_size || 0
end

#tags(*tags) ⇒ Object

Declare one or more tags for this node.



500
501
502
503
504
# File 'lib/arborist/node.rb', line 500

def tags( *tags )
	tags.flatten!
	@tags.merge( tags.map(&:to_s) ) unless tags.empty?
	return @tags.to_a
end

#to_h(depth: 0) ⇒ Object

Return a Hash of the node’s state. If depth is greater than 0, that many levels of child nodes are included in the node’s ‘:children` value. Setting depth to a negative number will return all of the node’s children.



1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
# File 'lib/arborist/node.rb', line 1144

def to_h( depth: 0 )
	hash = {
		identifier: self.identifier,
		type: self.class.name.to_s.sub( /.+::/, '' ).downcase,
		parent: self.parent,
		family: self.family,
		description: self.description,
		tags: self.tags,
		config: self.config,
		status: self.status,
		properties: self.properties.dup,
		ack: self.ack ? self.ack.to_h : nil,
		last_contacted: self.last_contacted ? self.last_contacted.iso8601 : nil,
		status_changed: self.status_changed ? self.status_changed.iso8601 : nil,
		status_last_changed: self.status_last_changed ? self.status_last_changed.iso8601 : nil,
		status_history: self.status_history,
		flapping: self.flapping?,
		errors: self.errors,
		warnings: self.warnings,
		dependencies: self.dependencies.to_h,
		quieted_reasons: self.quieted_reasons,
	}

	if depth.nonzero?
		# self.log.debug "including children for depth %p" % [ depth ]
		hash[ :children ] = self.children.each_with_object( {} ) do |(ident, node), h|
			h[ ident ] = node.to_h( depth: depth - 1 )
		end
	else
		hash[ :children ] = {}
	end

	return hash
end

#typeObject

Return the simple type of this node (e.g., Arborist::Node::Host => ‘host’)



565
566
567
568
# File 'lib/arborist/node.rb', line 565

def type
	return 'anonymous' unless self.class.name
	return self.class.name.sub( /.*::/, '' ).downcase
end

#unacknowledgeObject

Clear any current acknowledgement.



715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
# File 'lib/arborist/node.rb', line 715

def unacknowledge
	super()

	self.ack = nil

	events = self.pending_change_events.clone
	events << self.make_delta_event unless self.update_delta.empty?
	results = self.broadcast_events( *events )
	self.log.debug ">>> Results from broadcast: %p" % [ results ]
	events.concat( results )

	return events
ensure
	self.clear_transition_temp_vars
end

#unreachable?Boolean

Returns true if the node’s status indicates it shouldn’t be included by default when traversing nodes.

Returns:

  • (Boolean)


1022
1023
1024
1025
# File 'lib/arborist/node.rb', line 1022

def unreachable?
	self.log.debug "Testing for reachability; status is: %p" % [ self.status ]
	return UNREACHABLE_STATES.include?( self.status )
end

#update(new_properties, monitor_key = '_') ⇒ Object

Update specified properties for the node.



603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
# File 'lib/arborist/node.rb', line 603

def update( new_properties, monitor_key='_' )
	self.last_contacted = Time.now
	self.update_properties( new_properties, monitor_key )

	# Super to the state machine event method
	super

	events = self.pending_change_events.clone
	events << self.make_update_event
	events << self.make_delta_event unless self.update_delta.empty?

	results = self.broadcast_events( *events )
	self.log.debug ">>> Results from broadcast: %p" % [ results ]
	events.concat( results )

	return events
ensure
	self.clear_transition_temp_vars
end

#update_errors(monitor_key, value = nil) ⇒ Object

Update the errors hash for the specified monitor_key to value.



677
678
679
680
681
682
683
# File 'lib/arborist/node.rb', line 677

def update_errors( monitor_key, value=nil )
	if value
		self.errors[ monitor_key ] = value
	else
		self.errors.delete( monitor_key )
	end
end

#update_properties(new_properties, monitor_key) ⇒ Object

Update the node’s properties with those in new_properties (a String-keyed Hash)



625
626
627
628
629
630
631
632
633
634
# File 'lib/arborist/node.rb', line 625

def update_properties( new_properties, monitor_key )
	monitor_key ||= '_'
	new_properties = stringify_keys( new_properties )

	self.log.debug "Updated via a %s monitor: %p" % [ monitor_key, new_properties ]
	self.update_errors( monitor_key, new_properties.delete('error') )
	self.update_warnings( monitor_key, new_properties.delete('warning') )

	self.merge_new_properties( new_properties )
end

#update_warnings(monitor_key, value = nil) ⇒ Object

Update the warnings hash for the specified monitor_key to value.



687
688
689
690
691
692
693
# File 'lib/arborist/node.rb', line 687

def update_warnings( monitor_key, value=nil )
	if value
		self.warnings[ monitor_key ] = value
	else
		self.warnings.delete( monitor_key )
	end
end