arborist 0.0.1.pre20160128152542 → 0.0.1.pre20160606141735

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +2 -0
  4. data/ChangeLog +426 -1
  5. data/Manifest.txt +17 -2
  6. data/Nodes.md +70 -0
  7. data/Protocol.md +68 -9
  8. data/README.md +3 -5
  9. data/Rakefile +4 -1
  10. data/TODO.md +52 -20
  11. data/lib/arborist.rb +19 -6
  12. data/lib/arborist/cli.rb +39 -25
  13. data/lib/arborist/client.rb +97 -4
  14. data/lib/arborist/command/client.rb +2 -1
  15. data/lib/arborist/command/start.rb +51 -5
  16. data/lib/arborist/dependency.rb +286 -0
  17. data/lib/arborist/event.rb +7 -2
  18. data/lib/arborist/event/{node_matching.rb → node.rb} +11 -5
  19. data/lib/arborist/event/node_acked.rb +5 -7
  20. data/lib/arborist/event/node_delta.rb +30 -3
  21. data/lib/arborist/event/node_disabled.rb +16 -0
  22. data/lib/arborist/event/node_down.rb +10 -0
  23. data/lib/arborist/event/node_quieted.rb +11 -0
  24. data/lib/arborist/event/node_unknown.rb +10 -0
  25. data/lib/arborist/event/node_up.rb +10 -0
  26. data/lib/arborist/event/node_update.rb +2 -11
  27. data/lib/arborist/event/sys_node_added.rb +10 -0
  28. data/lib/arborist/event/sys_node_removed.rb +10 -0
  29. data/lib/arborist/exceptions.rb +4 -0
  30. data/lib/arborist/manager.rb +188 -18
  31. data/lib/arborist/manager/event_publisher.rb +1 -1
  32. data/lib/arborist/manager/tree_api.rb +92 -13
  33. data/lib/arborist/mixins.rb +17 -0
  34. data/lib/arborist/monitor.rb +10 -1
  35. data/lib/arborist/monitor/socket.rb +123 -2
  36. data/lib/arborist/monitor_runner.rb +6 -5
  37. data/lib/arborist/node.rb +420 -94
  38. data/lib/arborist/node/ack.rb +72 -0
  39. data/lib/arborist/node/host.rb +43 -8
  40. data/lib/arborist/node/resource.rb +73 -0
  41. data/lib/arborist/node/root.rb +6 -0
  42. data/lib/arborist/node/service.rb +89 -22
  43. data/lib/arborist/observer.rb +1 -1
  44. data/lib/arborist/subscription.rb +11 -6
  45. data/spec/arborist/client_spec.rb +93 -5
  46. data/spec/arborist/dependency_spec.rb +375 -0
  47. data/spec/arborist/event/node_delta_spec.rb +66 -0
  48. data/spec/arborist/event/node_down_spec.rb +84 -0
  49. data/spec/arborist/event/node_spec.rb +59 -0
  50. data/spec/arborist/event/node_update_spec.rb +14 -3
  51. data/spec/arborist/event_spec.rb +3 -3
  52. data/spec/arborist/manager/tree_api_spec.rb +295 -3
  53. data/spec/arborist/manager_spec.rb +240 -57
  54. data/spec/arborist/monitor_spec.rb +26 -3
  55. data/spec/arborist/node/ack_spec.rb +74 -0
  56. data/spec/arborist/node/host_spec.rb +79 -0
  57. data/spec/arborist/node/resource_spec.rb +56 -0
  58. data/spec/arborist/node/service_spec.rb +68 -2
  59. data/spec/arborist/node_spec.rb +288 -11
  60. data/spec/arborist/subscription_spec.rb +23 -14
  61. data/spec/arborist_spec.rb +0 -4
  62. data/spec/data/observers/webservices.rb +10 -2
  63. data/spec/spec_helper.rb +8 -0
  64. metadata +58 -15
  65. metadata.gz.sig +0 -0
  66. data/LICENSE +0 -29
@@ -58,10 +58,11 @@ class Arborist::Client
58
58
 
59
59
 
60
60
  ### Return the manager's current node tree.
61
- def make_list_request( from: nil )
61
+ def make_list_request( from: nil, depth: nil )
62
62
  header = {}
63
63
  self.log.debug "From is: %p" % [ from ]
64
64
  header[:from] = from if from
65
+ header[:depth] = depth if depth
65
66
 
66
67
  return self.pack_message( :list, header )
67
68
  end
@@ -75,19 +76,54 @@ class Arborist::Client
75
76
 
