arborist 0.0.1.pre20160106113421
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.document +4 -0
- data/.simplecov +9 -0
- data/ChangeLog +417 -0
- data/Events.md +20 -0
- data/History.md +4 -0
- data/LICENSE +29 -0
- data/Manifest.txt +72 -0
- data/Monitors.md +141 -0
- data/Nodes.md +0 -0
- data/Observers.md +72 -0
- data/Protocol.md +214 -0
- data/README.md +75 -0
- data/Rakefile +81 -0
- data/TODO.md +24 -0
- data/bin/amanagerd +10 -0
- data/bin/amonitord +12 -0
- data/bin/aobserverd +12 -0
- data/lib/arborist.rb +182 -0
- data/lib/arborist/client.rb +191 -0
- data/lib/arborist/event.rb +61 -0
- data/lib/arborist/event/node_acked.rb +18 -0
- data/lib/arborist/event/node_delta.rb +20 -0
- data/lib/arborist/event/node_matching.rb +34 -0
- data/lib/arborist/event/node_update.rb +19 -0
- data/lib/arborist/event/sys_reloaded.rb +15 -0
- data/lib/arborist/exceptions.rb +21 -0
- data/lib/arborist/manager.rb +508 -0
- data/lib/arborist/manager/event_publisher.rb +97 -0
- data/lib/arborist/manager/tree_api.rb +207 -0
- data/lib/arborist/mixins.rb +363 -0
- data/lib/arborist/monitor.rb +377 -0
- data/lib/arborist/monitor/socket.rb +163 -0
- data/lib/arborist/monitor_runner.rb +217 -0
- data/lib/arborist/node.rb +700 -0
- data/lib/arborist/node/host.rb +87 -0
- data/lib/arborist/node/root.rb +60 -0
- data/lib/arborist/node/service.rb +112 -0
- data/lib/arborist/observer.rb +176 -0
- data/lib/arborist/observer/action.rb +125 -0
- data/lib/arborist/observer/summarize.rb +105 -0
- data/lib/arborist/observer_runner.rb +181 -0
- data/lib/arborist/subscription.rb +82 -0
- data/spec/arborist/client_spec.rb +282 -0
- data/spec/arborist/event/node_update_spec.rb +71 -0
- data/spec/arborist/event_spec.rb +64 -0
- data/spec/arborist/manager/event_publisher_spec.rb +66 -0
- data/spec/arborist/manager/tree_api_spec.rb +458 -0
- data/spec/arborist/manager_spec.rb +442 -0
- data/spec/arborist/mixins_spec.rb +195 -0
- data/spec/arborist/monitor/socket_spec.rb +195 -0
- data/spec/arborist/monitor_runner_spec.rb +152 -0
- data/spec/arborist/monitor_spec.rb +251 -0
- data/spec/arborist/node/host_spec.rb +104 -0
- data/spec/arborist/node/root_spec.rb +29 -0
- data/spec/arborist/node/service_spec.rb +98 -0
- data/spec/arborist/node_spec.rb +552 -0
- data/spec/arborist/observer/action_spec.rb +205 -0
- data/spec/arborist/observer/summarize_spec.rb +294 -0
- data/spec/arborist/observer_spec.rb +146 -0
- data/spec/arborist/subscription_spec.rb +71 -0
- data/spec/arborist_spec.rb +146 -0
- data/spec/data/monitors/pings.rb +80 -0
- data/spec/data/monitors/port_checks.rb +27 -0
- data/spec/data/monitors/system_resources.rb +30 -0
- data/spec/data/monitors/web_services.rb +17 -0
- data/spec/data/nodes/duir.rb +20 -0
- data/spec/data/nodes/localhost.rb +15 -0
- data/spec/data/nodes/sidonie.rb +29 -0
- data/spec/data/nodes/yevaud.rb +26 -0
- data/spec/data/observers/auditor.rb +23 -0
- data/spec/data/observers/webservices.rb +18 -0
- data/spec/spec_helper.rb +117 -0
- metadata +368 -0
@@ -0,0 +1,217 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
#encoding: utf-8
|
3
|
+
|
4
|
+
require 'rbczmq'
|
5
|
+
require 'loggability'
|
6
|
+
|
7
|
+
require 'arborist' unless defined?( Arborist )
|
8
|
+
require 'arborist/client'
|
9
|
+
|
10
|
+
|
11
|
+
# Undo the useless scoping
|
12
|
+
class ZMQ::Loop
|
13
|
+
public_class_method :instance
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
# An event-driven runner for Arborist::Monitors.
|
18
|
+
class Arborist::MonitorRunner
|
19
|
+
extend Loggability
|
20
|
+
|
21
|
+
log_to :arborist
|
22
|
+
|
23
|
+
|
24
|
+
# A ZMQ::Handler object for managing IO for all running monitors.
|
25
|
+
class Handler < ZMQ::Handler
|
26
|
+
extend Loggability,
|
27
|
+
Arborist::MethodUtilities
|
28
|
+
|
29
|
+
log_to :arborist
|
30
|
+
|
31
|
+
### Create a ZMQ::Handler that acts as the agent that runs the specified
|
32
|
+
### +monitor+.
|
33
|
+
def initialize( reactor )
|
34
|
+
@reactor = reactor
|
35
|
+
@client = Arborist::Client.new
|
36
|
+
@pollitem = ZMQ::Pollitem.new( @client.tree_api, ZMQ::POLLOUT )
|
37
|
+
@pollitem.handler = self
|
38
|
+
|
39
|
+
@request_queue = {}
|
40
|
+
@registered = false
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
######
|
45
|
+
public
|
46
|
+
######
|
47
|
+
|
48
|
+
# The ZMQ::Loop that this runner is registered with
|
49
|
+
attr_reader :reactor
|
50
|
+
|
51
|
+
# The Queue of pending requests, keyed by the callback that should be called with the
|
52
|
+
# results.
|
53
|
+
attr_reader :request_queue
|
54
|
+
|
55
|
+
# The Arborist::Client that will provide the message packing and unpacking
|
56
|
+
attr_reader :client
|
57
|
+
|
58
|
+
##
|
59
|
+
# True if the Handler is registered to write one or more requests
|
60
|
+
attr_predicate :registered
|
61
|
+
|
62
|
+
|
63
|
+
### Run the specified +monitor+ and update nodes with the results.
|
64
|
+
def run_monitor( monitor )
|
65
|
+
criteria = monitor.positive_criteria
|
66
|
+
include_down = monitor.include_down?
|
67
|
+
props = monitor.node_properties
|
68
|
+
|
69
|
+
self.fetch( criteria, include_down, props ) do |nodes|
|
70
|
+
# :FIXME: Doesn't apply negative criteria
|
71
|
+
results = monitor.run( nodes )
|
72
|
+
self.update( results ) do
|
73
|
+
self.log.debug "Updated %d via the '%s' monitor" %
|
74
|
+
[ results.length, monitor.description ]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
### Create a fetch request using the runner's client, then queue the request up
|
81
|
+
### with the specified +block+ as the callback.
|
82
|
+
def fetch( criteria, include_down, properties, &block )
|
83
|
+
fetch = self.client.make_fetch_request( criteria,
|
84
|
+
include_down: include_down,
|
85
|
+
properties: properties
|
86
|
+
)
|
87
|
+
self.queue_request( fetch, &block )
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
### Create an update request using the runner's client, then queue the request up
|
92
|
+
### with the specified +block+ as the callback.
|
93
|
+
def update( nodemap, &block )
|
94
|
+
update = self.client.make_update_request( nodemap )
|
95
|
+
self.queue_request( update, &block )
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
### Add the specified +event+ to the queue to be published to the console event
|
100
|
+
### socket
|
101
|
+
def queue_request( request, &callback )
|
102
|
+
self.request_queue[ callback ] = request
|
103
|
+
self.register
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
### Register the handler's pollitem as being ready to write if it isn't already.
|
108
|
+
def register
|
109
|
+
# self.log.debug "Registering for writing."
|
110
|
+
self.reactor.register( self.pollitem ) unless @registered
|
111
|
+
@registered = true
|
112
|
+
end
|
113
|
+
|
114
|
+
|
115
|
+
### Unregister the handler's pollitem from the reactor when there's nothing ready
|
116
|
+
### to write.
|
117
|
+
def unregister
|
118
|
+
# self.log.debug "Unregistering for writing."
|
119
|
+
self.reactor.remove( self.pollitem ) if @registered
|
120
|
+
@registered = false
|
121
|
+
end
|
122
|
+
|
123
|
+
|
124
|
+
### Write commands from the queue
|
125
|
+
def on_writable
|
126
|
+
if (( pair = self.request_queue.shift ))
|
127
|
+
callback, request = *pair
|
128
|
+
res = self.client.send_tree_api_request( request )
|
129
|
+
callback.call( res )
|
130
|
+
end
|
131
|
+
|
132
|
+
self.unregister if self.request_queue.empty?
|
133
|
+
return true
|
134
|
+
end
|
135
|
+
|
136
|
+
end # class Handler
|
137
|
+
|
138
|
+
|
139
|
+
### Create a new Arborist::MonitorRunner
|
140
|
+
def initialize
|
141
|
+
@monitors = []
|
142
|
+
@handler = nil
|
143
|
+
@reactor = ZMQ::Loop.new
|
144
|
+
end
|
145
|
+
|
146
|
+
|
147
|
+
######
|
148
|
+
public
|
149
|
+
######
|
150
|
+
|
151
|
+
# The Array of loaded Arborist::Monitors the runner should run.
|
152
|
+
attr_reader :monitors
|
153
|
+
|
154
|
+
# The ZMQ::Handler subclass that handles all async IO
|
155
|
+
attr_accessor :handler
|
156
|
+
|
157
|
+
# The reactor (a ZMQ::Loop) the runner uses to drive everything
|
158
|
+
attr_accessor :reactor
|
159
|
+
|
160
|
+
|
161
|
+
### Load monitors from the specified +enumerator+.
|
162
|
+
def load_monitors( enumerator )
|
163
|
+
@monitors += enumerator.to_a
|
164
|
+
end
|
165
|
+
|
166
|
+
|
167
|
+
### Run the specified +monitors+
|
168
|
+
def run
|
169
|
+
self.handler = Arborist::MonitorRunner::Handler.new( self.reactor )
|
170
|
+
|
171
|
+
self.monitors.each do |mon|
|
172
|
+
self.add_timer_for( mon )
|
173
|
+
end
|
174
|
+
|
175
|
+
self.reactor.start
|
176
|
+
end
|
177
|
+
|
178
|
+
|
179
|
+
### Register a timer for the specified +monitor+.
|
180
|
+
def add_timer_for( monitor )
|
181
|
+
interval = monitor.interval
|
182
|
+
|
183
|
+
timer = if monitor.splay.nonzero?
|
184
|
+
self.splay_timer_for( monitor )
|
185
|
+
else
|
186
|
+
self.interval_timer_for( monitor )
|
187
|
+
end
|
188
|
+
|
189
|
+
self.reactor.register_timer( timer )
|
190
|
+
end
|
191
|
+
|
192
|
+
|
193
|
+
### Create a repeating ZMQ::Timer that will run the specified monitor on its interval.
|
194
|
+
def interval_timer_for( monitor )
|
195
|
+
interval = monitor.interval
|
196
|
+
self.log.info "Creating timer for %s monitor to run every %ds" % [ monitor, interval ]
|
197
|
+
|
198
|
+
return ZMQ::Timer.new( interval, 0 ) do
|
199
|
+
self.handler.run_monitor( monitor )
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
|
204
|
+
### Create a one-shot ZMQ::Timer that will register the interval timer for the specified
|
205
|
+
### +monitor+ after a random number of seconds no greater than its splay.
|
206
|
+
def splay_timer_for( monitor )
|
207
|
+
delay = rand( monitor.splay )
|
208
|
+
self.log.info "Splaying registration of %s monitor for %ds" % [ monitor, delay ]
|
209
|
+
|
210
|
+
return ZMQ::Timer.new( delay, 1 ) do
|
211
|
+
interval_timer = self.interval_timer_for( monitor )
|
212
|
+
self.reactor.register_timer( interval_timer )
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
end # class Arborist::MonitorRunner
|
217
|
+
|
@@ -0,0 +1,700 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
#encoding: utf-8
|
3
|
+
|
4
|
+
require 'set'
|
5
|
+
require 'uri'
|
6
|
+
require 'time'
|
7
|
+
require 'pathname'
|
8
|
+
require 'state_machines'
|
9
|
+
|
10
|
+
require 'loggability'
|
11
|
+
require 'pluggability'
|
12
|
+
require 'arborist' unless defined?( Arborist )
|
13
|
+
require 'arborist/mixins'
|
14
|
+
|
15
|
+
using Arborist::TimeRefinements
|
16
|
+
|
17
|
+
|
18
|
+
# The basic node class for an Arborist tree
|
19
|
+
class Arborist::Node
|
20
|
+
include Enumerable,
|
21
|
+
Arborist::HashUtilities
|
22
|
+
extend Loggability,
|
23
|
+
Pluggability,
|
24
|
+
Arborist::MethodUtilities
|
25
|
+
|
26
|
+
|
27
|
+
##
|
28
|
+
# The key for the thread local that is used to track instances as they're
|
29
|
+
# loaded.
|
30
|
+
LOADED_INSTANCE_KEY = :loaded_node_instances
|
31
|
+
|
32
|
+
##
|
33
|
+
# The glob pattern to use for searching for node
|
34
|
+
NODE_FILE_PATTERN = '**/*.rb'
|
35
|
+
|
36
|
+
|
37
|
+
##
|
38
|
+
# The struct for the 'ack' operational property
|
39
|
+
ACK = Struct.new( 'ArboristNodeACK', :message, :via, :sender, :time )
|
40
|
+
|
41
|
+
##
|
42
|
+
# The keys required to be set for an ACK
|
43
|
+
ACK_REQUIRED_PROPERTIES = %w[ message sender ]
|
44
|
+
|
45
|
+
|
46
|
+
##
|
47
|
+
# Log via the Arborist logger
|
48
|
+
log_to :arborist
|
49
|
+
|
50
|
+
##
|
51
|
+
# Search for plugins in lib/arborist/node directories in loaded gems
|
52
|
+
plugin_prefixes 'arborist/node'
|
53
|
+
|
54
|
+
|
55
|
+
state_machine( :status, initial: :unknown ) do
|
56
|
+
|
57
|
+
state :unknown,
|
58
|
+
:up,
|
59
|
+
:down,
|
60
|
+
:acked
|
61
|
+
|
62
|
+
event :update do
|
63
|
+
transition any - [:acked] => :acked, if: :ack_set?
|
64
|
+
transition any - [:up] => :up, if: :last_contact_successful?
|
65
|
+
transition any - [:down, :acked] => :down, unless: :last_contact_successful?
|
66
|
+
end
|
67
|
+
|
68
|
+
after_transition any => :acked, do: :on_ack
|
69
|
+
after_transition :acked => :up, do: :on_ack_cleared
|
70
|
+
after_transition :down => :up, do: :on_node_up
|
71
|
+
after_transition [:unknown, :up] => :down, do: :on_node_down
|
72
|
+
|
73
|
+
after_transition do: :add_status_to_update_delta
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
### Return a curried Proc for the ::create method for the specified +type+.
|
78
|
+
def self::curried_create( type )
|
79
|
+
return self.method( :create ).to_proc.curry( 2 )[ type ]
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
### Overridden to track instances of created nodes for the DSL.
|
84
|
+
def self::new( * )
|
85
|
+
new_instance = super
|
86
|
+
Arborist::Node.add_loaded_instance( new_instance )
|
87
|
+
return new_instance
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
### Create a new node with its state read from the specified +hash+.
|
92
|
+
def self::from_hash( hash )
|
93
|
+
return self.new( hash[:identifier] ) do
|
94
|
+
self.marshal_load( hash )
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
### Record a new loaded instance if the Thread-local variable is set up to track
|
100
|
+
### them.
|
101
|
+
def self::add_loaded_instance( new_instance )
|
102
|
+
instances = Thread.current[ LOADED_INSTANCE_KEY ] or return
|
103
|
+
instances << new_instance
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
### Inheritance hook -- add a DSL declarative function for the given +subclass+.
|
108
|
+
def self::inherited( subclass )
|
109
|
+
super
|
110
|
+
|
111
|
+
if name = subclass.name
|
112
|
+
name.sub!( /.*::/, '' )
|
113
|
+
body = self.curried_create( subclass )
|
114
|
+
Arborist.add_dsl_constructor( name, &body )
|
115
|
+
else
|
116
|
+
self.log.info "Skipping DSL constructor for anonymous class."
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
### Load the specified +file+ and return any new Nodes created as a result.
|
123
|
+
def self::load( file )
|
124
|
+
self.log.info "Loading node file %s..." % [ file ]
|
125
|
+
Thread.current[ LOADED_INSTANCE_KEY ] = []
|
126
|
+
|
127
|
+
begin
|
128
|
+
Kernel.load( file )
|
129
|
+
rescue => err
|
130
|
+
self.log.error "%p while loading %s: %s" % [ err.class, file, err.message ]
|
131
|
+
raise
|
132
|
+
end
|
133
|
+
|
134
|
+
return Thread.current[ LOADED_INSTANCE_KEY ]
|
135
|
+
ensure
|
136
|
+
Thread.current[ LOADED_INSTANCE_KEY ] = nil
|
137
|
+
end
|
138
|
+
|
139
|
+
|
140
|
+
### Return an iterator for all the node files in the specified +directory+.
|
141
|
+
def self::each_in( directory )
|
142
|
+
path = Pathname( directory )
|
143
|
+
paths = if path.directory?
|
144
|
+
Pathname.glob( directory + NODE_FILE_PATTERN )
|
145
|
+
else
|
146
|
+
[ path ]
|
147
|
+
end
|
148
|
+
|
149
|
+
return paths.flat_map do |file|
|
150
|
+
file_url = "file://%s" % [ file.expand_path ]
|
151
|
+
nodes = self.load( file )
|
152
|
+
self.log.debug "Loaded nodes %p..." % [ nodes ]
|
153
|
+
nodes.each do |node|
|
154
|
+
node.source = file_url
|
155
|
+
end
|
156
|
+
nodes
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
|
161
|
+
### Create a new Node with the specified +identifier+, which must be unique to the
|
162
|
+
### loaded tree.
|
163
|
+
def initialize( identifier, &block )
|
164
|
+
raise "Invalid identifier %p" % [identifier] unless
|
165
|
+
identifier =~ /^\w[\w\-]*$/
|
166
|
+
|
167
|
+
@identifier = identifier
|
168
|
+
@parent = '_'
|
169
|
+
@description = nil
|
170
|
+
@tags = Set.new
|
171
|
+
@source = nil
|
172
|
+
@children = {}
|
173
|
+
|
174
|
+
@status = 'unknown'
|
175
|
+
@status_changed = Time.at( 0 )
|
176
|
+
|
177
|
+
@error = nil
|
178
|
+
@ack = nil
|
179
|
+
@properties = {}
|
180
|
+
@last_contacted = Time.at( 0 )
|
181
|
+
|
182
|
+
@update_delta = Hash.new do |h,k|
|
183
|
+
h[ k ] = Hash.new( &h.default_proc )
|
184
|
+
end
|
185
|
+
@pending_update_events = []
|
186
|
+
@subscriptions = {}
|
187
|
+
|
188
|
+
self.instance_eval( &block ) if block
|
189
|
+
end
|
190
|
+
|
191
|
+
|
192
|
+
######
|
193
|
+
public
|
194
|
+
######
|
195
|
+
|
196
|
+
##
|
197
|
+
# The node's identifier
|
198
|
+
attr_reader :identifier
|
199
|
+
|
200
|
+
##
|
201
|
+
# The URI of the source the object was read from
|
202
|
+
attr_reader :source
|
203
|
+
|
204
|
+
##
|
205
|
+
# The Hash of nodes which are children of this node, keyed by identifier
|
206
|
+
attr_reader :children
|
207
|
+
|
208
|
+
##
|
209
|
+
# Arbitrary attributes attached to this node via the manager API
|
210
|
+
attr_reader :properties
|
211
|
+
|
212
|
+
##
|
213
|
+
# The Time the node was last contacted
|
214
|
+
attr_accessor :last_contacted
|
215
|
+
|
216
|
+
##
|
217
|
+
# The Time the node's status last changed.
|
218
|
+
attr_accessor :status_changed
|
219
|
+
|
220
|
+
##
|
221
|
+
# The last error encountered by a monitor attempting to update this node.
|
222
|
+
attr_accessor :error
|
223
|
+
|
224
|
+
##
|
225
|
+
# The acknowledgement currently in effect. Should be an instance of Arborist::Node::ACK
|
226
|
+
attr_accessor :ack
|
227
|
+
|
228
|
+
##
|
229
|
+
# The Hash of changes tracked during an #update.
|
230
|
+
attr_reader :update_delta
|
231
|
+
|
232
|
+
##
|
233
|
+
# The Array of events generated by the current update event
|
234
|
+
attr_reader :pending_update_events
|
235
|
+
|
236
|
+
##
|
237
|
+
# The Hash of Subscription objects observing this node and its children, keyed by
|
238
|
+
# subscription ID.
|
239
|
+
attr_reader :subscriptions
|
240
|
+
|
241
|
+
|
242
|
+
### Set the source of the node to +source+, which should be a valid URI.
|
243
|
+
def source=( source )
|
244
|
+
@source = URI( source )
|
245
|
+
end
|
246
|
+
|
247
|
+
|
248
|
+
#
|
249
|
+
# :section: DSLish declaration methods
|
250
|
+
# These methods are both getter and setter for a node's attributes, used
|
251
|
+
# in the node source.
|
252
|
+
#
|
253
|
+
|
254
|
+
### Get/set the node's parent node, which should either be an identifier or an object
|
255
|
+
### that responds to #identifier with one.
|
256
|
+
def parent( new_parent=nil )
|
257
|
+
return @parent if new_parent.nil?
|
258
|
+
|
259
|
+
@parent = if new_parent.respond_to?( :identifier )
|
260
|
+
new_parent.identifier.to_s
|
261
|
+
else
|
262
|
+
@parent = new_parent.to_s
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
|
267
|
+
### Get/set the node's description.
|
268
|
+
def description( new_description=nil )
|
269
|
+
return @description unless new_description
|
270
|
+
@description = new_description.to_s
|
271
|
+
end
|
272
|
+
|
273
|
+
|
274
|
+
### Declare one or more +tags+ for this node.
|
275
|
+
def tags( *tags )
|
276
|
+
@tags.merge( tags.map(&:to_s) ) unless tags.empty?
|
277
|
+
return @tags.to_a
|
278
|
+
end
|
279
|
+
|
280
|
+
|
281
|
+
#
|
282
|
+
# :section: Manager API
|
283
|
+
# Methods used by the manager to manage its nodes.
|
284
|
+
#
|
285
|
+
|
286
|
+
|
287
|
+
### Return the simple type of this node (e.g., Arborist::Node::Host => 'host')
|
288
|
+
def type
|
289
|
+
return 'anonymous' unless self.class.name
|
290
|
+
return self.class.name.sub( /.*::/, '' ).downcase
|
291
|
+
end
|
292
|
+
|
293
|
+
|
294
|
+
### Add the specified +subscription+ (an Arborist::Subscription) to the node.
|
295
|
+
def add_subscription( subscription )
|
296
|
+
self.subscriptions[ subscription.id ] = subscription
|
297
|
+
end
|
298
|
+
|
299
|
+
|
300
|
+
### Remove the specified +subscription+ (an Arborist::Subscription) from the node.
|
301
|
+
def remove_subscription( subscription_id )
|
302
|
+
return self.subscriptions.delete( subscription_id )
|
303
|
+
end
|
304
|
+
|
305
|
+
|
306
|
+
### Return subscriptions matching the specified +event+ on the receiving node.
|
307
|
+
def find_matching_subscriptions( event )
|
308
|
+
return self.subscriptions.values.find_all {|sub| event =~ sub }
|
309
|
+
end
|
310
|
+
|
311
|
+
|
312
|
+
### Publish the specified +events+ to any subscriptions the node has which match them.
|
313
|
+
def publish_events( *events )
|
314
|
+
self.subscriptions.each_value do |sub|
|
315
|
+
sub.on_events( *events )
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
|
320
|
+
### Update specified +properties+ for the node.
|
321
|
+
def update( new_properties )
|
322
|
+
new_properties = stringify_keys( new_properties )
|
323
|
+
self.log.debug "Updated: %p" % [ new_properties ]
|
324
|
+
|
325
|
+
self.last_contacted = Time.now
|
326
|
+
self.error = new_properties.delete( 'error' )
|
327
|
+
self.ack = new_properties.delete( 'ack' ) if new_properties.key?( 'ack' )
|
328
|
+
|
329
|
+
self.properties.merge!( new_properties, &self.method(:merge_and_record_delta) )
|
330
|
+
compact_hash( self.properties )
|
331
|
+
|
332
|
+
# Super to the state machine event method
|
333
|
+
super
|
334
|
+
|
335
|
+
events = self.pending_update_events.clone
|
336
|
+
events << self.make_update_event
|
337
|
+
events << self.make_delta_event unless self.update_delta.empty?
|
338
|
+
|
339
|
+
return events
|
340
|
+
ensure
|
341
|
+
self.update_delta.clear
|
342
|
+
self.pending_update_events.clear
|
343
|
+
end
|
344
|
+
|
345
|
+
|
346
|
+
### Merge the specified +new_properties+ into the node's properties, recording
|
347
|
+
### each change in the node's #update_delta.
|
348
|
+
def merge_and_record_delta( key, oldval, newval, prefixes=[] )
|
349
|
+
self.log.debug "Merging property %s: %p -> %p" % [
|
350
|
+
(prefixes + [key]).join('.'),
|
351
|
+
oldval,
|
352
|
+
newval
|
353
|
+
]
|
354
|
+
|
355
|
+
# Merge them (recursively) if they're both merge-able
|
356
|
+
if oldval.respond_to?( :merge! ) && newval.respond_to?( :merge! )
|
357
|
+
return oldval.merge( newval ) do |ikey, ioldval, inewval|
|
358
|
+
self.merge_and_record_delta( ikey, ioldval, inewval, prefixes + [key] )
|
359
|
+
end
|
360
|
+
|
361
|
+
# Otherwise just directly compare them and record any changes
|
362
|
+
else
|
363
|
+
unless oldval == newval
|
364
|
+
prefixed_delta = prefixes.inject( self.update_delta ) do |hash, key|
|
365
|
+
hash[ key ]
|
366
|
+
end
|
367
|
+
prefixed_delta[ key ] = [ oldval, newval ]
|
368
|
+
end
|
369
|
+
|
370
|
+
return newval
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
|
375
|
+
### Return the node's state in an Arborist::Event of type 'node.update'.
|
376
|
+
def make_update_event
|
377
|
+
return Arborist::Event.create( 'node_update', self )
|
378
|
+
end
|
379
|
+
|
380
|
+
|
381
|
+
### Return an Event generated from the node's state changes.
|
382
|
+
def make_delta_event
|
383
|
+
self.log.debug "Making node.delta event: %p" % [ self.update_delta ]
|
384
|
+
return Arborist::Event.create( 'node_delta', self, self.update_delta )
|
385
|
+
end
|
386
|
+
|
387
|
+
|
388
|
+
### Returns +true+ if the node's state has changed since the last time
|
389
|
+
### #snapshot_state was called.
|
390
|
+
def state_has_changed?
|
391
|
+
return ! self.update_delta.empty?
|
392
|
+
end
|
393
|
+
|
394
|
+
|
395
|
+
### Returns +true+ if the specified search +criteria+ all match this node.
|
396
|
+
def matches?( criteria )
|
397
|
+
self.log.debug "Matching %p against criteria: %p" % [ self, criteria ]
|
398
|
+
return criteria.all? do |key, val|
|
399
|
+
self.match_criteria?( key, val )
|
400
|
+
end.tap {|match| self.log.debug " node %s match." % [ match ? "DID" : "did not"] }
|
401
|
+
end
|
402
|
+
|
403
|
+
|
404
|
+
### Returns +true+ if the node matches the specified +key+ and +val+ criteria.
|
405
|
+
def match_criteria?( key, val )
|
406
|
+
return case key
|
407
|
+
when 'status'
|
408
|
+
self.status == val
|
409
|
+
when 'type'
|
410
|
+
self.log.debug "Checking node type %p against %p" % [ self.type, val ]
|
411
|
+
self.type == val
|
412
|
+
when 'tag' then @tags.include?( val.to_s )
|
413
|
+
when 'tags' then Array(val).all? {|tag| @tags.include?(tag) }
|
414
|
+
when 'identifier' then @identifier == val
|
415
|
+
else
|
416
|
+
hash_matches( @properties, key, val )
|
417
|
+
end
|
418
|
+
end
|
419
|
+
|
420
|
+
|
421
|
+
### Return a Hash of node state values that match the specified +value_spec+.
|
422
|
+
def fetch_values( value_spec=nil )
|
423
|
+
state = self.properties.merge( self.operational_values )
|
424
|
+
state = stringify_keys( state )
|
425
|
+
|
426
|
+
if value_spec
|
427
|
+
self.log.debug "Eliminating all values except: %p (from keys: %p)" %
|
428
|
+
[ value_spec, state.keys ]
|
429
|
+
state.delete_if {|key, _| !value_spec.include?(key) }
|
430
|
+
end
|
431
|
+
|
432
|
+
return state
|
433
|
+
end
|
434
|
+
|
435
|
+
|
436
|
+
### Return a Hash of the operational values that are included with the node's
|
437
|
+
### monitor state.
|
438
|
+
def operational_values
|
439
|
+
values = {
|
440
|
+
type: self.type,
|
441
|
+
status: self.status,
|
442
|
+
tags: self.tags
|
443
|
+
}
|
444
|
+
values[:ack] = self.ack.to_h if self.ack
|
445
|
+
|
446
|
+
return values
|
447
|
+
end
|
448
|
+
|
449
|
+
|
450
|
+
#
|
451
|
+
# :section: Hierarchy API
|
452
|
+
#
|
453
|
+
|
454
|
+
### Enumerable API -- iterate over the children of this node.
|
455
|
+
def each( &block )
|
456
|
+
return self.children.values.each( &block )
|
457
|
+
end
|
458
|
+
|
459
|
+
|
460
|
+
### Returns +true+ if the node has one or more child nodes.
|
461
|
+
def has_children?
|
462
|
+
return !self.children.empty?
|
463
|
+
end
|
464
|
+
|
465
|
+
|
466
|
+
### Returns +true+ if the node is considered operational.
|
467
|
+
def operational?
|
468
|
+
return self.identifier.start_with?( '_' )
|
469
|
+
end
|
470
|
+
|
471
|
+
|
472
|
+
### Register the specified +node+ as a child of this node, replacing any existing
|
473
|
+
### node with the same identifier.
|
474
|
+
def add_child( node )
|
475
|
+
self.log.debug "Adding node %p as a child. Parent = %p" % [ node, node.parent ]
|
476
|
+
raise "%p is not a child of %p" % [ node, self ] if
|
477
|
+
node.parent && node.parent != self.identifier
|
478
|
+
self.children[ node.identifier ] = node
|
479
|
+
end
|
480
|
+
|
481
|
+
|
482
|
+
### Append operator -- add the specified +node+ as a child and return +self+.
|
483
|
+
def <<( node )
|
484
|
+
self.add_child( node )
|
485
|
+
return self
|
486
|
+
end
|
487
|
+
|
488
|
+
|
489
|
+
### Unregister the specified +node+ as a child of this node.
|
490
|
+
def remove_child( node )
|
491
|
+
self.log.debug "Removing node %p from children" % [ node ]
|
492
|
+
return self.children.delete( node.identifier )
|
493
|
+
end
|
494
|
+
|
495
|
+
|
496
|
+
#
|
497
|
+
# :section: Utility methods
|
498
|
+
#
|
499
|
+
|
500
|
+
### Return a string describing the node's status.
|
501
|
+
def status_description
|
502
|
+
case self.status
|
503
|
+
when 'up', 'down'
|
504
|
+
return "%s as of %s" % [ self.status.upcase, self.last_contacted ]
|
505
|
+
when 'acked'
|
506
|
+
return "ACKed by %s %s" % [ self.ack.sender, self.ack.time.as_delta ]
|
507
|
+
else
|
508
|
+
return "in an unknown state"
|
509
|
+
end
|
510
|
+
end
|
511
|
+
|
512
|
+
|
513
|
+
### Return a string describing node details; returns +nil+ for the base class. Subclasses
|
514
|
+
### may override this to add to the output of #inspect.
|
515
|
+
def node_description
|
516
|
+
return nil
|
517
|
+
end
|
518
|
+
|
519
|
+
|
520
|
+
### Return a String representation of the object suitable for debugging.
|
521
|
+
def inspect
|
522
|
+
return "#<%p:%#x [%s] -> %s %p %s%s, %d children, %s>" % [
|
523
|
+
self.class,
|
524
|
+
self.object_id * 2,
|
525
|
+
self.identifier,
|
526
|
+
self.parent || 'root',
|
527
|
+
self.description || "(no description)",
|
528
|
+
self.node_description.to_s,
|
529
|
+
self.source,
|
530
|
+
self.children.length,
|
531
|
+
self.status_description,
|
532
|
+
]
|
533
|
+
end
|
534
|
+
|
535
|
+
|
536
|
+
#
|
537
|
+
# :section: Serialization API
|
538
|
+
#
|
539
|
+
|
540
|
+
### Return a Hash of the node's state.
|
541
|
+
def to_hash
|
542
|
+
return {
|
543
|
+
identifier: self.identifier,
|
544
|
+
type: self.class.name.to_s.sub( /.+::/, '' ).downcase,
|
545
|
+
parent: self.parent,
|
546
|
+
description: self.description,
|
547
|
+
tags: self.tags,
|
548
|
+
properties: self.properties.dup,
|
549
|
+
status: self.status,
|
550
|
+
ack: self.ack ? self.ack.to_h : nil,
|
551
|
+
last_contacted: self.last_contacted ? self.last_contacted.iso8601 : nil,
|
552
|
+
status_changed: self.status_changed ? self.status_changed.iso8601 : nil,
|
553
|
+
error: self.error,
|
554
|
+
}
|
555
|
+
end
|
556
|
+
|
557
|
+
|
558
|
+
### Marshal API -- return the node as an object suitable for marshalling.
|
559
|
+
def marshal_dump
|
560
|
+
return self.to_hash
|
561
|
+
end
|
562
|
+
|
563
|
+
|
564
|
+
### Marshal API -- set up the object's state using the +hash+ from a
|
565
|
+
### previously-marshalled node.
|
566
|
+
def marshal_load( hash )
|
567
|
+
@identifier = hash[:identifier]
|
568
|
+
@properties = hash[:properties]
|
569
|
+
|
570
|
+
@parent = hash[:parent]
|
571
|
+
@description = hash[:description]
|
572
|
+
@tags = Set.new( hash[:tags] )
|
573
|
+
@children = {}
|
574
|
+
|
575
|
+
@status = hash[:status]
|
576
|
+
@status_changed = Time.parse( hash[:status_changed] )
|
577
|
+
|
578
|
+
@error = hash[:error]
|
579
|
+
@properties = hash[:properties]
|
580
|
+
@last_contacted = Time.parse( hash[:last_contacted] )
|
581
|
+
|
582
|
+
if hash[:ack]
|
583
|
+
ack_values = hash[:ack].values_at( *Arborist::Node::ACK.members )
|
584
|
+
@ack = Arborist::Node::ACK.new( *ack_values )
|
585
|
+
end
|
586
|
+
end
|
587
|
+
|
588
|
+
|
589
|
+
### Equality operator -- returns +true+ if +other_node+ has the same identifier, parent, and
|
590
|
+
### state as the receiving one.
|
591
|
+
def ==( other_node )
|
592
|
+
return \
|
593
|
+
other_node.identifier == self.identifier &&
|
594
|
+
other_node.parent == self.parent &&
|
595
|
+
other_node.description == self.description &&
|
596
|
+
other_node.tags == self.tags &&
|
597
|
+
other_node.properties == self.properties &&
|
598
|
+
other_node.status == self.status &&
|
599
|
+
other_node.ack == self.ack &&
|
600
|
+
other_node.error == self.error
|
601
|
+
end
|
602
|
+
|
603
|
+
|
604
|
+
#########
|
605
|
+
protected
|
606
|
+
#########
|
607
|
+
|
608
|
+
### Ack the node with the specified +ack_data+, which should contain
|
609
|
+
def ack=( ack_data )
|
610
|
+
self.log.debug "ACKed with data: %p" % [ ack_data ]
|
611
|
+
|
612
|
+
ack_data['time'] ||= Time.now
|
613
|
+
ack_values = ack_data.values_at( *Arborist::Node::ACK.members.map(&:to_s) )
|
614
|
+
new_ack = Arborist::Node::ACK.new( *ack_values )
|
615
|
+
|
616
|
+
if missing = ACK_REQUIRED_PROPERTIES.find {|prop| new_ack[prop].nil? }
|
617
|
+
raise "Missing required ACK attribute %s" % [ missing ]
|
618
|
+
end
|
619
|
+
|
620
|
+
@ack = new_ack
|
621
|
+
end
|
622
|
+
|
623
|
+
|
624
|
+
### State machine guard predicate -- Returns +true+ if the node has an ACK status set.
|
625
|
+
def ack_set?
|
626
|
+
self.log.debug "Checking to see if this node has been ACKed (it %s)" %
|
627
|
+
[ @ack ? "has" : "has not" ]
|
628
|
+
return @ack ? true : false
|
629
|
+
end
|
630
|
+
|
631
|
+
|
632
|
+
### State machine guard predicate -- Returns +true+ if the last time the node
|
633
|
+
### was monitored resulted in an update.
|
634
|
+
def last_contact_successful?
|
635
|
+
self.log.debug "Checking to see if last contact was successful (it %s)" %
|
636
|
+
[ self.error ? "wasn't" : "was" ]
|
637
|
+
return !self.error
|
638
|
+
end
|
639
|
+
|
640
|
+
|
641
|
+
#
|
642
|
+
# :section: State Callbacks
|
643
|
+
#
|
644
|
+
|
645
|
+
### Callback for when an acknowledgement is set.
|
646
|
+
def on_ack( transition )
|
647
|
+
self.log.warn "ACKed: %s" % [ self.status_description ]
|
648
|
+
self.pending_update_events <<
|
649
|
+
Arborist::Event.create( 'node_acked', self.fetch_values, self.ack.to_h )
|
650
|
+
end
|
651
|
+
|
652
|
+
|
653
|
+
### Callback for when an acknowledgement is cleared.
|
654
|
+
def on_ack_cleared( transition )
|
655
|
+
self.log.warn "ACK cleared for %s" % [ self.identifier ]
|
656
|
+
end
|
657
|
+
|
658
|
+
|
659
|
+
### Callback for when a node goes from down to up
|
660
|
+
def on_node_up( transition )
|
661
|
+
self.log.warn "%s is %s" % [ self.identifier, self.status_description ]
|
662
|
+
end
|
663
|
+
|
664
|
+
|
665
|
+
### Callback for when a node goes from up to down
|
666
|
+
def on_node_down( transition )
|
667
|
+
self.log.error "%s is %s" % [ self.identifier, self.status_description ]
|
668
|
+
self.update_delta[ 'error' ] = [ nil, self.error ]
|
669
|
+
end
|
670
|
+
|
671
|
+
|
672
|
+
### Add the transition from one state to another to the data used to build
|
673
|
+
### deltas for the #update event.
|
674
|
+
def add_status_to_update_delta( transition )
|
675
|
+
self.update_delta[ 'status' ] = [ transition.from, transition.to ]
|
676
|
+
end
|
677
|
+
|
678
|
+
|
679
|
+
#######
|
680
|
+
private
|
681
|
+
#######
|
682
|
+
|
683
|
+
### Returns true if the specified +hash+ includes the specified +key+, and the value
|
684
|
+
### associated with the +key+ either includes +val+ if it is a Hash, or equals +val+ if it's
|
685
|
+
### anything but a Hash.
|
686
|
+
def hash_matches( hash, key, val )
|
687
|
+
actual = hash[ key ] or return false
|
688
|
+
|
689
|
+
if actual.is_a?( Hash )
|
690
|
+
if val.is_a?( Hash )
|
691
|
+
return val.all? {|subkey, subval| hash_matches(actual, subkey, subval) }
|
692
|
+
else
|
693
|
+
return false
|
694
|
+
end
|
695
|
+
else
|
696
|
+
return actual == val
|
697
|
+
end
|
698
|
+
end
|
699
|
+
|
700
|
+
end # class Arborist::Node
|