chook 1.0.1.b2 → 1.1.0

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 (38) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGES.md +21 -0
  3. data/README.md +243 -36
  4. data/bin/chook-server +29 -1
  5. data/data/chook.conf.example +104 -0
  6. data/data/sample_handlers/RestAPIOperation.rb +12 -8
  7. data/data/sample_handlers/SmartGroupComputerMembershipChange.rb +3 -6
  8. data/data/sample_jsons/SmartGroupComputerMembershipChange.json +3 -1
  9. data/data/sample_jsons/SmartGroupMobileDeviceMembershipChange.json +3 -1
  10. data/lib/chook/configuration.rb +20 -8
  11. data/lib/chook/event/handled_event.rb +15 -12
  12. data/lib/chook/event/handled_event/handlers.rb +136 -83
  13. data/lib/chook/event/handled_event_logger.rb +86 -0
  14. data/lib/chook/event_handling.rb +1 -0
  15. data/lib/chook/foundation.rb +2 -0
  16. data/lib/chook/procs.rb +17 -1
  17. data/lib/chook/server.rb +71 -74
  18. data/lib/chook/server/log.rb +215 -0
  19. data/lib/chook/server/public/css/chook.css +125 -0
  20. data/lib/chook/server/public/imgs/ChookLogoAlMcWhiggin.png +0 -0
  21. data/lib/chook/server/public/js/chook.js +127 -0
  22. data/lib/chook/server/public/js/logstream.js +101 -0
  23. data/lib/chook/server/routes.rb +45 -0
  24. data/lib/chook/server/routes/handle_webhook_event.rb +22 -3
  25. data/lib/chook/server/routes/handlers.rb +52 -0
  26. data/lib/chook/server/routes/home.rb +34 -1
  27. data/lib/chook/server/routes/log.rb +106 -0
  28. data/lib/chook/server/views/admin.haml +11 -0
  29. data/lib/chook/server/views/bak.haml +48 -0
  30. data/lib/chook/server/views/config.haml +15 -0
  31. data/lib/chook/server/views/handlers.haml +48 -0
  32. data/lib/chook/server/views/layout.haml +39 -0
  33. data/lib/chook/server/views/logstream.haml +32 -0
  34. data/lib/chook/server/views/sketch_admin +44 -0
  35. data/lib/chook/subject.rb +1 -2
  36. data/lib/chook/subject/smart_group.rb +6 -0
  37. data/lib/chook/version.rb +1 -1
  38. metadata +73 -18
@@ -23,23 +23,27 @@
23
23
  ###
24
24
  ###
25
25
 
26
- Chook.event_handler do |event|
27
- eobject = event.event_object
28
- whook = event.webhook
26
+ # this module is just a namespace so we don't conflict with other handlers
27
+ module APIOpHandler
29
28
 
30
29
  REPORT_ACTIONS = {
31
30
  'PUT' => 'update',
32
31
  'POST' => 'create',
33
32
  'DELETE' => 'delete'
34
- }.freeze unless defined? REPORT_ACTIONS
33
+ }.freeze
34
+
35
+ end
36
+
37
+ Chook.event_handler do |event|
38
+ event.logger.debug "This is a handler-level debug message for event #{event.object_id}"
35
39
 
36
- action = REPORT_ACTIONS[eobject.restAPIOperationType]
40
+ action = APIOpHandler::REPORT_ACTIONS[event.subject.restAPIOperationType]
37
41
 
38
42
  return nil unless action
39
43
 
40
44
  puts <<-ENDMSG
41
- The JSS WebHook named '#{whook.name}' was just triggered.
42
- It indicates that Casper user '#{eobject.authorizedUsername}' just used the JSS API to #{action}
43
- the JSS #{eobject.objectTypeName} named '#{eobject.objectName}' (id #{eobject.objectID})
45
+ The JSS WebHook named '#{event.webhook_name}' was just triggered.
46
+ It indicates that Casper user '#{event.subject.authorizedUsername}' just used the JSS API to #{action}
47
+ the JSS #{event.subject.objectTypeName} named '#{event.subject.objectName}' (id #{event.subject.objectID})
44
48
  ENDMSG
45
49
  end
@@ -23,11 +23,8 @@
23
23
  ###
24
24
  ###
25
25
 
26
- HANDLER_NAME = File.basename __FILE__
27
- CHANGELOG = '/tmp/smart-computer-group-changes.log'.freeze
28
-
29
26
  Chook.event_handler do |event|