76
77
 
77
78
  ### Return the manager's current node tree.
78
- def make_fetch_request( criteria, include_down: false, properties: :all )
79
+ def make_fetch_request( criteria, include_down: false, properties: :all, exclude: {} )
79
80
  header = {}
80
81
  header[ :include_down ] = true if include_down
81
82
  header[ :return ] = properties if properties != :all
82
83
 
83
- return self.pack_message( :fetch, header, criteria )
84
+ return self.pack_message( :fetch, header, [ criteria, exclude ] )
84
85
  end
85
86
 
86
87
 
88
+ ### Mark a node as 'acknowledged' if it's down, or 'disabled' if
89
+ ### it's up. (A pre-emptive acknowledgement.) Requires the node
90
+ ### +identifier+, an acknowledgement +message+, and +sender+. You
91
+ ### can optionally include a +via+ (source), and override the default
92
+ ### +time+ of now.
93
+ def acknowledge( node, message, sender, via=nil, time=Time.now )
94
+ data = {
95
+ node => {
96
+ ack: {
97
+ message: message,
98
+ sender: sender,
99
+ via: via,
100
+ time: time.to_s
101
+ }
102
+ }
103
+ }
104
+
105
+ request = self.make_update_request( data )
106
+ self.send_tree_api_request( request )
107
+ return true
108
+ end
109
+ alias_method :ack, :acknowledge
110
+
111
+
112
+ ### Clear an acknowledged/disabled +node+.
113
+ def clear_acknowledgement( node )
114
+ data = { node => { ack: nil } }
115
+ request = self.make_update_request( data )
116
+ self.send_tree_api_request( request )
117
+ return true
118
+ end
119
+ alias_method :clear_ack, :clear_acknowledgement
120
+
121
+
87
122
  ### Update the identified nodes in the manager with the specified data.
88
123
  def update( *args )
89
124
  request = self.make_update_request( *args )
90
- return self.send_tree_api_request( request )
125
+ self.send_tree_api_request( request )
126
+ return true
91
127
  end
92
128
 
93
129
 
@@ -133,6 +169,63 @@ class Arborist::Client
133
169
  end
134
170
 
135
171
 
172
+ ### Remove a node
173
+ def prune( *args )
174
+ request = self.make_prune_request( *args )
175
+ response = self.send_tree_api_request( request )
176
+ return response
177
+ end
178
+
179
+
180
+ ### Remove the node with the specified +identfier+.
181
+ def make_prune_request( identifier )
182
+ self.log.debug "Making prune request for identifier: %s" % [ identifier ]
183
+
184
+ return self.pack_message( :prune, identifier: identifier )
185
+ end
186
+
187
+
188
+ ### Add a new node to the tree.
189
+ def graft( *args )
190
+ request = self.make_graft_request( *args )
191
+ response = self.send_tree_api_request( request )
192
+ return response
193
+ end
194
+
195
+
196
+ ### Add a node with the specified +identifier+ and +arguments+.
197
+ def make_graft_request( identifier, attributes={} )
198
+ self.log.debug "Making graft request for identifer: %s" % [ identifier ]
199
+
200
+ parent = attributes.delete( :parent )
201
+ type = attributes.delete( :type )
202
+
203
+ header = {
204
+ identifier: identifier,
205
+ parent: parent,
206
+ type: type
207
+ }
208
+
209
+ return self.pack_message( :graft, header, attributes )
210
+ end
211
+
212
+
213
+ ### Modify operational attributes of a node.
214
+ def modify( *args )
215
+ request = self.make_modify_request( *args )
216
+ response = self.send_tree_api_request( request )
217
+ return true
218
+ end
219
+
220
+
221
+ ### Modify the operations +attributes+ of the node with the specified +identifier+.
222
+ def make_modify_request( identifier, attributes={} )
223
+ self.log.debug "Making modify request for identifer: %s" % [ identifier ]
224
+
225
+ return self.pack_message( :modify, {identifier: identifier}, attributes )
226
+ end
227
+
228
+
136
229
  ### Send the packed +request+ via the Tree API socket, raise an error on
137
230
  ### unsuccessful response, and return the response body.
138
231
  def send_tree_api_request( request )
@@ -17,7 +17,8 @@ module Arborist::CLI::Client
17
17
  cmd.action do |globals, options, args|
18
18
  begin
19
19
  require 'pry'
20
- rescue LoadError
20
+ rescue LoadError => err
21
+ self.log.debug( err )
21
22
  exit_now! "This command requires the 'pry' gem."
