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