rsmp 0.1.19 → 0.1.31

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,206 @@
1
+ # waiting for various types of messages and reponses from remote sites
2
+ module RSMP
3
+ module SiteProxyWait
4
+
5
+ def wait_for_status_updates parent_task, options={}, &send_block
6
+ send_while_collecting parent_task, send_block do |task, m_id|
7
+ collect_status_updates_or_responses task, 'StatusUpdate', options, m_id
8
+ end
9
+ end
10
+
11
+ def wait_for_status_responses parent_task, options={}, &send_block
12
+ send_while_collecting parent_task, send_block do |task, m_id|
13
+ collect_status_updates_or_responses task, 'StatusResponse', options, m_id
14
+ end
15
+ end
16
+
17
+ def wait_for_command_responses parent_task, options={}, &send_block
18
+ send_while_collecting parent_task, send_block do |task, m_id|
19
+ collect_command_responses task, options, m_id
20
+ end
21
+ end
22
+
23
+ def wait_for_alarm parent_task, options={}
24
+ matching_alarm = nil
25
+ message = collect(parent_task,options.merge(type: "Alarm", with_message: true, num: 1)) do |message|
26
+ # TODO check components
27
+ matching_alarm = nil
28
+ alarm = message
29
+ next if options[:aCId] && options[:aCId] != alarm.attribute("aCId")
30
+ next if options[:aSp] && options[:aSp] != alarm.attribute("aSp")
31
+ next if options[:aS] && options[:aS] != alarm.attribute("aS")
32
+ matching_alarm = alarm
33
+ break
34
+ end
35
+ if item
36
+ { message: message, status: matching_alarm }
37
+ end
38
+ end
39
+
40
+ def collect_status_updates task, options, m_id
41
+ collect_status_updates_or_responses task, 'StatusUpdate', options, m_id
42
+ end
43
+
44
+ def collect_status_responses task, options, m_id
45
+ collect_status_updates_or_responses task, 'StatusResponse', options, m_id
46
+ end
47
+
48
+ def collect_command_responses parent_task, options, m_id
49
+ task.annotate "wait for command response"
50
+ want = options[:command_list].clone
51
+ result = {}
52
+ messages = []
53
+ collect(parent_task,options.merge({
54
+ type: ['CommandResponse','MessageNotAck'],
55
+ num: 1
56
+ })) do |message|
57
+ if message.is_a?(MessageNotAck)
58
+ if message.attribute('oMId') == m_id
59
+ # set result to an exception, but don't raise it.
60
+ # this will be returned by the task and stored as the task result
61
+ # when the parent task call wait() on the task, the exception
62
+ # will be raised in the parent task, and caught by rspec.
63
+ # rspec will then show the error and record the test as failed
64
+ m_id_short = RSMP::Message.shorten_m_id m_id, 8
65
+ result = RSMP::MessageRejected.new "Command request #{m_id_short} was rejected: #{message.attribute('rea')}"
66
+ next true # done, no more messages wanted
67
+ else
68
+ false
69
+ end
70
+ else
71
+ add = false
72
+ # look through querues
73
+ want.each_with_index do |query,i|
74
+ # look through items in message
75
+ message.attributes['rvs'].each do |input|
76
+ matching = command_match? query, input
77
+ if matching == true
78
+ result[query] = input
79
+ add = true
80
+ elsif matching == false
81
+ result.delete query
82
+ end
83
+ end
84
+ end
85
+ messages << message if add
86
+ result.size == want.size # any queries left to match?
87
+ end
88
+ end
89
+ return result, messages
90
+ rescue Async::TimeoutError
91
+ raise RSMP::TimeoutError.new "Did not receive correct command response to #{m_id} within #{options[:timeout]}s"
92
+ end
93
+
94
+ def collect_status_updates_or_responses task, type, options, m_id
95
+ want = options[:status_list].clone
96
+ result = {}
97
+ messages = []
98
+ # wait for a status update
99
+ collect(task,options.merge({
100
+ type: [type,'MessageNotAck'],
101
+ num: 1
102
+ })) do |message|
103
+ if message.is_a?(MessageNotAck)
104
+ if message.attribute('oMId') == m_id
105
+ # set result to an exception, but don't raise it.
106
+ # this will be returned by the task and stored as the task result
107
+ # when the parent task call wait() on the task, the exception
108
+ # will be raised in the parent task, and caught by rspec.
109
+ # rspec will then show the error and record the test as failed
110
+ m_id_short = RSMP::Message.shorten_m_id m_id, 8
111
+ result = RSMP::MessageRejected.new "Status request #{m_id_short} was rejected: #{message.attribute('rea')}"
112
+ next true # done, no more messages wanted
113
+ end
114
+ false
115
+ else
116
+ found = []
117
+ add = false
118
+ # look through querues
119
+ want.each_with_index do |query,i|
120
+ # look through status items in message
121
+ message.attributes['sS'].each do |input|
122
+ matching = status_match? query, input
123
+ if matching == true
124
+ result[query] = input
125
+ add = true
126
+ elsif matching == false
127
+ result.delete query
128
+ end
129
+ end
130
+ end
131
+ messages << message if add
132
+ result.size == want.size # any queries left to match?
133
+ end
134
+ end
135
+ return result, messages
136
+ rescue Async::TimeoutError
137
+ type_str = {'StatusUpdate'=>'update', 'StatusResponse'=>'response'}[type]
138
+ raise RSMP::TimeoutError.new "Did not received correct status #{type_str} in reply to #{m_id} within #{options[:timeout]}s"
139
+ end
140
+
141
+ def status_match? query, item
142
+ return nil if query['sCI'] && query['sCI'] != item['sCI']
143
+ return nil if query['n'] && query['n'] != item['n']
144
+ return false if query['q'] && query['q'] != item['q']
145
+ if query['s'].is_a? Regexp
146
+ return false if query['s'] && item['s'] !~ query['s']
147
+ else
148
+ return false if query['s'] && item['s'] != query['s']
149
+ end
150
+ true
151
+ end
152
+
153
+ def command_match? query, item
154
+ return nil if query['cCI'] && query['cCI'] != item['cCI']
155
+ return nil if query['n'] && query['n'] != item['n']
156
+ if query['v'].is_a? Regexp
157
+ return false if query['v'] && item['v'] !~ query['v']
158
+ else
159
+ return false if query['v'] && item['v'] != query['v']
160
+ end
161
+ true
162
+ end
163
+
164
+ def send_while_collecting parent_task, send_block, &collect_block
165
+ m_id = RSMP::Message.make_m_id # make message id so we can start waiting for it
166
+
167
+ # wait for command responses in an async task
168
+ task = parent_task.async do |task|
169
+ collect_block.call task, m_id
170
+ rescue StandardError => e
171
+ notify_error e, level: :internal
172
+ end
173
+
174
+ # call block, it should send command request using the given m_id
175
+ send_block.call m_id
176
+
177
+ # wait for the response and return it, raise exception if NotAck received, it it timed out
178
+ task.wait
179
+ end
180
+
181
+ def wait_for_aggregated_status parent_task, options={}
182
+ collect(parent_task,options.merge({
183
+ type: ['AggregatedStatus','MessageNotAck'],
184
+ num: 1
185
+ })) do |message|
186
+ if message.is_a?(MessageNotAck)
187
+ if message.attribute('oMId') == m_id
188
+ # set result to an exception, but don't raise it.
189
+ # this will be returned by the task and stored as the task result
190
+ # when the parent task call wait() on the task, the exception
191
+ # will be raised in the parent task, and caught by rspec.
192
+ # rspec will then show the error and record the test as failed
193
+ m_id_short = RSMP::Message.shorten_m_id m_id, 8
194
+ result = RSMP::MessageRejected.new "Aggregated status request #{m_id_short} was rejected: #{message.attribute('rea')}"
195
+ next true # done, no more messages wanted
196
+ else
197
+ false
198
+ end
199
+ else
200
+ true
201
+ end
202
+ end
203
+ end
204
+
205
+ end
206
+ end
@@ -17,47 +17,57 @@ module RSMP
17
17
  @supervisor_settings['site_id']
