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
|
+
module Orchestrator
|
2
|
+
module Core
|
3
|
+
class RequestsProxy
|
4
|
+
def initialize(thread, modules)
|
5
|
+
if modules.nil?
|
6
|
+
@modules = []
|
7
|
+
else
|
8
|
+
@modules = modules.is_a?(Array) ? modules : [modules]
|
9
|
+
end
|
10
|
+
@thread = thread
|
11
|
+
end
|
12
|
+
|
13
|
+
# Returns true if there is no object to proxy
|
14
|
+
#
|
15
|
+
# @return [true|false]
|
16
|
+
def nil?
|
17
|
+
@modules.empty?
|
18
|
+
end
|
19
|
+
alias_method :empty?, :nil?
|
20
|
+
|
21
|
+
def method_missing(name, *args, &block)
|
22
|
+
if ::Orchestrator::Core::PROTECTED[name]
|
23
|
+
err = Error::ProtectedMethod.new "attempt to access a protected method '#{name}' in multiple modules"
|
24
|
+
::Libuv::Q.reject(@thread, err)
|
25
|
+
# TODO:: log warning err.message
|
26
|
+
else
|
27
|
+
promises = @modules.map do |mod|
|
28
|
+
defer = mod.thread.defer
|
29
|
+
mod.thread.schedule do
|
30
|
+
begin
|
31
|
+
defer.resolve(
|
32
|
+
mod.instance.public_send(name, *args, &block)
|
33
|
+
)
|
34
|
+
rescue => e
|
35
|
+
mod.logger.print_error(e)
|
36
|
+
defer.reject(e)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
defer.promise
|
40
|
+
end
|
41
|
+
|
42
|
+
@thread.finally(*promises)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
|
4
|
+
module Orchestrator
|
5
|
+
module Core
|
6
|
+
class ScheduleProxy
|
7
|
+
def initialize(thread)
|
8
|
+
@scheduler = thread.scheduler
|
9
|
+
@schedules = Set.new
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :schedules
|
13
|
+
|
14
|
+
def every(*args, &block)
|
15
|
+
add_schedule @scheduler.every(*args, &block)
|
16
|
+
end
|
17
|
+
|
18
|
+
def in(*args, &block)
|
19
|
+
add_schedule @scheduler.in(*args, &block)
|
20
|
+
end
|
21
|
+
|
22
|
+
def at(*args, &block)
|
23
|
+
add_schedule @scheduler.at(*args, &block)
|
24
|
+
end
|
25
|
+
|
26
|
+
def cron(*args, &block)
|
27
|
+
add_schedule @scheduler.cron(*args, &block)
|
28
|
+
end
|
29
|
+
|
30
|
+
def clear
|
31
|
+
@schedules.each do |schedule|
|
32
|
+
schedule.cancel
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
protected
|
38
|
+
|
39
|
+
|
40
|
+
def add_schedule(schedule)
|
41
|
+
@schedules.add(schedule)
|
42
|
+
schedule.finally do
|
43
|
+
@schedules.delete(schedule)
|
44
|
+
end
|
45
|
+
schedule
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
|
4
|
+
module Orchestrator
|
5
|
+
module Core
|
6
|
+
class SystemProxy
|
7
|
+
def initialize(thread, sys_id, origin = nil)
|
8
|
+
@system = sys_id.to_sym
|
9
|
+
@thread = thread
|
10
|
+
@origin = origin # This is the module that requested the proxy
|
11
|
+
end
|
12
|
+
|
13
|
+
# Alias for get
|
14
|
+
def [](mod)
|
15
|
+
get mod
|
16
|
+
end
|
17
|
+
|
18
|
+
# Provides a proxy to a module for a safe way to communicate across threads
|
19
|
+
#
|
20
|
+
# @param module [String, Symbol] the name of the module in the system
|
21
|
+
# @param index [Integer] the index of the desired module (starting at 1)
|
22
|
+
# @return [::Orchestrator::Core::RequestsProxy] proxies requests to a single module
|
23
|
+
def get(mod, index = 1)
|
24
|
+
index -= 1 # Get the real index
|
25
|
+
name = mod.to_sym
|
26
|
+
|
27
|
+
RequestProxy.new(@thread, system.get(name, index))
|
28
|
+
end
|
29
|
+
|
30
|
+
# Checks for the existence of a particular module
|
31
|
+
#
|
32
|
+
# @param module [String, Symbol] the name of the module in the system
|
33
|
+
# @param index [Integer] the index of the desired module (starting at 1)
|
34
|
+
# @return [true, false] does the module exist?
|
35
|
+
def exists?(mod, index = 1)
|
36
|
+
index -= 1 # Get the real index
|
37
|
+
name = mod.to_sym
|
38
|
+
|
39
|
+
!system.get(name, index).nil?
|
40
|
+
end
|
41
|
+
|
42
|
+
# Provides a proxy to multiple modules. A simple way to send commands to multiple devices
|
43
|
+
#
|
44
|
+
# @param module [String, Symbol] the name of the module in the system
|
45
|
+
# @return [::Orchestrator::Core::RequestsProxy] proxies requests to multiple modules
|
46
|
+
def all(mod)
|
47
|
+
name = mod.to_sym
|
48
|
+
RequestsProxy.new(@thread, system.all(name))
|
49
|
+
end
|
50
|
+
|
51
|
+
# Iterates over the modules in the system. Can also specify module types.
|
52
|
+
#
|
53
|
+
# @param mod_name [String, Symbol] the optional names of modules to iterate over
|
54
|
+
# @yield [Module Instance, Symbol, Integer] yields the modules with their name and index
|
55
|
+
def each(*args)
|
56
|
+
mods = args.empty? ? modules : args
|
57
|
+
mods.each do |mod|
|
58
|
+
number = count(mod)
|
59
|
+
(1..number).each do |index|
|
60
|
+
yield(get(mod, index), mod, index)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Grabs the number of a particular device type
|
66
|
+
#
|
67
|
+
# @param module [String, Symbol] the name of the module in the system
|
68
|
+
# @return [Integer] the number of modules with a shared name
|
69
|
+
def count(mod)
|
70
|
+
name = mod.to_sym
|
71
|
+
system.count(name)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Returns a list of all the module names in the system
|
75
|
+
#
|
76
|
+
# @return [Array] a list of all the module names
|
77
|
+
def modules
|
78
|
+
system.modules
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns the system name as defined in the database
|
82
|
+
#
|
83
|
+
# @return [String] the name of the system
|
84
|
+
def name
|
85
|
+
system.config.name
|
86
|
+
end
|
87
|
+
|
88
|
+
# Used to be notified when an update to a status value occurs
|
89
|
+
#
|
90
|
+
# @param module [String, Symbol] the name of the module in the system
|
91
|
+
# @param index [Integer] the index of the module as there may be more than one
|
92
|
+
# @param status [String, Symbol] the name of the status variable
|
93
|
+
# @param callback [Proc] method, block, proc or lambda to be called when a change occurs
|
94
|
+
# @return [Object] a reference to the subscription for un-subscribing
|
95
|
+
def subscribe(mod_name, index, status = nil, callback = nil, &block)
|
96
|
+
# Allow index to be optional
|
97
|
+
if not index.is_a?(Integer)
|
98
|
+
callback = status || block
|
99
|
+
status = index.to_sym
|
100
|
+
index = 1
|
101
|
+
else
|
102
|
+
callback ||= block
|
103
|
+
end
|
104
|
+
mod_name = mod_name.to_sym
|
105
|
+
|
106
|
+
raise 'callback required' unless callback.respond_to? :call
|
107
|
+
|
108
|
+
# We need to get the system to schedule threads
|
109
|
+
sys = system
|
110
|
+
options = {
|
111
|
+
sys_id: @system,
|
112
|
+
sys_name: sys.config.name,
|
113
|
+
mod_name: mod_name,
|
114
|
+
index: index,
|
115
|
+
status: status,
|
116
|
+
callback: callback,
|
117
|
+
on_thread: @thread
|
118
|
+
}
|
119
|
+
|
120
|
+
# if the module exists, subscribe on the correct thread
|
121
|
+
# use a bit of promise magic as required
|
122
|
+
mod_man = sys.get(mod_name, index - 1)
|
123
|
+
sub = if mod_man
|
124
|
+
defer = @thread.defer
|
125
|
+
|
126
|
+
options[:mod_id] = mod_man.settings.id.to_sym
|
127
|
+
options[:mod] = mod_man
|
128
|
+
thread = mod_man.thread
|
129
|
+
thread.schedule do
|
130
|
+
defer.resolve (
|
131
|
+
thread.observer.subscribe(options)
|
132
|
+
)
|
133
|
+
end
|
134
|
+
|
135
|
+
defer.promise
|
136
|
+
else
|
137
|
+
@thread.observer.subscribe(options)
|
138
|
+
end
|
139
|
+
|
140
|
+
@origin.add_subscription sub if @origin
|
141
|
+
sub
|
142
|
+
end
|
143
|
+
|
144
|
+
|
145
|
+
protected
|
146
|
+
|
147
|
+
|
148
|
+
def system
|
149
|
+
::Orchestrator::System.get(@system)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
|
2
|
+
module Orchestrator
|
3
|
+
class UdpService < ::UV::DatagramConnection
|
4
|
+
def initialize(*args)
|
5
|
+
super(*args)
|
6
|
+
|
7
|
+
@callbacks = {
|
8
|
+
# ip => port => callback
|
9
|
+
}
|
10
|
+
end
|
11
|
+
|
12
|
+
def attach(ip, port, callback)
|
13
|
+
@loop.schedule do
|
14
|
+
ports = @callbacks[ip.to_sym] ||= {}
|
15
|
+
ports[port.to_i] = callback
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def detach(ip_raw, port)
|
20
|
+
@loop.schedule do
|
21
|
+
ip = ip_raw.to_sym
|
22
|
+
ip_ports = @callbacks[ip]
|
23
|
+
if ip_ports
|
24
|
+
ip_ports.delete(port.to_i)
|
25
|
+
@callbacks.delete(ip) if ip_ports.empty?
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def on_read(data, ip, port, transport)
|
31
|
+
ip_ports = @callbacks[ip.to_sym]
|
32
|
+
if ip_ports
|
33
|
+
callback = ip_ports[port.to_i]
|
34
|
+
if callback
|
35
|
+
callback.call(data)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def send(ip, port, data)
|
41
|
+
@loop.schedule do
|
42
|
+
send_datagram(data, ip, port)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
class UdpBroadcast < ::UV::DatagramConnection
|
49
|
+
def post_init
|
50
|
+
@transport.enable_broadcast
|
51
|
+
end
|
52
|
+
|
53
|
+
def send(ip, port, data)
|
54
|
+
@loop.schedule do
|
55
|
+
send_datagram(data, ip, port)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
module Libuv
|
63
|
+
class Loop
|
64
|
+
def udp_service
|
65
|
+
if @udp_service
|
66
|
+
@udp_service
|
67
|
+
else
|
68
|
+
CRITICAL.synchronize {
|
69
|
+
return @udp_service if @udp_service
|
70
|
+
|
71
|
+
port = Rails.configuration.orchestrator.datagram_port || 0
|
72
|
+
|
73
|
+
if port == 0
|
74
|
+
@udp_service = ::UV.open_datagram_socket(::Orchestrator::UdpService)
|
75
|
+
elsif defined? @@udp_service
|
76
|
+
@udp_service = @@udp_service
|
77
|
+
else # define a class variable at the specified port
|
78
|
+
@udp_service = ::UV.open_datagram_socket(::Orchestrator::UdpService, '0.0.0.0', port)
|
79
|
+
@@udp_service = @udp_service
|
80
|
+
end
|
81
|
+
}
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def udp_broadcast(data, port = 9, ip = '<broadcast>')
|
86
|
+
if @udp_broadcast
|
87
|
+
@udp_broadcast.send(ip, port, data)
|
88
|
+
else
|
89
|
+
CRITICAL.synchronize {
|
90
|
+
return @udp_broadcast.send(ip, port, data) if @udp_broadcast
|
91
|
+
|
92
|
+
port = Rails.configuration.orchestrator.broadcast_port || 0
|
93
|
+
|
94
|
+
if port == 0
|
95
|
+
@udp_broadcast = ::UV.open_datagram_socket(::Orchestrator::UdpBroadcast)
|
96
|
+
elsif defined? @@udp_broadcast
|
97
|
+
@udp_broadcast = @@udp_broadcast
|
98
|
+
else # define a class variable at the specified port
|
99
|
+
@udp_broadcast = ::UV.open_datagram_socket(::Orchestrator::UdpBroadcast, '0.0.0.0', port)
|
100
|
+
@@udp_broadcast = @udp_broadcast
|
101
|
+
end
|
102
|
+
|
103
|
+
@udp_broadcast.send(ip, port, data)
|
104
|
+
}
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def wake_device(mac, ip = '<broadcast>')
|
109
|
+
mac = mac.gsub(/(0x|[^0-9A-Fa-f])*/, "").scan(/.{2}/).pack("H*H*H*H*H*H*")
|
110
|
+
magicpacket = (0xff.chr) * 6 + mac * 16
|
111
|
+
udp_broadcast(magicpacket, 9, ip)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
require 'thread' # For Mutex
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
|
5
|
+
module Orchestrator
|
6
|
+
class DependencyManager
|
7
|
+
include Singleton
|
8
|
+
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@load_mutex = Mutex.new
|
12
|
+
@dependencies = ThreadSafe::Cache.new
|
13
|
+
@loop = ::Libuv::Loop.default
|
14
|
+
@loop.next_tick do
|
15
|
+
@logger = ::Orchestrator::Control.instance.logger
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
def load(dependency, force = false)
|
21
|
+
defer = @loop.defer
|
22
|
+
|
23
|
+
classname = dependency.class_name
|
24
|
+
class_lookup = classname.to_sym
|
25
|
+
class_object = @dependencies[class_lookup]
|
26
|
+
|
27
|
+
if class_object && force == false
|
28
|
+
defer.resolve(class_object)
|
29
|
+
else
|
30
|
+
# We need to ensure only one file loads at a time
|
31
|
+
@load_mutex.synchronize {
|
32
|
+
perform_load(dependency, defer, classname, class_lookup, force)
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
defer.promise
|
37
|
+
end
|
38
|
+
|
39
|
+
def force_load(file)
|
40
|
+
defer = @loop.defer
|
41
|
+
|
42
|
+
if File.exists?(file)
|
43
|
+
begin
|
44
|
+
@load_mutex.synchronize {
|
45
|
+
load file
|
46
|
+
}
|
47
|
+
defer.resolve(file)
|
48
|
+
rescue Exception => e
|
49
|
+
defer.reject(e)
|
50
|
+
print_error(e, 'force load failed')
|
51
|
+
end
|
52
|
+
else
|
53
|
+
defer.reject(Error::FileNotFound.new("could not find '#{file}'"))
|
54
|
+
end
|
55
|
+
|
56
|
+
defer.promise
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
protected
|
61
|
+
|
62
|
+
|
63
|
+
# Always called from within a Mutex
|
64
|
+
def perform_load(dependency, defer, classname, class_lookup, force)
|
65
|
+
if force == false
|
66
|
+
class_object = @dependencies[class_lookup]
|
67
|
+
if class_object
|
68
|
+
defer.resolve(class_object)
|
69
|
+
return
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
begin
|
74
|
+
file = "#{classname.underscore}.rb"
|
75
|
+
class_object = nil
|
76
|
+
|
77
|
+
::Rails.configuration.orchestrator.module_paths.each do |path|
|
78
|
+
if ::File.exists?("#{path}/#{file}")
|
79
|
+
|
80
|
+
::Kernel.load "#{path}/#{file}"
|
81
|
+
class_object = classname.constantize
|
82
|
+
|
83
|
+
case dependency.role
|
84
|
+
when :device
|
85
|
+
include_device(class_object)
|
86
|
+
when :service
|
87
|
+
include_service(class_object)
|
88
|
+
else
|
89
|
+
include_logic(class_object)
|
90
|
+
end
|
91
|
+
|
92
|
+
@dependencies[class_lookup] = class_object
|
93
|
+
defer.resolve(class_object)
|
94
|
+
break
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
if class_object.nil?
|
99
|
+
defer.reject(Error::FileNotFound.new("could not find '#{file}'"))
|
100
|
+
end
|
101
|
+
rescue Exception => e
|
102
|
+
defer.reject(e)
|
103
|
+
print_error(e, 'error loading dependency')
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def include_logic(klass)
|
108
|
+
klass.class_eval do
|
109
|
+
include ::Orchestrator::Logic::Mixin
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def include_device(klass)
|
114
|
+
klass.class_eval do
|
115
|
+
include ::Orchestrator::Device::Mixin
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def include_service(klass)
|
120
|
+
klass.class_eval do
|
121
|
+
include ::Orchestrator::Service::Mixin
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def print_error(e, msg = '')
|
126
|
+
msg << "\n#{e.message}"
|
127
|
+
msg << "\n#{e.backtrace.join("\n")}" if e.respond_to? :backtrace
|
128
|
+
@logger.error(msg)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|