From c7e957fb13a87f452980c2ee9dd4fee7eb3f0420 Mon Sep 17 00:00:00 2001
From: gifrerenom <lluis.gifre@cttc.es>
Date: Fri, 22 Dec 2023 15:23:01 +0000
Subject: [PATCH] NBI Component - ETSI BMW:

- Cosmetic cleanup in unitary test data
- Multiple bug fixes
- Fixed unitary tests
- Added new Python requirement
---
 src/nbi/requirements.in                       |   1 +
 .../nbi_plugins/etsi_bwm/Resources.py         |  10 +-
 .../rest_server/nbi_plugins/etsi_bwm/Tools.py |  12 +-
 src/nbi/tests/data/topology-dummy.json        |   2 +-
 src/nbi/tests/test_etsi_bwm.py                | 218 ++++++++++--------
 src/nbi/tests/test_ietf_network.py            |   2 +-
 6 files changed, 137 insertions(+), 108 deletions(-)

diff --git a/src/nbi/requirements.in b/src/nbi/requirements.in
index 52094c61f..6e3eb9440 100644
--- a/src/nbi/requirements.in
+++ b/src/nbi/requirements.in
@@ -13,6 +13,7 @@
 # limitations under the License.
 
 deepdiff==6.7.*
+deepmerge==1.1.*
 Flask==2.1.3
 Flask-HTTPAuth==4.5.0
 Flask-RESTful==0.3.9
diff --git a/src/nbi/service/rest_server/nbi_plugins/etsi_bwm/Resources.py b/src/nbi/service/rest_server/nbi_plugins/etsi_bwm/Resources.py
index 4c858990b..3fccbbb55 100644
--- a/src/nbi/service/rest_server/nbi_plugins/etsi_bwm/Resources.py
+++ b/src/nbi/service/rest_server/nbi_plugins/etsi_bwm/Resources.py
@@ -12,7 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import copy, json, logging
+import copy, deepmerge, json, logging
 from common.Constants import DEFAULT_CONTEXT_NAME
 from context.client.ContextClient import ContextClient
 from flask_restful import Resource, request
@@ -69,8 +69,14 @@ class BwInfoId(_Resource):
         json_data = request.get_json()
         if not 'appInsId' in json_data:
             json_data['appInsId'] = allocationId
-        service = bwInfo_2_service(self.client, json_data)
+        
+        service = self.client.GetService(grpc_service_id(DEFAULT_CONTEXT_NAME, json_data['appInsId']))
+        current_bwm = service_2_bwInfo(service)
+        new_bmw = deepmerge.always_merger.merge(current_bwm, json_data)
+        
+        service = bwInfo_2_service(self.client, new_bmw)
         self.service_client.UpdateService(service)
+
         service = self.client.GetService(grpc_service_id(DEFAULT_CONTEXT_NAME, json_data['appInsId']))
         response_bwm = service_2_bwInfo(service)
 
diff --git a/src/nbi/service/rest_server/nbi_plugins/etsi_bwm/Tools.py b/src/nbi/service/rest_server/nbi_plugins/etsi_bwm/Tools.py
index a1b66f032..a78d28193 100644
--- a/src/nbi/service/rest_server/nbi_plugins/etsi_bwm/Tools.py
+++ b/src/nbi/service/rest_server/nbi_plugins/etsi_bwm/Tools.py
@@ -15,8 +15,11 @@
 import json
 import logging
 import time
+from decimal import ROUND_HALF_EVEN, Decimal
 from flask.json import jsonify
-from common.proto.context_pb2 import ContextId, Empty, EndPointId, ServiceId, ServiceTypeEnum, Service, Constraint, Constraint_SLA_Capacity, ConfigRule, ConfigRule_Custom, ConfigActionEnum
+from common.proto.context_pb2 import (
+    ContextId, Empty, EndPointId, ServiceId, ServiceTypeEnum, Service, Constraint, Constraint_SLA_Capacity,
+    ConfigRule, ConfigRule_Custom, ConfigActionEnum)
 from common.tools.grpc.Tools import grpc_message_to_json
 from common.tools.object_factory.Context import json_context_id
 from common.tools.object_factory.Service import json_service_id
