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.
- checksums.yaml +7 -0
- data/LICENSE.md +158 -0
- data/README.md +13 -0
- data/Rakefile +7 -0
- data/app/controllers/orchestrator/api/dependencies_controller.rb +109 -0
- data/app/controllers/orchestrator/api/modules_controller.rb +183 -0
- data/app/controllers/orchestrator/api/systems_controller.rb +294 -0
- data/app/controllers/orchestrator/api/zones_controller.rb +62 -0
- data/app/controllers/orchestrator/api_controller.rb +13 -0
- data/app/controllers/orchestrator/base.rb +59 -0
- data/app/controllers/orchestrator/persistence_controller.rb +29 -0
- data/app/models/orchestrator/access_log.rb +35 -0
- data/app/models/orchestrator/control_system.rb +160 -0
- data/app/models/orchestrator/dependency.rb +87 -0
- data/app/models/orchestrator/mod/by_dependency/map.js +6 -0
- data/app/models/orchestrator/mod/by_module_type/map.js +6 -0
- data/app/models/orchestrator/module.rb +127 -0
- data/app/models/orchestrator/sys/by_modules/map.js +9 -0
- data/app/models/orchestrator/sys/by_zones/map.js +9 -0
- data/app/models/orchestrator/zone.rb +47 -0
- data/app/models/orchestrator/zone/all/map.js +6 -0
- data/config/routes.rb +43 -0
- data/lib/generators/module/USAGE +8 -0
- data/lib/generators/module/module_generator.rb +52 -0
- data/lib/orchestrator.rb +52 -0
- data/lib/orchestrator/control.rb +303 -0
- data/lib/orchestrator/core/mixin.rb +123 -0
- data/lib/orchestrator/core/module_manager.rb +258 -0
- data/lib/orchestrator/core/request_proxy.rb +109 -0
- data/lib/orchestrator/core/requests_proxy.rb +47 -0
- data/lib/orchestrator/core/schedule_proxy.rb +49 -0
- data/lib/orchestrator/core/system_proxy.rb +153 -0
- data/lib/orchestrator/datagram_server.rb +114 -0
- data/lib/orchestrator/dependency_manager.rb +131 -0
- data/lib/orchestrator/device/command_queue.rb +213 -0
- data/lib/orchestrator/device/manager.rb +83 -0
- data/lib/orchestrator/device/mixin.rb +35 -0
- data/lib/orchestrator/device/processor.rb +441 -0
- data/lib/orchestrator/device/transport_makebreak.rb +221 -0
- data/lib/orchestrator/device/transport_tcp.rb +139 -0
- data/lib/orchestrator/device/transport_udp.rb +89 -0
- data/lib/orchestrator/engine.rb +70 -0
- data/lib/orchestrator/errors.rb +23 -0
- data/lib/orchestrator/logger.rb +115 -0
- data/lib/orchestrator/logic/manager.rb +18 -0
- data/lib/orchestrator/logic/mixin.rb +11 -0
- data/lib/orchestrator/service/manager.rb +63 -0
- data/lib/orchestrator/service/mixin.rb +56 -0
- data/lib/orchestrator/service/transport_http.rb +55 -0
- data/lib/orchestrator/status.rb +229 -0
- data/lib/orchestrator/system.rb +108 -0
- data/lib/orchestrator/utilities/constants.rb +41 -0
- data/lib/orchestrator/utilities/transcoder.rb +57 -0
- data/lib/orchestrator/version.rb +3 -0
- data/lib/orchestrator/websocket_manager.rb +425 -0
- data/orchestrator.gemspec +35 -0
- data/spec/orchestrator/queue_spec.rb +200 -0
- metadata +271 -0
@@ -0,0 +1,47 @@
|
|
1
|
+
|
2
|
+
module Orchestrator
|
3
|
+
class Zone < Couchbase::Model
|
4
|
+
design_document :zone
|
5
|
+
include ::CouchbaseId::Generator
|
6
|
+
|
7
|
+
|
8
|
+
attribute :name
|
9
|
+
attribute :description
|
10
|
+
attribute :settings, default: lambda { {} }
|
11
|
+
|
12
|
+
attribute :created_at, default: lambda { Time.now.to_i }
|
13
|
+
|
14
|
+
|
15
|
+
|
16
|
+
# Loads all the zones
|
17
|
+
def self.all
|
18
|
+
all(stale: false)
|
19
|
+
end
|
20
|
+
view :all
|
21
|
+
|
22
|
+
|
23
|
+
protected
|
24
|
+
|
25
|
+
|
26
|
+
validates :name, presence: true
|
27
|
+
|
28
|
+
|
29
|
+
before_delete :remove_zone
|
30
|
+
def remove_zone
|
31
|
+
::Orchestrator::Control.instance.zones.delete(self.id)
|
32
|
+
::Orchestrator::ControlSystem.in_zone(self.id).each do |cs|
|
33
|
+
cs.zones.delete(self.id)
|
34
|
+
cs.save
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Expire both the zone cache and any systems that use the zone
|
39
|
+
after_save :expire_caches
|
40
|
+
def expire_caches
|
41
|
+
::Orchestrator::Control.instance.zones[self.id] = self
|
42
|
+
::Orchestrator::ControlSystem.in_zone(self.id).each do |cs|
|
43
|
+
cs.expire_cache
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/config/routes.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
Orchestrator::Engine.routes.draw do
|
2
|
+
|
3
|
+
match '/*path' => 'api#options', :via => :options
|
4
|
+
|
5
|
+
# Restful access to services
|
6
|
+
namespace :api do
|
7
|
+
# Allows multiple routes to resolve to the one controller
|
8
|
+
concern :mods do
|
9
|
+
resources :modules do # modules have settings
|
10
|
+
post 'start', on: :member
|
11
|
+
post 'stop', on: :member
|
12
|
+
get 'state', on: :member
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Trusted Sessions - Create Trust (returns id), Update Session and Destroy Trust
|
17
|
+
resources :trusts
|
18
|
+
|
19
|
+
resources(:systems, {as: :control_system}) do # systems have settings and define what zone they are in
|
20
|
+
post 'remove', on: :member
|
21
|
+
post 'start', on: :member
|
22
|
+
post 'stop', on: :member
|
23
|
+
post 'exec', on: :member
|
24
|
+
get 'state', on: :member
|
25
|
+
get 'funcs', on: :member
|
26
|
+
get 'count', on: :member
|
27
|
+
get 'types', on: :member
|
28
|
+
|
29
|
+
concerns :mods
|
30
|
+
end
|
31
|
+
resources :dependencies do # dependencies have settings
|
32
|
+
post 'reload', on: :member
|
33
|
+
end
|
34
|
+
resources :groups # users define the groups they are in
|
35
|
+
resources :zones # zones define what groups can access them
|
36
|
+
|
37
|
+
concerns :mods
|
38
|
+
end
|
39
|
+
|
40
|
+
# These are non-restful endpoints
|
41
|
+
# Websockets and Eventsources
|
42
|
+
get 'websocket', to: 'persistence#websocket', via: :all
|
43
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
class ModuleGenerator < Rails::Generators::NamedBase
|
2
|
+
#source_root File.expand_path('../templates', __FILE__)
|
3
|
+
|
4
|
+
def create_module_file
|
5
|
+
|
6
|
+
name = file_name.downcase.gsub(/\s|-/, '_')
|
7
|
+
param = class_path
|
8
|
+
param.map! {|item| item.downcase.gsub(/\s|-/, '_')}
|
9
|
+
|
10
|
+
path = File.join('app/modules', *param)
|
11
|
+
|
12
|
+
scope = []
|
13
|
+
text = ""
|
14
|
+
param.map! {|item|
|
15
|
+
item = item.camelcase
|
16
|
+
scope << item
|
17
|
+
text += "module #{scope.join('::')}; end\n"
|
18
|
+
item
|
19
|
+
}
|
20
|
+
param << name.camelcase
|
21
|
+
scope = param.join('::')
|
22
|
+
|
23
|
+
|
24
|
+
create_file File.join(path, "#{name}.rb") do
|
25
|
+
text += <<-FILE
|
26
|
+
|
27
|
+
|
28
|
+
class #{scope}
|
29
|
+
include ::Orchestrator::Constants # On, Off and other useful constants
|
30
|
+
include ::Orchestrator::Transcoder # binary, hex and string helper methods
|
31
|
+
# For stream tokenization use ::UV::BufferedTokenizer or ::UV::AbstractTokenizer
|
32
|
+
|
33
|
+
def on_load
|
34
|
+
# module has been started
|
35
|
+
end
|
36
|
+
|
37
|
+
def on_unload
|
38
|
+
# module has been stopped
|
39
|
+
end
|
40
|
+
|
41
|
+
# Called when class updated at runtime
|
42
|
+
def on_update
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
FILE
|
47
|
+
|
48
|
+
text
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
data/lib/orchestrator.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'orchestrator/engine'
|
2
|
+
|
3
|
+
|
4
|
+
# Gems
|
5
|
+
require 'couchbase-id'
|
6
|
+
require 'uv-rays'
|
7
|
+
require 'co-elastic-query'
|
8
|
+
|
9
|
+
# Optional utility modules
|
10
|
+
require 'orchestrator/utilities/transcoder' # functions for data manipulation
|
11
|
+
require 'orchestrator/utilities/constants' # constants for readable code
|
12
|
+
|
13
|
+
# System Main
|
14
|
+
require 'orchestrator/dependency_manager' # Manages code loading
|
15
|
+
require 'orchestrator/websocket_manager' # Websocket interface
|
16
|
+
require 'orchestrator/datagram_server' # UDP abstraction management
|
17
|
+
require 'orchestrator/control' # Module control and system loader
|
18
|
+
require 'orchestrator/version' # orchestrator version
|
19
|
+
require 'orchestrator/system' # This is the source of truth for all system information
|
20
|
+
require 'orchestrator/status' # Manages status subscriptions across threads
|
21
|
+
require 'orchestrator/logger' # Logs events of interest as well as coordinating live log feedback
|
22
|
+
require 'orchestrator/errors' # A list of errors that can occur within the system
|
23
|
+
|
24
|
+
# Core Abstractions
|
25
|
+
require 'orchestrator/core/module_manager' # Base class of logic, device and service managers
|
26
|
+
require 'orchestrator/core/schedule_proxy' # Common proxy for all module schedules
|
27
|
+
require 'orchestrator/core/requests_proxy' # Sends a command to all modules of that type
|
28
|
+
require 'orchestrator/core/request_proxy' # Sends a command to a single module
|
29
|
+
require 'orchestrator/core/system_proxy' # prevents stale system objects (maintains loose coupling)
|
30
|
+
require 'orchestrator/core/mixin' # Common mixin functions for modules classes
|
31
|
+
|
32
|
+
# Logic abstractions
|
33
|
+
require 'orchestrator/logic/manager' # control system manager for logic modules
|
34
|
+
require 'orchestrator/logic/mixin' # helper functions for logic module classes
|
35
|
+
|
36
|
+
# Device abstractions
|
37
|
+
require 'orchestrator/device/transport_makebreak'
|
38
|
+
require 'orchestrator/device/command_queue'
|
39
|
+
require 'orchestrator/device/transport_tcp'
|
40
|
+
require 'orchestrator/device/transport_udp'
|
41
|
+
require 'orchestrator/device/processor'
|
42
|
+
require 'orchestrator/device/manager'
|
43
|
+
require 'orchestrator/device/mixin'
|
44
|
+
|
45
|
+
# Service abstractions
|
46
|
+
require 'orchestrator/service/transport_http'
|
47
|
+
require 'orchestrator/service/manager'
|
48
|
+
require 'orchestrator/service/mixin'
|
49
|
+
|
50
|
+
|
51
|
+
module Orchestrator
|
52
|
+
end
|
@@ -0,0 +1,303 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
|
5
|
+
module Orchestrator
|
6
|
+
class Control
|
7
|
+
include Singleton
|
8
|
+
|
9
|
+
#
|
10
|
+
#
|
11
|
+
# 1. Load the modules allocated to this node
|
12
|
+
# 2. Allocate modules to CPUs
|
13
|
+
# * Modules load dependencies as required
|
14
|
+
# * Logics are streamed in after devices and services
|
15
|
+
#
|
16
|
+
# Logic modules will fetch their system when they interact with other modules.
|
17
|
+
# Devices and services do not have a system associated with them
|
18
|
+
# This makes systems very loosely coupled to the modules
|
19
|
+
# which should make distributing the system slightly simpler
|
20
|
+
#
|
21
|
+
#
|
22
|
+
|
23
|
+
def initialize
|
24
|
+
# critical sections
|
25
|
+
@critical = ::Mutex.new
|
26
|
+
@loaded = ::ThreadSafe::Cache.new
|
27
|
+
@zones = ::ThreadSafe::Cache.new
|
28
|
+
@loader = DependencyManager.instance
|
29
|
+
@loop = ::Libuv::Loop.default
|
30
|
+
@exceptions = method(:log_unhandled_exception)
|
31
|
+
|
32
|
+
@ready = false
|
33
|
+
|
34
|
+
if Rails.env.production?
|
35
|
+
logger = ::Logger.new(::Rails.root.join('log/control.log').to_s, 10, 4194304)
|
36
|
+
else
|
37
|
+
logger = ::Logger.new(STDOUT)
|
38
|
+
end
|
39
|
+
logger.formatter = proc { |severity, datetime, progname, msg|
|
40
|
+
"#{datetime.strftime("%d/%m/%Y @ %I:%M%p")} #{severity}: #{progname} - #{msg}\n"
|
41
|
+
}
|
42
|
+
@logger = ::ActiveSupport::TaggedLogging.new(logger)
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
attr_reader :logger, :loop, :ready, :zones
|
47
|
+
|
48
|
+
|
49
|
+
# Start the control reactor
|
50
|
+
def mount
|
51
|
+
return @server.loaded if @server
|
52
|
+
|
53
|
+
@critical.synchronize {
|
54
|
+
return if @server # Protect against multiple mounts
|
55
|
+
|
56
|
+
# Cache all the zones in the system
|
57
|
+
::Orchestrator::Zone.all.each do |zone|
|
58
|
+
@zones[zone.id] = zone
|
59
|
+
end
|
60
|
+
|
61
|
+
@server = ::SpiderGazelle::Spider.instance
|
62
|
+
@server.loaded.then do
|
63
|
+
# Share threads with SpiderGazelle (one per core)
|
64
|
+
if @server.mode == :thread
|
65
|
+
@threads = @server.threads
|
66
|
+
else # We are either running no_ipc or process (unsupported for control)
|
67
|
+
@threads = Set.new
|
68
|
+
|
69
|
+
cpus = ::Libuv.cpu_count || 1
|
70
|
+
cpus.times &method(:start_thread)
|
71
|
+
|
72
|
+
@loop.signal :INT, method(:kill_workers)
|
73
|
+
end
|
74
|
+
|
75
|
+
@selector = @threads.cycle
|
76
|
+
end
|
77
|
+
}
|
78
|
+
|
79
|
+
return @server.loaded
|
80
|
+
end
|
81
|
+
|
82
|
+
# Boot the control system, running all defined modules
|
83
|
+
def boot(*args)
|
84
|
+
# Only boot if running as a server
|
85
|
+
Thread.new &method(:load_all)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Load the modules on the loop references in round robin
|
89
|
+
# This method is thread safe.
|
90
|
+
def load(mod_settings)
|
91
|
+
mod_id = mod_settings.id.to_sym
|
92
|
+
defer = @loop.defer
|
93
|
+
mod = @loaded[mod_id]
|
94
|
+
|
95
|
+
if mod
|
96
|
+
defer.resolve(mod)
|
97
|
+
else
|
98
|
+
defer.resolve(
|
99
|
+
@loader.load(mod_settings.dependency).then(proc { |klass|
|
100
|
+
# We will always be on the default loop here
|
101
|
+
thread = @selector.next
|
102
|
+
|
103
|
+
# We'll resolve the promise if the module loads on the deferred thread
|
104
|
+
defer = @loop.defer
|
105
|
+
thread.schedule do
|
106
|
+
defer.resolve(start_module(thread, klass, mod_settings))
|
107
|
+
end
|
108
|
+
|
109
|
+
# update the module cache
|
110
|
+
defer.promise.then do |mod_manager|
|
111
|
+
@loaded[mod_id] = mod_manager
|
112
|
+
end
|
113
|
+
defer.promise
|
114
|
+
}, @exceptions)
|
115
|
+
)
|
116
|
+
end
|
117
|
+
defer.promise
|
118
|
+
end
|
119
|
+
|
120
|
+
# Checks if a module with the ID specified is loaded
|
121
|
+
def loaded?(mod_id)
|
122
|
+
@loaded[mod_id.to_sym]
|
123
|
+
end
|
124
|
+
|
125
|
+
# Starts a module running
|
126
|
+
def start(mod_id)
|
127
|
+
defer = @loop.defer
|
128
|
+
|
129
|
+
mod = loaded? mod_id
|
130
|
+
if mod
|
131
|
+
mod.thread.schedule do
|
132
|
+
mod.start
|
133
|
+
defer.resolve(true)
|
134
|
+
end
|
135
|
+
else
|
136
|
+
err = Error::ModuleNotFound.new "unable to start module '#{mod_id}', not found"
|
137
|
+
defer.reject(err)
|
138
|
+
@logger.warn err.message
|
139
|
+
end
|
140
|
+
|
141
|
+
defer.promise
|
142
|
+
end
|
143
|
+
|
144
|
+
# Stops a module running
|
145
|
+
def stop(mod_id)
|
146
|
+
defer = @loop.defer
|
147
|
+
|
148
|
+
mod = loaded? mod_id
|
149
|
+
if mod
|
150
|
+
mod.thread.schedule do
|
151
|
+
mod.stop
|
152
|
+
defer.resolve(true)
|
153
|
+
end
|
154
|
+
else
|
155
|
+
err = Error::ModuleNotFound.new "unable to stop module '#{mod_id}', not found"
|
156
|
+
defer.reject(err)
|
157
|
+
@logger.warn err.message
|
158
|
+
end
|
159
|
+
|
160
|
+
defer.promise
|
161
|
+
end
|
162
|
+
|
163
|
+
# Stop the module gracefully
|
164
|
+
# Then remove it from @loaded
|
165
|
+
def unload(mod_id)
|
166
|
+
stop(mod_id).then(proc {
|
167
|
+
@loaded.delete(mod_id.to_sym)
|
168
|
+
true # promise response
|
169
|
+
})
|
170
|
+
end
|
171
|
+
|
172
|
+
# Unload then
|
173
|
+
# Get a fresh version of the settings from the database
|
174
|
+
# load the module
|
175
|
+
def update(mod_id)
|
176
|
+
unload(mod_id).then(proc {
|
177
|
+
# Grab database model in the thread pool
|
178
|
+
res = @loop.work do
|
179
|
+
::Orchestrator::Module.find(mod_id)
|
180
|
+
end
|
181
|
+
|
182
|
+
# Load the module if model found
|
183
|
+
res.then(proc { |config|
|
184
|
+
load(config) # Promise chaining to here
|
185
|
+
})
|
186
|
+
})
|
187
|
+
end
|
188
|
+
|
189
|
+
def reload(dep_id)
|
190
|
+
@loop.work do
|
191
|
+
reload_dep(dep_id)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def notify_ready
|
196
|
+
# Clear the system cache (in case it has been populated at all)
|
197
|
+
System.clear_cache
|
198
|
+
@ready = true
|
199
|
+
end
|
200
|
+
|
201
|
+
def log_unhandled_exception(*args)
|
202
|
+
msg = ''
|
203
|
+
err = args[-1]
|
204
|
+
if err && err.respond_to?(:backtrace)
|
205
|
+
msg << "exception: #{err.message} (#{args[0..-2]})"
|
206
|
+
msg << "\n#{err.backtrace.join("\n")}" if err.respond_to?(:backtrace) && err.backtrace
|
207
|
+
else
|
208
|
+
msg << "unhandled exception: #{args}"
|
209
|
+
end
|
210
|
+
@logger.error msg
|
211
|
+
::Libuv::Q.reject(@loop, msg)
|
212
|
+
end
|
213
|
+
|
214
|
+
|
215
|
+
protected
|
216
|
+
|
217
|
+
|
218
|
+
# This will always be called on the thread reactor here
|
219
|
+
def start_module(thread, klass, settings)
|
220
|
+
# Initialize the connection / logic / service handler here
|
221
|
+
case settings.dependency.role
|
222
|
+
when :device
|
223
|
+
Device::Manager.new(thread, klass, settings)
|
224
|
+
when :service
|
225
|
+
Service::Manager.new(thread, klass, settings)
|
226
|
+
else
|
227
|
+
Logic::Manager.new(thread, klass, settings)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
|
232
|
+
# Grab the modules from the database and load them
|
233
|
+
def load_all
|
234
|
+
loading = []
|
235
|
+
wait = nil
|
236
|
+
|
237
|
+
modules = ::Orchestrator::Module.all
|
238
|
+
modules.each do |mod|
|
239
|
+
if mod.role < 3
|
240
|
+
loading << load(mod) # modules are streamed in
|
241
|
+
else
|
242
|
+
if wait.nil?
|
243
|
+
wait = ::Libuv::Q.finally(@loop, *loading)
|
244
|
+
loading.clear
|
245
|
+
|
246
|
+
# Clear here in case rest api calls have built the cache
|
247
|
+
System.clear_cache
|
248
|
+
end
|
249
|
+
|
250
|
+
loading << mod
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
# In case there were no logic modules
|
255
|
+
if wait.nil?
|
256
|
+
wait = ::Libuv::Q.finally(@loop, *loading)
|
257
|
+
loading.clear
|
258
|
+
end
|
259
|
+
|
260
|
+
# Mark system as ready
|
261
|
+
wait.finally do
|
262
|
+
continue_loading(loading)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
# Load all the logic modules after the device modules are complete
|
267
|
+
def continue_loading(modules)
|
268
|
+
loading = []
|
269
|
+
|
270
|
+
modules.each do |mod|
|
271
|
+
loading << load(mod) # grab the load promises
|
272
|
+
end
|
273
|
+
|
274
|
+
# Once load is complete we'll accept websockets
|
275
|
+
::Libuv::Q.finally(@loop, *loading).finally method(:notify_ready)
|
276
|
+
end
|
277
|
+
|
278
|
+
|
279
|
+
|
280
|
+
##
|
281
|
+
# Methods called when we manage the threads:
|
282
|
+
def start_thread(num)
|
283
|
+
thread = Libuv::Loop.new
|
284
|
+
@threads << thread
|
285
|
+
Thread.new do
|
286
|
+
thread.run do |promise|
|
287
|
+
promise.progress @exceptions
|
288
|
+
|
289
|
+
thread.async do
|
290
|
+
p 'noop'
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
def kill_workers(*args)
|
297
|
+
@threads.each do |thread|
|
298
|
+
thread.stop
|
299
|
+
end
|
300
|
+
@loop.stop
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|