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,70 @@
1
+ require 'set'
2
+
3
+
4
+ module Orchestrator
5
+ class Engine < ::Rails::Engine
6
+ isolate_namespace Orchestrator
7
+
8
+
9
+ # NOTE:: if we ever have any tasks
10
+ #rake_tasks do
11
+ # load "tasks/orchestrator_tasks.rake"
12
+ #end
13
+
14
+ #
15
+ # Define the application configuration
16
+ #
17
+ config.before_initialize do |app| # Rails.configuration
18
+ app.config.orchestrator = ActiveSupport::OrderedOptions.new
19
+ app.config.orchestrator.module_paths = []
20
+
21
+ # Clearance levels defined in code
22
+ #app.config.orchestrator.clearance_levels = Set.new([:Admin, :Support, :User, :Public])
23
+
24
+ # Access checking callback - used at the system level
25
+ # Will always be passed a system id and the user attempting to access
26
+ app.config.orchestrator.check_access = proc { |system, user|
27
+ true
28
+ }
29
+
30
+ # if not zero all UDP sockets must be transmitted from a single thread
31
+ app.config.orchestrator.datagram_port = 0 # ephemeral port (random selection)
32
+ app.config.orchestrator.broadcast_port = 0 # ephemeral port (random selection)
33
+ end
34
+
35
+ #
36
+ # Discover the possible module location paths after initialization is complete
37
+ #
38
+ config.after_initialize do |app|
39
+
40
+ ActiveSupport::Dependencies.autoload_paths.each do |path|
41
+ Pathname.new(path).ascend do |v|
42
+ if ['app', 'vendor'].include?(v.basename.to_s)
43
+ app.config.orchestrator.module_paths << "#{v.to_s}/modules"
44
+ break
45
+ end
46
+ end
47
+ end
48
+
49
+ app.config.orchestrator.module_paths.uniq!
50
+
51
+ # Force design documents
52
+ temp = ::Couchbase::Model::Configuration.design_documents_paths
53
+ ::Couchbase::Model::Configuration.design_documents_paths = [File.expand_path(File.join(File.expand_path("../", __FILE__), '../../app/models/orchestrator'))]
54
+ ::Orchestrator::ControlSystem.ensure_design_document!
55
+ ::Orchestrator::Module.ensure_design_document!
56
+ ::Orchestrator::Zone.ensure_design_document!
57
+ ::Couchbase::Model::Configuration.design_documents_paths = temp
58
+
59
+ # Start the control system by initializing it
60
+ ctrl = ::Orchestrator::Control.instance
61
+
62
+ # Don't auto-load if running in the console or as a rake task
63
+ unless ENV['ORC_NO_BOOT'] || defined?(Rails::Console) || Rails.env.test? || defined?(Rake)
64
+ ctrl.loop.next_tick do
65
+ ctrl.mount.then ctrl.method(:boot)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,23 @@
1
+ module Orchestrator
2
+ class Error < StandardError
3
+
4
+ # Called from:
5
+ # * request_proxy
6
+ # * requests_proxy
7
+ class ProtectedMethod < Error; end
8
+ class ModuleUnavailable < Error; end
9
+
10
+ # Called from:
11
+ # * dependency_manager
12
+ class FileNotFound < Error; end
13
+
14
+ # Called from:
15
+ # * control
16
+ class ModuleNotFound < Error; end
17
+
18
+ # Called from:
19
+ # * Device -> Processor
20
+ class CommandFailure < Error; end
21
+ class CommandCanceled < Error; end
22
+ end
23
+ end
@@ -0,0 +1,115 @@
1
+ require 'set'
2
+
3
+
4
+ # Note:: We should be logging the User id in the log
5
+ # see: http://pastebin.com/Wrjp8b8e (rails log_tags)
6
+ module Orchestrator
7
+ class Logger
8
+ LEVEL = {
9
+ debug: 0,
10
+ info: 1,
11
+ warn: 2,
12
+ error: 3,
13
+ fatal: 4
14
+ }.freeze
15
+
16
+ # TODO:: Make this a config item
17
+ DEFAULT_LEVEL = 1
18
+
19
+ def initialize(loop, mod)
20
+ @loop = loop
21
+ @mod_id = mod.id
22
+ if mod.respond_to? :dependency
23
+ @klass = mod.dependency.class_name
24
+ else
25
+ @klass = 'User' # Filter by user driven events and behavior
26
+ end
27
+ @level = DEFAULT_LEVEL
28
+ @listeners = Set.new
29
+ @logger = ::Orchestrator::Control.instance.logger
30
+ end
31
+
32
+ def level=(level)
33
+ @level = LEVEL[level] || level
34
+ end
35
+ attr_reader :level
36
+
37
+ # Add listener
38
+ def add(listener)
39
+ @loop.schedule do
40
+ @listeners.add listener
41
+ end
42
+ listener.promise.finally do
43
+ @loop.schedule do
44
+ @listeners.delete listener
45
+ end
46
+ end
47
+ listener
48
+ end
49
+
50
+ def delete(listener)
51
+ @loop.schedule do
52
+ @listeners.delete listener
53
+ if @listeners.size == 0
54
+ level = DEFAULT_LEVEL # back to efficient logging
55
+ end
56
+ end
57
+ end
58
+
59
+
60
+ def debug(msg)
61
+ if @level <= 0
62
+ log(:debug, msg)
63
+ end
64
+ end
65
+
66
+ def info(msg)
67
+ if @level <= 1
68
+ log(:info, msg)
69
+ end
70
+ end
71
+
72
+ def warn(msg)
73
+ if @level <= 2
74
+ log(:warn, msg)
75
+ end
76
+ end
77
+
78
+ def error(msg)
79
+ if @level <= 3
80
+ log(:error, msg)
81
+ end
82
+ end
83
+
84
+ def fatal(msg)
85
+ if @level <= 4
86
+ log(:fatal, msg)
87
+ end
88
+ end
89
+
90
+ def print_error(e, msg = '')
91
+ msg << "\n#{e.message}"
92
+ msg << "\n#{e.backtrace.join("\n")}" if e.respond_to?(:backtrace) && e.backtrace
93
+ error(msg)
94
+ end
95
+
96
+
97
+ protected
98
+
99
+
100
+ def log(level, msg)
101
+ @loop.schedule do
102
+ if LEVEL[level] >= DEFAULT_LEVEL
103
+ @loop.work do
104
+ @logger.tagged(@klass, @mod_id) {
105
+ @logger.send(level, msg)
106
+ }
107
+ end
108
+ end
109
+ @listeners.each do |listener|
110
+ listener.notify(@klass, @mod_id, level, msg)
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,18 @@
1
+ module Orchestrator
2
+ module Logic
3
+ class Manager < ::Orchestrator::Core::ModuleManager
4
+ def initialize(*args)
5
+ super(*args)
6
+
7
+ # Do we want to start here?
8
+ # Should be ok.
9
+ @thread.next_tick method(:start)
10
+ end
11
+
12
+ # Access to other modules in the same control system
13
+ def system
14
+ @system ||= ::Orchestrator::Core::SystemProxy.new(@thread, @settings.control_system_id)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,11 @@
1
+ module Orchestrator
2
+ module Logic
3
+ module Mixin
4
+ include ::Orchestrator::Core::Mixin
5
+
6
+ def system
7
+ @__config__.system
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,63 @@
1
+ module Orchestrator
2
+ module Service
3
+ class Manager < ::Orchestrator::Core::ModuleManager
4
+ def initialize(*args)
5
+ super(*args)
6
+
7
+ # Do we want to start here?
8
+ # Should be ok.
9
+ @thread.next_tick method(:start)
10
+ end
11
+
12
+ attr_reader :processor, :connection
13
+
14
+ def start
15
+ @processor = Orchestrator::Device::Processor.new(self)
16
+
17
+ super # Calls on load (allows setting of tls certs)
18
+
19
+ @connection = TransportHttp.new(self, @processor)
20
+ @processor.transport = @connection
21
+ end
22
+
23
+ def stop
24
+ super
25
+ @processor.terminate
26
+ @processor = nil
27
+ @connection.terminate
28
+ @connection = nil
29
+ end
30
+
31
+ # NOTE:: Same as Device::Manager:-------
32
+ # TODO:: Need to have a guess about when a device may be off line
33
+
34
+ def notify_connected
35
+ if @instance.respond_to? :connected, true
36
+ begin
37
+ @instance.__send__(:connected)
38
+ rescue => e
39
+ @logger.print_error(e, 'error in module connected callback')
40
+ end
41
+ end
42
+
43
+ update_connected_status(true)
44
+ end
45
+
46
+ def notify_received(data, resolve, command = nil)
47
+ begin
48
+ blk = command.nil? ? nil : command[:on_receive]
49
+ if blk.respond_to? :call
50
+ blk.call(data, resolve, command)
51
+ elsif @instance.respond_to? :received, true
52
+ @instance.__send__(:received, data, resolve, command)
53
+ else
54
+ @logger.warn('no received function provided')
55
+ :abort
56
+ end
57
+ rescue => e
58
+ @logger.print_error(e, 'error in received callback')
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,56 @@
1
+ module Orchestrator
2
+ module Service
3
+ module Mixin
4
+ include ::Orchestrator::Core::Mixin
5
+
6
+ def request(method, path, options = {}, &blk)
7
+ defer = @__config__.thread.defer
8
+ options[:method] = method
9
+ options[:path] = path
10
+ options[:defer] = defer
11
+ options[:max_waits] = 0 # HTTP will only ever respond to a request
12
+ options[:on_receive] = blk if blk # on command success
13
+ @__config__.thread.schedule do
14
+ @__config__.processor.queue_command(options)
15
+ end
16
+ defer.promise
17
+ end
18
+
19
+ def get(path, options = {}, &blk)
20
+ request(:get, path, options, &blk)
21
+ end
22
+
23
+ def post(path, options = {}, &blk)
24
+ request(:post, path, options, &blk)
25
+ end
26
+
27
+ def put(path, options = {}, &blk)
28
+ request(:put, path, options, &blk)
29
+ end
30
+
31
+ def delete(path, options = {}, &blk)
32
+ request(:delete, path, options, &blk)
33
+ end
34
+
35
+ def config(options)
36
+ @__config__.thread.schedule do
37
+ @__config__.processor.config = options
38
+ end
39
+ end
40
+
41
+ def defaults(options)
42
+ @__config__.thread.schedule do
43
+ @__config__.processor.send_options(options)
44
+ end
45
+ end
46
+
47
+ def clear_cookies
48
+ # TODO::
49
+ end
50
+
51
+ def use_middleware(klass)
52
+ # TODO::
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,55 @@
1
+
2
+ module Orchestrator
3
+ module Service
4
+ class TransportHttp
5
+ def initialize(manager, processor)
6
+ @manager = manager
7
+ @settings = @manager.settings
8
+ @processor = processor
9
+
10
+ # Load http endpoint after module has had a chance to update the config
11
+ config = @processor.config
12
+ config[:tls] ||= @settings.tls
13
+ config[:tokenize] = false
14
+ @server = UV::HttpEndpoint.new @settings.uri, config
15
+
16
+ @manager.thread.next_tick do
17
+ # Call connected (we only need to do this once)
18
+ # We may never be connected, this is just to signal that we are ready
19
+ @processor.connected
20
+ end
21
+ end
22
+
23
+ def transmit(cmd)
24
+ return if @terminated
25
+
26
+ # TODO:: Support multiple simultaneous requests (multiple servers)
27
+
28
+ @server.request(cmd[:method], cmd).then(
29
+ proc { |result|
30
+ # Make sure the request information is always available
31
+ result[:request] = cmd
32
+ @processor.buffer(result)
33
+ nil
34
+ },
35
+ proc { |failure|
36
+ # Fail fast (no point waiting for the timeout)
37
+ if @processor.queue.waiting #== cmd
38
+ @processor.__send__(:resp_failure, :failed)
39
+ end
40
+
41
+ # TODO:: Log failure with more detail
42
+ nil
43
+ }
44
+ )
45
+
46
+ nil
47
+ end
48
+
49
+ def terminate
50
+ @terminated = true
51
+ @server.close_connection(:after_writing)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,229 @@
1
+ require 'set'
2
+
3
+
4
+ module Orchestrator
5
+ Subscription = Struct.new(:sys_name, :sys_id, :mod_name, :mod_id, :index, :status, :callback, :on_thread) do
6
+ def initialize(*args)
7
+ super(*args)
8
+
9
+ @do_callback = method(:do_callback)
10
+ end
11
+
12
+ def notify(update)
13
+ if update != @last_update
14
+ @last_update = update
15
+ on_thread.schedule @do_callback
16
+ end
17
+ end
18
+
19
+ def value
20
+ @last_update
21
+ end
22
+
23
+
24
+ protected
25
+
26
+
27
+ # This is always called on the subscribing thread
28
+ def do_callback
29
+ callback.call(self)
30
+ end
31
+ end
32
+
33
+ class Status
34
+ def initialize(thread)
35
+ @thread = thread
36
+ @controller = ::Orchestrator::Control.instance
37
+
38
+ @find_subscription = method(:find_subscription)
39
+
40
+ # {:mod_id => {status => Subscriptions}}
41
+ @subscriptions = {}
42
+ # {:system_id => Subscriptions}
43
+ @systems = {}
44
+ end
45
+
46
+
47
+ attr_reader :thread
48
+
49
+
50
+ # Subscribes to updates from a system module
51
+ # Modules do not have to exist and updates will be triggered as soon as they are
52
+ def subscribe(opt) # sys_name, mod_name, index, status, callback, on_thread
53
+ if opt[:sys_name] && !opt[:sys_id]
54
+ @thread.work(proc {
55
+ id = ::Orchestrator::ControlSystem.bucket.get("sysname-#{sys_name}")
56
+ opt[:sys_id] = id
57
+
58
+ # Grabbing system here as thread-safe and has the potential to block
59
+ ::Orchestrator::System.get(id)
60
+ }).then(proc { |sys|
61
+ mod = sys.get(opt[:mod_name], opt[:index] - 1)
62
+ if mod
63
+ opt[:mod_id] = mod.settings.id.to_sym
64
+ opt[:mod] = mod
65
+ end
66
+
67
+ do_subscribe(opt)
68
+ })
69
+ else
70
+ do_subscribe(opt)
71
+ end
72
+ end
73
+
74
+ # Removes subscription callback from the lookup
75
+ def unsubscribe(sub)
76
+ if sub.is_a? ::Libuv::Q::Promise
77
+ sub.then @find_subscription
78
+ else
79
+ find_subscription(sub)
80
+ end
81
+ end
82
+
83
+ # Triggers an update to be sent to listening callbacks
84
+ def update(mod_id, status, value)
85
+ mod = @subscriptions[mod_id]
86
+ if mod
87
+ subscribed = mod[status]
88
+ if subscribed
89
+ subscribed.each do |subscription|
90
+ begin
91
+ subscription.notify(value)
92
+ rescue => e
93
+ @controller.log_unhandled_exception(e)
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+ # TODO:: we also need the system class to contact each of the threads
101
+ def reloaded_system(sys_id)
102
+ subscriptions = @systems[sys_id]
103
+ if subscriptions
104
+ sys = ::Orchestrator::System.get(@system)
105
+
106
+ subscriptions.each do |sub|
107
+ old_id = sub.mod_id
108
+
109
+ # re-index the subscription
110
+ mod = sys.get(sub.mod_name, sub.index - 1)
111
+ sub.mod_id = mod ? mod.settings.id.to_sym : nil
112
+
113
+ # Check for changes (order, removal, replacement)
114
+ if old_id != sub.mod_id
115
+ @subscriptions[old_id][sub.status].delete(sub)
116
+
117
+ # Update to the new module
118
+ if sub.mod_id
119
+ @subscriptions[sub.mod_id] ||= {}
120
+ @subscriptions[sub.mod_id][sub.status] ||= Set.new
121
+ @subscriptions[sub.mod_id][sub.status].add(sub)
122
+
123
+ # Check for existing status to send to subscriber
124
+ value = mod.status[sub.status]
125
+ sub.notify(value) unless value.nil?
126
+ end
127
+
128
+ # Perform any required cleanup
129
+ if @subscriptions[old_id][sub.status].empty?
130
+ @subscriptions[old_id].delete(sub.status)
131
+ if @subscriptions[old_id].empty?
132
+ @subscriptions.delete(old_id)
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
139
+
140
+
141
+ # NOTE:: Only to be called from subscription thread
142
+ def exec_unsubscribe(sub)
143
+ # Update the system lookup if a system was specified
144
+ if sub.sys_id
145
+ subscriptions = @systems[sub.sys_id]
146
+ if subscriptions
147
+ subscriptions.delete(sub)
148
+
149
+ if subscriptions.empty?
150
+ @systems.delete(sub.sys_id)
151
+ end
152
+ end
153
+ end
154
+
155
+ # Update the module lookup
156
+ statuses = @subscriptions[sub.mod_id]
157
+ if statuses
158
+ subscriptions = statuses[sub.status]
159
+ if subscriptions
160
+ subscriptions.delete(sub)
161
+
162
+ if subscriptions.empty?
163
+ statuses.delete(sub.status)
164
+
165
+ if statuses.empty?
166
+ @subscriptions.delete(sub.mod_id)
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
172
+
173
+
174
+ protected
175
+
176
+
177
+ def do_subscribe(opt)
178
+ # Build the subscription object (as loosely coupled as we can)
179
+ sub = Subscription.new(opt[:sys_name], opt[:sys_id], opt[:mod_name], opt[:mod_id], opt[:index], opt[:status], opt[:callback], opt[:on_thread])
180
+
181
+ if sub.sys_id
182
+ @systems[sub.sys_id] ||= Set.new
183
+ @systems[sub.sys_id].add(sub)
184
+ end
185
+
186
+ # Now if the module is added later we'll still receive updates
187
+ # and also support direct module status bindings
188
+ if sub.mod_id
189
+ @subscriptions[sub.mod_id] ||= {}
190
+ @subscriptions[sub.mod_id][sub.status] ||= Set.new
191
+ @subscriptions[sub.mod_id][sub.status].add(sub)
192
+
193
+ # Check for existing status to send to subscriber
194
+ value = opt[:mod].status[sub.status]
195
+ sub.notify(value) unless value.nil?
196
+ end
197
+
198
+ # return the subscription
199
+ sub
200
+ end
201
+
202
+ def find_subscription(sub)
203
+ # Find module thread
204
+ if sub.mod_id
205
+ manager = @controller.loaded?(sub.mod_id)
206
+ if manager
207
+ thread = manager.thread
208
+ thread.schedule do
209
+ thread.observer.exec_unsubscribe(sub)
210
+ end
211
+ else
212
+ # Should be in our schedule
213
+ exec_unsubscribe(sub)
214
+ end
215
+ else
216
+ exec_unsubscribe(sub)
217
+ end
218
+ end
219
+ end
220
+ end
221
+
222
+ module Libuv
223
+ class Loop
224
+ def observer
225
+ @observer ||= ::Orchestrator::Status.new(@loop)
226
+ @observer
227
+ end
228
+ end
229
+ end