@@ -30,7 +33,10 @@ def service_2_bwInfo(service: Service) -> dict:
     response['appInsId'] = service.service_id.service_uuid.uuid # String: Application instance identifier
     for constraint in service.service_constraints:
         if constraint.WhichOneof('constraint') == 'sla_capacity':
-            response['fixedAllocation'] = str(constraint.sla_capacity.capacity_gbps*1000) # String: Size of requested fixed BW allocation in [bps]
+            # String: Size of requested fixed BW allocation in [bps]
+            fixed_allocation = Decimal(constraint.sla_capacity.capacity_gbps * 1.e9)
+            fixed_allocation = fixed_allocation.quantize(Decimal('0.1'), rounding=ROUND_HALF_EVEN)
+            response['fixedAllocation'] = str(fixed_allocation)
             break
 
     for config_rule in service.service_config.config_rules:
@@ -89,7 +95,7 @@ def bwInfo_2_service(client, bwInfo: dict) -> Service:
 
     if 'fixedAllocation' in bwInfo:
         capacity = Constraint_SLA_Capacity()
-        capacity.capacity_gbps = float(bwInfo['fixedAllocation'])
+        capacity.capacity_gbps = float(bwInfo['fixedAllocation']) / 1.e9
         constraint = Constraint()
         constraint.sla_capacity.CopyFrom(capacity)
         service.service_constraints.append(constraint)
diff --git a/src/nbi/tests/data/topology-dummy.json b/src/nbi/tests/data/topology-dummy.json
index 4c0f58255..4735bf446 100644
--- a/src/nbi/tests/data/topology-dummy.json
+++ b/src/nbi/tests/data/topology-dummy.json
@@ -2837,4 +2837,4 @@
             }
         }
     ]
-}
+}
\ No newline at end of file
diff --git a/src/nbi/tests/test_etsi_bwm.py b/src/nbi/tests/test_etsi_bwm.py
index 6d77aa749..8925897a7 100644
--- a/src/nbi/tests/test_etsi_bwm.py
+++ b/src/nbi/tests/test_etsi_bwm.py
@@ -12,8 +12,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import deepdiff, json, logging, pytest
 from typing import Dict
-import json, logging, pytest
 from common.Constants import DEFAULT_CONTEXT_NAME, DEFAULT_TOPOLOGY_NAME
 from common.proto.context_pb2 import ContextId, TopologyId
 from common.tools.descriptor.Loader import DescriptorLoader, check_descriptor_load_results, validate_empty_scenario
@@ -21,8 +21,11 @@ from common.tools.object_factory.Context import json_context_id
 from common.tools.object_factory.Topology import json_topology_id
 from context.client.ContextClient import ContextClient
 from nbi.service.rest_server import RestServer
-from .PrepareTestScenario import do_rest_delete_request, do_rest_post_request, do_rest_get_request, do_rest_put_request, do_rest_patch_request, mock_service, nbi_service_rest, context_client
-
+from .PrepareTestScenario import ( # pylint: disable=unused-import
+    # be careful, order of symbols is important here!
+    do_rest_delete_request, do_rest_get_request, do_rest_patch_request, do_rest_post_request, do_rest_put_request,
+    mock_service, nbi_service_rest, context_client
+)
 
 LOGGER = logging.getLogger(__name__)
 LOGGER.setLevel(logging.DEBUG)
@@ -38,24 +41,24 @@ BASE_URL = '/restconf/bwm/v1'
 def storage() -> Dict:
     yield dict()
 
