arborist 0.0.1.pre20160128152542 → 0.0.1.pre20160606141735

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 (66) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +2 -0
  4. data/ChangeLog +426 -1
  5. data/Manifest.txt +17 -2
  6. data/Nodes.md +70 -0
  7. data/Protocol.md +68 -9
  8. data/README.md +3 -5
  9. data/Rakefile +4 -1
  10. data/TODO.md +52 -20
  11. data/lib/arborist.rb +19 -6
  12. data/lib/arborist/cli.rb +39 -25
  13. data/lib/arborist/client.rb +97 -4
  14. data/lib/arborist/command/client.rb +2 -1
  15. data/lib/arborist/command/start.rb +51 -5
  16. data/lib/arborist/dependency.rb +286 -0
  17. data/lib/arborist/event.rb +7 -2
  18. data/lib/arborist/event/{node_matching.rb → node.rb} +11 -5
  19. data/lib/arborist/event/node_acked.rb +5 -7
  20. data/lib/arborist/event/node_delta.rb +30 -3
  21. data/lib/arborist/event/node_disabled.rb +16 -0
  22. data/lib/arborist/event/node_down.rb +10 -0
  23. data/lib/arborist/event/node_quieted.rb +11 -0
  24. data/lib/arborist/event/node_unknown.rb +10 -0
  25. data/lib/arborist/event/node_up.rb +10 -0
  26. data/lib/arborist/event/node_update.rb +2 -11
  27. data/lib/arborist/event/sys_node_added.rb +10 -0
  28. data/lib/arborist/event/sys_node_removed.rb +10 -0
  29. data/lib/arborist/exceptions.rb +4 -0
  30. data/lib/arborist/manager.rb +188 -18
  31. data/lib/arborist/manager/event_publisher.rb +1 -1
  32. data/lib/arborist/manager/tree_api.rb +92 -13
  33. data/lib/arborist/mixins.rb +17 -0
  34. data/lib/arborist/monitor.rb +10 -1
  35. data/lib/arborist/monitor/socket.rb +123 -2
  36. data/lib/arborist/monitor_runner.rb +6 -5
  37. data/lib/arborist/node.rb +420 -94
  38. data/lib/arborist/node/ack.rb +72 -0
  39. data/lib/arborist/node/host.rb +43 -8
  40. data/lib/arborist/node/resource.rb +73 -0
  41. data/lib/arborist/node/root.rb +6 -0
  42. data/lib/arborist/node/service.rb +89 -22
  43. data/lib/arborist/observer.rb +1 -1
  44. data/lib/arborist/subscription.rb +11 -6
  45. data/spec/arborist/client_spec.rb +93 -5
  46. data/spec/arborist/dependency_spec.rb +375 -0
  47. data/spec/arborist/event/node_delta_spec.rb +66 -0
  48. data/spec/arborist/event/node_down_spec.rb +84 -0
  49. data/spec/arborist/event/node_spec.rb +59 -0
  50. data/spec/arborist/event/node_update_spec.rb +14 -3
  51. data/spec/arborist/event_spec.rb +3 -3
  52. data/spec/arborist/manager/tree_api_spec.rb +295 -3
  53. data/spec/arborist/manager_spec.rb +240 -57
  54. data/spec/arborist/monitor_spec.rb +26 -3
  55. data/spec/arborist/node/ack_spec.rb +74 -0
  56. data/spec/arborist/node/host_spec.rb +79 -0
  57. data/spec/arborist/node/resource_spec.rb +56 -0
  58. data/spec/arborist/node/service_spec.rb +68 -2
  59. data/spec/arborist/node_spec.rb +288 -11
  60. data/spec/arborist/subscription_spec.rb +23 -14
  61. data/spec/arborist_spec.rb +0 -4
  62. data/spec/data/observers/webservices.rb +10 -2
  63. data/spec/spec_helper.rb +8 -0
  64. metadata +58 -15
  65. metadata.gz.sig +0 -0
  66. data/LICENSE +0 -29
@@ -41,7 +41,7 @@ class Arborist::Manager::EventPublisher < ZMQ::Handler
41
41
 
42
42
  ### Publish the specified +event+.
43
43
  def publish( identifier, event )
44
- @event_queue << [ identifier, MessagePack.pack(event.to_hash) ]
44
+ @event_queue << [ identifier, MessagePack.pack(event.to_h) ]
45
45
  self.register
46
46
  return self
47
47
  end
@@ -43,7 +43,15 @@ class Arborist::Manager::TreeAPI < ZMQ::Handler
43
43
  self.log.error "%p: %s" % [ err.class, err.message ]
44
44
  err.backtrace.each {|frame| self.log.debug " #{frame}" }
45
45
 
46
- errtype = err.is_a?( Arborist::RequestError ) ? 'client' : 'server'
46
+ errtype = case err
47
+ when Arborist::RequestError,
48
+ Arborist::ConfigError,
49
+ Arborist::NodeError
50
+ 'client'
51
+ else
52
+ 'server'
53
+ end
54
+
47
55
  return self.error_response( errtype, err.message )
48
56
  end
49
57
 
@@ -116,8 +124,9 @@ class Arborist::Manager::TreeAPI < ZMQ::Handler
116
124
  raise Arborist::RequestError, "missing required header 'action'" unless
117
125
  header.key?( 'action' )
118
126
 
119
- raise Arborist::RequestError, "body must be a Map or Nil" unless
120
- body.is_a?( Hash ) || body.nil?
127
+ raise Arborist::RequestError, "body must be Nil, a Map, or an Array of Maps" unless
128
+ body.is_a?( Hash ) || body.nil? ||
129
+ ( body.is_a?(Array) && body.all? {|obj| obj.is_a?(Hash) } )
121
130
 
122
131
  return header, body
123
132
  end
@@ -125,7 +134,7 @@ class Arborist::Manager::TreeAPI < ZMQ::Handler
125
134
 
126
135
  ### Return a response to the `status` action.
127
136
  def handle_status_request( header, body )
