arborist 0.0.1.pre20160128152542 → 0.0.1.pre20160606141735

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