arborist 0.0.1.pre20160106113421

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 (74) hide show
  1. checksums.yaml +7 -0
  2. data/.document +4 -0
  3. data/.simplecov +9 -0
  4. data/ChangeLog +417 -0
  5. data/Events.md +20 -0
  6. data/History.md +4 -0
  7. data/LICENSE +29 -0
  8. data/Manifest.txt +72 -0
  9. data/Monitors.md +141 -0
  10. data/Nodes.md +0 -0
  11. data/Observers.md +72 -0
  12. data/Protocol.md +214 -0
  13. data/README.md +75 -0
  14. data/Rakefile +81 -0
  15. data/TODO.md +24 -0
  16. data/bin/amanagerd +10 -0
  17. data/bin/amonitord +12 -0
  18. data/bin/aobserverd +12 -0
  19. data/lib/arborist.rb +182 -0
  20. data/lib/arborist/client.rb +191 -0
  21. data/lib/arborist/event.rb +61 -0
  22. data/lib/arborist/event/node_acked.rb +18 -0
  23. data/lib/arborist/event/node_delta.rb +20 -0
  24. data/lib/arborist/event/node_matching.rb +34 -0
  25. data/lib/arborist/event/node_update.rb +19 -0
  26. data/lib/arborist/event/sys_reloaded.rb +15 -0
  27. data/lib/arborist/exceptions.rb +21 -0
  28. data/lib/arborist/manager.rb +508 -0
  29. data/lib/arborist/manager/event_publisher.rb +97 -0
  30. data/lib/arborist/manager/tree_api.rb +207 -0
  31. data/lib/arborist/mixins.rb +363 -0
  32. data/lib/arborist/monitor.rb +377 -0
  33. data/lib/arborist/monitor/socket.rb +163 -0
  34. data/lib/arborist/monitor_runner.rb +217 -0
  35. data/lib/arborist/node.rb +700 -0
  36. data/lib/arborist/node/host.rb +87 -0
  37. data/lib/arborist/node/root.rb +60 -0
  38. data/lib/arborist/node/service.rb +112 -0
  39. data/lib/arborist/observer.rb +176 -0
  40. data/lib/arborist/observer/action.rb +125 -0
  41. data/lib/arborist/observer/summarize.rb +105 -0
  42. data/lib/arborist/observer_runner.rb +181 -0
  43. data/lib/arborist/subscription.rb +82 -0
  44. data/spec/arborist/client_spec.rb +282 -0
  45. data/spec/arborist/event/node_update_spec.rb +71 -0
  46. data/spec/arborist/event_spec.rb +64 -0
  47. data/spec/arborist/manager/event_publisher_spec.rb +66 -0
  48. data/spec/arborist/manager/tree_api_spec.rb +458 -0
  49. data/spec/arborist/manager_spec.rb +442 -0
  50. data/spec/arborist/mixins_spec.rb +195 -0
  51. data/spec/arborist/monitor/socket_spec.rb +195 -0
  52. data/spec/arborist/monitor_runner_spec.rb +152 -0
  53. data/spec/arborist/monitor_spec.rb +251 -0
  54. data/spec/arborist/node/host_spec.rb +104 -0
  55. data/spec/arborist/node/root_spec.rb +29 -0
  56. data/spec/arborist/node/service_spec.rb +98 -0
  57. data/spec/arborist/node_spec.rb +552 -0
  58. data/spec/arborist/observer/action_spec.rb +205 -0
  59. data/spec/arborist/observer/summarize_spec.rb +294 -0
  60. data/spec/arborist/observer_spec.rb +146 -0
  61. data/spec/arborist/subscription_spec.rb +71 -0
  62. data/spec/arborist_spec.rb +146 -0
  63. data/spec/data/monitors/pings.rb +80 -0
  64. data/spec/data/monitors/port_checks.rb +27 -0
  65. data/spec/data/monitors/system_resources.rb +30 -0
  66. data/spec/data/monitors/web_services.rb +17 -0
  67. data/spec/data/nodes/duir.rb +20 -0
  68. data/spec/data/nodes/localhost.rb +15 -0
  69. data/spec/data/nodes/sidonie.rb +29 -0
  70. data/spec/data/nodes/yevaud.rb +26 -0
  71. data/spec/data/observers/auditor.rb +23 -0
  72. data/spec/data/observers/webservices.rb +18 -0
  73. data/spec/spec_helper.rb +117 -0
  74. metadata +368 -0
