orchestrator 1.0.1

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 (58) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.md +158 -0
  3. data/README.md +13 -0
  4. data/Rakefile +7 -0
  5. data/app/controllers/orchestrator/api/dependencies_controller.rb +109 -0
  6. data/app/controllers/orchestrator/api/modules_controller.rb +183 -0
  7. data/app/controllers/orchestrator/api/systems_controller.rb +294 -0
  8. data/app/controllers/orchestrator/api/zones_controller.rb +62 -0
  9. data/app/controllers/orchestrator/api_controller.rb +13 -0
  10. data/app/controllers/orchestrator/base.rb +59 -0
  11. data/app/controllers/orchestrator/persistence_controller.rb +29 -0
  12. data/app/models/orchestrator/access_log.rb +35 -0
  13. data/app/models/orchestrator/control_system.rb +160 -0
  14. data/app/models/orchestrator/dependency.rb +87 -0
  15. data/app/models/orchestrator/mod/by_dependency/map.js +6 -0
  16. data/app/models/orchestrator/mod/by_module_type/map.js +6 -0
  17. data/app/models/orchestrator/module.rb +127 -0
  18. data/app/models/orchestrator/sys/by_modules/map.js +9 -0
  19. data/app/models/orchestrator/sys/by_zones/map.js +9 -0
  20. data/app/models/orchestrator/zone.rb +47 -0
  21. data/app/models/orchestrator/zone/all/map.js +6 -0
  22. data/config/routes.rb +43 -0
  23. data/lib/generators/module/USAGE +8 -0
  24. data/lib/generators/module/module_generator.rb +52 -0
  25. data/lib/orchestrator.rb +52 -0
  26. data/lib/orchestrator/control.rb +303 -0
  27. data/lib/orchestrator/core/mixin.rb +123 -0
  28. data/lib/orchestrator/core/module_manager.rb +258 -0
  29. data/lib/orchestrator/core/request_proxy.rb +109 -0
  30. data/lib/orchestrator/core/requests_proxy.rb +47 -0
  31. data/lib/orchestrator/core/schedule_proxy.rb +49 -0
  32. data/lib/orchestrator/core/system_proxy.rb +153 -0
  33. data/lib/orchestrator/datagram_server.rb +114 -0
  34. data/lib/orchestrator/dependency_manager.rb +131 -0
  35. data/lib/orchestrator/device/command_queue.rb +213 -0
  36. data/lib/orchestrator/device/manager.rb +83 -0
  37. data/lib/orchestrator/device/mixin.rb +35 -0
  38. data/lib/orchestrator/device/processor.rb +441 -0
  39. data/lib/orchestrator/device/transport_makebreak.rb +221 -0
  40. data/lib/orchestrator/device/transport_tcp.rb +139 -0
  41. data/lib/orchestrator/device/transport_udp.rb +89 -0
  42. data/lib/orchestrator/engine.rb +70 -0
  43. data/lib/orchestrator/errors.rb +23 -0
  44. data/lib/orchestrator/logger.rb +115 -0
  45. data/lib/orchestrator/logic/manager.rb +18 -0
  46. data/lib/orchestrator/logic/mixin.rb +11 -0
  47. data/lib/orchestrator/service/manager.rb +63 -0
  48. data/lib/orchestrator/service/mixin.rb +56 -0
  49. data/lib/orchestrator/service/transport_http.rb +55 -0
  50. data/lib/orchestrator/status.rb +229 -0
  51. data/lib/orchestrator/system.rb +108 -0
  52. data/lib/orchestrator/utilities/constants.rb +41 -0
  53. data/lib/orchestrator/utilities/transcoder.rb +57 -0
  54. data/lib/orchestrator/version.rb +3 -0
  55. data/lib/orchestrator/websocket_manager.rb +425 -0
  56. data/orchestrator.gemspec +35 -0
  57. data/spec/orchestrator/queue_spec.rb +200 -0
  58. metadata +271 -0