22
23
  end
23
24
 
@@ -22,11 +22,15 @@ module Arborist::CLI::Start
22
22
  cmd.flag :loader, desc: "Specify a loader type to use.",
23
23
  default_value: 'file'
24
24
 
25
+ cmd.desc "Run under the profiler in the given MODE (one of wall, cpu, or object; defaults to wall)."
26
+ cmd.arg_name :MODE
27
+ cmd.flag [:p, 'profiler'], must_match: ['wall', 'cpu', 'object']
28
+
25
29
  cmd.action do |globals, options, args|
26
30
  appname = args.shift
27
31
  source = args.shift
28
32
 
29
- loader = Arborist::Loader.create( options.loader, source )
33
+ loader = Arborist::Loader.create( options[:loader], source )
30
34
  runner = case appname
31
35
  when 'manager'
32
36
  Arborist.manager_for( loader )
@@ -39,7 +43,7 @@ module Arborist::CLI::Start
39
43
  end
40
44
 
41
45
  unless_dryrun( "starting #{appname}" ) do
42
- start( runner )
46
+ start( runner, options[:p] )
43
47
  end
44
48
  end
45
49
  end
@@ -51,10 +55,52 @@ module Arborist::CLI::Start
51
55
 
52
56
  ### Start the specified +runner+ instance after setting up the environment for
53
57
  ### it.
54
- def start( runner )
55
- $0 = runner.class.name
56
- runner.run
58
+ def start( runner, profile_mode=nil )
59
+ Process.setproctitle( runner.class.name )
60
+
61
+ if profile_mode
62
+ self.with_profiling_enabled( profile_mode, runner ) do
63
+ runner.run
64
+ end
65
+ else
66
+ runner.run
67
+ end
68
+ end
69
+
70
+
71
+ ### Wrap the profiler around the specified +callable+.
72
+ def self::with_profiling_enabled( profile_arg, runner, &block )
73
+ require 'stackprof'
74
+ mode, outfile = self.parse_profile_args( profile_arg, runner )
75
+
76
+ self.log.info "Profiling in %s mode, outputting to %s" % [ mode, outfile ]
77
+ StackProf.run( mode: mode.to_sym, out: outfile, &block )
78
+ rescue LoadError => err
79
+ self.log.debug "%p while loading the StackProf profiler: %s"
80
+ exit_now!( "Couldn't load the profiler; you probably need to `gem install stackprof`", 254 )
57
81
  end
58
82
 
83
+
84
+ ### Set up the StackProf profiler to run in the given +mode+.
85
+ def self::parse_profile_args( arg, runner )
86
+ profile_mode, profile_filename = arg.split( ':', 2 )
87
+ profile_filename ||= self.default_profile_filename( profile_mode, runner )
88
+
89
+ return profile_mode, profile_filename
90
+ end
91
+
92
+
93
+ ### Return a filename for a StackProf profile run over the given +runner+.
94
+ def self::default_profile_filename( mode, runner )
95
+ basename = runner.class.name.gsub( /.*::/, '' )
96
+ return "%s-%s-%s.%d.dump" % [
97
+ basename,
98
+ mode,
99
+ Time.now.strftime('%Y%m%d%H%M%S'),
100
+ Process.pid,
101
+ ]
102
+ end
103
+
104
+
59
105
  end # module Arborist::CLI::Start
60
106
 