30
- now = Time.now.strftime '%Y-%m-%d %H:%M:%S'
31
- msg = "#{now} #{HANDLER_NAME}: Smart Computer Group '#{event.event_object.name}' changed.\n"
32
- open(CHANGELOG, 'a') { |file| file.write msg }
27
+ event.logger.debug "Computer Smart Group Changed: #{event.subject.name}"
28
+ event.logger.debug " Additions: #{event.subject.groupAddedDevicesIds.join ', '}"
29
+ event.logger.debug " Removals: #{event.subject.groupRemovedDevicesIds.join ', '}"
33
30
  end
@@ -8,6 +8,8 @@
8
8
  "name": "OddComputers",
9
9
  "smartGroup": true,
10
10
  "jssid": 95,
11
- "computer": true
11
+ "computer": true,
12
+ "groupAddedDevicesIds": [1,2,3,4,5],
13
+ "groupRemovedDevicesIds": [6,7,8,9,10]
12
14
  }
13
15
  }
@@ -8,6 +8,8 @@
8
8
  "name": "OddMobileDevs",
9
9
  "smartGroup": true,
10
10
  "jssid": 99,
11
- "computer": false
11
+ "computer": false,
12
+ "groupAddedDevicesIds": [1,2,3,4,5],
13
+ "groupRemovedDevicesIds": [6,7,8,9,10]
12
14
  }
13
15
  }
@@ -35,16 +35,26 @@ module Chook
35
35
  # The location of the default config file
36
36
  DEFAULT_CONF_FILE = Pathname.new '/etc/chook.conf'
37
37
 
38
- # The attribute keys we maintain, and the type they should be stored as
38
+ SAMPLE_CONF_FILE = Pathname.new(__FILE__).parent.parent.parent + 'data/chook.conf.example'
39
+
40
+ # The attribute keys we maintain, and how they should be converted to
41
+ # the value used by chook internally.
42
+ #
43
+ # For descriptions of the keys, see data/chook.conf.example
44
+ #
39
45
  CONF_KEYS = {
40
- server_port: :to_i,
41
- server_engine: :to_sym,
42
- handler_dir: nil,
46
+ port: :to_i,
47
+ concurrency: Chook::Procs::STRING_TO_BOOLEAN,
48
+ handler_dir: Chook::Procs::STRING_TO_PATHNAME,
43
49
  use_ssl: Chook::Procs::STRING_TO_BOOLEAN,
44
- ssl_private_key_path: Chook::Procs::STRING_TO_PATHNAME,
45
- ssl_private_key_pw_path: nil,
46
50
  ssl_cert_path: Chook::Procs::STRING_TO_PATHNAME,
47
- ssl_cert_name: nil
51
+ ssl_private_key_path: Chook::Procs::STRING_TO_PATHNAME,
52
+ log_file: Chook::Procs::STRING_TO_PATHNAME,
53
+ log_level: Chook::Procs::STRING_TO_LOG_LEVEL,
54
+ log_max_megs: :to_i,
55
+ logs_to_keep: :to_i,
56
+ webhooks_user: nil,
57
+ webhooks_user_pw: nil
48
58
  }.freeze
49
59
 
50
60
  # Class Variables
@@ -193,6 +203,8 @@ module Chook
193
203
  end # class Configuration
194
204
 
195
205
  # The single instance of Configuration
196
- CONFIG = Chook::Configuration.instance
206
+ def self.config
207
+ Chook::Configuration.instance
208
+ end
197
209
 
198
210
  end # module
@@ -25,7 +25,6 @@
25
25
 
26
26
  require 'chook/event/handled_event/handlers'
27
27
 
28
- #
29
28
  module Chook
30
29
 
31
30
  # Load sample JSON files, one per event type
@@ -62,9 +61,6 @@ module Chook
62
61
 
63
62
  #### Class Methods
64
63
 
65
- # Given some raw_json from the jss, create and return the correct
66
- # HandledEvent subclass
67
-
68
64
  # For each event type in Chook::Event::EVENTS
69
65
  # generate a class for it, set its SUBJECT_CLASS constant
70
66
  # and add it to the HandledEvents module.
@@ -99,6 +95,7 @@ module Chook
99
95
  # @return [JSSWebHooks::Event subclass] the Event subclass matching the event
100
96
  #
101
97
  def self.parse_event(raw_event_json)
98
+ return nil if raw_event_json.to_s.empty?
102
99
  event_json = JSON.parse(raw_event_json, symbolize_names: true)
103
100
  event_name = event_json[:webhook][:webhookEvent]
104
101
  Chook::HandledEvents.const_get(event_name).new raw_event_json
@@ -122,12 +119,15 @@ module Chook
122
119
  end # init
123
120
 
124
121
  def handle