-def compare_dicts(dict1, dict2):
-    # Function to recursively sort dictionaries
-    def recursively_sort(d):
-        if isinstance(d, dict):
-            return {k: recursively_sort(v) for k, v in sorted(d.items())}
-        if isinstance(d, list):
-            return [recursively_sort(item) for item in d]
-        return d
-
-    # Sort dictionaries to ignore the order of fields
-    sorted_dict1 = recursively_sort(dict1)
-    sorted_dict2 = recursively_sort(dict2)
-
-    if sorted_dict1 != sorted_dict2:
-        LOGGER.error(sorted_dict1)
-        LOGGER.error(sorted_dict2)
-
-    return sorted_dict1 != sorted_dict2
+#def compare_dicts(dict1, dict2):
+#    # Function to recursively sort dictionaries
+#    def recursively_sort(d):
+#        if isinstance(d, dict):
+#            return {k: recursively_sort(v) for k, v in sorted(d.items())}
+#        if isinstance(d, list):
+#            return [recursively_sort(item) for item in d]
+#        return d
+#
+#    # Sort dictionaries to ignore the order of fields
+#    sorted_dict1 = recursively_sort(dict1)
+#    sorted_dict2 = recursively_sort(dict2)
+#
+#    if sorted_dict1 != sorted_dict2:
+#        LOGGER.error(sorted_dict1)
+#        LOGGER.error(sorted_dict2)
+#
+#    return sorted_dict1 != sorted_dict2
 
 def check_timestamps(bwm_service):
     assert 'timeStamp' in bwm_service
@@ -71,7 +74,7 @@ def test_prepare_environment(context_client : ContextClient) -> None: # pylint:
 
     # Verify the scenario has no services/slices
     response = context_client.GetContext(ADMIN_CONTEXT_ID)
-    assert len(response.topology_ids) == 3
+    assert len(response.topology_ids) == 1
     assert len(response.service_ids ) == 0
     assert len(response.slice_ids   ) == 0
 
@@ -81,23 +84,21 @@ def test_get_allocations_empty(nbi_service_rest : RestServer, storage : Dict): #
     LOGGER.debug('retrieved_data={:s}'.format(json.dumps(retrieved_data, sort_keys=True)))
     assert len(retrieved_data) == 0
 
-def test_allocation(nbi_service_rest : RestServer, storage : Dict):
+def test_allocation(nbi_service_rest : RestServer, storage : Dict): # pylint: disable=redefined-outer-name, unused-argument
     URL = BASE_URL + '/bw_allocations'
     data = {
-        "allocationDirection":"string",
-        "appInsId":"service_uuid_01",
-        "fixedAllocation":"123000.0",
-        "fixedBWPriority":"SEE_DESCRIPTION",
-        "requestType":0,
-        "sessionFilter":[
-            {
-                "dstAddress":"192.168.3.2",
-                "dstPort":["b"],
-                "protocol":"string",
-                "sourceIp":"192.168.1.2",
-                "sourcePort":["a"]
-            }
-        ]
+        "appInsId"            : "service_uuid_01",
+        "allocationDirection" : "00",
+        "fixedAllocation"     : "123000.0",
+        "fixedBWPriority"     : "SEE_DESCRIPTION",
+        "requestType"         : 0,
+        "sessionFilter"       : [{
+            "sourceIp"   : "192.168.1.2",
+            "sourcePort" : ["a"],
+            "protocol"   : "string",
+            "dstAddress" : "192.168.3.2",
+            "dstPort"    : ["b"],
+        }]
     }
     retrieved_data = do_rest_post_request(URL, body=data, logger=LOGGER, expected_status_codes={200})
     LOGGER.debug('retrieved_data={:s}'.format(json.dumps(retrieved_data, sort_keys=True)))
@@ -112,24 +113,25 @@ def test_get_allocations(nbi_service_rest : RestServer, storage : Dict): # pylin
     assert len(retrieved_data) == 1
     good_result = [
         {
-            "appInsId":"service_uuid_01",
-            "fixedAllocation":"123000.0",
-            "allocationDirection":"string",
-            "fixedBWPriority":"SEE_DESCRIPTION",
-            "requestType":"0",
-            "sessionFilter":[
-                {
-                    "dstAddress":"192.168.3.2",
-                    "dstPort":["b"],
-                    "protocol":"string",
-                    "sourceIp":"192.168.1.2",
-                    "sourcePort":["a"]
-                }
-            ],
+            "appInsId"            : "service_uuid_01",
+            "fixedAllocation"     : "123000.0",
+            "allocationDirection" : "00",
+            "fixedBWPriority"     : "SEE_DESCRIPTION",
+            "requestType"         : "0",
+            "sessionFilter"       : [{
+                "sourceIp"   : "192.168.1.2",
+                "sourcePort" : ["a"],
+                "protocol"   : "string",
+                "dstAddress" : "192.168.3.2",
+                "dstPort"    : ["b"],
+            }],
         }
     ]
