rsmp 0.1.0

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