@@ -0,0 +1,108 @@
1
+ require 'thread'
2
+
3
+
4
+ module Orchestrator
5
+ class System
6
+ @@systems = ThreadSafe::Cache.new
7
+ @@critical = Mutex.new
8
+
9
+ def self.get(id)
10
+ name = id.to_sym
11
+ system = @@systems[name]
12
+ if system.nil?
13
+ system = self.load(name)
14
+ end
15
+ return system
16
+ end
17
+
18
+ def self.expire(id)
19
+ @@systems.delete(id.to_sym)
20
+ end
21
+
22
+ def self.clear_cache
23
+ @@critical.synchronize {
24
+ @@systems = ThreadSafe::Cache.new
25
+ }
26
+ end
27
+
28
+
29
+ attr_reader :zones, :config
30
+
31
+
32
+ def initialize(control_system)
33
+ @config = control_system
34
+ @controller = ::Orchestrator::Control.instance
35
+
36
+ @modules = {}
37
+ @config.modules.each &method(:index_module)
38
+
39
+ # Build an ordered zone cache for setting lookup
40
+ zones = ::Orchestrator::Control.instance.zones
41
+ @zones = []
42
+ @config.zones.each do |zone_id|
43
+ zone = zones[zone_id]
44
+ @zones << zone unless zone.nil?
45
+ end
46
+ end
47
+
48
+ def get(mod, index)
49
+ mods = @modules[mod]
50
+ if mods
51
+ mods[index]
52
+ else
53
+ nil # As subscriptions can be made to modules that don't exist
54
+ end
55
+ end
56
+
57
+ def all(mod)
58
+ @modules[mod] || []
59
+ end
60
+
61
+ def count(name)
62
+ mod = @modules[name.to_sym]
63
+ mod.nil? ? 0 : mod.length
64
+ end
65
+
66
+ def modules
67
+ @modules.keys
68
+ end
69
+
70
+ def settings
71
+ @config.settings
72
+ end
73
+
74
+
75
+ protected
76
+
77
+
78
+ # looks for the system in the database
79
+ def self.load(id)
80
+ @@critical.synchronize {
81
+ system = @@systems[id]
82
+ return system unless system.nil?
83
+
84
+ sys = ControlSystem.find_by_id(id.to_s)
85
+ if sys.nil?
86
+ return nil
87
+ else
88
+ system = System.new(sys)
89
+ @@systems[id] = system
90
+ end
91
+ return system
92
+ }
93
+ end
94
+
95
+ def index_module(mod_id)
96
+ manager = @controller.loaded?(mod_id)
97
+ if manager
98
+ mod_name = if manager.settings.custom_name.nil?
99
+ manager.settings.dependency.module_name.to_sym
100
+ else
101
+ manager.settings.custom_name.to_sym
102
+ end
103
+ @modules[mod_name] ||= []
104
+ @modules[mod_name] << manager
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,41 @@
1
+ require 'set'
2
+
3
+
4
+ module Orchestrator
5
+ module Constants
6
+ On = true # On is active
7
+ Off = false # Off is inactive
8
+ Down = true # Down is usually active (projector screen for instance)
9
+ Up = false # Up is usually inactive
10
+ Open = true
11
+ Close = false
12
+ Short = false
13
+
14
+ On_vars = Set.new([1, true, 'true', 'True',
15
+ :on, :On, 'on', 'On',
16
+ :yes, :Yes, 'yes', 'Yes',
17
+ 'down', 'Down', :down, :Down,
18
+ 'open', 'Open', :open, :Open])
19
+ Off_vars = Set.new([0, false, 'false', 'False',
20
+ :off, :Off, 'off', 'Off',
21
+ :no, :No, 'no', 'No',
22
+ 'up', 'Up', :up, :Up,
23
+ 'close', 'Close', :close, :Close,
24
+ 'short', 'Short', :short, :Short])
25
+
26
+
27
+ def in_range(num, max, min = 0)
28
+ num = min if num < min
29
+ num = max if num > max
30
+ num
31
+ end
32
+
33
+ def is_affirmative?(val)
34
+ On_vars.include?(val)
35
+ end
36
+
37
+ def is_negatory?(val)
38
+ Off_vars.include?(val)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,57 @@
1
+ module Orchestrator
2
+ module Transcoder
3
+ # Converts a hex encoded string into a binary string
4
+ #
5
+ # @param data [String] a hex encoded string
6
+ # @return [String]
7
+ def hex_to_byte(data)
8
+ # Removes invalid characters
9
+ data = data.gsub(/(0x|[^0-9A-Fa-f])*/, "")
10
+
11
+ # Ensure we have an even number of characters
12
+ data.prepend('0') if data.length % 2 > 0
13
+
14
+ # Breaks string into an array of characters
15
+ output = ""
16
+ data.scan(/.{2}/) { |byte| output << byte.hex}
17
+ return output
18
+ end
19
+
20
+ # Converts a binary string into a hex encoded string
21
+ #
22
+ # @param data [String] a binary string
23
+ # @return [String]
24
+ def byte_to_hex(data)
25
+ output = ""
26
+ data.each_byte { |c|
27
+ s = c.to_s(16)
28
+ s.prepend('0') if s.length % 2 > 0
29
+ output << s
30
+ }
31
+ return output
32
+ end
33
+
34
+ # Converts a string into an array of bytes
35
+ #
36
+ # @param data [String] data to be converted to bytes
37
+ # @return [Array]
38
+ def str_to_array(data)
39
+ data.bytes.to_a
40
+ end
41
+
42
+ # Converts a byte array into a binary string
43
+ #
44
+ # @param data [Array] an array of bytes
45
+ # @return [String]
46
+ def array_to_str(data)
47
+ data.pack('c*')
48
+ end
49
+
50
+
51
+ # Makes the functions private when included
52
+ module_function :hex_to_byte
53
+ module_function :byte_to_hex
54
+ module_function :str_to_array
55
+ module_function :array_to_str
56
+ end
57
+ end
@@ -0,0 +1,3 @@
1
+ module Orchestrator
2
+ VERSION = "1.0.1"
3
+ end
@@ -0,0 +1,425 @@
1
+ require 'set'
2
+ require 'json'
3
+
4
+
5
+ module Orchestrator
6
+ class WebsocketManager
7
+ def initialize(ws, user)
8
+ @ws = ws
9
+ @user = user
10
+ @loop = ws.loop
11
+
12
+ @bindings = ::ThreadSafe::Cache.new
13
+ @stattrak = @loop.observer
14
+ @notify_update = method(:notify_update)
15
+
16
+ @logger = ::Orchestrator::Logger.new(@loop, user)
17
+
18
+ @ws.progress method(:on_message)
19
+ @ws.finally method(:on_shutdown)
20
+ #@ws.on_open method(:on_open)
21
+
22
+ @accessed = ::Set.new
23
+ @access_log = ::Orchestrator::AccessLog.new
24
+ @access_log.user_id = @user.id
25
+ end
26
+
27
+
28
+ DECODE_OPTIONS = {
29
+ symbolize_names: true
30
+ }.freeze
31
+
32
+ PARAMS = [:id, :cmd, :sys, :mod, :index, :name, {args: [].freeze}.freeze].freeze
33
+ REQUIRED = [:id, :cmd, :sys, :mod, :name].freeze
34
+ COMMANDS = Set.new([:exec, :bind, :unbind, :debug, :ignore])
35
+
36
+ ERRORS = {
37
+ parse_error: 0,
38
+ bad_request: 1,
39
+ access_denied: 2,
40
+ request_failed: 3,
41
+ unknown_command: 4,
42
+
43
+ system_not_found: 5,
44
+ module_not_found: 6,
45
+ unexpected_failure: 7
46
+ }.freeze
47
+
48
+
49
+ protected
50
+
51
+
52
+ def on_message(data, ws)
53
+ if data == 'ping'
54
+ @ws.text('pong')
55
+ return
56
+ end
57
+
58
+ begin
59
+ raw_parameters = ::JSON.parse(data, DECODE_OPTIONS)
60
+ parameters = ::ActionController::Parameters.new(raw_parameters)
61
+ params = parameters.permit(PARAMS).tap do |whitelist|
62
+ whitelist[:args] = parameters[:args]
63
+ end
64
+ rescue => e
65
+ @logger.print_error(e, 'error parsing websocket request')
66
+ error_response(nil, ERRORS[:parse_error], e.message)
67
+ return
68
+ end
69
+
70
+ if check_requirements(params)
71
+ # Perform the security check in a nonblocking fashion
72
+ # (Database access is probably required)
73
+ result = @loop.work do
74
+ sys = params[:sys]
75
+ params[:sys] = ::Orchestrator::ControlSystem.bucket.get("sysname-#{sys.downcase}", {quiet: true}) || sys
76
+ Rails.configuration.orchestrator.check_access.call(params[:sys], @user)
77
+ end
78
+
79
+ # The result should be an access level if these are implemented
80
+ result.then do |access|
81
+ begin
82
+ cmd = params[:cmd].to_sym
83
+ if COMMANDS.include?(cmd)
84
+ @accessed << params[:sys] # Log the access
85
+ self.__send__(cmd, params) # Execute the request
86
+ else
87
+ @access_log.suspected = true
88
+ @logger.warn("websocket requested unknown command '#{params[:cmd]}'")
89
+ error_response(params[:id], ERRORS[:unknown_command], "unknown command: #{params[:cmd]}")
90
+ end
91
+ rescue => e
92
+ @logger.print_error(e, "websocket request failed: #{data}")
93
+ error_response(params[:id], ERRORS[:request_failed], e.message)
94
+ end
95
+ end
96
+
97
+ # Raise an error if access is not granted
98
+ result.catch do |err|
99
+ @access_log.suspected = true
100
+ @logger.print_error(e, 'security check failed for websocket request')
101
+ error_response(params[:id], ERRORS[:access_denied], e.message)
102
+ end
103
+ else
104
+ # log user information here (possible probing attempt)
105
+ @access_log.suspected = true
106
+ reason = 'required parameters were missing from the request'
107
+ @logger.warn(reason)
108
+ error_response(params[:id], ERRORS[:bad_request], reason)
109
+ end
110
+ end
111
+
112
+ def check_requirements(params)
113
+ REQUIRED.each do |key|
114
+ return false if params[key].nil?
115
+ end
116
+ true
117
+ end
118
+
119
+
120
+ def exec(params)
121
+ id = params[:id]
122
+ sys = params[:sys]
123
+ mod = params[:mod].to_sym
124
+ name = params[:name].to_sym
125
+ index_s = params[:index] || 1
126
+ index = index_s.to_i
127
+
128
+ args = params[:args] || []
129
+
130
+ @loop.work do
131
+ do_exec(id, sys, mod, index, name, args)
132
+ end
133
+ end
134
+
135
+ def do_exec(id, sys, mod, index, name, args)
136
+ system = ::Orchestrator::System.get(sys)
137
+
138
+ if system
139
+ mod_man = system.get(mod, index - 1)
140
+ if mod_man
141
+ req = Core::RequestProxy.new(@loop, mod_man)
142
+ result = req.send(name, *args)
143
+ result.then(proc { |res|
144
+ output = nil
145
+ begin
146
+ ::JSON.generate([res])
147
+ output = res
148
+ rescue => e
149
+ # respond with nil if object cannot be converted
150
+ # TODO:: need a better way of dealing with this
151
+ # ALSO in systems controller
152
+ end
153
+ @ws.text(::JSON.generate({
154
+ id: id,
155
+ type: :success,
156
+ value: output
157
+ }))
158
+ }, proc { |err|
159
+ # Request proxy will log the error
160
+ error_response(id, ERRORS[:request_failed], err.message)
161
+ })
162
+ else
163
+ @logger.debug("websocket exec could not find module: {sys: #{sys}, mod: #{mod}, index: #{index}, name: #{name}}")
164
+ error_response(id, ERRORS[:module_not_found], "could not find module: #{mod}")
165
+ end
166
+ else
167
+ @logger.debug("websocket exec could not find system: {sys: #{sys}, mod: #{mod}, index: #{index}, name: #{name}}")
168
+ error_response(id, ERRORS[:system_not_found], "could not find system: #{sys}")
169
+ end
170
+ end
171
+
172
+
173
+ def unbind(params)
174
+ id = params[:id]
175
+ sys = params[:sys]
176
+ mod = params[:mod]
177
+ name = params[:name]
178
+ index_s = params[:index] || 1
179
+ index = index_s.to_i
180
+
181
+ lookup = :"#{sys}_#{mod}_#{index}_#{name}"
182
+ binding = @bindings.delete(lookup)
183
+ do_unbind(binding) if binding
184
+
185
+ @ws.text(::JSON.generate({
186
+ id: id,
187
+ type: :success
188
+ }))
189
+ end
190
+
191
+ def do_unbind(binding)
192
+ @stattrak.unsubscribe(binding)
193
+ end
194
+
195
+
196
+ def bind(params)
197
+ id = params[:id]
198
+ sys = params[:sys]
199
+ mod = params[:mod].to_sym
200
+ name = params[:name].to_sym
201
+ index_s = params[:index] || 1
202
+ index = index_s.to_i
203
+
204
+ # perform binding on the thread pool
205
+ @loop.work(proc {
206
+ check_binding(id, sys, mod, index, name)
207
+ }).catch do |err|
208
+ @logger.print_error(err, "websocket request failed: #{params}")
209
+ error_response(id, ERRORS[:unexpected_failure], err.message)
210
+ end
211
+ end
212
+
213
+ # Called from a worker thread
214
+ def check_binding(id, sys, mod, index, name)
215
+ system = ::Orchestrator::System.get(sys)
216
+
217
+ if system
218
+ lookup = :"#{sys}_#{mod}_#{index}_#{name}"
219
+ binding = @bindings[lookup]
220
+
221
+ if binding.nil?
222
+ try_bind(id, sys, system, mod, index, name, lookup)
223
+ else
224
+ # binding already made - return success
225
+ @ws.text(::JSON.generate({
226
+ id: id,
227
+ type: :success,
228
+ meta: {
229
+ sys: sys,
230
+ mod: mod,
231
+ index: index,
232
+ name: name
233
+ }
234
+ }))
235
+ end
236
+ else
237
+ @logger.debug("websocket binding could not find system: {sys: #{sys}, mod: #{mod}, index: #{index}, name: #{name}}")
238
+ error_response(id, ERRORS[:system_not_found], "could not find system: #{sys}")
239
+ end
240
+ end
241
+
242
+ def try_bind(id, sys, system, mod_name, index, name, lookup)
243
+ options = {
244
+ sys_id: sys,
245
+ sys_name: system.config.name,
246
+ mod_name: mod_name,
247
+ index: index,
248
+ status: name,
249
+ callback: @notify_update,
250
+ on_thread: @loop
251
+ }
252
+
253
+ # if the module exists, subscribe on the correct thread
254
+ # use a bit of promise magic as required
255
+ mod_man = system.get(mod_name, index - 1)
256
+ defer = @loop.defer
257
+
258
+ # Ensure browser sees this before the first status update
259
+ # At this point subscription will be successful
260
+ @bindings[lookup] = defer.promise
261
+ @ws.text(::JSON.generate({
262
+ id: id,
263
+ type: :success,
264
+ meta: {
265
+ sys: sys,
266
+ mod: mod_name,
267
+ index: index,
268
+ name: name
269
+ }
270
+ }))
271
+
272
+ if mod_man
273
+ options[:mod_id] = mod_man.settings.id.to_sym
274
+ options[:mod] = mod_man
275
+ thread = mod_man.thread
276
+ thread.schedule do
277
+ defer.resolve (
278
+ thread.observer.subscribe(options)
279
+ )
280
+ end
281
+ else
282
+ @loop.schedule do
283
+ defer.resolve @stattrak.subscribe(options)
284
+ end
285
+ end
286
+ end
287
+
288
+ def notify_update(update)
289
+ output = nil
290
+ begin
291
+ ::JSON.generate([update.value])
292
+ output = update.value
293
+ rescue => e
294
+ # respond with nil if object cannot be converted
295
+ # TODO:: need a better way of dealing with this
296
+ end
297
+ @ws.text(::JSON.generate({
298
+ type: :notify,
299
+ value: output,
300
+ meta: {
301
+ sys: update.sys_id,
302
+ mod: update.mod_name,
303
+ index: update.index,
304
+ name: update.status
305
+ }
306
+ }))
307
+ end
308
+
309
+
310
+ def debug(params)
311
+ id = params[:id]
312
+ sys = params[:sys]
313
+ mod_s = params[:mod]
314
+ mod = mod_s.to_sym if mod_s
315
+
316
+ if @debug.nil?
317
+ @debug = @loop.defer
318
+ @inspecting = Set.new # modules
319
+ @debug.promise.progress method(:debug_update)
320
+ end
321
+
322
+ # Set mod to get module level errors
323
+ if mod && !@inspecting.include?(mod)
324
+ mod_man = ::Orchestrator::Control.instance.loaded?(mod)
325
+ if mod_man
326
+ log = mod_man.logger
327
+ log.add @debug
328
+ log.level = :debug
329
+ @inspecting.add mod
330
+
331
+ # Set sys to get errors occurring outside of the modules
332
+ if !@inspecting.include?(:self)
333
+ @logger.add @debug
334
+ @logger.level = :debug
335
+ @inspecting.add :self
336
+ end
337
+
338
+ @ws.text(::JSON.generate({
339
+ id: id,
340
+ type: :success
341
+ }))
342
+ else
343
+ @logger.info("websocket debug could not find module: #{mod}")
344
+ error_response(id, ERRORS[:module_not_found], "could not find module: #{mod}")
345
+ end
346
+ else
347
+ @ws.text(::JSON.generate({
348
+ id: id,
349
+ type: :success
350
+ }))
351
+ end
352
+ end
353
+
354
+ def debug_update(klass, id, level, msg)
355
+ @ws.text(::JSON.generate({
356
+ type: :debug,
357
+ mod: id,
358
+ klass: klass,
359
+ level: level,
360
+ msg: msg
361
+ }))
362
+ end
363
+
364
+
365
+ def ignore(params)
366
+ id = params[:id]
367
+ sys = params[:sys]
368
+ mod_s = params[:mod]
369
+ mod = mod_s.to_sym if mod_s
370
+
371
+ if @debug.nil?
372
+ @debug = @loop.defer
373
+ @inspecting = Set.new # modules
374
+ @debug.promise.progress method(:debug_update)
375
+ end
376
+
377
+ # Remove module level errors
378
+ if mod && @inspecting.include?(mod)
379
+ mod_man = ::Orchestrator::Control.instance.loaded?(mod)
380
+ if mod_man
381
+ mod_man.logger.delete @debug
382
+ @inspecting.delete mod
383
+
384
+ # Stop logging all together if no more modules being watched
385
+ if @inspecting.empty?
386
+ @logger.delete @debug
387
+ @inspecting.delete :self
388
+ end
389
+
390
+ @ws.text(::JSON.generate({
391
+ id: id,
392
+ type: :success
393
+ }))
394
+ else
395
+ @logger.info("websocket ignore could not find module: #{mod}")
396
+ error_response(id, ERRORS[:module_not_found], "could not find module: #{mod}")
397
+ end
398
+ else
399
+ @ws.text(::JSON.generate({
400
+ id: id,
401
+ type: :success
402
+ }))
403
+ end
404
+ end
405
+
406
+
407
+ def error_response(id, code, message)
408
+ @ws.text(::JSON.generate({
409
+ id: id,
410
+ type: :error,
411
+ code: code,
412
+ msg: message
413
+ }))
414
+ end
415
+
416
+ def on_shutdown
417
+ @bindings.each_value &method(:do_unbind)
418
+ @bindings = nil
419
+ @debug.resolve(true) if @debug # detach debug listeners
420
+
421
+ @access_log.systems = @accessed.to_a
422
+ @access_log.save
423
+ end
424
+ end
425
+ end