Use proper netconf testtool artifact
[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(
39     protocol, primitive_params, content=None, proto_specific_params=None
40 ):
41     """Creates primitive builder without any default data"""
42     builder = (
43         IoTDMJsonPrimitiveBuilder()
44         .set_communication_protocol(protocol)
45         .set_content(content)
46         .set_parameters(primitive_params)
47         .set_protocol_specific_parameters(proto_specific_params)
48     )
49     return builder
50
51
52 def new_primitive_raw(
53     protocol, primitive_params, content=None, proto_specific_params=None
54 ):
55     """Creates primitive object without any default data"""
56     return prepare_primitive_builder_raw(
57         protocol, primitive_params, content, proto_specific_params
58     ).build()
59
60
61 def prepare_primitive_builder(
62     primitive_params,
63     content=None,
64     proto_specific_params=None,
65     allias="default",
66     communication=None,
67 ):
68     """Creates primitive builder with default data set according communication object used"""
69     communication = __get_session(allias, communication)
70
71     builder = (
72         IoTDMJsonPrimitiveBuilder()
73         .set_communication_protocol(communication.get_protocol())
74         .set_parameters(communication.get_primitive_params())
75         .set_protocol_specific_parameters(communication.get_protocol_params())
76         .set_content(content)
77     )
78
79     if communication.get_protocol() == onem2m_http.HTTPPROTOCOLNAME and content:
80         builder.set_proto_param(
81             onem2m_http.http_header_content_length, str(len(content))
82         )
83
84     builder.append_parameters(primitive_params).append_protocol_specific_parameters(
85         proto_specific_params
86     )
87
88     return builder
89
90
91 def new_primitive(
92     primitive_params,
93     content=None,
94     proto_specific_params=None,
95     allias="default",
96     communication=None,
97 ):
98     """Creates new primitive object with default data set according communication object used"""
99     return prepare_primitive_builder(
100         primitive_params, content, proto_specific_params, allias, communication
101     ).build()
102
103
104 def _add_param(params, name, value):
105     if not name or not value:
106         return
107     params[name] = value
108
109
110 def prepare_request_primitive_builder(
111     target_resource,
112     content=None,
113     operation=None,
114     resource_type=None,
115     result_content=None,
116     allias="default",
117     communication=None,
118 ):
119     """
120     Creates builder for request primitive with default data set according
121     communication object used
122     """
123     communication = __get_session(allias, communication)
124     if not communication or not target_resource:
125         raise AttributeError("Mandatory attributes not specified")
126
127     primitive_params = {}
128     _add_param(primitive_params, OneM2M.short_to, target_resource)
129     _add_param(primitive_params, OneM2M.short_operation, operation)
130     _add_param(primitive_params, OneM2M.short_resource_type, resource_type)
131     _add_param(primitive_params, OneM2M.short_result_content, result_content)
132
133     primitive_params = json.dumps(primitive_params)
134
135     builder = prepare_primitive_builder(
136         primitive_params, content, communication=communication
137     )
138     return builder
139
140
141 def new_create_request_primitive(
142     target_resource,
143     content,
144     resource_type,
145     result_content=None,
146     allias="default",
147     communication=None,
148 ):
149     """Creates request primitive for Create operation"""
150     return prepare_request_primitive_builder(
151         target_resource,
152         content,
153         operation=OneM2M.operation_create,
154         resource_type=resource_type,
155         result_content=result_content,
156         allias=allias,
157         communication=communication,
158     ).build()
159
160
161 def new_update_request_primitive(
162     target_resource, content, result_content=None, allias="default", communication=None
163 ):
164     """Creates request primitive for Update operation"""
165     return prepare_request_primitive_builder(
166         target_resource,
167         content,
168         operation=OneM2M.operation_update,
169         resource_type=None,
170         result_content=result_content,
171         allias=allias,
172         communication=communication,
173     ).build()
174
175
176 def new_retrieve_request_primitive(
177     target_resource, result_content=None, allias="default", communication=None
178 ):
179     """Creates request primitive for Retrieve operation"""
180     return prepare_request_primitive_builder(
181         target_resource,
182         content=None,
183         operation=OneM2M.operation_retrieve,
184         resource_type=None,
185         result_content=result_content,
186         allias=allias,
187         communication=communication,
188     ).build()
189
190
191 def new_delete_request_primitive(
192     target_resource, result_content=None, allias="default", communication=None
193 ):
194     """Creates request primitive for Delete operation"""
195     return prepare_request_primitive_builder(
196         target_resource,
197         content=None,
198         operation=OneM2M.operation_delete,
199         result_content=result_content,
200         allias=allias,
201         communication=communication,
202     ).build()
203
204
205 def send_primitive(primitive, allias="default", communication=None):
206     """Sends primitive object using the communication object"""
207     communication = __get_session(allias, communication)
208     rsp = communication.send(primitive)
209     return rsp
210
211
212 def verify_exchange(request_primitive, response_primitive, status_code=None):
213     """Verifies request and response primitive parameters"""
214     request_primitive.check_exchange(response_primitive, rsc=status_code)
215
216
217 def verify_exchange_negative(
218     request_primitive, response_primitive, status_code, error_message=None
219 ):
220     """Verifies request and error response primitive parameters"""
221     request_primitive.check_exchange_negative(
222         response_primitive, status_code, error_message
223     )
224
225
226 def verify_request(request_primitive):
227     """Verifies request primitive only"""
228     request_primitive.check_request()
229
230
231 def verify_response(response_primitive, rqi=None, rsc=None, request_operation=None):
232     """Verifies response primitive only"""
233     response_primitive.check_response(rqi, rsc, request_operation)
234
235
236 def verify_response_negative(
237     response_primitive, rqi=None, rsc=None, error_message=None
238 ):
239     """Verifies error response primitive only"""
240     response_primitive.check_response_negative(rqi, rsc, error_message)
241
242
243 def receive_request_primitive(allias="default", communication=None):
244     """
245     Blocking call which receives request primitive. If the request
246     primitive was received, the underlying Rx channel stays blocked
247     until response primitive (related to the request primitive) is
248     provided using respond_response_primitive() method
249     """
250     communication = __get_session(allias, communication)
251     req = communication.receive()
252     return req
253
254
255 def respond_response_primitive(
256     response_primitive, allias="default", communication=None
257 ):
258     """
259     Sends response primitive related to the last request primitive received by
260     receive_request_primitive() method
261     """
262     communication = __get_session(allias, communication)
263     communication.respond(response_primitive)
264
265
266 def create_notification_response(
267     notification_request_primitive, allias="default", communication=None
268 ):
269     """Creates response primitive for provided notification request primitive"""
270     communication = __get_session(allias, communication)
271     return communication.create_auto_response(
272         notification_request_primitive, OneM2M.result_code_ok
273     )
274
275
276 def create_notification_response_negative(
277     notification_request_primitive,
278     result_code,
279     error_message,
280     allias="default",
281     communication=None,
282 ):
283     """Creates negative response primitive for provided notification request primitive"""
284     communication = __get_session(allias, communication)
285     builder = (
286         IoTDMJsonPrimitiveBuilder()
287         .set_communication_protocol(communication.get_protocol())
288         .set_param(
289             OneM2M.short_request_identifier,
290             notification_request_primitive.get_param(OneM2M.short_request_identifier),
291         )
292         .set_param(OneM2M.short_response_status_code, result_code)
293         .set_proto_param(
294             onem2m_http.http_result_code,
295             onem2m_http.onem2m_to_http_result_codes[result_code],
296         )
297         .set_content('{"error": "' + error_message + '"}')
298     )
299     return builder.build()
300
301
302 # JSON pointer strings used by methods providing automatic reply mechanism
303 JSON_POINTER_NOTIFICATION_RN = "/nev/rep/rn"
304 JSON_POINTER_NOTIFICATION_SUR = "/sur"
305
306
307 def _on_subscription_create_notificaton_matching_cb(request_primitive):
308     """
309     Is used as callback which returns True if the provided request primitive is
310     notification request triggered by creation of new subscription resource.
311     """
312     if request_primitive.get_param(OneM2M.short_operation) != OneM2M.operation_notify:
313         return False
314
315     if not request_primitive.has_attr(JSON_POINTER_NOTIFICATION_RN):
316         return False
317
318     if not request_primitive.has_attr(JSON_POINTER_NOTIFICATION_SUR):
319         return False
320
321     rn = request_primitive.get_attr(JSON_POINTER_NOTIFICATION_RN)
322     sur = request_primitive.get_attr(JSON_POINTER_NOTIFICATION_SUR)
323
324     if rn != sur:
325         return False
326     return True
327
328
329 # Description of such notification request primitive which is received
330 # as result of new subscription resource
331 ON_SUBSCRIPTION_CREATE_DESCRIPTION = RequestAutoHandlingDescription(
332     None,
333     None,
334     None,
335     onem2m_result_code=OneM2M.result_code_ok,
336     matching_cb=_on_subscription_create_notificaton_matching_cb,
337 )
338
339
340 def _prepare_notification_auto_reply_builder():
341     return (
342         RequestAutoHandlingDescriptionBuilder()
343         .add_param_criteria(OneM2M.short_operation, OneM2M.operation_notify)
344         .set_onem2m_result_code(OneM2M.result_code_ok)
345     )
346
347
348 def add_notification_auto_reply_on_subscription_create(
349     allias="default", communication=None
350 ):
351     """Sets auto reply for notification requests received due to subscription resource creation"""
352     communication = __get_session(allias, communication)
353     communication.add_auto_reply_description(ON_SUBSCRIPTION_CREATE_DESCRIPTION)
354
355
356 def remove_notification_auto_reply_on_subscription_create(
357     allias="default", communication=None
358 ):
359     """Removes auto reply for notification requests received due to subscription resource creation"""
360     communication = __get_session(allias, communication)
361     communication.remove_auto_reply_description(ON_SUBSCRIPTION_CREATE_DESCRIPTION)
362
363
364 def get_number_of_auto_replies_on_subscription_create(
365     allias="default", communication=None
366 ):
367     """Returns number of auto replies on notification requests received when new subscription created"""
368     communication = __get_session(allias, communication)
369     return communication.get_auto_handling_statistics(
370         ON_SUBSCRIPTION_CREATE_DESCRIPTION
371     ).counter
372
373
374 def verify_number_of_auto_replies_on_subscription_create(
375     replies, allias="default", communication=None
376 ):
377     """Compares number of auto replies on notifications received when new subscription created"""
378     count = get_number_of_auto_replies_on_subscription_create(allias, communication)
379     if replies != count:
380         raise AssertionError(
381             "Unexpected number of auto replies on subscription create: {}, expected: {}".format(
382                 count, replies
383             )
384         )
385
386
387 __SUBSCRIPTION_RESOURCE_ID_DESCRIPTION_MAPPING = {}
388
389
390 def add_auto_reply_to_notification_from_subscription(
391     subscription_resource_id, allias="default", communication=None
392 ):
393     """
394     Sets auto reply for notifications from specific subscription resource
395     identified by its CSE-relative resource ID
396     """
397     communication = __get_session(allias, communication)
398     builder = _prepare_notification_auto_reply_builder()
399     if subscription_resource_id in __SUBSCRIPTION_RESOURCE_ID_DESCRIPTION_MAPPING:
400         raise RuntimeError(
401             "Auto reply for subscription resource {} already set".format(
402                 subscription_resource_id
403             )
404         )
405
406     builder.add_content_criteria(
407         JSON_POINTER_NOTIFICATION_SUR, subscription_resource_id
408     )
409     new_description = builder.build()
410     __SUBSCRIPTION_RESOURCE_ID_DESCRIPTION_MAPPING[
411         subscription_resource_id
412     ] = new_description
413     communication.add_auto_reply_description(new_description)
414
415
416 def remove_auto_reply_to_notification_from_subscription(
417     subscription_resource_id, allias="default", communication=None
418 ):
419     """Removes auto reply for specific subscription identified by its CSE-relative resource ID"""
420     communication = __get_session(allias, communication)
421     description = __SUBSCRIPTION_RESOURCE_ID_DESCRIPTION_MAPPING[
422         subscription_resource_id
423     ]
424     if not description:
425         raise RuntimeError(
426             "No auto reply set for specific subscription resource: {}".format(
427                 subscription_resource_id
428             )
429         )
430     communication.remove_auto_reply_description(description)
431
432
433 def get_number_of_auto_replies_to_notifications_from_subscription(
434     subscription_resource_id, allias="default", communication=None
435 ):
436     """
437     Returns number of automatic replies for specific subscription resource
438     identified by its CSE-relative resource ID
439     """
440     communication = __get_session(allias, communication)
441     description = __SUBSCRIPTION_RESOURCE_ID_DESCRIPTION_MAPPING[
442         subscription_resource_id
443     ]
444     if not description:
445         raise RuntimeError(
446             "No auto reply set for specific subscription resource: {}".format(
447                 subscription_resource_id
448             )
449         )
450     return communication.get_auto_handling_statistics(description).counter
451
452
453 def verify_number_of_auto_replies_to_notification_from_subscription(
454     subscription_resource_id, replies, allias="default", communication=None
455 ):
456     """
457     Compares number of automatic replies for specific subscription resource
458     identified by its CSE-relative resource ID
459     """
460     count = get_number_of_auto_replies_to_notifications_from_subscription(
461         subscription_resource_id, allias, communication
462     )
463     if replies != count:
464         raise AssertionError(
465             (
466                 "Unexpected number of auto replies to notification from subscription {}, "
467                 + "auto replies: {}, expected: {}"
468             ).format(subscription_resource_id, count, replies)
469         )
470
471
472 # Primitive getters uses JSON pointer object or string
473 # to identify specific parameter/attribute/protocol_specific_parameter
474 def get_primitive_content(primitive):
475     return primitive.get_content_str()
476
477
478 def get_primitive_content_attribute(primitive, pointer):
479     return primitive.get_attr(pointer)
480
481
482 def get_primitive_parameters(primitive):
483     return primitive.get_parameters_str()
484
485
486 def get_primitive_param(primitive, pointer):
487     return primitive.get_param(pointer)
488
489
490 def get_primitive_protocol_specific_parameters(primitive):
491     return primitive.get_protocol_specific_parameters_str()
492
493
494 def get_primitive_protocol_specific_param(primitive, pointer):
495     return primitive.get_proto_param(pointer)
496
497
498 # Communication
499 def close_iotdm_communication(allias="default", communication=None):
500     """Closes communication identified by allias or provided as object"""
501     communication = __get_session(allias, communication)
502     communication.stop()
503     if allias in __sessions:
504         del __sessions[allias]
505
506
507 def create_iotdm_communication(
508     entity_id, protocol, protocol_params=None, rx_port=None, allias="default"
509 ):
510     """
511     Creates communication object and starts the communication.
512     :param entity_id: ID which will be used in From parameter of request primitives
513     :param protocol: Communication protocol to use for communication
514     :param protocol_params: Default protocol specific parameters to be used
515     :param rx_port: Local Rx port number
516     :param allias: Allias to be assigned to the created communication object
517     :return: The new communication object
518     """
519     if protocol == onem2m_http.HTTPPROTOCOLNAME:
520         conn = IoTDMItCommunicationFactory().create_http_json_primitive_communication(
521             entity_id, protocol, protocol_params, rx_port
522         )
523     else:
524         raise RuntimeError("Unsupported protocol: {}".format(protocol))
525
526     conn.start()
527     if allias:
528         __sessions[allias] = conn
529     return conn
530
531
532 def get_local_ip_from_list(iotdm_ip, local_ip_list_str):
533     """
534     Looks for longest prefix matching local interface IP address with the
535     IP address of IoTDM.
536     :param iotdm_ip: IP address of IoTDM
537     :param local_ip_list_str: String of IP address of local interfaces separated with space
538     :return: The longed prefix matching IP or the first one
539     """
540     if not local_ip_list_str:
541         raise RuntimeError("Local IP address not provided")
542
543     if not iotdm_ip:
544         raise RuntimeError("IoTDM IP address not provided")
545
546     ip_list = local_ip_list_str.split(" ")
547
548     if len(ip_list) == 1:
549         return ip_list[0]
550
551     for i in range(len(iotdm_ip), 0, -1):
552         # TODO this is not real longest prefix match
553         # TODO fix if needed
554         for ip in ip_list:
555             if ip.startswith(iotdm_ip[0:i]):
556                 return ip
557
558     # no match, just choose the first one
559     return ip_list[0]
560
561
562 # HTTP
563 def create_http_default_communication_parameters(address, port, content_type):
564     """Returns JSON string including default HTTP specific parameters"""
565     return '{{"{}": "{}", "{}": {}, "Content-Type": "{}"}}'.format(
566         onem2m_http.protocol_address,
567         address,
568         onem2m_http.protocol_port,
569         port,
570         content_type,
571     )
572
573
574 def create_iotdm_http_connection(
575     entity_id, address, port, content_type, rx_port=None, allias="default"
576 ):
577     """Creates HTTP communication"""
578     default_params = create_http_default_communication_parameters(
579         address, port, content_type
580     )
581     return create_iotdm_communication(
582         entity_id, "http", default_params, rx_port, allias
583     )