chook 1.0.1.b1 → 1.1.5b1

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 (46) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGES.md +56 -0
  3. data/README.md +397 -145
  4. data/bin/chook-server +31 -1
  5. data/data/chook.conf.example +183 -0
  6. data/data/com.pixar.chook-server.plist +20 -0
  7. data/data/sample_handlers/RestAPIOperation.rb +11 -11
  8. data/data/sample_handlers/SmartGroupComputerMembershipChange.rb +3 -6
  9. data/data/sample_jsons/SmartGroupComputerMembershipChange.json +3 -1
  10. data/data/sample_jsons/SmartGroupMobileDeviceMembershipChange.json +3 -1
  11. data/lib/chook/configuration.rb +27 -8
  12. data/lib/chook/event.rb +6 -1
  13. data/lib/chook/event/handled_event.rb +36 -9
  14. data/lib/chook/event/handled_event/handlers.rb +252 -99
  15. data/lib/chook/event/handled_event_logger.rb +86 -0
  16. data/lib/chook/event_handling.rb +1 -0
  17. data/lib/chook/foundation.rb +3 -0
  18. data/lib/chook/procs.rb +17 -1
  19. data/lib/chook/server.rb +73 -72
  20. data/lib/chook/server/auth.rb +164 -0
  21. data/lib/chook/server/log.rb +215 -0
  22. data/lib/chook/server/public/css/chook.css +133 -0
  23. data/lib/chook/server/public/imgs/ChookLogoAlMcWhiggin.png +0 -0
  24. data/lib/chook/server/public/js/chook.js +126 -0
  25. data/lib/chook/server/public/js/logstream.js +101 -0
  26. data/lib/chook/server/routes.rb +28 -0
  27. data/lib/chook/server/routes/handle_by_name.rb +65 -0
  28. data/lib/chook/server/routes/handle_webhook_event.rb +27 -3
  29. data/lib/chook/server/routes/handlers.rb +52 -0
  30. data/lib/chook/server/routes/home.rb +48 -1
  31. data/lib/chook/server/routes/log.rb +105 -0
  32. data/lib/chook/server/routes/login_logout.rb +48 -0
  33. data/lib/chook/server/views/admin.haml +11 -0
  34. data/lib/chook/server/views/bak.haml +48 -0
  35. data/lib/chook/server/views/config.haml +15 -0
  36. data/lib/chook/server/views/handlers.haml +63 -0
  37. data/lib/chook/server/views/layout.haml +64 -0
  38. data/lib/chook/server/views/logstream.haml +33 -0
  39. data/lib/chook/server/views/sketch_admin +44 -0
  40. data/lib/chook/subject.rb +13 -2
  41. data/lib/chook/subject/dep_device.rb +81 -0
  42. data/lib/chook/subject/policy_finished.rb +43 -0
  43. data/lib/chook/subject/smart_group.rb +6 -0
  44. data/lib/chook/subject/test_subject.rb +2 -2
  45. data/lib/chook/version.rb +1 -1
  46. metadata +78 -17
@@ -1,36 +1,45 @@
1
- ### Copyright 2017 Pixar
2
-
3
- ###
4
- ### Licensed under the Apache License, Version 2.0 (the "Apache License")
5
- ### with the following modification; you may not use this file except in
6
- ### compliance with the Apache License and the following modification to it:
7
- ### Section 6. Trademarks. is deleted and replaced with:
8
- ###
9
- ### 6. Trademarks. This License does not grant permission to use the trade
10
- ### names, trademarks, service marks, or product names of the Licensor
11
- ### and its affiliates, except as required to comply with Section 4(c) of
12
- ### the License and to reproduce the content of the NOTICE file.
13
- ###
14
- ### You may obtain a copy of the Apache License at
15
- ###
16
- ### http://www.apache.org/licenses/LICENSE-2.0
17
- ###
18
- ### Unless required by applicable law or agreed to in writing, software
19
- ### distributed under the Apache License with the above modification is
20
- ### distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
21
- ### KIND, either express or implied. See the Apache License for the specific
22
- ### language governing permissions and limitations under the Apache License.
23
- ###
24
- ###
1
+ # Copyright 2017 Pixar
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "Apache License")
4
+ # with the following modification; you may not use this file except in
5
+ # compliance with the Apache License and the following modification to it:
6
+ # Section 6. Trademarks. is deleted and replaced with:
7
+ #
8
+ # 6. Trademarks. This License does not grant permission to use the trade
9
+ # names, trademarks, service marks, or product names of the Licensor
10
+ # and its affiliates, except as required to comply with Section 4(c) of
11
+ # the License and to reproduce the content of the NOTICE file.
12
+ #
13
+ # You may obtain a copy of the Apache License at
14
+ #
15
+ # http://www.apache.org/licenses/LICENSE-2.0
16
+ #
17
+ # Unless required by applicable law or agreed to in writing, software
18
+ # distributed under the Apache License with the above modification is
19
+ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
20
+ # KIND, either express or implied. See the Apache License for the specific
21
+ # language governing permissions and limitations under the Apache License.
22
+ #
23
+ #
24
+
25
25
  module Chook
