500f23ef34db6dae99b269fb9b17d1d759f9a9cc
[integration/test.git] / csit / libraries / IoTDM / iotdm_comm.py
1 """
2  This library provides methods using IoTDM client libs for testing
3  of communication with IoTDM.
4 """
5
6 #
7 # Copyright (c) 2017 Cisco Systems, Inc. and others.  All rights reserved.
8 #
9 # This program and the accompanying materials are made available under the
10 # terms of the Eclipse Public License v1.0 which accompanies this distribution,
11 # and is available at http://www.eclipse.org/legal/epl-v10.html
12 #
13
14 import json
15
16 from client_libs.iotdm_it_test_com import IoTDMItCommunicationFactory
17 from client_libs.iotdm_it_test_com import IoTDMJsonPrimitiveBuilder
18 from client_libs.iotdm_it_test_com import RequestAutoHandlingDescriptionBuilder
19 from client_libs.iotdm_it_test_com import RequestAutoHandlingDescription
20 import client_libs.onem2m_http as onem2m_http
21 from client_libs.onem2m_primitive import OneM2M
22
23
24 # Mapping of alliases to communication objects
25 __sessions = {}
26
27
28 def __get_session(allias, session):
29     if session:
30         return session
31
32     if allias in __sessions:
33         return __sessions[allias]
34
35     return None
36
37
38 def prepare_primitive_builder_raw(protocol, primitive_params, content=None, proto_specific_params=None):
39     """Creates primitive builder without any default data"""
40     builder = IoTDMJsonPrimitiveBuilder()\
41         .set_communication_protocol(protocol)\
42         .set_content(content)\
43         .set_parameters(primitive_params)\
44         .set_protocol_specific_parameters(proto_specific_params)
45     return builder
46
47
48 def new_primitive_raw(protocol, primitive_params, content=None, proto_specific_params=None):
49     """Creates primitive object without any default data"""
50     return prepare_primitive_builder_raw(protocol, primitive_params, content, proto_specific_params).build()
51
52
53 def prepare_primitive_builder(primitive_params, content=None, proto_specific_params=None,
54                               allias="default", communication=None):
55     """Creates primitive builder with default data set according communication object used"""
56     communication = __get_session(allias, communication)
57
58     builder = IoTDMJsonPrimitiveBuilder()\
59         .set_communication_protocol(communication.get_protocol())\
60         .set_parameters(communication.get_primitive_params())\
61         .set_protocol_specific_parameters(communication.get_protocol_params())\
62         .set_content(content)
63
64     if communication.get_protocol() == onem2m_http.HTTPPROTOCOLNAME and content:
65         builder.set_proto_param(onem2m_http.http_header_content_length, str(len(content)))
66
67     builder.append_parameters(primitive_params)\
68            .append_protocol_specific_parameters(proto_specific_params)
69
70     return builder
71
72
73 def new_primitive(primitive_params, content=None, proto_specific_params=None,
74                   allias="default", communication=None):
75     """Creates new primitive object with default data set according communication object used"""
76     return prepare_primitive_builder(primitive_params, content, proto_specific_params, allias, communication).build()
77
78
79 def _add_param(params, name, value):
80     if not name or not value:
81         return
82     params[name] = value
83
84
85 def prepare_request_primitive_builder(target_resource, content=None, operation=None, resource_type=None,
86                                       result_content=None, allias="default", communication=None):
87     """
88     Creates builder for request primitive with default data set according
89     communication object used
90     """
91     communication = __get_session(allias, communication)
92     if not communication or not target_resource:
93         raise AttributeError("Mandatory attributes not specified")
94
95     primitive_params = {}
96     _add_param(primitive_params, OneM2M.short_to, target_resource)
97     _add_param(primitive_params, OneM2M.short_operation, operation)
98     _add_param(primitive_params, OneM2M.short_resource_type, resource_type)
99     _add_param(primitive_params, OneM2M.short_result_content, result_content)
100
101     primitive_params = json.dumps(primitive_params)
102
103     builder = prepare_primitive_builder(primitive_params, content, communication=communication)
104     return builder
105
106
107 def new_create_request_primitive(target_resource, content, resource_type, result_content=None,
108                                  allias="default", communication=None):
109     """Creates request primitive for Create operation"""
110     return prepare_request_primitive_builder(target_resource, content, operation=OneM2M.operation_create,
111                                              resource_type=resource_type, result_content=result_content,
112                                              allias=allias, communication=communication).build()
113
114
115 def new_update_request_primitive(target_resource, content, result_content=None, allias="default", communication=None):
116     """Creates request primitive for Update operation"""
117     return prepare_request_primitive_builder(target_resource, content, operation=OneM2M.operation_update,
118                                              resource_type=None, result_content=result_content,
119                                              allias=allias, communication=communication).build()
120
121
122 def new_retrieve_request_primitive(target_resource, result_content=None, allias="default", communication=None):
123     """Creates request primitive for Retrieve operation"""
124     return prepare_request_primitive_builder(target_resource, content=None,
125                                              operation=OneM2M.operation_retrieve,
126                                              resource_type=None, result_content=result_content,
127                                              allias=allias, communication=communication).build()
128
129
130 def new_delete_request_primitive(target_resource, result_content=None, allias="default", communication=None):
131     """Creates request primitive for Delete operation"""
132     return prepare_request_primitive_builder(target_resource, content=None,
133                                              operation=OneM2M.operation_delete, result_content=result_content,
134                                              allias=allias, communication=communication).build()
135
136
137 def send_primitive(primitive, allias="default", communication=None):
138     """Sends primitive object using the communication object"""
139     communication = __get_session(allias, communication)
140     rsp = communication.send(primitive)
141     return rsp
142
143
144 def verify_exchange(request_primitive, response_primitive, status_code=None):
145     """Verifies request and response primitive parameters"""
146     request_primitive.check_exchange(response_primitive, rsc=status_code)
147
148
149 def verify_exchange_negative(request_primitive, response_primitive, status_code, error_message=None):
150     """Verifies request and error response primitive parameters"""
151     request_primitive.check_exchange_negative(response_primitive, status_code, error_message)
152
153
154 def verify_request(request_primitive):
155     """Verifies request primitive only"""
156     request_primitive.check_request()
157
158
159 def verify_response(response_primitive, rqi=None, rsc=None, request_operation=None):
160     """Verifies response primitive only"""
161     response_primitive.check_response(rqi, rsc, request_operation)
162
163
164 def verify_response_negative(response_primitive, rqi=None, rsc=None, error_message=None):
165     """Verifies error response primitive only"""
166     response_primitive.check_response_negative(rqi, rsc, error_message)
167
168
169 def receive_request_primitive(allias="default", communication=None):
170     """
171     Blocking call which receives request primitive. If the request
172     primitive was received, the underlying Rx channel stays blocked
173     until response primitive (related to the request primitive) is
174     provided using respond_response_primitive() method
175     """
176     communication = __get_session(allias, communication)
177     req = communication.receive()
178     return req
179
180
181 def respond_response_primitive(response_primitive, allias="default", communication=None):
182     """
183     Sends response primitive related to the last request primitive received by
184     receive_request_primitive() method
185     """
186     communication = __get_session(allias, communication)
187     communication.respond(response_primitive)
188
189
190 def create_notification_response(notification_request_primitive, allias="default", communication=None):
191     """Creates response primitive for provided notification request primitive"""
192     communication = __get_session(allias, communication)
193     return communication.create_auto_response(notification_request_primitive,
194                                               OneM2M.result_code_ok)
195
196
197 def create_notification_response_negative(notification_request_primitive, result_code, error_message,
198                                           allias="default", communication=None):
199     """Creates negative response primitive for provided notification request primitive"""
200     communication = __get_session(allias, communication)
201     builder = IoTDMJsonPrimitiveBuilder() \
202         .set_communication_protocol(communication.get_protocol()) \
203         .set_param(OneM2M.short_request_identifier,
204                    notification_request_primitive.get_param(OneM2M.short_request_identifier)) \
205         .set_param(OneM2M.short_response_status_code, result_code) \
206         .set_proto_param(onem2m_http.http_result_code, onem2m_http.onem2m_to_http_result_codes[result_code])\
207         .set_content('{"error": "' + error_message + '"}')
208     return builder.build()
209
210
211 # JSON pointer strings used by methods providing automatic reply mechanism
212 JSON_POINTER_NOTIFICATION_RN = "/nev/rep/rn"
213 JSON_POINTER_NOTIFICATION_SUR = "/sur"
214
215
216 def _on_subscription_create_notificaton_matching_cb(request_primitive):
217     """
218     Is used as callback which returns True if the provided request primitive is
219     notification request triggered by creation of new subscription resource.
220     """
221     if request_primitive.get_param(OneM2M.short_operation) != OneM2M.operation_notify:
222         return False
223
224     if not request_primitive.has_attr(JSON_POINTER_NOTIFICATION_RN):
225         return False
226
227     if not request_primitive.has_attr(JSON_POINTER_NOTIFICATION_SUR):
228         return False
229
230     rn = request_primitive.get_attr(JSON_POINTER_NOTIFICATION_RN)
231     sur = request_primitive.get_attr(JSON_POINTER_NOTIFICATION_SUR)
232
233     if rn != sur:
234         return False
235     return True
236
237
238 # Description of such notification request primitive which is received
239 # as result of new subscription resource
240 ON_SUBSCRIPTION_CREATE_DESCRIPTION =\
241     RequestAutoHandlingDescription(None, None, None,
242                                    onem2m_result_code=OneM2M.result_code_ok,
243                                    matching_cb=_on_subscription_create_notificaton_matching_cb)
244
245
246 def _prepare_notification_auto_reply_builder():
247     return RequestAutoHandlingDescriptionBuilder()\
248         .add_param_criteria(OneM2M.short_operation, OneM2M.operation_notify)\
249         .set_onem2m_result_code(OneM2M.result_code_ok)
250
251
252 def add_notification_auto_reply_on_subscription_create(allias="default", communication=None):
253     """Sets auto reply for notification requests received due to subscription resource creation"""
254     communication = __get_session(allias, communication)
255     communication.add_auto_reply_description(ON_SUBSCRIPTION_CREATE_DESCRIPTION)
256
257
258 def remove_notification_auto_reply_on_subscription_create(allias="default", communication=None):
259     """Removes auto reply for notification requests received due to subscription resource creation"""
260     communication = __get_session(allias, communication)
261     communication.remove_auto_reply_description(ON_SUBSCRIPTION_CREATE_DESCRIPTION)
262
263
264 def get_number_of_auto_replies_on_subscription_create(allias="default", communication=None):
265     """Returns number of auto replies on notification requests received when new subscription created"""
266     communication = __get_session(allias, communication)
267     return communication.get_auto_handling_statistics(ON_SUBSCRIPTION_CREATE_DESCRIPTION).counter
268
269
270 def verify_number_of_auto_replies_on_subscription_create(replies, allias="default", communication=None):
271     """Compares number of auto replies on notifications received when new subscription created"""
272     count = get_number_of_auto_replies_on_subscription_create(allias, communication)
273     if replies != count:
274         raise AssertionError("Unexpected number of auto replies on subscription create: {}, expected: {}".format(
275                              count, replies))
276
277
278 __SUBSCRIPTION_RESOURCE_ID_DESCRIPTION_MAPPING = {}
279
280
281 def add_auto_reply_to_notification_from_subscription(subscription_resource_id, allias="default", communication=None):
282     """
283     Sets auto reply for notifications from specific subscription resource
284     identified by its CSE-relative resource ID
285     """
286     communication = __get_session(allias, communication)
287     builder = _prepare_notification_auto_reply_builder()
288     if subscription_resource_id in __SUBSCRIPTION_RESOURCE_ID_DESCRIPTION_MAPPING:
289         raise RuntimeError("Auto reply for subscription resource {} already set".format(subscription_resource_id))
290
291     builder.add_content_criteria(JSON_POINTER_NOTIFICATION_SUR, subscription_resource_id)
292     new_description = builder.build()
293     __SUBSCRIPTION_RESOURCE_ID_DESCRIPTION_MAPPING[subscription_resource_id] = new_description
294     communication.add_auto_reply_description(new_description)
295
296
297 def remove_auto_reply_to_notification_from_subscription(subscription_resource_id, allias="default", communication=None):
298     """Removes auto reply for specific subscription identified by its CSE-relative resource ID"""
299     communication = __get_session(allias, communication)
300     description = __SUBSCRIPTION_RESOURCE_ID_DESCRIPTION_MAPPING[subscription_resource_id]
301     if not description:
302         raise RuntimeError("No auto reply set for specific subscription resource: {}".format(subscription_resource_id))
303     communication.remove_auto_reply_description(description)
304
305
306 def get_number_of_auto_replies_to_notifications_from_subscription(subscription_resource_id,
307                                                                   allias="default", communication=None):
308     """
309     Returns number of automatic replies for specific subscription resource
310     identified by its CSE-relative resource ID
311     """
312     communication = __get_session(allias, communication)
313     description = __SUBSCRIPTION_RESOURCE_ID_DESCRIPTION_MAPPING[subscription_resource_id]
314     if not description:
315         raise RuntimeError("No auto reply set for specific subscription resource: {}".format(subscription_resource_id))
316     return communication.get_auto_handling_statistics(description).counter
317
318
319 def verify_number_of_auto_replies_to_notification_from_subscription(subscription_resource_id, replies,
320                                                                     allias="default", communication=None):
321     """
322     Compares number of automatic replies for specific subscription resource
323     identified by its CSE-relative resource ID
324     """
325     count = get_number_of_auto_replies_to_notifications_from_subscription(subscription_resource_id,
326                                                                           allias, communication)
327     if replies != count:
328         raise AssertionError(("Unexpected number of auto replies to notification from subscription {}, " +
329                               "auto replies: {}, expected: {}").format(subscription_resource_id, count, replies))
330
331
332 # Primitive getters uses JSON pointer object or string
333 # to identify specific parameter/attribute/protocol_specific_parameter
334 def get_primitive_content(primitive):
335     return primitive.get_content_str()
336
337
338 def get_primitive_content_attribute(primitive, pointer):
339     return primitive.get_attr(pointer)
340
341
342 def get_primitive_parameters(primitive):
343     return primitive.get_parameters_str()
344
345
346 def get_primitive_param(primitive, pointer):
347     return primitive.get_param(pointer)
348
349
350 def get_primitive_protocol_specific_parameters(primitive):
351     return primitive.get_protocol_specific_parameters_str()
352
353
354 def get_primitive_protocol_specific_param(primitive, pointer):
355     return primitive.get_proto_param(pointer)
356
357
358 # Communication
359 def close_iotdm_communication(allias="default", communication=None):
360     """Closes communication identified by allias or provided as object"""
361     communication = __get_session(allias, communication)
362     communication.stop()
363     if allias in __sessions:
364         del __sessions[allias]
365
366
367 def create_iotdm_communication(entity_id, protocol, protocol_params=None, rx_port=None, allias="default"):
368     """
369     Creates communication object and starts the communication.
370     :param entity_id: ID which will be used in From parameter of request primitives
371     :param protocol: Communication protocol to use for communication
372     :param protocol_params: Default protocol specific parameters to be used
373     :param rx_port: Local Rx port number
374     :param allias: Allias to be assigned to the created communication object
375     :return: The new communication object
376     """
377     if protocol == onem2m_http.HTTPPROTOCOLNAME:
378         conn = IoTDMItCommunicationFactory().create_http_json_primitive_communication(entity_id, protocol,
379                                                                                       protocol_params, rx_port)
380     else:
381         raise RuntimeError("Unsupported protocol: {}".format(protocol))
382
383     conn.start()
384     if allias:
385         __sessions[allias] = conn
386     return conn
387
388
389 def get_local_ip_from_list(iotdm_ip, local_ip_list_str):
390     """
391     Looks for longest prefix matching local interface IP address with the
392     IP address of IoTDM.
393     :param iotdm_ip: IP address of IoTDM
394     :param local_ip_list_str: String of IP address of local interfaces separated with space
395     :return: The longed prefix matching IP or the first one
396     """
397     if not local_ip_list_str:
398         raise RuntimeError("Local IP address not provided")
399
400     if not iotdm_ip:
401         raise RuntimeError("IoTDM IP address not provided")
402
403     ip_list = local_ip_list_str.split(" ")
404
405     if len(ip_list) == 1:
406         return ip_list[0]
407
408     for i in range(len(iotdm_ip), 0, -1):
409         # TODO this is not real longest prefix match
410         # TODO fix if needed
411         for ip in ip_list:
412             if ip.startswith(iotdm_ip[0: i]):
413                 return ip
414
415     # no match, just choose the first one
416     return ip_list[0]
417
418
419 # HTTP
420 def create_http_default_communication_parameters(address, port, content_type):
421     """Returns JSON string including default HTTP specific parameters"""
422     return '{{"{}": "{}", "{}": {}, "Content-Type": "{}"}}'.format(
423         onem2m_http.protocol_address, address,
424         onem2m_http.protocol_port, port,
425         content_type)
426
427
428 def create_iotdm_http_connection(entity_id, address, port, content_type, rx_port=None, allias="default"):
429     """Creates HTTP communication"""
430     default_params = create_http_default_communication_parameters(address, port, content_type)
431     return create_iotdm_communication(entity_id, "http", default_params, rx_port, allias)