-    compare_dicts(retrieved_data, good_result)
     check_timestamps(retrieved_data[0])
+    del retrieved_data[0]['timeStamp']
+    diff_data = deepdiff.DeepDiff(good_result, retrieved_data)
+    LOGGER.error('Differences:\n{:s}'.format(str(diff_data.pretty())))
+    assert len(diff_data) == 0
 
 
 def test_get_allocation(nbi_service_rest : RestServer, storage : Dict): # pylint: disable=redefined-outer-name, unused-argument
@@ -138,48 +140,49 @@ def test_get_allocation(nbi_service_rest : RestServer, storage : Dict): # pylint
     retrieved_data = do_rest_get_request(URL, logger=LOGGER, expected_status_codes={200})
     LOGGER.debug('retrieved_data={:s}'.format(json.dumps(retrieved_data, sort_keys=True)))
     good_result = {
-        "appInsId":"service_uuid_01",
-        "fixedAllocation":"123000.0",
-        "allocationDirection":"string",
-        "fixedBWPriority":"SEE_DESCRIPTION",
-        "requestType":"0",
-        "sessionFilter":[
-            {
-                "dstAddress":"192.168.3.2",
-                "dstPort":["b"],
-                "protocol":"string",
-                "sourceIp":"192.168.1.2",
-                "sourcePort":["a"]
-            }
-        ]
+        "appInsId"           : "service_uuid_01",
+        "fixedAllocation"    : "123000.0",
+        "allocationDirection": "00",
+        "fixedBWPriority"    : "SEE_DESCRIPTION",
+        "requestType"        : "0",
+        "sessionFilter"      : [{
+            "sourceIp"   : "192.168.1.2",
+            "sourcePort" : ["a"],
+            "protocol"   : "string",
+            "dstAddress" : "192.168.3.2",
+            "dstPort"    : ["b"],
+        }]
     }
-    
-    compare_dicts(retrieved_data, good_result)
     check_timestamps(retrieved_data)
+    del retrieved_data['timeStamp']
+    diff_data = deepdiff.DeepDiff(good_result, retrieved_data)
+    LOGGER.error('Differences:\n{:s}'.format(str(diff_data.pretty())))
+    assert len(diff_data) == 0
 
 
 def test_put_allocation(nbi_service_rest : RestServer, storage : Dict): # pylint: disable=redefined-outer-name, unused-argument
     assert 'service_uuid_01' in storage
     URL = BASE_URL + '/bw_allocations/service_uuid_01'
     changed_allocation = {
-        "appInsId":"service_uuid_01",
-        "fixedAllocation":"200.0",
-        "allocationDirection":"parriba",
-        "fixedBWPriority":"NOPRIORITY",
-        "requestType":"0",
-        "sessionFilter":[
-            {
-                "dstAddress":"192.168.3.2",
-                "dstPort":["b"],
-                "protocol":"string",
-                "sourceIp":"192.168.1.2",
-                "sourcePort":["a"]
-            }
-        ]
+        "appInsId"           : "service_uuid_01",
+        "fixedAllocation"    : "200.0",
+        "allocationDirection": "00",
+        "fixedBWPriority"    : "NOPRIORITY",
+        "requestType"        : "0",
+        "sessionFilter"      : [{
+            "sourceIp"   : "192.168.1.2",
+            "sourcePort" : ["a"],
+            "protocol"   : "string",
+            "dstAddress" : "192.168.3.2",
+            "dstPort"    : ["b"],
+        }]
     }
     retrieved_data = do_rest_put_request(URL, body=json.dumps(changed_allocation), logger=LOGGER, expected_status_codes={200})
-    compare_dicts(retrieved_data, changed_allocation)
     check_timestamps(retrieved_data)
