rsmp 0.1.0

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.
data/lib/rsmp/site.rb ADDED
@@ -0,0 +1,165 @@
1
+ # RSMP site
2
+ # The site initializes the connection to the supervisor.
3
+ # Connections to supervisors are handles via supervisor proxies.
4
+
5
+ module RSMP
6
+ class Site < Node
7
+ include SiteBase
8
+
9
+ attr_reader :rsmp_versions, :site_settings, :logger, :proxies
10
+
11
+ def initialize options={}
12
+ initialize_site
13
+ handle_site_settings options
14
+ super options.merge log_settings: @site_settings["log"]
15
+ @proxies = []
16
+ @sleep_condition = Async::Notification.new
17
+ end
18
+
19
+ def site_id
20
+ @site_settings['site_id']
21
+ end
22
+
23
+ def handle_site_settings options
24
+ @site_settings = {
25
+ 'site_id' => 'RN+SI0001',
26
+ 'supervisors' => [
27
+ { 'ip' => '127.0.0.1', 'port' => 12111 }
28
+ ],
29
+ 'rsmp_versions' => ['3.1.1','3.1.2','3.1.3','3.1.4'],
30
+ 'timer_interval' => 0.1,
31
+ 'watchdog_interval' => 1,
32
+ 'watchdog_timeout' => 2,
33
+ 'acknowledgement_timeout' => 2,
34
+ 'command_response_timeout' => 1,
35
+ 'status_response_timeout' => 1,
36
+ 'status_update_timeout' => 1,
37
+ 'site_connect_timeout' => 2,
38
+ 'site_ready_timeout' => 1,
39
+ 'reconnect_interval' => 0.1,
40
+ 'send_after_connect' => true,
41
+ 'components' => {
42
+ 'C1' => {}
43
+ },
44
+ 'log' => {
45
+ 'active' => true,
46
+ 'color' => true,
47
+ 'ip' => false,
48
+ 'timestamp' => true,
49
+ 'site_id' => true,
50
+ 'level' => false,
51
+ 'acknowledgements' => false,
52
+ 'watchdogs' => false,
53
+ 'json' => false,
54
+ 'statistics' => false
55
+ }
56
+ }
57
+ if options[:site_settings_path]
58
+ if File.exist? options[:site_settings_path]
59
+ @site_settings.merge! YAML.load_file(options[:site_settings_path])
60
+ else
61
+ puts "Error: Config #{options[:site_settings_path]} not found, pwd"
62
+ exit
63
+ end
64
+ end
65
+
66
+ if options[:site_settings]
67
+ converted = options[:site_settings].map { |k,v| [k.to_s,v] }.to_h #convert symbol keys to string keys
68
+ converted.compact!
69
+ @site_settings.merge! converted
70
+ end
71
+
72
+ required = [:supervisors,:rsmp_versions,:site_id,:watchdog_interval,:watchdog_timeout,
73
+ :acknowledgement_timeout,:command_response_timeout,:log]
74
+ check_required_settings @site_settings, required
75
+
76
+ setup_components @site_settings['components']
77
+ end
78
+
79
+ def reconnect
80
+ @sleep_condition.signal
81
+ end
82
+
83
+ def start_action
84
+ @site_settings["supervisors"].each do |supervisor_settings|
85
+ @task.async do |task|
86
+ task.annotate "site_proxy"
87
+ connect_to_supervisor task, supervisor_settings
88
+ end
89
+ end
90
+ end
91
+
92
+ def build_connector settings
93
+ SupervisorProxy.new settings
94
+ end
95
+
96
+ def aggrated_status_changed component
97
+ @proxies.each do |proxy|
98
+ proxy.send_aggregated_status component
99
+ end
100
+ end
101
+
102
+ def connect_to_supervisor task, supervisor_settings
103
+ proxy = build_connector({
104
+ site: self,
105
+ task: @task,
106
+ settings: @site_settings,
107
+ ip: supervisor_settings['ip'],
108
+ port: supervisor_settings['port'],
109
+ logger: @logger,
110
+ archive: @archive
111
+ })
112
+ @proxies << proxy
113
+ run_site_proxy task, proxy
114
+ ensure
115
+ @proxies.delete proxy
116
+ end
117
+
118
+ def run_site_proxy task, proxy
119
+ loop do
120
+ proxy.run # run until disconnected
121
+ rescue IOError => e
122
+ log "Stream error: #{e}", level: :warning
123
+ rescue SystemCallError => e # all ERRNO errors
124
+ log "Reader exception: #{e.to_s}", level: :error
125
+ rescue StandardError => e
126
+ log ["Reader exception: #{e}",e.backtrace].flatten.join("\n"), level: :error
127
+ ensure
128
+ begin
129
+ if @site_settings["reconnect_interval"] != :no
130
+ # sleep until waken by reconnect() or the reconnect interval passed
131
+ proxy.set_state :wait_for_reconnect
132
+ task.with_timeout(@site_settings["reconnect_interval"]) { @sleep_condition.wait }
133
+ else
134
+ proxy.set_state :cannot_connect
135
+ break
136
+ end
137
+ rescue Async::TimeoutError
138
+ # ignore
139
+ end
140
+ end
141
+ end
142
+
143
+ def stop
144
+ log "Stopping site #{@site_settings["site_id"]}", level: :info
145
+ @proxies.each do |proxy|
146
+ proxy.stop
147
+ end
148
+ @proxies.clear
149
+ super
150
+ end
151
+
152
+ def starting
153
+ log "Starting site #{@site_settings["site_id"]}",
154
+ level: :info,
155
+ timestamp: RSMP.now_object
156
+ end
157
+
158
+ def alarm
159
+ @proxies.each do |proxy|
160
+ proxy.stop
161
+ end
162
+ end
163
+
164
+ end
165
+ end
@@ -0,0 +1,26 @@
1
+ # Things shared between sites and site proxies
2
+
3
+ module RSMP
4
+ module SiteBase
5
+ attr_reader :components
6
+
7
+ def initialize_site
8
+ @components = {}
9
+ end
10
+
11
+ def aggrated_status_changed component
12
+ end
13
+
14
+ def setup_components settings
15
+ return unless settings
16
+ settings.each_pair do |id,settings|
17
+ @components[id] = build_component(id,settings)
18
+ end
19
+ end
20
+
21
+ def build_component id, settings={}
22
+ Component.new id: id, node: self, grouped: true
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,220 @@
1
+ # Handles a supervisor connection to a remote client
2
+
3
+ module RSMP
4
+ class SiteProxy < Proxy
5
+ include SiteBase
6
+
7
+ attr_reader :supervisor
8
+
9
+ def initialize options
10
+ super options
11
+ initialize_site
12
+ @supervisor = options[:supervisor]
13
+ @settings = @supervisor.supervisor_settings.clone
14
+ end
15
+
16
+ def node
17
+ supervisor
18
+ end
19
+
20
+ def start
21
+ super
22
+ start_reader
23
+ end
24
+
25
+ def connection_complete
26
+ super
27
+ log "Connection to site #{@site_ids.first} established", level: :info
28
+ end
29
+
30
+ def process_message message
31
+ case message
32
+ when CommandRequest
33
+ when StatusRequest
34
+ when StatusSubscribe
35
+ when StatusUnsubscribe
36
+ will_not_handle message
37
+ when AggregatedStatus
38
+ process_aggregated_status message
39
+ when Alarm
40
+ process_alarm message
41
+ when CommandResponse
42
+ process_command_response message
43
+ when StatusResponse
44
+ process_status_response message
45
+ when StatusUpdate
46
+ process_status_update message
47
+ else
48
+ super message
49
+ end
50
+ end
51
+
52
+ def version_accepted message, rsmp_version
53
+ log "Received Version message for sites [#{@site_ids.join(',')}] using RSMP #{rsmp_version}", message: message, level: :log
54
+ start_timer
55
+ acknowledge message
56
+ send_version rsmp_version
57
+ @version_determined = true
58
+
59
+ site_id = @site_ids.first
60
+ if @settings['sites']
61
+ @site_settings = @settings['sites'][site_id]
62
+ if @site_settings
63
+ setup_components @site_settings['components']
64
+ end
65
+ end
66
+ end
67
+
68
+ def validate_aggregated_status message, se
69
+ unless se && se.is_a?(Array) && se.size == 8
70
+ reason = "invalid AggregatedStatus, 'se' must be an Array of size 8"
71
+ dont_acknowledge message, "Received", reaons
72
+ raise InvalidMessage
73
+ end
74
+ end
75
+
76
+ def process_aggregated_status message
77
+ se = message.attribute("se")
78
+ validate_aggregated_status(message,se) == false
79
+ c_id = message.attributes["cId"]
80
+ component = @components[c_id]
81
+ if component == nil
82
+ if @site_settings == nil || @site_settings['components'] == nil
83
+ component = build_component c_id
84
+ log "Adding component #{c_id} to site #{site_id}", level: :info
85
+ else
86
+ reason = "component #{c_id} not found"
87
+ dont_acknowledge message, "Ignoring #{message.type}:", reason
88
+ return
89
+ end
90
+ end
91
+
92
+ component.set_aggregated_status_bools se
93
+ log "Received #{message.type} status for component #{c_id} [#{component.aggregated_status.join(', ')}]", message: message
94
+ acknowledge message
95
+ end
96
+
97
+ def aggrated_status_changed component
98
+ @supervisor.aggregated_status_changed self, component
99
+ end
100
+
101
+ def process_alarm message
102
+ alarm_code = message.attribute("aCId")
103
+ asp = message.attribute("aSp")
104
+ status = ["ack","aS","sS"].map { |key| message.attribute(key) }.join(',')
105
+ log "Received #{message.type}, #{alarm_code} #{asp} [#{status}]", message: message, level: :log
106
+ acknowledge message
107
+ end
108
+
109
+ def version_acknowledged
110
+ connection_complete
111
+ end
112
+
113
+ def process_watchdog message
114
+ super
115
+ if @watchdog_started == false
116
+ start_watchdog
117
+ end
118
+ end
119
+
120
+ def site_ids_changed
121
+ @supervisor.site_ids_changed
122
+ end
123
+
124
+ def check_site_id site_id
125
+ @site_settings = @supervisor.check_site_id site_id
126
+ end
127
+
128
+ def request_status component, status_list, timeout=nil
129
+ raise NotReady unless @state == :ready
130
+ message = RSMP::StatusRequest.new({
131
+ "ntsOId" => '',
132
+ "xNId" => '',
133
+ "cId" => component,
134
+ "sS" => status_list
135
+ })
136
+ send_message message
137
+ return message, wait_for_status_response(component: component, timeout: timeout)
138
+ end
139
+
140
+ def process_status_response message
141
+ log "Received #{message.type}", message: message, level: :log
142
+ acknowledge message
143
+ end
144
+
145
+ def wait_for_status_response options
146
+ raise ArgumentError unless options[:component]
147
+ item = @archive.capture(@task, options.merge(type: "StatusResponse", with_message: true, num: 1)) do |item|
148
+ # check component
149
+ end
150
+ item[:message] if item
151
+ end
152
+
153
+ def subscribe_to_status component, status_list
154
+ raise NotReady unless @state == :ready
155
+ message = RSMP::StatusSubscribe.new({
156
+ "ntsOId" => '',
157
+ "xNId" => '',
158
+ "cId" => component,
159
+ "sS" => status_list
160
+ })
161
+ send_message message
162
+ message
163
+ end
164
+
165
+ def unsubscribe_to_status component, status_list
166
+ raise NotReady unless @state == :ready
167
+ message = RSMP::StatusUnsubscribe.new({
168
+ "ntsOId" => '',
169
+ "xNId" => '',
170
+ "cId" => component,
171
+ "sS" => status_list
172
+ })
173
+ send_message message
174
+ message
175
+ end
176
+
177
+ def process_status_update message
178
+ log "Received #{message.type}", message: message, level: :log
179
+ acknowledge message
180
+ end
181
+
182
+ def wait_for_status_update options={}
183
+ raise ArgumentError unless options[:component]
184
+ item = @archive.capture(options.merge(type: "StatusUpdate", with_message: true, num: 1)) do |item|
185
+ # check component
186
+ end
187
+ item[:message] if item
188
+ end
189
+
190
+ def send_command component, args
191
+ raise NotReady unless @state == :ready
192
+ message = RSMP::CommandRequest.new({
193
+ "ntsOId" => '',
194
+ "xNId" => '',
195
+ "cId" => component,
196
+ "arg" => args
197
+ })
198
+ send_message message
199
+ message
200
+ end
201
+
202
+ def process_command_response message
203
+ log "Received #{message.type}", message: message, level: :log
204
+ acknowledge message
205
+ end
206
+
207
+ def wait_for_command_response options
208
+ raise ArgumentError unless options[:component]
209
+ item = @archive.capture(@task,options.merge(num: 1, type: "CommandResponse", with_message: true)) do |item|
210
+ # check component
211
+ end
212
+ item[:message] if item
213
+ end
214
+
215
+ def set_watchdog_interval interval
216
+ @settings["watchdog_interval"] = interval
217
+ end
218
+
219
+ end
220
+ end
@@ -0,0 +1,220 @@
1
+ # RSMP supervisor (server)
2
+ # The supervisor waits for sites to connect.
3
+ # Connections to sites are handles via site proxies.
4
+
5
+ module RSMP
6
+ class Supervisor < Node
7
+ attr_reader :rsmp_versions, :site_id, :supervisor_settings, :proxies, :logger
8
+
9
+ def initialize options={}
10
+ handle_supervisor_settings options
11
+ super options.merge log_settings: @supervisor_settings["log"]
12
+ @proxies = []
13
+ @site_id_condition = Async::Notification.new
14
+ end
15
+
16
+ def site_id
17
+ @supervisor_settings['site_id']
18
+ end
19
+
20
+ def handle_supervisor_settings options
21
+ @supervisor_settings = {
22
+ 'site_id' => 'RN+SU0001',
23
+ 'port' => 12111,
24
+ 'rsmp_versions' => ['3.1.1','3.1.2','3.1.3','3.1.4'],
25
+ 'timer_interval' => 0.1,
26
+ 'watchdog_interval' => 1,
27
+ 'watchdog_timeout' => 2,
28
+ 'acknowledgement_timeout' => 2,
29
+ 'command_response_timeout' => 1,
30
+ 'status_response_timeout' => 1,
31
+ 'status_update_timeout' => 1,
32
+ 'site_connect_timeout' => 2,
33
+ 'site_ready_timeout' => 1,
34
+ 'log' => {
35
+ 'active' => true,
36
+ 'color' => true,
37
+ 'ip' => false,
38
+ 'timestamp' => true,
39
+ 'site_id' => true,
40
+ 'level' => false,
41
+ 'acknowledgements' => false,
42
+ 'watchdogs' => false,
43
+ 'json' => false
44
+ }
45
+ }
46
+
47
+ if options[:supervisor_settings_path]
48
+ if File.exist? options[:supervisor_settings_path]
49
+ @supervisor_settings.merge! YAML.load_file(options[:supervisor_settings_path])
50
+ else
51
+ puts "Error: Site settings #{options[:supervisor_settings_path]} not found"
52
+ exit
53
+ end
54
+
55
+ end
56
+
57
+ if options[:supervisor_settings]
58
+ converted = options[:supervisor_settings].map { |k,v| [k.to_s,v] }.to_h #convert symbol keys to string keys
59
+ @supervisor_settings.merge! converted
60
+ end
61
+
62
+ required = [:port, :rsmp_versions, :site_id, :watchdog_interval, :watchdog_timeout,
63
+ :acknowledgement_timeout, :command_response_timeout, :log]
64
+ check_required_settings @supervisor_settings, required
65
+
66
+ @rsmp_versions = @supervisor_settings["rsmp_versions"]
67
+
68
+ # randomize site id
69
+ #@supervisor_settings["site_id"] = "RN+SU#{rand(9999).to_i}"
70
+
71
+ # randomize port
72
+ #@supervisor_settings["port"] = @supervisor_settings["port"] + rand(10).to_i
73
+ end
74
+
75
+ def start_action
76
+ @endpoint = Async::IO::Endpoint.tcp('0.0.0.0', @supervisor_settings["port"])
77
+ @endpoint.accept do |socket|
78
+ handle_connection(socket)
79
+ end
80
+ rescue SystemCallError => e # all ERRNO errors
81
+ log "Exception: #{e.to_s}", level: :error
82
+ rescue StandardError => e
83
+ log ["Exception: #{e.inspect}",e.backtrace].flatten.join("\n"), level: :error
84
+ end
85
+
86
+ def stop
87
+ log "Stopping supervisor #{@supervisor_settings["site_id"]}", level: :info
88
+ @proxies.each { |proxy| proxy.stop }
89
+ @proxies.clear
90
+ super
91
+ @tcp_server.close if @tcp_server
92
+ @tcp_server = nil
93
+ end
94
+
95
+ def handle_connection socket
96
+ remote_port = socket.remote_address.ip_port
97
+ remote_hostname = socket.remote_address.ip_address
98
+ remote_ip = socket.remote_address.ip_address
99
+
100
+ info = {ip:remote_ip, port:remote_port, hostname:remote_hostname, now:RSMP.now_string()}
101
+ if accept? socket, info
102
+ connect socket, info
103
+ else
104
+ reject socket, info
105
+ end
106
+ rescue SystemCallError => e # all ERRNO errors
107
+ log "Exception: #{e.to_s}", level: :error
108
+ rescue StandardError => e
109
+ log "Exception: #{e}", exception: e, level: :error
110
+ ensure
111
+ close socket, info
112
+ end
113
+
114
+ def starting
115
+ log "Starting supervisor #{@supervisor_settings["site_id"]} on port #{@supervisor_settings["port"]}",
116
+ level: :info,
117
+ timestamp: RSMP.now_object
118
+ end
119
+
120
+ def accept? socket, info
121
+ true
122
+ end
123
+
124
+ def build_connector settings
125
+ SiteProxy.new settings
126
+ end
127
+
128
+ def connect socket, info
129
+ if @supervisor_settings['log']['hide_ip_and_port']
130
+ port_and_port = '********'
131
+ else
132
+ port_and_port = "#{info[:ip]}:#{info[:port]}"
133
+ end
134
+
135
+ log "Site connected from #{port_and_port}",
136
+ ip: port_and_port,
137
+ level: :info,
138
+ timestamp: RSMP.now_object
139
+
140
+ proxy = build_connector({
141
+ supervisor: self,
142
+ task: @task,
143
+ settings: @supervisor_settings[:sites],
144
+ socket: socket,
145
+ info: info,
146
+ logger: @logger,
147
+ archive: @archive
148
+ })
149
+ @proxies.push proxy
150
+
151
+ proxy.run # will run until the site disconnects
152
+ @proxies.delete proxy
153
+ site_ids_changed
154
+ end
155
+
156
+ def site_ids_changed
157
+ @site_id_condition.signal
158
+ end
159
+
160
+ def reject socket, info
161
+ log "Site rejected", ip: info[:ip], level: :info
162
+ end
163
+
164
+ def close socket, info
165
+ if info
166
+ log "Connection to #{info[:ip]}:#{info[:port]} closed", ip: info[:ip], level: :info, timestamp: RSMP.now_object
167
+ else
168
+ log "Connection closed", level: :info, timestamp: RSMP.now_object
169
+ end
170
+
171
+ socket.close
172
+ end
173
+
174
+ def site_connected? site_id
175
+ return find_site(site_id) != nil
176
+ end
177
+
178
+ def find_site site_id
179
+ @proxies.each do |site|
180
+ return site if site_id == :any || site.site_ids.include?(site_id)
181
+ end
182
+ nil
183
+ end
184
+
185
+ def wait_for_site site_id, timeout
186
+ RSMP::Wait.wait_for(@task,@site_id_condition,timeout) { find_site site_id }
187
+ rescue Async::TimeoutError
188
+ nil
189
+ end
190
+
191
+ def wait_for_site_disconnect site_id, timeout
192
+ RSMP::Wait.wait_for(@task,@site_id_condition,timeout) { true unless find_site site_id }
193
+ rescue Async::TimeoutError
194
+ false
195
+ end
196
+
197
+ def check_site_id site_id
198
+ check_site_already_connected site_id
199
+ return find_allowed_site_setting site_id
200
+ end
201
+
202
+ def check_site_already_connected site_id
203
+ raise FatalError.new "Site #{site_id} already connected" if find_site(site_id)
204
+ end
205
+
206
+ def find_allowed_site_setting site_id
207
+ return {} unless @supervisor_settings['sites']
208
+ @supervisor_settings['sites'].each_pair do |id,settings|
209
+ if id == site_id
210
+ return settings
211
+ end
212
+ end
213
+ raise FatalError.new "site id #{site_id} rejected"
214
+ end
215
+
216
+ def aggregated_status_changed site_proxy, component
217
+ end
218
+
219
+ end
220
+ end
@@ -0,0 +1,10 @@
1
+ # Things shared between sites and site proxies
2
+
3
+ module RSMP
4
+ module SupervisorBase
5
+
6
+ def initialize_supervisor
7
+ end
8
+
9
+ end
10
+ end