26
26
 
27
- # This method is used by the Ruby event-handler files.
27
+ # This method is used by the Ruby 'internal' event-handler files.
28
+ #
29
+ # those handlers are defined by passing a block to this method, like so:
30
+ #
31
+ # Chook.event_handler do |event|
32
+ # # so something with the event
33
+ # end
28
34
  #
29
- # Loading them should call this method and pass in a block
35
+ # Loading them will call this method and pass in a block
30
36
  # with one parameter: a Chook::HandledEvent subclass instance.
31
37
  #
32
- # The block is then converted to a Proc instance in @loaded_event_handler
33
- # and from there can be stored for use by the event identified by the filename.
38
+ # The block is then converted to a #handle method in an anonymous object.
39
+ # The object is stored for use by the event identified by the filename.
40
+ #
41
+ # By storing it as a method in an object, the handlers themselves
42
+ # can use #break or #return to exit (or even #next)
34
43
  #
35
44
  # NOTE: the files should be read with 'load' not 'require', so that they can
36
45
  # be re-loaded as needed
@@ -44,7 +53,13 @@ module Chook
44
53
  # @return [Proc] the block converted to a Proc
45
54
  #
46
55
  def self.event_handler(&block)
47
- HandledEvent::Handlers.loaded_handler = Proc.new(&block)
56
+ obj = Object.new
57
+ obj.define_singleton_method(:handle, &block)
58
+ # Loading the file created the object by calling this method
59
+ # but to access it after loading the file, we need to
60
+ # store it in here:
61
+ HandledEvent::Handlers.loaded_handler = obj
62
+ Chook.logger.debug "Code block for 'Chook.event_handler' loaded into \#handle method of runner-object #{obj.object_id}"
48
63
  end
49
64
 
50
65
  # the server class
@@ -53,56 +68,101 @@ module Chook
53
68
  # The Handlers namespace module
54
69
  module Handlers
55
70
 
56
- # Module Constants
57
- ############################
58
-
59
71
  DEFAULT_HANDLER_DIR = '/Library/Application Support/Chook'.freeze
60
72
 
61
- # Module Instance Variables, & accessors
62
- ############################
73
+ # Handlers that are only called by name using the route:
74
+ # post '/handler/:handler_name'
75
+ # are located in this subdirection of the handler directory
76
+ NAMED_HANDLER_SUBDIR = 'NamedHandlers'.freeze
63
77
 