125
- handlers = Handlers.handlers[self.class.const_get(Chook::Event::EVENT_NAME_CONST)]
122
+ handler_key = self.class.const_get(Chook::Event::EVENT_NAME_CONST)
123
+ handlers = Handlers.handlers[handler_key]
124
+ return 'No handlers loaded' unless handlers.is_a? Array
125
+
126
126
  handlers.each do |handler|
127
127
  case handler
128
128
  when Pathname
129
129
  pipe_to_executable handler
130
- when Proc
130
+ when Object
131
131
  handle_with_proc handler
132
132
  end # case
133
133
  end # @handlers.each do |handler|
@@ -135,18 +135,21 @@ module Chook
135
135
  # the handle method should return a string,
136
136
  # which is the body of the HTTP result for
137
137
  # POSTing the event
138
- "Processed by #{handlers.count} handlers\n"
138
+ "Processed by #{handlers.count} handlers"
139
139
  end # def handle
140
140
 
141
- # TODO: Add something here that cleans up old threads and forks
142
141
  def pipe_to_executable(handler)
143
- _thread = Thread.new do
144
- IO.popen([handler.to_s], 'w') { |h| h.puts @raw_json }
145
- end
142
+ logger.debug "Sending JSON to stdin of '#{handler}'"
143
+ IO.popen([handler.to_s], 'w') { |h| h.puts @raw_json }
146
144
  end
147
145
 
148
146
  def handle_with_proc(handler)
149
- _thread = Thread.new { handler.call self }
147
+ logger.debug "Running Handler defined in #{handler.handler_file}"
148
+ handler.handle self
149
+ end
150
+
151
+ def logger
152
+ @logger ||= Chook::HandledEventLogger.new self
150
153
  end
151
154
 
152
155
  end # class HandledEvent