@@ -0,0 +1,286 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'set'
5
+ require 'time'
6
+ require 'loggability'
7
+
8
+ require 'arborist' unless defined?( Arborist )
9
+
10
+
11
+
12
+ # A inter-node dependency that is outside of the implicit ones expressed by the
13
+ # tree.
14
+ class Arborist::Dependency
15
+ extend Loggability
16
+
17
+
18
+ # Loggability API -- log to the Arborist logger
19
+ log_to :arborist
20
+
21
+
22
+ ### Construct a new Dependency for the specified +behavior+ on the given +identifiers+
23
+ ### with +prefixes+.
24
+ def self::on( behavior, *identifiers, prefixes: nil )
25
+ deps, identifiers = identifiers.flatten.uniq.partition {|obj| obj.is_a?(self.class) }
26
+ prefixes = Array( prefixes ).uniq
27
+ identifiers = prefixes.product( identifiers ).map {|pair| pair.join('-') } unless
28
+ prefixes.empty?
29
+
30
+ return self.new( behavior, identifiers + deps )
31
+ end
32
+
33
+
34
+ ### Construct a new instance using the specified +hash+, which should be in the same form
35
+ ### as that generated by #to_h:
36
+ ###
37
+ ### {
38
+ ### behavior: <string>,
39
+ ### identifiers: [<identifier_1>, <identifier_n>],
40
+ ### subdeps: [<dephash_1>, <dephash_n>],
41
+ ### }
42
+ ###
43
+ def self::from_hash( hash )
44
+ self.log.debug "Creating a new %p from a hash: %p" % [ self, hash ]
45
+
46
+ hash[:subdeps] ||= []
47
+ subdeps = hash[:subdeps].map {|subhash| self.from_hash(subhash) }
48
+
49
+ return self.new( hash[:behavior], hash[:identifiers] + subdeps )
50
+ end
51
+
52
+
53
+ ### Create a new Dependency on the specified +nodes_or_subdeps+ with the given +behavior+
54
+ ### (one of :any or :all)
55
+ def initialize( behavior, *nodes_or_subdeps )
56
+ @behavior = behavior
57
+ @subdeps, identifiers = nodes_or_subdeps.flatten.
58
+ partition {|obj| obj.is_a?(self.class) }
59
+ @identifier_states = identifiers.product([ nil ]).to_h
60
+ end
61
+
62
+
63
+ ### Dup constructor -- dup internal datastructures without ephemeral state on #dup.
64
+ def initialize_dup( original ) # :nodoc:
65
+ @subdeps = @subdeps.map( &:dup )
66
+ @identifier_states = @identifier_states.keys.product([ nil ]).to_h
67
+ end
68
+
69
+
70
+ ### Clone constructor -- clone internal datastructures without ephemeral state on #clone.
71
+ def initialize_clone( original ) # :nodoc:
72
+ @subdeps = @subdeps.map( &:clone )
73
+ @identifier_states = @identifier_states.keys.product([ nil ]).to_h
74
+ end
75
+
76
+
77
+ ######
78
+ public
79
+ ######
80
+
81
+ ##
82
+ # The behavior that determines if the dependency is met by any or all of the
83
+ # nodes.
84
+ attr_reader :behavior
85
+
86
+ ##
87
+ # The Hash of identifier states
88
+ attr_reader :identifier_states
89
+
90
+ ##
91
+ # The Array of sub-dependencies (instances of Dependency).
92
+ attr_reader :subdeps
93
+
94
+
95
+ ### Return a Set of identifiers belonging to this dependency.
96
+ def identifiers
97
+ return Set.new( self.identifier_states.keys )
98
+ end
99
+
100
+
101
+ ### Return a Set of identifiers which have been marked down in this dependency.
102
+ def down_identifiers
103
+ return Set.new( self.identifier_states.select {|_, mark| mark }.map(&:first) )
104
+ end
105
+
106
+
107
+ ### Return a Set of identifiers which have not been marked down in this dependency.
108
+ def up_identifiers
109
+ return Set.new( self.identifier_states.reject {|_, mark| mark }.map(&:first) )
110
+ end
111
+
112
+
113
+ ### Return a Set of identifiers for all of this Dependency's sub-dependencies.
114
+ def subdep_identifiers
115
+ return self.subdeps.map( &:all_identifiers ).reduce( :+ ) || Set.new
116
+ end
117
+
118
+
119
+ ### Return the Set of this Dependency's identifiers as well as those of all of its
120
+ ### sub-dependencies.
121
+ def all_identifiers
122
+ return self.identifiers + self.subdep_identifiers
123
+ end
124
+
125
+
126
+ ### Return any of this dependency's sub-dependencies that are down.
127
+ def down_subdeps
128
+ return self.subdeps.select( &:down? )
129
+ end
130
+
131
+
132
+ ### Return any of this dependency's sub-dependencies that are up.
133
+ def up_subdeps
134
+ return self.subdeps.select( &:up? )
135
+ end
136
+
137
+
138
+ ### Yield each unique identifier and Time of downed nodes from both direct and
139
+ ### sub-dependencies.
140
+ def each_downed
141
+ return enum_for( __method__ ) unless block_given?
142
+
143
+ yielded = Set.new
144
+ self.identifier_states.each do |ident, time|
145
+ if time
146
+ yield( ident, time ) unless yielded.include?( ident )
147
+ yielded.add( ident )
148
+ end
149
+ end
150
+ self.subdeps.each do |subdep|
151
+ subdep.each_downed do |ident, time|
152
+ if time
153
+ yield( ident, time ) unless yielded.include?( ident )
154
+ yielded.add( ident )
155
+ end
156
+ end
157
+ end
158
+ end
159
+
160
+
161
+ ### Returns +true+ if the receiver includes all of the given +identifiers+.
162
+ def include?( *identifiers )
163
+ return self.all_identifiers.include?( *identifiers )
164
+ end
165
+
166
+
167
+ ### Returns +true+ if this dependency doesn't contain any identifiers or
168
+ ### sub-dependencies.
169
+ def empty?
170
+ return self.all_identifiers.empty?
171
+ end
172
+
173
+
174
+ ### Mark the specified +identifier+ as being down and propagate it to any subdependencies.
175
+ def mark_down( identifier, time=Time.now )
176
+ self.identifier_states[ identifier ] = time if self.identifier_states.key?( identifier )
177
+ self.subdeps.each do |dep|
178
+ dep.mark_down( identifier, time )
179
+ end
180
+ end
181
+
182
+
183
+ ### Mark the specified +identifier+ as being up and propagate it to any subdependencies.
184
+ def mark_up( identifier )
185
+ self.subdeps.each do |dep|
186
+ dep.mark_up( identifier )
187
+ end
188
+ self.identifier_states[ identifier ] = nil if self.identifier_states.key?( identifier )
189
+ end
190
+
191
+
192
+ ### Returns +true+ if this dependency cannot be met.
193
+ def down?
194
+ case self.behavior
195
+ when :all
196
+ self.identifier_states.values.any? || self.subdeps.any?( &:down? )
197
+ when :any
198
+ self.identifier_states.values.all? && self.subdeps.all?( &:down? )
199
+ end
200
+ end
201
+
202
+
203
+ ### Returns +true+ if this dependency is met.
204
+ def up?
205
+ return !self.down?
206
+ end
207
+
208
+
209
+ ### Returns the earliest Time a node was marked down.
210
+ def earliest_down_time
211
+ return self.identifier_states.values.compact.min
212
+ end
213
+
214
+
215
+ ### Returns the latest Time a node was marked down.
216
+ def latest_down_time
217
+ return self.identifier_states.values.compact.max
218
+ end
219
+
220
+
221
+ ### Return an English description of why this dependency is not met. If it is
222
+ ### met, returns +nil+.
223
+ def down_reason
224
+ ids = self.down_identifiers
225
+ subdeps = self.down_subdeps
226
+
227
+ return nil if ids.empty? && subdeps.empty?
228
+
229
+ msg = nil
230
+ case self.behavior
231
+ when :all
232
+ msg = ids.first.dup
233
+ if ids.size == 1
234
+ msg << " is down"
235
+ else
236
+ msg << " (and %d other%s) are down" % [ ids.size - 1, ids.size == 2 ? '' : 's' ]
237
+ end
238
+
239
+ msg << " as of %s" % [ self.earliest_down_time ]
240
+
241
+ when :any
242
+ msg = "%s are all down" % [ ids.to_a.join(', ') ]
243
+ msg << " as of %s" % [ self.latest_down_time ]
244
+
245
+ else
246
+ raise "Don't know how to build a description of down behavior for %p" % [ self.behavior ]
247
+ end
248
+
249
+ return msg
250
+ end
251
+
252
+
253
+ ### Return the entire dependency tree as a nested Hash.
254
+ def to_h
255
+ return {
256
+ behavior: self.behavior,
257
+ identifiers: self.identifier_states.keys,
258
+ subdeps: self.subdeps.map( &:to_h )
259
+ }
260
+ end
261
+
262
+
263
+ ### Returns true if +other+ is the same object or if they both have the same
264
+ ### identifiers, sub-dependencies, and identifier states.
265
+ def eql?( other )
266
+ self.log.debug "Comparing %p to %p (with states)" % [ self, other ]
267
+ return true if other.equal?( self )
268
+ return self == other &&
269
+ self.identifier_states.eql?( other.identifier_states ) &&
270
+ self.subdeps.eql?( other.subdeps )
271
+ end
272
+
273
+
274
+ ### Equality comparison operator -- return true if the +other+ dependency has the same
275
+ ### behavior, identifiers, and sub-dependencies. Does not consider identifier states.
276
+ def ==( other )
277
+ return true if other.equal?( self )
278
+ return false unless other.is_a?( self.class )
279
+
280
+ return self.behavior == other.behavior &&
281
+ self.identifiers == other.identifiers &&
282
+ self.subdeps == other.subdeps
283
+ end
284
+
285
+ end # class Arborist::Dependency
286
+