18
18
  end
19
19
 
20
- def handle_supervisor_settings options
21
- @supervisor_settings = {
20
+ def handle_supervisor_settings options={}
21
+ defaults = {
22
22
  'port' => 12111,
23
- 'rsmp_versions' => ['3.1.1','3.1.2','3.1.3','3.1.4'],
24
- 'timer_interval' => 0.1,
25
- 'watchdog_interval' => 1,
26
- 'watchdog_timeout' => 2,
27
- 'acknowledgement_timeout' => 2,
28
- 'command_response_timeout' => 1,
29
- 'status_response_timeout' => 1,
30
- 'status_update_timeout' => 1,
31
- 'site_connect_timeout' => 2,
32
- 'site_ready_timeout' => 1,
33
- 'stop_after_first_session' => false,
34
- 'sites' => {
35
- :any => {}
23
+ 'ips' => 'all',
24
+ 'guest' => {
25
+ 'rsmp_versions' => 'all',
26
+ 'sxl' => 'tlc',
27
+ 'intervals' => {
28
+ 'timer' => 1,
29
+ 'watchdog' => 1
30
+ },
31
+ 'timeouts' => {
32
+ 'watchdog' => 2,
33
+ 'acknowledgement' => 2
34
+ }
36
35
  }
37
36
  }
38
-
39
- if options[:supervisor_settings]
40
- converted = options[:supervisor_settings].map { |k,v| [k.to_s,v] }.to_h #convert symbol keys to string keys
41
- converted.compact!
42
- @supervisor_settings.merge! converted
43
- end
44
-
45
- required = [:port, :rsmp_versions, :watchdog_interval, :watchdog_timeout,
46
- :acknowledgement_timeout, :command_response_timeout]
47
- check_required_settings @supervisor_settings, required
48
37
 
38
+ # merge options into defaults
39
+ @supervisor_settings = defaults.deep_merge(options[:supervisor_settings] || {})
49
40
  @rsmp_versions = @supervisor_settings["rsmp_versions"]
41
+ check_site_sxl_types
42
+ end
43
+
44
+ def check_site_sxl_types
45
+ sites = @supervisor_settings['sites'].clone || {}
46
+ sites['guest'] = @supervisor_settings['guest']
47
+ sites.each do |site_id,settings|
48
+ unless settings
49
+ raise RSMP::ConfigurationError.new("Configuration for site '#{site_id}' is empty")
50
+ end
51
+ sxl = settings['sxl']
52
+ sxl = 'tlc' unless sxl # temporary fix until configs are updated
53
+ unless sxl
54
+ raise RSMP::ConfigurationError.new("Configuration error for site '#{site_id}': No SXL specified")
55
+ end
56
+ RSMP::Schemer.find_schemas! sxl if sxl
57
+ rescue RSMP::Schemer::UnknownSchemaError => e
58
+ raise RSMP::ConfigurationError.new("Configuration error for site '#{site_id}': #{e}")
59
+ end
50
60
  end
51
61
 
52
62
  def start_action
53
63
  @endpoint = Async::IO::Endpoint.tcp('0.0.0.0', @supervisor_settings["port"])
54
- @endpoint.accept do |socket|
64
+ @endpoint.accept do |socket| # creates async tasks
55
65
  handle_connection(socket)
66
+ rescue StandardError => e
67
+ notify_error e, level: :internal
56
68
  end
57
- rescue SystemCallError => e # all ERRNO errors
58
- log "Exception: #{e.to_s}", level: :error
59
69
  rescue StandardError => e
60
- log ["Exception: #{e.inspect}",e.backtrace].flatten.join("\n"), level: :error
70
+ notify_error e, level: :internal
61
71
  end
62
72
 
63
73
  def stop
@@ -74,16 +84,18 @@ module RSMP
74
84
  remote_hostname = socket.remote_address.ip_address
75
85
  remote_ip = socket.remote_address.ip_address
76
86
 
77
- info = {ip:remote_ip, port:remote_port, hostname:remote_hostname, now:RSMP.now_string()}
87
+ info = {ip:remote_ip, port:remote_port, hostname:remote_hostname, now:Clock.now}
78
88
  if accept? socket, info
79
89
  connect socket, info
80
90
  else
81
91
  reject socket, info
82
92
  end
83
- rescue SystemCallError => e # all ERRNO errors
84
- log "Exception: #{e.to_s}", level: :error
93
+ rescue ConnectionError => e
94
+ log "Rejected connection from #{remote_ip}:#{remote_port}, #{e.to_s}", level: :warning
95
+ notify_error e
85
96
  rescue StandardError => e
86
- log "Exception: #{e}", exception: e, level: :error
97
+ log "Connection: #{e.to_s}", exception: e, level: :error
98
+ notify_error e, level: :internal
87
99
  ensure
88
100
  close socket, info
89
101
  end
@@ -91,7 +103,7 @@ module RSMP
91
103
  def starting
92
104
  log "Starting supervisor on port #{@supervisor_settings["port"]}",
93
105
  level: :info,
94
- timestamp: RSMP.now_object
106
+ timestamp: @clock.now
95
107
  end
96
108
 
97
109
  def accept? socket, info
@@ -110,30 +122,49 @@ module RSMP
110
122
  end
111
123
  end
112
124
 
125
+ def authorize_ip ip
126
+ return if @supervisor_settings['ips'] == 'all'
127
+ return if @supervisor_settings['ips'].include? ip
128
+ raise ConnectionError.new('guest ip not allowed')
129
+ end
130
+
131
+ def check_max_sites
132
+ max = @supervisor_settings['max_sites']
133
+ if max
134
+ if @proxies.size >= max
135
+ raise ConnectionError.new("maximum of #{max} sites already connected")
136
+ end
137
+ end
138
+ end
139
+
113
140
  def connect socket, info
114
141
  log "Site connected from #{format_ip_and_port(info)}",
115
142
  ip: info[:ip],
116
143
  port: info[:port],
117
144
  level: :info,
118
- timestamp: RSMP.now_object
145
+ timestamp: Clock.now
146
+
147
+ authorize_ip info[:ip]
148
+ check_max_sites
119
149
 
120
150
  proxy = build_proxy({
121
151
  supervisor: self,
152
+ ip: info[:ip],
153
+ port: info[:port],
122
154
  task: @task,
123
- settings: @supervisor_settings[:sites],
155
+ settings: {'collect'=>@supervisor_settings['collect']},
124
156
  socket: socket,
125
157
  info: info,
126
158
  logger: @logger,
127
159
  archive: @archive
128
160
  })
129
161
  @proxies.push proxy
130
-
131
162
  proxy.run # will run until the site disconnects
132
163
  ensure
133
164
  @proxies.delete proxy
134
165
  site_ids_changed
135
166
 
136
- stop if @supervisor_settings['stop_after_first_session']
167
+ stop if @supervisor_settings['one_shot']
137
168
  end
138
169
 
139
170
  def site_ids_changed
@@ -146,9 +177,9 @@ module RSMP
146
177
 
147
178
  def close socket, info
148
179
  if info
149
- log "Connection to #{format_ip_and_port(info)} closed", ip: info[:ip], level: :info, timestamp: RSMP.now_object
180
+ log "Connection to #{format_ip_and_port(info)} closed", ip: info[:ip], level: :info, timestamp: Clock.now
150
181
  else
151
- log "Connection closed", level: :info, timestamp: RSMP.now_object
182
+ log "Connection closed", level: :info, timestamp: Clock.now
152
183
  end
153
184
 
154
185
  socket.close
@@ -170,32 +201,36 @@ module RSMP
170
201
  return site if site
171
202
  wait_for(@site_id_condition,timeout) { find_site site_id }
172
203
  rescue Async::TimeoutError
173
- nil
204
+ raise RSMP::TimeoutError.new "Site '#{site_id}' did not connect within #{timeout}s"
174
205
  end
175
206
 
176
207
  def wait_for_site_disconnect site_id, timeout
177
208
  wait_for(@site_id_condition,timeout) { true unless find_site site_id }
178
209
  rescue Async::TimeoutError
179
- false
210
+ raise RSMP::TimeoutError.new "Site '#{site_id}' did not disconnect within #{timeout}s"
180
211
  end
181
212
 
182
213
  def check_site_id site_id
183
214
  check_site_already_connected site_id
184
- return find_allowed_site_setting site_id
215
+ return site_id_to_site_setting site_id
185
216
  end
186
217
 
187
218
  def check_site_already_connected site_id
188
- raise FatalError.new "Site #{site_id} already connected" if find_site(site_id)
219
+ raise FatalError.new "Site '#{site_id}' already connected" if find_site(site_id)
189
220
  end
190
221
 
191
- def find_allowed_site_setting site_id
222
+ def site_id_to_site_setting site_id
192
223
  return {} unless @supervisor_settings['sites']
193
224
  @supervisor_settings['sites'].each_pair do |id,settings|
194
- if id == :any || id == site_id
225
+ if id == 'guest' || id == site_id
195
226
  return settings
196
227
  end
197
228
  end
198
- raise FatalError.new "site id #{site_id} rejected"
229
+ raise FatalError.new "site id #{site_id} unknown"
230
+ end
231
+
232
+ def ip_to_site_settings ip
233
+ @supervisor_settings['sites'][ip] || @supervisor_settings['sites']['guest']
199
234
  end
200
235
 
201
236
  def aggregated_status_changed site_proxy, component
@@ -14,7 +14,6 @@ module RSMP
14
14
  @ip = options[:ip]
15
15
  @port = options[:port]
16
16
  @status_subscriptions = {}
17
- @status_subscriptions_mutex = Mutex.new
18
17
  @sxl = @site_settings['sxl']
19
18
  @synthetic_id = Supervisor.build_id_from_ip_port @ip, @port
20
19
  end
@@ -33,8 +32,8 @@ module RSMP
33
32
  send_version @site_settings['site_id'], @site_settings["rsmp_versions"]
34
33
  rescue Errno::ECONNREFUSED
35
34
  log "No connection to supervisor at #{@ip}:#{@port}", level: :error
36
- unless @site.site_settings["reconnect_interval"] == :no
37
- log "Will try to reconnect again every #{@site.site_settings["reconnect_interval"]} seconds..", level: :info
35
+ unless @site.site_settings['intervals']['reconnect'] == :no
36
+ log "Will try to reconnect again every #{@site.site_settings['intervals']['reconnect']} seconds..", level: :info
38
37
  @logger.mute @ip, @port
39
38
  end
40
39
  end
@@ -50,12 +49,12 @@ module RSMP
50
49
  @endpoint = Async::IO::Endpoint.tcp(@ip, @port)
51
50
  @socket = @endpoint.connect
52
51
  @stream = Async::IO::Stream.new(@socket)
53
- @protocol = Async::IO::Protocol::Line.new(@stream,RSMP::WRAPPING_DELIMITER) # rsmp messages are json terminated with a form-feed
52
+ @protocol = Async::IO::Protocol::Line.new(@stream,WRAPPING_DELIMITER) # rsmp messages are json terminated with a form-feed
54
53
  end
55
54
 
56
55
  def connection_complete
57
56
  super
58
- log "Connection to supervisor established", level: :info
57
+ log "Connection to supervisor established, using core #{@rsmp_version}, #{sxl} #{sxl_version}", level: :info
59
58
  start_watchdog
60
59
  end
61
60
 
@@ -66,6 +65,8 @@ module RSMP
66
65
  when StatusUpdate
67
66
  when AggregatedStatus
68
67
  will_not_handle message
68
+ when AggregatedStatusRequest
69
+ process_aggregated_status_request message
69
70
  when CommandRequest
70
71
  process_command_request message
71
72
  when CommandResponse
@@ -109,7 +110,7 @@ module RSMP
109
110
  end
110
111
 
111
112
  def reconnect_delay
112
- interval = @site_settings["reconnect_interval"]
113
+ interval = @site_settings['intervals']['reconnect']
113
114
  log "Waiting #{interval} seconds before trying to reconnect", level: :info
114
115
  @task.sleep interval
115
116
  end
@@ -124,7 +125,7 @@ module RSMP
124
125
 
125
126
  def send_aggregated_status component
126
127
  message = AggregatedStatus.new({
127
- "aSTS" => RSMP.now_string,
128
+ "aSTS" => clock.to_s,
128
129
  "cId" => component.c_id,
129
130
  "fP" => 'NormalControl',
130
131
  "fS" => nil,
@@ -162,6 +163,14 @@ module RSMP
162
163
  sorted
163
164
  end
164
165
 
166
+ def process_aggregated_status_request message
167
+ log "Received #{message.type}", message: message, level: :log
168
+ component_id = message.attributes["cId"]
169
+ component = @site.find_component component_id
170
+ acknowledge message
171
+ send_aggregated_status component
172
+ end
173
+
165
174
  def process_command_request message
166
175
  log "Received #{message.type}", message: message, level: :log
167
176
  component_id = message.attributes["cId"]
@@ -178,7 +187,7 @@ module RSMP
178
187
  end
179
188
  response = CommandResponse.new({
180
189
  "cId"=>component_id,
181
- "cTS"=>RSMP.now_string,
190
+ "cTS"=>clock.to_s,
182
191
  "rvs"=>rvs
183
192
  })
184
193
  acknowledge message
@@ -195,7 +204,7 @@ module RSMP
195
204
  end
196
205
  response = StatusResponse.new({
197
206
  "cId"=>component_id,
198
- "sTs"=>RSMP.now_string,
207
+ "sTs"=>clock.to_s,
199
208
  "sS"=>sS,
200
209
  "mId" => options[:m_id]
201
210
  })
@@ -222,7 +231,7 @@ module RSMP
222
231
  update_list[component] ||= {}
223
232
 
224
233
  subs = @status_subscriptions[component]
225
- now = RSMP::now_object
234
+ now = Time.now # internal timestamp
226
235
 
227
236
  message.attributes["sS"].each do |arg|
228
237
  sCI = arg["sCI"]
@@ -311,12 +320,10 @@ module RSMP
311
320
  end
312
321
  end
313
322
  send_status_updates update_list
314
- rescue StandardError => e
315
- log ["Status update exception: #{e}",e.backtrace].flatten.join("\n"), level: :error
316
323
  end
317
324
 
318
325
  def send_status_updates update_list
319
- now = RSMP.now_string
326
+ now = clock.to_s
320
327
  update_list.each_pair do |component_id,by_code|
321
328
  component = @site.find_component component_id
322
329
  sS = []
@@ -344,7 +351,7 @@ module RSMP
344
351
 
345
352
  def send_alarm
346
353
  message = Alarm.new({
347
- "aSTS"=>RSMP.now_string,
354
+ "aSTS"=>clock.to_s,
348
355
  "fP"=>nil,
349
356
  "fS"=>nil,
350
357
  "se"=>@site.aggregated_status_bools