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
@@ -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
+