128
- self.log.info "STATUS: %p" % [ header ]
137
+ self.log.debug "STATUS: %p" % [ header ]
129
138
  return successful_response(
130
139
  server_version: Arborist::VERSION,
131
140
  state: @manager.running? ? 'running' : 'not running',
@@ -137,7 +146,7 @@ class Arborist::Manager::TreeAPI < ZMQ::Handler
137
146
 
138
147
  ### Return a response to the `subscribe` action.
139
148
  def handle_subscribe_request( header, body )
140
- self.log.info "SUBSCRIBE: %p" % [ header ]
149
+ self.log.debug "SUBSCRIBE: %p" % [ header ]
141
150
  event_type = header[ 'event_type' ]
142
151
  node_identifier = header[ 'identifier' ]
143
152
  subscription = @manager.create_subscription( node_identifier, event_type, body )
@@ -148,7 +157,7 @@ class Arborist::Manager::TreeAPI < ZMQ::Handler
148
157
 
149
158
  ### Return a response to the `unsubscribe` action.
150
159
  def handle_unsubscribe_request( header, body )
151
- self.log.info "UNSUBSCRIBE: %p" % [ header ]
160
+ self.log.debug "UNSUBSCRIBE: %p" % [ header ]
152
161
  subscription_id = header[ 'subscription_id' ] or
153
162
  return error_response( 'client', 'No identifier specified for UNSUBSCRIBE.' )
154
163
  subscription = @manager.remove_subscription( subscription_id ) or
@@ -163,13 +172,20 @@ class Arborist::Manager::TreeAPI < ZMQ::Handler
163
172
 
164
173
  ### Return a repsonse to the `list` action.
165
174
  def handle_list_request( header, body )
166
- self.log.info "LIST: %p" % [ header ]
167
- from = header['from'] || '_'
175
+ self.log.debug "LIST: %p" % [ header ]
176
+ from = header['from'] || '_'
177
+ depth = header['depth']
168
178
 
169
179
  start_node = @manager.nodes[ from ]
170
180
  self.log.debug " Listing nodes under %p" % [ start_node ]
171
- iter = @manager.enumerator_for( start_node )
172
- data = iter.map( &:to_hash )
181
+ iter = if depth
182
+ self.log.debug " depth limited to %d" % [ depth ]
183
+ @manager.depth_limited_enumerator_for( start_node, depth )
184
+ else
185
+ self.log.debug " no depth limit"
186
+ @manager.enumerator_for( start_node )
187
+ end
188
+ data = iter.map( &:to_h )
173
189
  self.log.debug " got data for %d nodes" % [ data.length ]
174
190
 
175
191
  return successful_response( data )
@@ -178,7 +194,7 @@ class Arborist::Manager::TreeAPI < ZMQ::Handler
178
194
 
179
195
  ### Return a response to the 'fetch' action.
180
196
  def handle_fetch_request( header, body )
181
- self.log.info "FETCH: %p" % [ header ]
197
+ self.log.debug "FETCH: %p" % [ header ]
182
198
 
183
199
  include_down = header['include_down']
184
200
  values = if header.key?( 'return' )
@@ -186,7 +202,11 @@ class Arborist::Manager::TreeAPI < ZMQ::Handler
186
202
  else
187
203
  nil
188
204
  end
189
- states = @manager.fetch_matching_node_states( body, values, include_down )
205
+
206
+ body = [ body ] unless body.is_a?( Array )
207
+ positive = body.shift
208
+ negative = body.shift || {}
209
+ states = @manager.fetch_matching_node_states( positive, values, include_down, negative )
190
210
 
191
211
  return successful_response( states )
192
212
  end
@@ -194,7 +214,7 @@ class Arborist::Manager::TreeAPI < ZMQ::Handler
194
214
 
195
215
  ### Update nodes using the data from the update request's +body+.
196
216
  def handle_update_request( header, body )
197
- self.log.info "UPDATE: %p" % [ header ]
217
+ self.log.debug "UPDATE: %p" % [ header ]
198
218
 
199
219
  body.each do |identifier, properties|
200
220
  @manager.update_node( identifier, properties )
@@ -203,5 +223,64 @@ class Arborist::Manager::TreeAPI < ZMQ::Handler
203
223
  return successful_response( nil )
204
224
  end
205
225
 
226
+
227
+ ### Remove a node and its children.
228
+ def handle_prune_request( header, body )
229
+ self.log.debug "PRUNE: %p" % [ header ]
230
+
231
+ identifier = header[ 'identifier' ] or
232
+ return error_response( 'client', 'No identifier specified for PRUNE.' )
233
+ node = @manager.remove_node( identifier )
234
+
235
+ return successful_response( node ? true : nil )
236
+ end
237
+
238
+
239
+ ### Add a node
240
+ def handle_graft_request( header, body )
241
+ self.log.debug "GRAFT: %p" % [ header ]
242
+
243
+ identifier = header[ 'identifier' ] or
244
+ return error_response( 'client', 'No identifier specified for GRAFT.' )
245
+ type = header[ 'type' ] or
246
+ return error_response( 'client', 'No type specified for GRAFT.' )
247
+ parent = header[ 'parent' ] || '_'
248
+ parent_node = @manager.nodes[ parent ] or
249
+ return error_response( 'client', 'No parent node found for %s.' % [parent] )
250
+
251
+ self.log.debug "Grafting a new %s node under %p" % [ type, parent_node ]
252
+
253
+ # If the parent has a factory method for the node type, use it, otherwise
254
+ # use the Pluggability factory
255
+ node = if parent_node.respond_to?( type )
256
+ parent_node.method( type ).call( identifier, body )
257
+ else
258
+ body.merge!( parent: parent )
259
+ Arborist::Node.create( type, identifier, body )
260
+ end
261
+
262
+ @manager.add_node( node )
263
+
264
+ return successful_response( node ? node.identifier : nil )
265
+ end
266
+
267
+
268
+ ### Modify a node's operational attributes
269
+ def handle_modify_request( header, body )
270
+ self.log.debug "MODIFY: %p" % [ header ]
271
+
272
+ identifier = header[ 'identifier' ] or
273
+ return error_response( 'client', 'No identifier specified for MODIFY.' )
274
+ return error_response( 'client', "Unable to MODIFY root node." ) if identifier == '_'
275
+ node = @manager.nodes[ identifier ] or
276
+ return error_response( 'client', "No such node %p" % [identifier] )
277
+
278
+ self.log.debug "Modifying operational attributes of the %s node: %p" % [ identifier, body ]
279
+
280
+ node.modify( body )
281
+
282
+ return successful_response( nil )
283
+ end
284
+
206
285
  end # class Arborist::Manager::TreeAPI
207
286
 
@@ -358,6 +358,23 @@ module Arborist
358
358
  end
359
359
 
360
360
 
361
+ ### Returns true if the specified +hash+ includes the specified +key+, and the value
362
+ ### associated with the +key+ either includes +val+ if it is a Hash, or equals +val+ if it's
363
+ ### anything but a Hash.
364
+ def hash_matches( hash, key, val )
365
+ actual = hash[ key ] or return false
366
+
367
+ if actual.is_a?( Hash )
368
+ if val.is_a?( Hash )
369
+ return val.all? {|subkey, subval| hash_matches(actual, subkey, subval) }
370
+ else
371
+ return false
372
+ end
373
+ else
374
+ return actual == val
375
+ end
376
+ end
377
+
361
378
  end # HashUtilities
362
379
 
363
380
  end # module Arborist
@@ -35,7 +35,7 @@ class Arborist::Monitor
35
35
  DEFAULT_SPLAY = 0
36
36
 
37
37
 
38
- Arborist.add_dsl_constructor( :Monitor ) do |description, &block|
38
+ Arborist.add_dsl_constructor( self ) do |description, &block|
39
39
  Arborist::Monitor.new( description, &block )
40
40
  end
41
41
 
@@ -250,6 +250,15 @@ class Arborist::Monitor
250
250
  parent_writer.close
251
251
 
252
252
  return context.handle_results( pid, parent_reader, parent_err_reader )
253
+ rescue SystemCallError => err
254
+ self.log.error "%p while running external monitor command `%s`: %s" % [
255
+ err.class,
256
+ Shellwords.join( command ),
257
+ err.message
258
+ ]
259
+ self.log.debug " %s" % [ err.backtrace.join("\n ") ]
260
+ return {}
261
+
253
262
  ensure
254
263
  if pid
255
264
  begin
@@ -3,6 +3,7 @@
3
3
 
4
4
  require 'loggability'
5
5
  require 'timeout'
6
+ require 'socket'
6
7
 
7
8
  require 'arborist/monitor' unless defined?( Arborist::Monitor )
8
9
 
@@ -12,7 +13,6 @@ using Arborist::TimeRefinements
12
13
  # Socket-related Arborist monitor logic
13
14
  module Arborist::Monitor::Socket
14
15
 
15
-
16
16
  # Arborist TCP socket monitor logic
17
17
  class TCP
18
18
  extend Loggability
@@ -119,6 +119,8 @@ module Arborist::Monitor::Socket
119
119
  until connections.empty? || timeout_at.past?
120
120
  self.log.debug "Waiting on %d connections for %0.3ds..." %
121
121
  [ connections.values.length, timeout_at - Time.now ]
122
+
123
+ # :FIXME: Race condition: errors if timeout_at - Time.now is 0
122
124
  _, ready, _ = IO.select( nil, connections.keys, nil, timeout_at - Time.now )
123
125
 
124
126
  self.log.debug " select returned: %p" % [ ready ]
@@ -154,10 +156,129 @@ module Arborist::Monitor::Socket
154
156
 
155
157
  return results
156
158
  end
157
-
158
159
  end # class TCP
159
160
 
160
161
 
162
+ # Arborist UDP socket monitor logic
163
+ class UDP
164
+ extend Loggability
165
+ log_to :arborist
166
+
167
+
168
+ # Defaults for instances of this monitor
169
+ DEFAULT_OPTIONS = {
170
+ timeout: 0.001
171
+ }
172
+
173
+
174
+ ### Instantiate a monitor check and run it for the specified +nodes+.
175
+ def self::run( nodes )
176
+ return self.new.run( nodes )
177
+ end
178
+
179
+
180
+ ### Create a new UDP monitor with the specified +options+. Valid options are:
181
+ ###
182
+ ### +:timeout+
183
+ ### Set the number of seconds to wait for a connection for each node.
184
+ def initialize( options=DEFAULT_OPTIONS )
185
+ options = DEFAULT_OPTIONS.merge( options || {} )
186
+
187
+ options.each do |name, value|
188
+ self.public_send( "#{name}=", value )
189
+ end
190
+ end
191
+
192
+
193
+ ######
194
+ public
195
+ ######
196
+
197
+ # The timeout for connecting, in seconds.
198
+ attr_accessor :timeout
199
+
200
+
201
+ ### Run the UDP check for each of the specified Hash of +nodes+ and return a Hash of
202
+ ### updates for them based on trying to connect to them.
203
+ def run( nodes )
204
+ self.log.debug "Got nodes to UDP check: %p" % [ nodes ]
205
+
206
+ connections = self.make_connections( nodes )
207
+ return self.wait_for_connections( connections )
208
+ end
209
+
210
+
211
+ ### Open a socket for each of the specified nodes and return a Hash of
212
+ ### the sockets (or the error from the connection attempt) keyed by
213
+ ### node identifier.
214
+ def make_connections( nodes )
215
+ return nodes.each_with_object( {} ) do |(identifier, node_data), accum|
216
+
217
+ address = node_data['addresses'].first
218
+ port = node_data['port']
219
+
220
+ self.log.debug "Creating UDP connection for %s:%d" % [ address, port ]
221
+ sock = Socket.new( :INET, :DGRAM )
222
+
223
+ conn = begin
224
+ sockaddr = Socket.sockaddr_in( port, address )
225
+ sock.connect( sockaddr )
226
+ sock.send( '', 0 )
227
+ sock
228
+ rescue SocketError => err
229
+ self.log.error " %p setting up connection: %s" % [ err.class, err.message ]
230
+ err
231
+ end
232
+
233
+ accum[ conn ] = [ identifier, sock ]
234
+ end
235
+ end
236
+
237
+
238
+ ### For any elements of +connections+ that are sockets, wait on them to complete or error
239
+ ### and then return a Hash of node updates keyed by identifier based on the results.
240
+ def wait_for_connections( connections )
241
+ results = {}
242
+ start = Time.now
243
+
244
+ # First strip out all the ones that failed in the first #connect
245
+ connections.delete_if do |sock, (identifier, _)|
246
+ next false if sock.respond_to?( :recvfrom_nonblock ) # Keep sockets
247
+ self.log.debug " removing connect error for node %s" % [ identifier ]
248
+ results[ identifier ] = { error: sock.message }
249
+ end
250
+
251
+ # Test all connections
252
+ connections.each do |sock, (identifier, _)|
253
+ begin
254
+ sock.recvfrom_nonblock( 1 )
255
+
256
+ rescue IO::WaitReadable
257
+ ready, _, _ = IO.select( [sock], [], [], self.timeout )
258
+ if ready.nil?
259
+ now = Time.now
260
+ results[ identifier ] = {
261
+ udp_socket_connect: { time: now.to_s, duration: now - start }
262
+ }
263
+ self.log.debug " connection successful"
264
+ else
265
+ retry
266
+ end
267
+
268
+ rescue SocketError, SystemCallError => err
269
+ self.log.debug "%p during connection: %s" % [ err.class, err.message ]
270
+ results[ identifier ] = { error: err.message }
271
+
272
+ ensure
273
+ sock.close
274
+ end
275
+ end
276
+
277
+ return results
278
+ end
279
+ end # class UDP
280
+
281
+
161
282
  end # module Arborist::Monitor::Socket
162
283
 
163
284
 
@@ -62,12 +62,12 @@ class Arborist::MonitorRunner
62
62
 
63
63
  ### Run the specified +monitor+ and update nodes with the results.
64
64
  def run_monitor( monitor )
65
- criteria = monitor.positive_criteria
65
+ positive = monitor.positive_criteria
66
+ negative = monitor.negative_criteria
66
67
  include_down = monitor.include_down?
67
68
  props = monitor.node_properties
68
69
 
69
- self.fetch( criteria, include_down, props ) do |nodes|
70
- # :FIXME: Doesn't apply negative criteria
70
+ self.fetch( positive, include_down, props, negative ) do |nodes|
71
71
  results = monitor.run( nodes )
72
72
  self.update( results ) do
73
73
  self.log.debug "Updated %d via the '%s' monitor" %
@@ -79,10 +79,11 @@ class Arborist::MonitorRunner
79
79
 
80
80
  ### Create a fetch request using the runner's client, then queue the request up
81
81
  ### with the specified +block+ as the callback.
82
- def fetch( criteria, include_down, properties, &block )
82
+ def fetch( criteria, include_down, properties, negative={}, &block )
83
83
  fetch = self.client.make_fetch_request( criteria,
84
84
  include_down: include_down,
85
- properties: properties
85
+ properties: properties,
86
+ exclude: negative
86
87
  )
87
88
  self.queue_request( fetch, &block )
88
89
  end
@@ -11,6 +11,8 @@ require 'loggability'
11
11
  require 'pluggability'
12
12
  require 'arborist' unless defined?( Arborist )
13
13
  require 'arborist/mixins'
14
+ require 'arborist/exceptions'
15
+ require 'arborist/dependency'
14
16
 
15
17
  using Arborist::TimeRefinements
16
18
 
@@ -24,37 +26,76 @@ class Arborist::Node
24
26
  Arborist::MethodUtilities
25
27
 
26
28
 
27
- ##
28
29
  # The key for the thread local that is used to track instances as they're
29
30
  # loaded.
30
31
  LOADED_INSTANCE_KEY = :loaded_node_instances
31
32
 
33
+ # Regex to match a valid identifier
34
+ VALID_IDENTIFIER = /^\w[\w\-]*$/
32
35
 
33
- ##
34
- # The struct for the 'ack' operational property
35
- ACK = Struct.new( 'ArboristNodeACK', :message, :via, :sender, :time )
36
36
 
37
- ##
38
- # The keys required to be set for an ACK
39
- ACK_REQUIRED_PROPERTIES = %w[ message sender ]
37
+ autoload :Root, 'arborist/node/root'
38
+ autoload :Ack, 'arborist/node/ack'
40
39
 
41
40
 
42
- ##
43
41
  # Log via the Arborist logger
44
42
  log_to :arborist
45
43
 
46
- ##
47
44
  # Search for plugins in lib/arborist/node directories in loaded gems
48
45
  plugin_prefixes 'arborist/node'
49
46
 
50
47
 
48
+ ##
49
+ # :method: unknown?
50
+ # Returns +true+ if the node is in an 'unknown' state.
51
+
52
+ ##
53
+ # :method: up?
54
+ # Returns +true+ if the node is in an 'up' state.
55
+
56
+ ##
57
+ # :method: down?
58
+ # Returns +true+ if the node is in an 'down' state.
59
+
60
+ ##
61
+ # :method: acked?
62
+ # Returns +true+ if the node is in an 'acked' state.
63
+
64
+ ##
65
+ # :method: disabled?
66
+ # Returns +true+ if the node is in an 'disabled' state.
67
+
68
+ ##
69
+ # :method: human_status_name
70
+ # Return the node's status as a human-readable String.
71
+
72
+ ##
73
+ # :method: status
74
+ # Return the +status+ of the node. This will be one of: +unknown+, +up+, +down+, +acked+, or
75
+ # +disabled+.
76
+
77
+ ##
78
+ # :method: status=
79
+ # :call-seq:
80
+ # status=( new_status )
81
+ #
82
+ # Set the status of the node to +new_status+.
83
+
84
+ ##
85
+ # :method: status?
86
+ # :call-seq:
87
+ # status?( status_name )
88
+ #
89
+ # Returns +true+ if the node's status is +status_name+.
90
+
51
91
  state_machine( :status, initial: :unknown ) do
52
92
 
53
93
  state :unknown,
54
94
  :up,
55
95
  :down,
56
96
  :acked,
57
- :disabled
97
+ :disabled,
98
+ :quieted
58
99
 
59
100
  event :update do
60
101
  transition [:down, :unknown, :acked] => :up, if: :last_contact_successful?
@@ -64,14 +105,24 @@ class Arborist::Node
64
105
  transition :disabled => :unknown, unless: :ack_set?
65
106
  end
66
107
 
108
+ event :handle_event do
109
+ transition :unknown => :acked, if: :ack_and_error_set?
110
+ transition any - [:disabled, :quieted, :acked] => :quieted, if: :has_quieted_reason?
111
+ transition :quieted => :unknown, unless: :has_quieted_reason?
112
+ end
113
+
67
114
  after_transition any => :acked, do: :on_ack
68
115
  after_transition :acked => :up, do: :on_ack_cleared
69
116
  after_transition :down => :up, do: :on_node_up
70
117
  after_transition [:unknown, :up] => :down, do: :on_node_down
71
118
  after_transition [:unknown, :up] => :disabled, do: :on_node_disabled
119
+ after_transition any => :quieted, do: :on_node_quieted
72
120
  after_transition :disabled => :unknown, do: :on_node_enabled
121
+ after_transition :quieted => :unknown, do: :on_node_unquieted
73
122
 
74
123
  after_transition any => any, do: :log_transition
124
+ after_transition any => any, do: :make_transition_event
125
+ after_transition any => any, do: :update_status_changed
75
126
 
76
127
  after_transition do: :add_status_to_update_delta
77
128
  end
@@ -79,7 +130,11 @@ class Arborist::Node
79
130
 
80
131
  ### Return a curried Proc for the ::create method for the specified +type+.
81
132
  def self::curried_create( type )
82
- return self.method( :create ).to_proc.curry( 2 )[ type ]
133
+ if type.subnode_type?
134
+ return self.method( :create ).to_proc.curry( 3 )[ type ]
135
+ else
136
+ return self.method( :create ).to_proc.curry( 2 )[ type ]
137
+ end
83
138
  end
84
139
 
85
140
 
@@ -113,14 +168,46 @@ class Arborist::Node
113
168
  def self::inherited( subclass )
114
169
  super
115
170
 
116
- if name = subclass.name
117
- name.sub!( /.*::/, '' )
118
- body = self.curried_create( subclass )
119
- Arborist.add_dsl_constructor( name, &body )
120
- else
121
- self.log.info "Skipping DSL constructor for anonymous class."
171
+ body = self.curried_create( subclass )
172
+ Arborist.add_dsl_constructor( subclass, &body )
173
+ end
174
+
175
+
176
+ ### Get/set the node type instances of the class live under. If no parent_type is set, it
177
+ ### is a top-level node type.
178
+ def self::parent_types( *types )
179
+ @parent_types ||= []
180
+
181
+ types.each do |new_type|
182
+ subclass = Arborist::Node.get_subclass( new_type )
183
+ @parent_types << subclass
184
+ subclass.add_subnode_factory_method( self )
122
185
  end
123
186
 
187
+ return @parent_types
188
+ end
189
+ singleton_method_alias :parent_type, :parent_types
190
+
191
+
192
+ ### Returns +true+ if the receiver must be created under a specific node type.
193
+ def self::subnode_type?
194
+ return ! self.parent_types.empty?
195
+ end
196
+
197
+
198
+ ### Add a factory method that can be used to create subnodes of the specified +subnode_type+
199
+ ### on instances of the receiving class.
200
+ def self::add_subnode_factory_method( subnode_type )
201
+ if subnode_type.name
202
+ name = subnode_type.plugin_name
203
+ body = lambda do |identifier, attributes={}, &block|
204
+ return Arborist::Node.create( name, identifier, self, attributes, &block )
205
+ end
206
+
207
+ define_method( name, &body )
208
+ else
209
+ self.log.info "Skipping factory constructor for anonymous subnode class."
210
+ end
124
211
  end
125
212
 
126
213
 
@@ -150,31 +237,42 @@ class Arborist::Node
150
237
 
151
238
  ### Create a new Node with the specified +identifier+, which must be unique to the
152
239
  ### loaded tree.
153
- def initialize( identifier, &block )
154
- raise "Invalid identifier %p" % [identifier] unless
155
- identifier =~ /^\w[\w\-]*$/
240
+ def initialize( identifier, *args, &block )
241
+ attributes = args.last.is_a?( Hash ) ? args.pop : {}
242
+ parent_node = args.pop
156
243
 
157
- @identifier = identifier
158
- @parent = '_'
159
- @description = nil
160
- @tags = Set.new
161
- @source = nil
162
- @children = {}
163
-
164
- @status = 'unknown'
165
- @status_changed = Time.at( 0 )
166
-
167
- @error = nil
168
- @ack = nil
169
- @properties = {}
170
- @last_contacted = Time.at( 0 )
171
-
172
- @update_delta = Hash.new do |h,k|
244
+ raise "Invalid identifier %p" % [identifier] unless
245
+ identifier =~ VALID_IDENTIFIER
246
+
247
+ # Attributes of the target
248
+ @identifier = identifier
249
+ @parent = parent_node ? parent_node.identifier : '_'
250
+ @description = nil
251
+ @tags = Set.new
252
+ @properties = {}
253
+ @source = nil
254
+ @children = {}
255
+ @dependencies = Arborist::Dependency.new( :all )
256
+
257
+ # Primary state
258
+ @status = 'unknown'
259
+ @status_changed = Time.at( 0 )
260
+
261
+ # Attributes that govern state
262
+ @error = nil
263
+ @ack = nil
264
+ @last_contacted = Time.at( 0 )
265
+ @quieted_reasons = {}
266
+
267
+ # Event-handling
268
+ @update_delta = Hash.new do |h,k|
173
269
  h[ k ] = Hash.new( &h.default_proc )
174
270
  end
175
271
  @pending_update_events = []
176
272
  @subscriptions = {}
177
273
 
274
+ self.log.debug "Setting node attributes to: %p" % [ attributes ]
275
+ self.modify( attributes )
178
276
  self.instance_eval( &block ) if block
179
277
  end
180
278
 
@@ -228,6 +326,16 @@ class Arborist::Node
228
326
  # subscription ID.
229
327
  attr_reader :subscriptions
230
328
 
329
+ ##
330
+ # The node's secondary dependencies, expressed as an Arborist::Node::Sexp
331
+ attr_accessor :dependencies
332
+
333
+
334
+ ##
335
+ # The reasons this node was quieted. This is a Hash of text descriptions keyed by the
336
+ # type of dependency it came from (either :primary or :secondary).
337
+ attr_reader :quieted_reasons
338
+
231
339
 
232
340
  ### Set the source of the node to +source+, which should be a valid URI.
233
341
  def source=( source )
@@ -235,6 +343,23 @@ class Arborist::Node
235
343
  end
236
344
 
237
345
 
346
+ ### Set one or more node +attributes+. This should be overridden by subclasses which
347
+ ### wish to allow their operational attributes to be set/updated via the Tree API
348
+ ### (+modify+ and +graft+). Supported attributes are: +parent+, +description+, and
349
+ ### +tags+.
350
+ def modify( attributes )
351
+ attributes = stringify_keys( attributes )
352
+
353
+ self.parent( attributes['parent'] )
354
+ self.description( attributes['description'] )
355
+
356
+ if attributes['tags']
357
+ @tags.clear
358
+ self.tags( attributes['tags'] )
359
+ end
360
+ end
361
+
362
+
238
363
  #
239
364
  # :section: DSLish declaration methods
240
365
  # These methods are both getter and setter for a node's attributes, used
@@ -263,11 +388,40 @@ class Arborist::Node
263
388
 
264
389
  ### Declare one or more +tags+ for this node.
265
390
  def tags( *tags )
391
+ tags.flatten!
266
392
  @tags.merge( tags.map(&:to_s) ) unless tags.empty?
267
393
  return @tags.to_a
268
394
  end
269
395
 
270
396
 
397
+ ### Group +identifiers+ together in an 'any of' dependency.
398
+ def any_of( *identifiers, on: nil )
399
+ return Arborist::Dependency.on( :any, *identifiers, prefixes: on )
400
+ end
401
+
402
+
403
+ ### Group +identifiers+ together in an 'all of' dependency.
404
+ def all_of( *identifiers, on: nil )
405
+ return Arborist::Dependency.on( :all, *identifiers, prefixes: on )
406
+ end
407
+
408
+
409
+ ### Add secondary dependencies to the receiving node.
410
+ def depends_on( *dependencies, on: nil )
411
+ dependencies = self.all_of( *dependencies, on: on )
412
+
413
+ self.log.debug "Setting secondary dependencies to: %p" % [ dependencies ]
414
+ self.dependencies = check_dependencies( dependencies )
415
+ end
416
+
417
+
418
+ ### Returns +true+ if the node has one or more secondary dependencies.
419
+ def has_dependencies?
420
+ return !self.dependencies.empty?
421
+ end
422
+
423
+
424
+
271
425
  #
272
426
  # :section: Manager API
273
427
  # Methods used by the manager to manage its nodes.
@@ -299,14 +453,6 @@ class Arborist::Node
299
453
  end
300
454
 
301
455
 
302
- ### Publish the specified +events+ to any subscriptions the node has which match them.
303
- def publish_events( *events )
304
- self.subscriptions.each_value do |sub|
305
- sub.on_events( *events )
306
- end
307
- end
308
-
309
-
310
456
  ### Update specified +properties+ for the node.
311
457
  def update( new_properties )
312
458
  new_properties = stringify_keys( new_properties )
@@ -329,6 +475,7 @@ class Arborist::Node
329
475
  events << self.make_update_event
330
476
  events << self.make_delta_event unless self.update_delta.empty?
331
477
 
478
+ self.broadcast_events( *events )
332
479
  return events
333
480
  ensure
334
481
  self.update_delta.clear
@@ -397,6 +544,8 @@ class Arborist::Node
397
544
  ### Returns +true+ if the node matches the specified +key+ and +val+ criteria.
398
545
  def match_criteria?( key, val )
399
546
  return case key
547
+ when 'delta'
548
+ true
400
549
  when 'status'
401
550
  self.status == val
402
551
  when 'type'
@@ -440,6 +589,129 @@ class Arborist::Node
440
589
  end
441
590
 
442
591
 
592
+ ### Register subscriptions for secondary dependencies on the receiving node with the
593
+ ### given +manager+.
594
+ def register_secondary_dependencies( manager )
595
+ self.dependencies.all_identifiers.each do |identifier|
596
+ # Check to be sure the identifier isn't a descendant or ancestor
597
+ if manager.ancestors_for( self ).any? {|node| node.identifier == identifier}
598
+ raise Arborist::ConfigError, "Can't depend on ancestor node %p." % [ identifier ]
599
+ elsif manager.descendants_for( self ).any? {|node| node.identifier == identifier }
600
+ raise Arborist::ConfigError, "Can't depend on descendant node %p." % [ identifier ]
601
+ end
602
+
603
+ sub = Arborist::Subscription.new do |_, event|
604
+ self.handle_event( event )
605
+ end
606
+ manager.subscribe( identifier, sub )
607
+ end
608
+ end
609
+
610
+
611
+ ### Publish the specified +events+ to any subscriptions the node has which match them.
612
+ def publish_events( *events )
613
+ self.log.debug "Got published events: %p" % [ events ]
614
+ self.subscriptions.each_value do |sub|
615
+ sub.on_events( *events )
616
+ end
617
+ end
618
+
619
+
620
+ ### Send an event to this node's immediate children.
621
+ def broadcast_events( *events )
622
+ events.flatten!
623
+ self.children.each do |identifier, child|
624
+ self.log.debug "Broadcasting %d events to %p" % [ events.length, identifier ]
625
+ events.each do |event|
626
+ child.handle_event( event )
627
+ end
628
+ end
629
+ end
630
+
631
+
632
+ ### Handle the specified +event+, delivered either via broadcast or secondary
633
+ ### dependency subscription.
634
+ def handle_event( event )
635
+ self.log.debug "Handling %p" % [ event ]
636
+ handler_name = "handle_%s_event" % [ event.type.gsub('.', '_') ]
637
+ if self.respond_to?( handler_name )
638
+ self.log.debug "Handling a %s event." % [ event.type ]
639
+ self.method( handler_name ).call( event )
640
+ end
641
+ super
642
+ end
643
+
644
+
645
+ ### Returns +true+ if this node's dependencies are not met.
646
+ def dependencies_down?
647
+ return self.dependencies.down?
648
+ end
649
+ alias_method :has_downed_dependencies?, :dependencies_down?
650
+
651
+
652
+ ### Returns +true+ if this node's dependencies are met.
653
+ def dependencies_up?
654
+ return !self.dependencies_down?
655
+ end
656
+
657
+
658
+ ### Returns +true+ if any reasons have been set as to why the node has been
659
+ ### quieted. Guard condition for transition to and from `quieted` state.
660
+ def has_quieted_reason?
661
+ return !self.quieted_reasons.empty?
662
+ end
663
+
664
+
665
+ ### Handle a 'node.down' event received via broadcast.
666
+ def handle_node_down_event( event )
667
+ self.log.debug "Got a node.down event: %p" % [ event ]
668
+ self.dependencies.mark_down( event.node.identifier )
669
+
670
+ if self.dependencies_down?
671
+ self.quieted_reasons[ :secondary ] = "Secondary dependencies not met: %s" %
672
+ [ self.dependencies.down_reason ]
673
+ end
674
+
675
+ if event.node.identifier == self.parent
676
+ self.quieted_reasons[ :primary ] = "Parent down: %s" % [ self.parent ] # :TODO: backtrace?
677
+ end
678
+ end
679
+
680
+
681
+ ### Handle a 'node.quieted' event received via broadcast.
682
+ def handle_node_quieted_event( event )
683
+ self.log.debug "Got a node.quieted event: %p" % [ event ]
684
+ self.dependencies.mark_down( event.node.identifier )
685
+
686
+ if self.dependencies_down?
687
+ self.quieted_reasons[ :secondary ] = "Secondary dependencies not met: %s" %
688
+ [ self.dependencies.down_reason ]
689
+ end
690
+
691
+ if event.node.identifier == self.parent
692
+ self.quieted_reasons[ :primary ] = "Parent quieted: %s" % [ self.parent ] # :TODO: backtrace?
693
+ end
694
+ end
695
+
696
+
697
+ ### Handle a 'node.up' event received via broadcast.
698
+ def handle_node_up_event( event )
699
+ self.log.debug "Got a node.up event: %p" % [ event ]
700
+
701
+ self.dependencies.mark_up( event.node.identifier )
702
+ self.quieted_reasons.delete( :secondary ) if self.dependencies_up?
703
+
704
+ if event.node.identifier == self.parent
705
+ self.log.info "Parent of %s (%s) came back up." % [
706
+ self.identifier,
707
+ self.parent
708
+ ]
709
+ self.quieted_reasons.delete( :primary )
710
+ end
711
+ end
712
+
713
+
714
+
443
715
  #
444
716
  # :section: Hierarchy API
445
717
  #
@@ -490,15 +762,26 @@ class Arborist::Node
490
762
  # :section: Utility methods
491
763
  #
492
764
 
765
+
766
+ ### Return a description of the ack if it's set, or a generic string otherwise.
767
+ def acked_description
768
+ return self.ack.description if self.ack
769
+ return "(unset)"
770
+ end
771
+
772
+
493
773
  ### Return a string describing the node's status.
494
774
  def status_description
495
775
  case self.status
496
776
  when 'up', 'down'
497
777
  return "%s as of %s" % [ self.status.upcase, self.last_contacted ]
498
778
  when 'acked'
499
- return "ACKed by %s %s" % [ self.ack.sender, self.ack.time.as_delta ]
779
+ return "ACKed %s" % [ self.acked_description ]
500
780
  when 'disabled'
501
- return "disabled by %s %s" % [ self.ack.sender, self.ack.time.as_delta ]
781
+ return "disabled %s" % [ self.acked_description ]
782
+ when 'quieted'
783
+ reasons = self.quieted_reasons.values.join( ',' )
784
+ return "quieted: %s" % [ reasons ]
502
785
  else
503
786
  return "in an unknown state"
504
787
  end
@@ -514,7 +797,7 @@ class Arborist::Node
514
797
 
515
798
  ### Return a String representation of the object suitable for debugging.
516
799
  def inspect
517
- return "#<%p:%#x [%s] -> %s %p %s%s, %d children, %s>" % [
800
+ return "#<%p:%#x [%s] -> %s %p %s %s, %d children, %s>" % [
518
801
  self.class,
519
802
  self.object_id * 2,
520
803
  self.identifier,
@@ -532,52 +815,78 @@ class Arborist::Node
532
815
  # :section: Serialization API
533
816
  #
534
817
 
818
+ ### Restore any saved state from the +old_node+ loaded from the state file.
819
+ def restore( old_node )
820
+ @status = old_node.status
821
+ @properties = old_node.properties.dup
822
+ @ack = old_node.ack.dup if old_node.ack
823
+ @last_contacted = old_node.last_contacted
824
+ @status_changed = old_node.status_changed
825
+ @error = old_node.error
826
+ @quieted_reasons = old_node.quieted_reasons
827
+
828
+ # Only merge in downed dependencies.
829
+ old_node.dependencies.each_downed do |identifier, time|
830
+ @dependencies.mark_down( identifier, time )
831
+ end
832
+ end
833
+
834
+
535
835
  ### Return a Hash of the node's state.
536
- def to_hash
836
+ def to_h
537
837
  return {
538
838
  identifier: self.identifier,
539
839
  type: self.class.name.to_s.sub( /.+::/, '' ).downcase,
540
840
  parent: self.parent,
541
841
  description: self.description,
542
842
  tags: self.tags,
543
- properties: self.properties.dup,
544
843
  status: self.status,
844
+ properties: self.properties.dup,
545
845
  ack: self.ack ? self.ack.to_h : nil,
546
846
  last_contacted: self.last_contacted ? self.last_contacted.iso8601 : nil,
547
847
  status_changed: self.status_changed ? self.status_changed.iso8601 : nil,
548
848
  error: self.error,
849
+ dependencies: self.dependencies.to_h,
850
+ quieted_reasons: self.quieted_reasons,
549
851
  }
550
852
  end
551
853
 
552
854
 
553
855
  ### Marshal API -- return the node as an object suitable for marshalling.
554
856
  def marshal_dump
555
- return self.to_hash
857
+ return self.to_h.merge( dependencies: self.dependencies )
556
858
  end
557
859
 
558
860
 
559
861
  ### Marshal API -- set up the object's state using the +hash+ from a
560
862
  ### previously-marshalled node.
561
863
  def marshal_load( hash )
562
- @identifier = hash[:identifier]
563
- @properties = hash[:properties]
864
+ @identifier = hash[:identifier]
865
+ @properties = hash[:properties]
564
866
 
565
- @parent = hash[:parent]
566
- @description = hash[:description]
567
- @tags = Set.new( hash[:tags] )
568
- @children = {}
867
+ @parent = hash[:parent]
868
+ @description = hash[:description]
869
+ @tags = Set.new( hash[:tags] )
870
+ @children = {}
569
871
 
570
- @status = hash[:status]
571
- @status_changed = Time.parse( hash[:status_changed] )
872
+ @status = 'unknown'
873
+ @status_changed = Time.parse( hash[:status_changed] )
572
874
 
573
- @error = hash[:error]
574
- @properties = hash[:properties]
575
- @last_contacted = Time.parse( hash[:last_contacted] )
875
+ @error = hash[:error]
876
+ @properties = hash[:properties] || {}
877
+ @last_contacted = Time.parse( hash[:last_contacted] )
878
+ @quieted_reasons = hash[:quieted_reasons] || {}
879
+ self.log.debug "Deps are: %p" % [ hash[:dependencies] ]
880
+ @dependencies = hash[:dependencies]
576
881
 
577
- if hash[:ack]
578
- ack_values = hash[:ack].values_at( *Arborist::Node::ACK.members )
579
- @ack = Arborist::Node::ACK.new( *ack_values )
882
+ @update_delta = Hash.new do |h,k|
883
+ h[ k ] = Hash.new( &h.default_proc )
580
884
  end
885
+
886
+ @pending_update_events = []
887
+ @subscriptions = {}
888
+
889
+ self.ack = hash[:ack]
581
890
  end
582
891
 
583
892
 
@@ -588,11 +897,7 @@ class Arborist::Node
588
897
  other_node.identifier == self.identifier &&
589
898
  other_node.parent == self.parent &&
590
899
  other_node.description == self.description &&
591
- other_node.tags == self.tags &&
592
- other_node.properties == self.properties &&
593
- other_node.status == self.status &&
594
- other_node.ack == self.ack &&
595
- other_node.error == self.error
900
+ other_node.tags == self.tags
596
901
  end
597
902
 
598
903
 
@@ -604,15 +909,7 @@ class Arborist::Node
604
909
  def ack=( ack_data )
605
910
  if ack_data
606
911
  self.log.info "Node %s ACKed with data: %p" % [ self.identifier, ack_data ]
607
- ack_data['time'] ||= Time.now
608
- ack_values = ack_data.values_at( *Arborist::Node::ACK.members.map(&:to_s) )
609
- new_ack = Arborist::Node::ACK.new( *ack_values )
610
-
611
- if missing = ACK_REQUIRED_PROPERTIES.find {|prop| new_ack[prop].nil? }
612
- raise "Missing required ACK attribute %s" % [ missing ]
613
- end
614
-
615
- @ack = new_ack
912
+ @ack = Arborist::Node::Ack.from_hash( ack_data )
616
913
  else
617
914
  self.log.info "Node %s ACK cleared explicitly" % [ self.identifier ]
618
915
  @ack = nil
@@ -637,6 +934,12 @@ class Arborist::Node
637
934
  end
638
935
 
639
936
 
937
+ ### Returns +true+ if the node has been acked and also has an error set.
938
+ def ack_and_error_set?
939
+ return self.error && self.ack_set?
940
+ end
941
+
942
+
640
943
  #
641
944
  # :section: State Callbacks
642
945
  #
@@ -648,18 +951,30 @@ class Arborist::Node
648
951
  end
649
952
 
650
953
 
954
+ ### Update the last status change time.
955
+ def update_status_changed( transition )
956
+ self.status_changed = Time.now
957
+ end
958
+
959
+
960
+ ### Queue up a transition event whenever one happens
961
+ def make_transition_event( transition )
962
+ node_type = "node_%s" % [ transition.to ]
963
+ self.log.debug "Making a %s event for %p" % [ node_type, transition ]
964
+ self.pending_update_events << Arborist::Event.create( node_type, self )
965
+ end
966
+
967
+
651
968
  ### Callback for when an acknowledgement is set.
652
969
  def on_ack( transition )
653
970
  self.log.warn "ACKed: %s" % [ self.status_description ]
654
- self.pending_update_events <<
655
- Arborist::Event.create( 'node_acked', self.fetch_values, self.ack.to_h )
656
971
  end
657
972
 
658
973
 
659
974
  ### Callback for when an acknowledgement is cleared.
660
975
  def on_ack_cleared( transition )
661
- self.error = nil
662
976
  self.log.warn "ACK cleared for %s" % [ self.identifier ]
977
+ self.ack = nil
663
978
  end
664
979
 
665
980
 
@@ -683,6 +998,18 @@ class Arborist::Node
683
998
  end
684
999
 
685
1000
 
1001
+ ### Callback for when a node goes from any state to quieted
1002
+ def on_node_quieted( transition )
1003
+ self.log.warn "%s is %s" % [ self.identifier, self.status_description ]
1004
+ end
1005
+
1006
+
1007
+ ### Callback for when a node transitions from quieted to unknown
1008
+ def on_node_unquieted( transition )
1009
+ self.log.warn "%s is %s" % [ self.identifier, self.status_description ]
1010
+ end
1011
+
1012
+
686
1013
  ### Callback for when a node goes from disabled to unknown
687
1014
  def on_node_enabled( transition )
688
1015
  self.log.warn "%s is %s" % [ self.identifier, self.status_description ]
@@ -700,21 +1027,20 @@ class Arborist::Node
700
1027
  private
701
1028
  #######
702
1029
 
703
- ### Returns true if the specified +hash+ includes the specified +key+, and the value
704
- ### associated with the +key+ either includes +val+ if it is a Hash, or equals +val+ if it's
705
- ### anything but a Hash.
706
- def hash_matches( hash, key, val )
707
- actual = hash[ key ] or return false
1030
+ ### Check the specified +dependencies+ (an Arborist::Dependency) for illegal dependencies
1031
+ ### and raise an error if any are found.
1032
+ def check_dependencies( dependencies )
1033
+ identifiers = dependencies.all_identifiers
708
1034
 
709
- if actual.is_a?( Hash )
710
- if val.is_a?( Hash )
711
- return val.all? {|subkey, subval| hash_matches(actual, subkey, subval) }
712
- else
713
- return false
714
- end
715
- else
716
- return actual == val
1035
+ self.log.debug "Checking dependency identifiers: %p" % [ identifiers ]
1036
+ if identifiers.include?( '_' )
1037
+ raise Arborist::ConfigError, "a node can't depend on the root node"
1038
+ elsif identifiers.include?( self.identifier )
1039
+ raise Arborist::ConfigError, "a node can't depend on itself"
717
1040
  end
1041
+
1042
+ return dependencies
718
1043
  end
719
1044
 
1045
+
720
1046
  end # class Arborist::Node