orchestrator 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
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