@@ -0,0 +1,87 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'etc'
5
+ require 'ipaddr'
6
+
7
+ require 'arborist/node'
8
+
9
+
10
+ # A node type for Arborist trees that represent network-connected hosts.
11
+ class Arborist::Node::Host < Arborist::Node
12
+
13
+ # A union of IPv4 and IPv6 regular expressions.
14
+ IPADDR_RE = Regexp.union(
15
+ IPAddr::RE_IPV4ADDRLIKE,
16
+ IPAddr::RE_IPV6ADDRLIKE_COMPRESSED,
17
+ IPAddr::RE_IPV6ADDRLIKE_FULL
18
+ )
19
+
20
+
21
+ ### Create a new Host node.
22
+ def initialize( identifier, &block )
23
+ @addresses = []
24
+ super
25
+ end
26
+
27
+
28
+ ######
29
+ public
30
+ ######
31
+
32
+ ##
33
+ # The network address(es) of this Host as an Array of IPAddr objects
34
+ attr_reader :addresses
35
+
36
+
37
+ ### Return the host's operational attributes.
38
+ def operational_values
39
+ properties = super
40
+ return properties.merge( addresses: self.addresses.map(&:to_s) )
41
+ end
42
+
43
+
44
+ ### Set an IP address of the host.
45
+ def address( new_address, options={} )
46
+ self.log.debug "Adding address %p to %p" % [ new_address, self ]
47
+ case new_address
48
+ when IPAddr
49
+ @addresses << new_address
50
+ when IPADDR_RE
51
+ @addresses << IPAddr.new( new_address )
52
+ when String
53
+ ip_addr = TCPSocket.gethostbyname( new_address )
54
+ @addresses << IPAddr.new( ip_addr[3] )
55
+ @addresses << IPAddr.new( ip_addr[4] ) if ip_addr[4]
56
+ else
57
+ raise "I don't know how to parse a %p host address (%p)" %
58
+ [ new_address.class, new_address ]
59
+ end
60
+ end
61
+
62
+
63
+ ### Returns +true+ if the node matches the specified +key+ and +val+ criteria.
64
+ def match_criteria?( key, val )
65
+ return case key
66
+ when 'address'
67
+ search_addr = IPAddr.new( val )
68
+ @addresses.any? {|a| search_addr.include?(a) }
69
+ else
70
+ super
71
+ end
72
+ end
73
+
74
+
75
+ ### Add a service to the host
76
+ def service( name, options={}, &block )
77
+ return Arborist::Node.create( :service, name, self, options, &block )
78
+ end
79
+
80
+
81
+ ### Return host-node-specific information for #inspect.
82
+ def node_description
83
+ return "{no addresses}" if self.addresses.empty?
84
+ return "{addresses: %s}" % [ self.addresses.map(&:to_s).join(', ') ]
85
+ end
86
+
87
+ end # class Arborist::Node::Host
@@ -0,0 +1,60 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'arborist/node' unless defined?( Arborist::Node )
5
+ require 'arborist/mixins'
6
+
7
+
8
+ # The class of the root node of an Arborist tree. This class is a Singleton.
9
+ class Arborist::Node::Root < Arborist::Node
10
+ extend Arborist::MethodUtilities
11
+
12
+
13
+ # The instance of the Root node.
14
+ @instance = nil
15
+
16
+ ### Create the instance of the Root node (if necessary) and return it.
17
+ def self::new( * )
18
+ @instance ||= super
19
+ return @instance
20
+ end
21
+
22
+
23
+ ### Override the default constructor to use the singleton ::instance instead.
24
+ def self::instance( * )
25
+ @instance ||= new
26
+ return @instance
27
+ end
28
+
29
+
30
+ ### Reset the singleton instance; mainly used for testing.
31
+ def self::reset
32
+ @instance = nil
33
+ end
34
+
35
+
36
+ ### Set up the root node.
37
+ def initialize( * )
38
+ super( '_' ) do
39
+ description "The root node."
40
+ source = URI( __FILE__ )
41
+ end
42
+
43
+ @status = 'up'
44
+ @status.freeze
45
+ end
46
+
47
+
48
+ ### Ignore updates to the root node.
49
+ def update( properties )
50
+ self.log.warn "Update to the root node ignored."
51
+ end
52
+
53
+
54
+ ### Override the reader mode of Node#parent for the root node, which never has
55
+ ### a parent.
56
+ def parent( * )
57
+ return nil
58
+ end
59
+
60
+ end # class Arborist::Node::Root
@@ -0,0 +1,112 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'etc'
5
+ require 'ipaddr'
6
+ require 'socket'
7
+
8
+ require 'arborist/node'
9
+
10
+
11
+ # A node type for Arborist trees that represent services running on hosts.
12
+ class Arborist::Node::Service < Arborist::Node
13
+
14
+ # The default transport layer protocol to use for services that don't specify
15
+ # one
16
+ DEFAULT_PROTOCOL = 'tcp'
17
+
18
+
19
+ ### Create a new Service node.
20
+ def initialize( identifier, host, options={}, &block )
21
+ my_identifier = "%s-%s" % [ host.identifier, identifier ]
22
+ super( my_identifier )
23
+
24
+ @host = host
25
+ @parent = host.identifier
26
+ @app_protocol = options[:app_protocol] || identifier
27
+ @protocol = options[:protocol] || DEFAULT_PROTOCOL
28
+
29
+ service_port = options[:port] || default_port_for( @app_protocol, @protocol ) or
30
+ raise ArgumentError, "can't determine the port for %s/%s" % [ @app_protocol, @protocol ]
31
+ @port = Integer( service_port )
32
+
33
+ self.instance_eval( &block ) if block
34
+ end
35
+
36
+
37
+ ######
38
+ public
39
+ ######
40
+
41
+ ##
42
+ # The network port the service uses
43
+ attr_reader :port
44
+
45
+ ##
46
+ # The transport layer protocol the service uses
47
+ attr_reader :protocol
48
+
49
+ ##
50
+ # The (layer 7) protocol used by the service
51
+ attr_reader :app_protocol
52
+
53
+
54
+ ### Delegate the service's address to its host.
55
+ def addresses
56
+ return @host.addresses
57
+ end
58
+
59
+
60
+ ### Returns +true+ if the node matches the specified +key+ and +val+ criteria.
61
+ def match_criteria?( key, val )
62
+ self.log.debug "Matching %p: %p against %p" % [ key, val, self ]
63
+ return case key
64
+ when 'port'
65
+ val = default_port_for( val, @protocol ) unless val.is_a?( Fixnum )
66
+ self.port == val.to_i
67
+ when 'address'
68
+ search_addr = IPAddr.new( val )
69
+ self.addresses.any? {|a| search_addr.include?(a) }
70
+ when 'protocol' then self.protocol == val.downcase
71
+ when 'app', 'app_protocol' then self.app_protocol == val
72
+ else
73
+ super
74
+ end
75
+ end
76
+
77
+
78
+ ### Return a Hash of the operational values that are included with the node's
79
+ ### monitor state.
80
+ def operational_values
81
+ return super.merge(
82
+ addresses: self.addresses.map( &:to_s ),
83
+ port: self.port,
84
+ protocol: self.protocol,
85
+ app_protocol: self.app_protocol,
86
+ )
87
+ end
88
+
89
+
90
+ ### Return service-node-specific information for #inspect.
91
+ def node_description
92
+ return "{listening on %s port %d}" % [
93
+ self.protocol,
94
+ self.port,
95
+ ]
96
+ end
97
+
98
+
99
+ #######
100
+ private
101
+ #######
102
+
103
+ ### Try to default the appropriate port based on the node's +identifier+
104
+ ### and +protocol+. Raises a SocketError if the service port can't be
105
+ ### looked up.
106
+ def default_port_for( identifier, protocol )
107
+ return Socket.getservbyname( identifier, protocol )
108
+ rescue SocketError
109
+ return nil
110
+ end
111
+
112
+ end # class Arborist::Node::Service
@@ -0,0 +1,176 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'arborist' unless defined?( Arborist )
5
+
6
+
7
+ # The Arborist entity responsible for observing changes to the tree and
8
+ # reporting on them.
9
+ class Arborist::Observer
10
+ extend Loggability,
11
+ Arborist::MethodUtilities
12
+
13
+ # Loggability API -- write logs to the Arborist log host
14
+ log_to :arborist
15
+
16
+
17
+ autoload :Action, 'arborist/observer/action'
18
+ autoload :Summarize, 'arborist/observer/summarize'
19
+
20
+
21
+ ##
22
+ # The key for the thread local that is used to track instances as they're
23
+ # loaded.
24
+ LOADED_INSTANCE_KEY = :loaded_observer_instances
25
+
26
+ ##
27
+ # The glob pattern to use for searching for observers
28
+ OBSERVER_FILE_PATTERN = '**/*.rb'
29
+
30
+
31
+ Arborist.add_dsl_constructor( :Observer ) do |description, &block|
32
+ Arborist::Observer.new( description, &block )
33
+ end
34
+
35
+
36
+
37
+ ### Overridden to track instances of created nodes for the DSL.
38
+ def self::new( * )
39
+ new_instance = super
40
+ Arborist::Observer.add_loaded_instance( new_instance )
41
+ return new_instance
42
+ end
43
+
44
+
45
+ ### Record a new loaded instance if the Thread-local variable is set up to track
46
+ ### them.
47
+ def self::add_loaded_instance( new_instance )
48
+ instances = Thread.current[ LOADED_INSTANCE_KEY ] or return
49
+ instances << new_instance
50
+ end
51
+
52
+
53
+ ### Load the specified +file+ and return any new Nodes created as a result.
54
+ def self::load( file )
55
+ self.log.info "Loading observer file %s..." % [ file ]
56
+ Thread.current[ LOADED_INSTANCE_KEY ] = []
57
+ Kernel.load( file )
58
+ return Thread.current[ LOADED_INSTANCE_KEY ]
59
+ ensure
60
+ Thread.current[ LOADED_INSTANCE_KEY ] = nil
61
+ end
62
+
63
+
64
+ ### Return an iterator for all the observer files in the specified +directory+.
65
+ def self::each_in( directory )
66
+ path = Pathname( directory )
67
+ paths = if path.directory?
68
+ Pathname.glob( directory + OBSERVER_FILE_PATTERN )
69
+ else
70
+ [ path ]
71
+ end
72
+
73
+ return paths.flat_map do |file|
74
+ file_url = "file://%s" % [ file.expand_path ]
75
+ observers = self.load( file )
76
+ self.log.debug "Loaded observers %p..." % [ observers ]
77
+ observers.each do |observer|
78
+ observer.source = file_url
79
+ end
80
+ observers
81
+ end
82
+ end
83
+
84
+
85
+ ### Create a new Observer with the specified +description+.
86
+ def initialize( description, &block )
87
+ @description = description
88
+ @subscriptions = []
89
+ @actions = []
90
+
91
+ self.instance_exec( &block ) if block
92
+ end
93
+
94
+
95
+ ######
96
+ public
97
+ ######
98
+
99
+ ##
100
+ # The observer's description
101
+ attr_reader :description
102
+
103
+ ##
104
+ # The observer's actions
105
+ attr_reader :actions
106
+
107
+ ##
108
+ # The source file the observer was loaded from
109
+ attr_accessor :source
110
+
111
+
112
+ #
113
+ # DSL Methods
114
+ #
115
+
116
+ ### Specify a pattern for events the observer is interested in. Options:
117
+ ### to::
118
+ ### the name of the event; defaults to every event type
119
+ ### where::
120
+ ### a Hash of criteria to match against event data
121
+ ### on::
122
+ ### the identifier of the node to subscribe on, defaults to the root node
123
+ ## which receives all node events.
124
+ def subscribe( to: nil, where: {}, on: nil )
125
+ @subscriptions << { criteria: where, identifier: on, event_type: to }
126
+ end
127
+
128
+
129
+ ### Register an action that will be taken when a subscribed event is received.
130
+ def action( options={}, &block )
131
+ @actions << Arborist::Observer::Action.new( options, &block )
132
+ end
133
+
134
+
135
+ ### Register a summary action.
136
+ def summarize( options={}, &block )
137
+ @actions << Arborist::Observer::Summarize.new( options, &block )
138
+ end
139
+
140
+
141
+ #
142
+ # Observe Methods
143
+ #
144
+
145
+ ### Fetch the descriptions of which events this Observer would like to receive. If no
146
+ ### subscriptions have been specified, a subscription that will match any event is returned.
147
+ def subscriptions
148
+
149
+ # Subscribe to all events if there are no subscription criteria.
150
+ self.subscribe if @subscriptions.empty?
151
+
152
+ return @subscriptions
153
+ end
154
+
155
+
156
+ ### Handle a published event.
157
+ def handle_event( uuid, event )
158
+ self.actions.each do |action|
159
+ action.handle_event( event )
160
+ end
161
+ end
162
+
163
+
164
+ ### Return an Array of timer callbacks of the form:
165
+ ###
166
+ ### [ interval_seconds, callable ]
167
+ ###
168
+ def timers
169
+ return self.actions.map do |action|
170
+ next nil unless action.respond_to?( :on_timer ) &&
171
+ action.time_threshold.nonzero?
172
+ [ action.time_threshold, action.method(:on_timer) ]
173
+ end.compact
174
+ end
175
+
176
+ end # class Arborist::Observer
@@ -0,0 +1,125 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'schedulability'
5
+ require 'loggability'
6
+
7
+ require 'arborist/observer' unless defined?( Arborist::Observer )
8
+
9
+
10
+ # An action taken by an Observer.
11
+ class Arborist::Observer::Action
12
+ extend Loggability
13
+
14
+
15
+ # Loggability API -- log to the Arborist logger
16
+ log_to :arborist
17
+
18
+
19
+ ### Create a new Action that will call the specified +block+ +during+ the given schedule,
20
+ ### but only +after+ the specified number of events have arrived +within+ the given
21
+ ### time threshold.
22
+ def initialize( within: 0, after: 1, during: nil, &block )
23
+ raise ArgumentError, "Action requires a block" unless block
24
+
25
+ @block = block
26
+ @time_threshold = within
27
+ @schedule = Schedulability::Schedule.parse( during ) if during
28
+
29
+ if within.zero?
30
+ @count_threshold = after
31
+ else
32
+ # It should always be 2 or more if there is a time threshold
33
+ @count_threshold = [ after, 2 ].max
34
+ end
35
+
36
+ @event_history = {}
37
+ end
38
+
39
+
40
+ ######
41
+ public
42
+ ######
43
+
44
+ ##
45
+ # The object to #call when the action is triggered.
46
+ attr_reader :block
47
+
48
+ ##
49
+ # The maximum number of seconds between events that cause the action to be called
50
+ attr_reader :time_threshold
51
+
52
+ ##
53
+ # The minimum number of events that cause the action to be called when the #time_threshold
54
+ # is met.
55
+ attr_reader :count_threshold
56
+
57
+ ##
58
+ # The schedule that applies to this action.
59
+ attr_reader :schedule
60
+
61
+ ##
62
+ # The Hash of recent events, keyed by their arrival time.
63
+ attr_reader :event_history
64
+
65
+
66
+ ### Call the action for the specified +event+.
67
+ def handle_event( event )
68
+ self.record_event( event )
69
+ self.call_block( event ) if self.should_run?
70
+ end
71
+
72
+
73
+ ### Execute the action block with the specified +event+.
74
+ ###
75
+ def call_block( event )
76
+ if self.block.arity >= 2 || self.block.arity < 0
77
+ self.block.call( event.dup, self.event_history.dup )
78
+ else
79
+ self.block.call( event.dup )
80
+ end
81
+ ensure
82
+ self.event_history.clear
83
+ end
84
+
85
+
86
+ ### Record the specified +event+ in the event history if within the scheduled period(s).
87
+ def record_event( event )
88
+ return if self.schedule && !self.schedule.now?
89
+ self.event_history[ Time.now ] = event
90
+ self.event_history.keys.sort.each do |event_time|
91
+ break if self.event_history.size <= self.count_threshold
92
+ self.event_history.delete( event_time )
93
+ end
94
+ end
95
+
96
+
97
+ ### Returns +true+ if the threshold is exceeded and the current time is within the
98
+ ### action's schedule.
99
+ def should_run?
100
+ return self.time_threshold_exceeded? && self.count_threshold_exceeded?
101
+ end
102
+
103
+
104
+ ### Returns +true+ if the time between the first and last event in the #event_history is
105
+ ### less than the #time_threshold.
106
+ def time_threshold_exceeded?
107
+ return true if self.time_threshold.zero?
108
+ return false unless self.count_threshold_exceeded?
109
+
110
+ first = self.event_history.keys.min
111
+ last = self.event_history.keys.max
112
+
113
+ self.log.debug "Time between the %d events in the record (%p): %0.5fs" %
114
+ [ self.event_history.size, self.event_history, last - first ]
115
+ return last - first <= self.time_threshold
116
+ end
117
+
118
+
119
+ ### Returns +true+ if the number of events in the event history meet or exceed the
120
+ ### #count_threshold.
121
+ def count_threshold_exceeded?
122
+ return self.event_history.size >= self.count_threshold
123
+ end
124
+
125
+ end # class Arborist::Observer::Action