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