@@ -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,53 +68,42 @@ 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
- ############################
63
-
64
- # This holds the most recently loaded Proc handler
65
- # until it can be stored in the @handlers Hash
66
- @loaded_handler = nil
73
+ # internal handler files must match this regex somewhere
74
+ INTERNAL_HANDLER_BLOCK_START_RE = /Chook.event_handler( ?\{| do) *\|/
67
75
 
68
- # Getter for @loaded_handler
76
+ # self loaded_handler=
69
77
  #
70
- # @return [Proc,nil] the most recent Proc loaded from a handler file.
78
+ # @return [Obj,nil] the most recent Proc loaded from a handler file.
71
79
  # destined for storage in @handlers
72
80
  #
73
81
  def self.loaded_handler
74
82
  @loaded_handler
75
83
  end
76
84
 
77
- # Setter for @loaded_event_handler
85
+ # A holding place for internal handlers as they are loaded
86
+ # before being added to the @handlers Hash
87
+ # see Chook.event_handler(&block)
78
88
  #
79
- # @param a_proc [Proc] a Proc object for storage in @handlers
89
+ # @param a_proc [Object] An object instance with a #handle method
80
90
  #
81
- def self.loaded_handler=(a_proc)
82
- @loaded_handler = a_proc
91
+ def self.loaded_handler=(anon_obj)
92
+ @loaded_handler = anon_obj
83
93
  end
84
94
 
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
95
+ # Getter for @handlers
96
+ #
97
+ # @return [Hash{String => Array}] a mapping of Event Names as they
98
+ # come from the JSS to an Array of handlers for the event.
99
+ # The handlers are either Pathnames to executable external handlers
100
+ # or Objcts with a #handle method, for internal handlers
101
+ # (The objects also have a #handler_file attribute that is the Pathname)
92
102
  #
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
103
  def self.handlers
98
104
  @handlers
99
105
  end
100
-
101
- # Module Methods
102
- ############################
106
+ @handlers ||= {}
103
107
 
104
108
  # Load all the event handlers from the handler_dir or an arbitrary dir.
105
109
  #
@@ -112,23 +116,31 @@ module Chook
112
116
  #
113
117
  # @return [void]
114
118
  #
115
- def self.load_handlers(from_dir: Chook::CONFIG.handler_dir, reload: false)
119
+ def self.load_handlers(from_dir: Chook.config.handler_dir, reload: false)
120
+ # use default if needed
116
121
  from_dir ||= DEFAULT_HANDLER_DIR
122
+ handler_dir = Pathname.new(from_dir)
123
+ load_type = 'Loading'
124
+
117
125
  if reload
118
- @handlers_loaded_from = nil
119
126
  @handlers = {}
120
127
  @loaded_handler = nil
128
+ load_type = 'Re-loading'
121
129
  end
122
130
 
123
- handler_dir = Pathname.new(from_dir)
124
- return unless handler_dir.directory? && handler_dir.readable?
131
+ Chook.logger.info "#{load_type} handlers from directory: #{handler_dir}"
132
+
133
+ unless handler_dir.directory? && handler_dir.readable?
134
+ Chook.logger.error "Handler directory '#{from_dir}' not a readable directory. No handlers loaded. "
135
+ return
136
+ end
125
137
 
126
138
  handler_dir.children.each do |handler_file|
127
139
  load_handler(handler_file) if handler_file.file? && handler_file.readable?
128
140
  end
129
141
 
130
- @handlers_loaded_from = handler_dir
131
- @handlers.values.flatten.size
142
+ Chook.logger.info "Loaded #{@handlers.values.flatten.size} handlers for #{@handlers.keys.size} event triggers"
143
+ @loaded_handler = nil
132
144
  end # load handlers
133
145
 
134
146
  # Load an event handler from a file.
@@ -158,33 +170,67 @@ module Chook
158
170
  # @return [void]
159
171
  #
160
172
  def self.load_handler(from_file)
173
+ Chook.logger.debug "Starting load of handler file '#{from_file.basename}'"
161
174
  handler_file = Pathname.new from_file
162
175
  event_name = event_name_from_handler_filename(handler_file)
163
- return unless event_name
176
+ unless event_name
177
+ Chook.logger.debug "Ignoring file '#{from_file.basename}'"
178
+ return
179
+ end
164
180
 
165
181
  # create an array for this event's handlers, if needed
166
182
  @handlers[event_name] ||= []
167
183
 
168
- 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
184
+ return if load_external_handler(handler_file, event_name)
185
+
186
+ load_internal_handler(handler_file, event_name)
187
+ end # self.load_handler(handler_file)
188
+
189
+ # if the given file is executable, store it's path as a handler for the event
190
+ #
191
+ #
192
+ def self.load_external_handler(handler_file, event_name)
193
+ return false unless handler_file.executable?
194
+
195
+ Chook.logger.info "Loading external handler file '#{handler_file.basename}' for #{event_name} events"
196
+
197
+ # store the Pathname, we'll pipe JSON to it
198
+ @handlers[event_name] << handler_file
199
+ true
200
+ end
201
+
202
+ # if a given path is not executable, try to load it as an internal handler
203
+ #
204
+ #
205
+ def self.load_internal_handler(handler_file, event_name)
206
+ # load the file. If written correctly, it will
207
+ # put an anon. Object with a #handle method into @loaded_handler
208
+ Chook.logger.info "Loading internal handler file '#{handler_file.basename}' for #{event_name} events"
209
+
210
+ unless handler_file.read =~ INTERNAL_HANDLER_BLOCK_START_RE
211
+ Chook.logger.error "Internal handler file '#{handler_file.basename}' missing event_handler block"
174
212
  return
175
213
  end
176
214
 
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
184
- else
185
- puts "===> FAILED loading internal handler file '#{handler_file.basename}'"
215
+ # reset @loaded_handler - the `load` call will refill it
216
+ # see Chook.event_handler
217
+ @loaded_handler = nil
218
+ begin
219
+ load handler_file.to_s
220
+ raise '@loaded handler nil after loading file' unless @loaded_handler
221
+ rescue => e
222
+ Chook.logger.error "FAILED loading internal handler file '#{handler_file.basename}': #{e}"
223
+ return
186
224
  end
187
- end # self.load_handler(handler_file)
225
+
226
+ # add a method to the object to get its filename
227
+ @loaded_handler.define_singleton_method(:handler_file) { handler_file.basename.to_s }
228
+
229
+ @handlers[event_name] << @loaded_handler
230
+
231
+ Chook.logger.debug "Loaded internal handler file '#{handler_file.basename}'"
232
+ @loaded_handler = nil
233
+ end
188
234
 
189
235
  # Given a handler filename, return the event name it wants to handle
190
236
  #
@@ -194,9 +240,16 @@ module Chook
194
240
  # @return [String,nil] The matching event name or nil if no match
195
241
  #
196
242
  def self.event_name_from_handler_filename(filename)
243
+ filename = filename.basename
197
244
  @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
245
+ desired_event_name = filename.to_s.split(/\.|-|_/).first
246
+ ename = @event_names.select { |n| desired_event_name.casecmp(n).zero? }.first
247
+ if ename
248
+ Chook.logger.debug "Found event name '#{ename}' at start of filename '#{filename}'"
249
+ else
250
+ Chook.logger.debug "No known event name at start of filename '#{filename}'"
251
+ end
252
+ ename
200
253
  end
201
254
 
202
255
  end # module Handler