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