+    del retrieved_data['timeStamp']
+    diff_data = deepdiff.DeepDiff(changed_allocation, retrieved_data)
+    LOGGER.error('Differences:\n{:s}'.format(str(diff_data.pretty())))
+    assert len(diff_data) == 0
 
 
 def test_patch_allocation(nbi_service_rest : RestServer, storage : Dict): # pylint: disable=redefined-outer-name, unused-argument
@@ -189,26 +192,25 @@ def test_patch_allocation(nbi_service_rest : RestServer, storage : Dict): # pyli
         "fixedBWPriority":"FULLPRIORITY",
     }
     changed_allocation = {
-        "appInsId":"service_uuid_01",
-        "fixedAllocation":"200.0",
-        "allocationDirection":"parriba",
-        "fixedBWPriority":"FULLPRIORITY",
-        "requestType":"0",
-        "sessionFilter":[
-            {
-                "dstAddress":"192.168.3.2",
-                "dstPort":["b"],
-                "protocol":"string",
-                "sourceIp":"192.168.1.2",
-                "sourcePort":["a"]
-            }
-        ]
+        "appInsId"           : "service_uuid_01",
+        "fixedAllocation"    : "200.0",
+        "allocationDirection": "00",
+        "fixedBWPriority"    : "FULLPRIORITY",
+        "requestType"        : "0",
+        "sessionFilter"      : [{
+            "sourceIp"   : "192.168.1.2",
+            "sourcePort" : ["a"],
+            "protocol"   : "string",
+            "dstAddress" : "192.168.3.2",
+            "dstPort"    : ["b"],
+        }]
     }
-
-    retrieved_data = do_rest_patch_request(URL, body=changed_allocation, logger=LOGGER, expected_status_codes={200})
-    compare_dicts(retrieved_data, changed_allocation)
+    retrieved_data = do_rest_patch_request(URL, body=difference, logger=LOGGER, expected_status_codes={200})
     check_timestamps(retrieved_data)
-
+    del retrieved_data['timeStamp']
+    diff_data = deepdiff.DeepDiff(changed_allocation, retrieved_data)
+    LOGGER.error('Differences:\n{:s}'.format(str(diff_data.pretty())))
+    assert len(diff_data) == 0
 
 
 def test_delete_allocation(nbi_service_rest : RestServer, storage : Dict): # pylint: disable=redefined-outer-name, unused-argument
@@ -222,3 +224,17 @@ def test_get_allocations_empty_final(nbi_service_rest : RestServer, storage : Di
     retrieved_data = do_rest_get_request(URL, logger=LOGGER, expected_status_codes={200})
     LOGGER.debug('retrieved_data={:s}'.format(json.dumps(retrieved_data, sort_keys=True)))
     assert len(retrieved_data) == 0
+
+
+def test_cleanup_environment(context_client : ContextClient) -> None: # pylint: disable=redefined-outer-name
+    # Verify the scenario has no services/slices
+    response = context_client.GetContext(ADMIN_CONTEXT_ID)
+    assert len(response.topology_ids) == 1
+    assert len(response.service_ids ) == 0
+    assert len(response.slice_ids   ) == 0
+
+    # Load descriptors and validate the base scenario
+    descriptor_loader = DescriptorLoader(descriptors_file=DESCRIPTOR_FILE, context_client=context_client)
+    descriptor_loader.validate()
+    descriptor_loader.unload()
+    validate_empty_scenario(context_client)
diff --git a/src/nbi/tests/test_ietf_network.py b/src/nbi/tests/test_ietf_network.py
index e41a88af0..fb39a9192 100644
--- a/src/nbi/tests/test_ietf_network.py
+++ b/src/nbi/tests/test_ietf_network.py
@@ -12,8 +12,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from typing import Dict
 import deepdiff, json, logging, operator
+from typing import Dict
 from common.Constants import DEFAULT_CONTEXT_NAME
 from common.proto.context_pb2 import ContextId
 from common.tools.descriptor.Loader import DescriptorLoader, check_descriptor_load_results, validate_empty_scenario
-- 
GitLab