arborist 0.0.1.pre20160106113421

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