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,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