64
- # This holds the most recently loaded Proc handler
65
- # until it can be stored in the @handlers Hash
66
- @loaded_handler = nil
78
+ # internal handler files must match this regex somewhere
79
+ INTERNAL_HANDLER_BLOCK_START_RE = /Chook.event_handler( ?\{| do) *\|/
67
80
 
68
- # Getter for @loaded_handler
81
+ # self loaded_handler=
69
82
  #
70
- # @return [Proc,nil] the most recent Proc loaded from a handler file.
83
+ # @return [Obj,nil] the most recent Proc loaded from a handler file.
71
84
  # destined for storage in @handlers
72
85
  #
73
86
  def self.loaded_handler
74
87
  @loaded_handler
75
88
  end
76
89
 
77
- # Setter for @loaded_event_handler
90
+ # A holding place for internal handlers as they are loaded
91
+ # before being added to the @handlers Hash
92
+ # see Chook.event_handler(&block)
78
93
  #
79
- # @param a_proc [Proc] a Proc object for storage in @handlers
94
+ # @param a_proc [Object] An object instance with a #handle method
80
95
  #
81
- def self.loaded_handler=(a_proc)
82
- @loaded_handler = a_proc
96
+ def self.loaded_handler=(anon_obj)
97
+ @loaded_handler = anon_obj
83
98
  end
84
99
 
85
- # A hash of loaded handlers.
86
- # Keys are Strings - the name of the events handled
87
- # Values are Arrays of either Procs, or Pathnames to executable files.
88
- # See the .handlers getter Methods
89
- @handlers = {}
90
-
91
- # Getter for @event_handlers
100
+ # Getter for @handlers
101
+ #
102
+ # @return [Hash{String => Array}] a mapping of Event Names as they
103
+ # come from the JSS to an Array of handlers for the event.
104
+ # The handlers are either Pathnames to executable external handlers
105
+ # or Objcts with a #handle method, for internal handlers
106
+ # (The objects also have a #handler_file attribute that is the Pathname)
92
107
  #
93
- # @return [Hash{String => Array}] a mapping of Event Names as the come from
94
- # the JSS to an Array of handlers for the event. The handlers are either
95
- # Proc objects to call from within ruby, or Pathnames to executable files
96
- # which will take raw JSON on stdin.
97
108
  def self.handlers
98
- @handlers
109
+ @handlers ||= {}
99
110
  end
100
111
 
101
- # Module Methods
102
- ############################
112
+ # Handlers can check Chook::HandledEvent::Handlers.reloading?
113
+ # and do stuff if desired.
114
+ def self.reloading?
115
+ @reloading
116
+ end
117
+
118
+ # getter for @named_handlers
119
+ # These handlers are called by name via the route
120
+ # " post '/handler/:handler_name'"
121
+ #
122
+ # They are not tied to any event type by their filenames
123
+ # its up to the writers of the handlers to make sure
124
+ # the webhook that calls them is sending the correct event
125
+ # type.
126
+ #
127
+ # The data structure of @named_handlers is a
128
+ # Hash of Strings to Pathnames or Anon Objects:
129
+ # {
130
+ # handler_filename => Pathname or Obj,
131
+ # handler_filename => Pathname or Obj,
132
+ # handler_filename => Pathname or Obj
133
+ # }
134
+ #
135
+ # @return [Hash {String => Pathname, Proc}]
136
+ def self.named_handlers
137
+ @named_handlers ||= {}
138
+ end
139
+
140
+ # the Pathname objects for all loaded handlers
141
+ #
142
+ # @return [Array<Pathname>]
143
+ #
144
+ def self.all_handler_paths
145
+ hndlrs = named_handlers.values
146
+ hndlrs += handlers.values.flatten
147
+ hndlrs.map do |hndlr|
148
+ hndlr.is_a?(Pathname) ? hndlr : hndlr.handler_file
149
+ end
150
+ end
103
151
 
104
152
  # Load all the event handlers from the handler_dir or an arbitrary dir.
105
153
  #
154
+ #
155
+ # Handler files must be either:
156
+ # - An executable file, which will have the raw JSON from the JSS piped
157
+ # to it's stdin when executed
158
+ # or
159
+ # - A non-executable file of ruby code like this:
160
+ # Chook.event_handler do |event|
161
+ # # your code goes here.
162
+ # end
163
+ #
164
+ # (see the Chook README for details about writing the ruby handlers)
165
+ #
106
166
  # @param from_dir [String, Pathname] directory from which to load the
107
167
  # handlers. Defaults to CONFIG.handler_dir or DEFAULT_HANDLER_DIR if
108
168
  # config is unset
@@ -112,27 +172,49 @@ module Chook
112
172
  #
113
173
  # @return [void]
114
174
  #
115
- def self.load_handlers(from_dir: Chook::CONFIG.handler_dir, reload: false)
175
+ def self.load_handlers(from_dir: Chook.config.handler_dir, reload: false)
176
+ # use default if needed
116
177
  from_dir ||= DEFAULT_HANDLER_DIR
178
+ handler_dir = Pathname.new(from_dir)
179
+ named_handler_dir = handler_dir + NAMED_HANDLER_SUBDIR
180
+ load_type = 'Loading'
181
+
117
182
  if reload
118
- @handlers_loaded_from = nil
183
+ @reloading = true
119
184
  @handlers = {}
185
+ @named_handlers = {}
120
186
  @loaded_handler = nil
187
+ load_type = 'Re-loading'
121
188
  end
122
189
 
123
- handler_dir = Pathname.new(from_dir)
124
- return unless handler_dir.directory? && handler_dir.readable?
190
+ # General Handlers
191
+ Chook.logger.info "#{load_type} general handlers from directory: #{handler_dir}"
192
+ if handler_dir.directory? && handler_dir.readable?
193
+ handler_dir.children.each do |handler_file|
194
+ load_general_handler(handler_file) if handler_file.file? && handler_file.readable?
195
+ end
196
+ Chook.logger.info handlers.empty? ? 'No general handlers found' : "Loaded #{handlers.values.flatten.size} general handlers for #{handlers.keys.size} event triggers"
197
+ else
198
+ Chook.logger.error "General handler directory '#{from_dir}' not a readable directory. No general handlers loaded. "
199
+ end
125
200
 
126
- handler_dir.children.each do |handler_file|
127
- load_handler(handler_file) if handler_file.file? && handler_file.readable?
201
+ # Named Handlers
202
+ Chook.logger.info "#{load_type} named handlers from directory: #{named_handler_dir}"
203
+ if named_handler_dir.directory? && named_handler_dir.readable?
204
+ named_handler_dir.children.each do |handler_file|
205
+ load_named_handler(handler_file) if handler_file.file? && handler_file.readable?
206
+ end
207
+ Chook.logger.info "Loaded #{named_handlers.size} named handlers"
208
+ else
209
+ Chook.logger.error "Named handler directory '#{named_handler_dir}' not a readable directory. No named handlers loaded. "
128
210
  end
129
211
 
130
- @handlers_loaded_from = handler_dir
131
- @handlers.values.flatten.size
212
+ @reloading = false
132
213
  end # load handlers
133
214
 
134
- # Load an event handler from a file.
135
- # Handler files must begin with the name of the event they handle,
215
+ # Load a general event handler from a file.
216
+ #
217
+ # General Handler files must begin with the name of the event they handle,
136
218
  # e.g. ComputerAdded, followed by: nothing, a dot, a dash, or
137
219
  # and underscore. Case doesn't matter.
138
220
  # So all of these are OK:
@@ -142,49 +224,113 @@ module Chook
142
224
  # Computeradded-update-ldap
143
225
  # There can be as many as desired for each event.
144
226
  #
145
- # Each must be either:
146
- # - An executable file, which will have the raw JSON from the JSS piped
147
- # to it's stdin when executed
148
- # or
149
- # - A non-executable file of ruby code like this:
150
- # Chook.event_handler do |event|
151
- # # your code goes here.
152
- # end
153
- #
154
- # (see the Chook README for details about writing the ruby handlers)
155
- #
156
- # @param from_file [Pathname] the file from which to load the handler
227
+ # @param handler_file [Pathname] the file from which to load the handler
157
228
  #
158
229
  # @return [void]
159
230
  #
160
- def self.load_handler(from_file)
161
- handler_file = Pathname.new from_file
231
+ def self.load_general_handler(handler_file)
232
+ Chook.logger.debug "Starting load of general handler file '#{handler_file.basename}'"
233
+
162
234
  event_name = event_name_from_handler_filename(handler_file)
163
- return unless event_name
235
+ unless event_name
236
+ Chook.logger.debug "Ignoring general handler file '#{handler_file.basename}': Filename doesn't start with event name"
237
+ return
238
+ end
164
239
 
165
240
  # create an array for this event's handlers, if needed
166
- @handlers[event_name] ||= []
241
+ handlers[event_name] ||= []
167
242
 
243
+ # external? if so, its executable and we only care about its pathname
168
244
  if handler_file.executable?
169
- # store as a Pathname, we'll pipe JSON to it
170
- unless @handlers[event_name].include? handler_file
171
- @handlers[event_name] << handler_file
172
- puts "===> Loaded executable handler file '#{handler_file.basename}'"
173
- end
245
+ Chook.logger.info "Loading external general handler file '#{handler_file.basename}' for #{event_name} events"
246
+ handlers[event_name] << handler_file
174
247
  return
175
248
  end
176
249
 
177
- # load the file. If written correctly, it will
178
- # put a Proc into @loaded_handler
179
- load handler_file.to_s
180
- if @loaded_handler
181
- @handlers[event_name] << @loaded_handler
182
- puts "===> Loaded internal handler file '#{handler_file.basename}'"
183
- @loaded_handler = nil
250
+ # Internal, we store an object with a .handle method
251
+ Chook.logger.info "Loading internal general handler file '#{handler_file.basename}' for #{event_name} events"
252
+ load_internal_handler handler_file
253
+ handlers[event_name] << @loaded_handler if @loaded_handler
254
+
255
+ end # self.load_general_handler(handler_file)
256
+
257
+ # Load a named event handler from a file.
258
+ #
259
+ # Named Handler files can have any name, as they are called directly
260
+ # from a Jamf webhook via URL.
261
+ #
262
+ # @param handler_file [Pathname] the file from which to load the handler
263
+ #
264
+ # @return [void]
265
+ #
266
+ def self.load_named_handler(handler_file)
267
+ Chook.logger.debug "Starting load of named handler file '#{handler_file.basename}'"
268
+
269
+ # external? if so, its executable and we only care about its pathname
270
+ if handler_file.executable?
271
+ Chook.logger.info "Loading external named handler file '#{handler_file.basename}'"
272
+ named_handlers[handler_file.basename.to_s] = handler_file
273
+ return
274
+ end
275
+
276
+ # Internal, we store an object with a .handle method
277
+ Chook.logger.info "Loading internal named handler file '#{handler_file.basename}'"
278
+ load_internal_handler handler_file
279
+ named_handlers[handler_file.basename.to_s] = @loaded_handler if @loaded_handler
280
+ end # self.load_general_handler(handler_file)
281
+
282
+ # if the given file is executable, store it's path as a handler for the event
283
+ #
284
+ # @return [Boolean] did we load an external handler?
285
+ #
286
+ def self.load_external_handler(handler_file, event_name, named)
287
+ return false unless handler_file.executable?
288
+
289
+ say_named = named ? 'named ' : ''
290
+ Chook.logger.info "Loading #{say_named}external handler file '#{handler_file.basename}' for #{event_name} events"
291
+
292
+ if named
293
+ named_handlers[event_name][handler_file.basename.to_s] = handler_file
184
294
  else
185
- puts "===> FAILED loading internal handler file '#{handler_file.basename}'"
295
+ # store the Pathname, we'll pipe JSON to it
296
+ handlers[event_name] << handler_file
297
+ end
298
+
299
+ true
300
+ end
301
+
302
+ # if a given path is not executable, try to load it as an internal handler
303
+ #
304
+ # @param handler_file[Pathname] the handler file
305
+ #
306
+ # @return [Object] and anonymous object that has a .handle method
307
+ #
308
+ def self.load_internal_handler(handler_file)
309
+ # load the file. If written correctly, it will
310
+ # put an anon. Object with a #handle method into @loaded_handler
311
+ unless handler_file.read =~ INTERNAL_HANDLER_BLOCK_START_RE
312
+ Chook.logger.error "Internal handler file '#{handler_file}' missing event_handler block"
313
+ return nil
314
+ end
315
+
316
+ # reset @loaded_handler - the `load` call will refill it
317
+ # see Chook.event_handler
318
+ @loaded_handler = nil
319
+
320
+ begin
321
+ load handler_file.to_s
322
+ raise '@loaded handler nil after loading file' unless @loaded_handler
323
+ rescue => e
324
+ Chook.logger.error "FAILED loading internal handler file '#{handler_file}': #{e}"
325
+ return
186
326
  end
187
- end # self.load_handler(handler_file)
327
+
328
+ # add a method to the object to get its Pathname
329
+ @loaded_handler.define_singleton_method(:handler_file) { handler_file }
330
+
331
+ # return it
332
+ @loaded_handler
333
+ end
188
334
 
189
335
  # Given a handler filename, return the event name it wants to handle
190
336
  #
@@ -194,9 +340,16 @@ module Chook
194
340
  # @return [String,nil] The matching event name or nil if no match
195
341
  #
196
342
  def self.event_name_from_handler_filename(filename)
343
+ filename = filename.basename
197
344
  @event_names ||= Chook::Event::EVENTS.keys
198
- desired_event_name = filename.basename.to_s.split(/\.|-|_/).first
199
- @event_names.select { |n| desired_event_name.casecmp(n).zero? }.first
345
+ desired_event_name = filename.to_s.split(/\.|-|_/).first
346
+ ename = @event_names.select { |n| desired_event_name.casecmp(n).zero? }.first
347
+ if ename
348
+ Chook.logger.debug "Found event name '#{ename}' at start of filename '#{filename}'"
349
+ else
350
+ Chook.logger.debug "No known event name at start of filename '#{filename}'"
351
+ end
352
+ ename
200
353
  end
201
354
 
202
355
  end # module Handler
@@ -0,0 +1,86 @@
1
+ ### Copyright 2017 Pixar
2
+
3
+ ###
4
+ ### Licensed under the Apache License, Version 2.0 (the "Apache License")
5
+ ### with the following modification; you may not use this file except in
6
+ ### compliance with the Apache License and the following modification to it:
7
+ ### Section 6. Trademarks. is deleted and replaced with:
8
+ ###
9
+ ### 6. Trademarks. This License does not grant permission to use the trade
10
+ ### names, trademarks, service marks, or product names of the Licensor
11
+ ### and its affiliates, except as required to comply with Section 4(c) of
12
+ ### the License and to reproduce the content of the NOTICE file.
13
+ ###
14
+ ### You may obtain a copy of the Apache License at
15
+ ###
16
+ ### http://www.apache.org/licenses/LICENSE-2.0
17
+ ###
18
+ ### Unless required by applicable law or agreed to in writing, software
19
+ ### distributed under the Apache License with the above modification is
20
+ ### distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
21
+ ### KIND, either express or implied. See the Apache License for the specific
22
+ ### language governing permissions and limitations under the Apache License.
23
+ ###
24
+ ###
25
+
26
+ module Chook
27
+
28
+ # a simple object embedded in a Handled Event that
29
+ # allows a standardize way to note event-related log entries
30
+ # with the event object_id.
31
+ #
32
+ # Every Handled Event has one of these instances exposed in it's
33
+ # #logger attribute, and usable from within 'internal' handlers
34
+ #
35
+ # Here's an example.
36
+ #
37
+ # Say you have a ComputerSmartGroupMembershipChanged event
38
+ #
39
+ # calling `event.logger.info "foobar"` will generate the log message:
40
+ #
41
+ # Event 1234567: foobar
42
+ #
43
+ class HandledEventLogger
44
+
45
+ def initialize(event)
46
+ @event = event
47
+ end
48
+
49
+ def event_message(msg)
50
+ "Event #{@event.id}: #{msg}"
51
+ end
52
+
53
+ def debug(msg)
54
+ Chook::Server::Log.logger.debug event_message(msg)
55
+ end
56
+
57
+ def info(msg)
58
+ Chook::Server::Log.logger.info event_message(msg)
59
+ end
60
+
61
+ def warn(msg)
62
+ Chook::Server::Log.logger.warn event_message(msg)
63
+ end
64
+
65
+ def error(msg)
66
+ Chook::Server::Log.logger.error event_message(msg)
67
+ end
68
+
69
+ def fatal(msg)
70
+ Chook::Server::Log.logger.fatal event_message(msg)
71
+ end
72
+
73
+ def unknown(msg)
74
+ Chook::Server::Log.logger.unknown event_message(msg)
75
+ end
76
+
77
+ # log an exception - multiple log lines
78
+ # the first being the error message the rest being indented backtrace
79
+ def log_exception(exception)
80
+ error "#{exception.class}: #{exception}"
81
+ exception.backtrace.each { |l| error "..#{l}" }
82
+ end
83
+
84
+ end # class HandledEventLogger
85
+
86
+ end # module