From 1f0b8016184b007a27fe837f62820a338be2720e Mon Sep 17 00:00:00 2001 From: Carlos Manso Date: Tue, 4 Jul 2023 17:04:42 +0200 Subject: [PATCH 1/4] ETSI MEC BW Management API initial support --- src/compute/service/__main__.py | 3 + .../nbi_plugins/etsi_mec_015/Resources.py | 74 ++++++++++++ .../nbi_plugins/etsi_mec_015/Tools.py | 114 ++++++++++++++++++ .../nbi_plugins/etsi_mec_015/__init__.py | 29 +++++ 4 files changed, 220 insertions(+) create mode 100644 src/compute/service/rest_server/nbi_plugins/etsi_mec_015/Resources.py create mode 100644 src/compute/service/rest_server/nbi_plugins/etsi_mec_015/Tools.py create mode 100644 src/compute/service/rest_server/nbi_plugins/etsi_mec_015/__init__.py diff --git a/src/compute/service/__main__.py b/src/compute/service/__main__.py index 6c744d0dc..888391b71 100644 --- a/src/compute/service/__main__.py +++ b/src/compute/service/__main__.py @@ -23,6 +23,8 @@ from .rest_server.RestServer import RestServer from .rest_server.nbi_plugins.debug_api import register_debug_api from .rest_server.nbi_plugins.ietf_l2vpn import register_ietf_l2vpn from .rest_server.nbi_plugins.ietf_network_slice import register_ietf_nss +from .rest_server.nbi_plugins.etsi_mec_015 import register_mec_015_api + terminate = threading.Event() LOGGER = None @@ -62,6 +64,7 @@ def main(): register_ietf_l2vpn(rest_server) # Registering L2VPN entrypoint register_ietf_nss(rest_server) # Registering NSS entrypoint register_debug_api(rest_server) + register_mec_015_api(rest_server) rest_server.start() # Wait for Ctrl+C or termination signal diff --git a/src/compute/service/rest_server/nbi_plugins/etsi_mec_015/Resources.py b/src/compute/service/rest_server/nbi_plugins/etsi_mec_015/Resources.py new file mode 100644 index 000000000..09819ba10 --- /dev/null +++ b/src/compute/service/rest_server/nbi_plugins/etsi_mec_015/Resources.py @@ -0,0 +1,74 @@ +# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from flask_restful import Resource, request +from context.client.ContextClient import ContextClient +from service.client.ServiceClient import ServiceClient +from .Tools import ( + format_grpc_to_json, grpc_context_id, grpc_service_id, bwInfo_2_service, service_2_bwInfo) +import copy +from common.Constants import DEFAULT_CONTEXT_NAME + + +class _Resource(Resource): + def __init__(self) -> None: + super().__init__() + self.client = ContextClient() + self.service_client = ServiceClient() + + +class BwInfo(_Resource): + def get(self): + service_list = self.client.ListServices(grpc_context_id(DEFAULT_CONTEXT_NAME)) + bw_allocations = [service_2_bwInfo(service) for service in service_list.services] + return bw_allocations + + def post(self): + bwinfo = request.get_json() + service = bwInfo_2_service(self.client, bwinfo) + stripped_service = copy.deepcopy(service) + + stripped_service.ClearField('service_endpoint_ids') + stripped_service.ClearField('service_constraints') + stripped_service.ClearField('service_config') + + response = format_grpc_to_json(self.service_client.CreateService(stripped_service)) + response = format_grpc_to_json(self.service_client.UpdateService(service)) + + return response + + +class BwInfoId(_Resource): + + def get(self, allocationId: str): + service = self.client.GetService(grpc_service_id(DEFAULT_CONTEXT_NAME, allocationId)) + return service_2_bwInfo(service) + + def put(self, allocationId: str): + json_data = request.get_json() + service = bwInfo_2_service(self.client, json_data) + response = self.service_client.UpdateService(service) + return format_grpc_to_json(response) + + def patch(self, allocationId: str): + json_data = request.get_json() + if not 'appInsId' in json_data: + json_data['appInsId'] = allocationId + service = bwInfo_2_service(self.client, json_data) + response = self.service_client.UpdateService(service) + return format_grpc_to_json(response) + + def delete(self, allocationId: str): + self.service_client.DeleteService(grpc_service_id(DEFAULT_CONTEXT_NAME, allocationId)) + return diff --git a/src/compute/service/rest_server/nbi_plugins/etsi_mec_015/Tools.py b/src/compute/service/rest_server/nbi_plugins/etsi_mec_015/Tools.py new file mode 100644 index 000000000..444bac606 --- /dev/null +++ b/src/compute/service/rest_server/nbi_plugins/etsi_mec_015/Tools.py @@ -0,0 +1,114 @@ +# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +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.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 +import time +import json +import logging + +LOGGER = logging.getLogger(__name__) + + +def service_2_bwInfo(service: Service) -> dict: + response = {} + # allocationDirection = '??' # String: 00 = Downlink (towards the UE); 01 = Uplink (towards the application/session); 10 = Symmetrical + response['appInsId'] = service.service_id.context_id.context_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] + break + + + for config_rule in service.service_config.config_rules: + for key in ['allocationDirection', 'fixedBWPriority', 'requestType', 'sourceIp', 'sourcePort', 'dstPort', 'protocol', 'sessionFilter']: + if config_rule.custom.resource_key == key: + if key != 'sessionFilter': + response[key] = config_rule.custom.resource_value + else: + response[key] = json.loads(config_rule.custom.resource_value) + + + unixtime = time.time() + response['timeStamp'] = { # Time stamp to indicate when the corresponding information elements are sent + "seconds": int(unixtime), + "nanoseconds": int(unixtime%1*1e9) + } + + return response + +def bwInfo_2_service(client, bwInfo: dict) -> Service: + service = Service() + + for key in ['allocationDirection', 'fixedBWPriority', 'requestType', 'timeStamp', 'sessionFilter']: + if key not in bwInfo: + continue + config_rule = ConfigRule() + config_rule.action = ConfigActionEnum.CONFIGACTION_SET + config_rule_custom = ConfigRule_Custom() + config_rule_custom.resource_key = key + if key != 'sessionFilter': + config_rule_custom.resource_value = str(bwInfo[key]) + else: + config_rule_custom.resource_value = json.dumps(bwInfo[key]) + config_rule.custom.CopyFrom(config_rule_custom) + service.service_config.config_rules.append(config_rule) + + if 'sessionFilter' in bwInfo: + a_ip = bwInfo['sessionFilter'][0]['sourceIp'] + z_ip = bwInfo['sessionFilter'][0]['dstAddress'] + + devices = client.ListDevices(Empty()).devices + for device in devices: + for cr in device.device_config.config_rules: + if cr.WhichOneof('config_rule') == 'custom' and cr.custom.resource_key == '_connect/settings': + for ep in json.loads(cr.custom.resource_value)['endpoints']: + if 'ip' in ep and (ep['ip'] == a_ip or ep['ip'] == z_ip): + ep_id = EndPointId() + ep_id.endpoint_uuid.uuid = ep['uuid'] + ep_id.device_id.device_uuid.uuid = device.device_id.device_uuid.uuid + service.service_endpoint_ids.append(ep_id) + + if len(service.service_endpoint_ids) < 2: + LOGGER.error('No endpoints matched') + return None + + service.service_type = ServiceTypeEnum.SERVICETYPE_L3NM + + if 'appInsId' in bwInfo: + service.service_id.service_uuid.uuid = bwInfo['appInsId'] + service.service_id.context_id.context_uuid.uuid = 'admin' + service.name = bwInfo['appInsId'] + + if 'fixedAllocation' in bwInfo: + capacity = Constraint_SLA_Capacity() + capacity.capacity_gbps = float(bwInfo['fixedAllocation']) + constraint = Constraint() + constraint.sla_capacity.CopyFrom(capacity) + service.service_constraints.append(constraint) + + return service + + +def format_grpc_to_json(grpc_reply): + return jsonify(grpc_message_to_json(grpc_reply)) + +def grpc_context_id(context_uuid): + return ContextId(**json_context_id(context_uuid)) + +def grpc_service_id(context_uuid, service_uuid): + return ServiceId(**json_service_id(service_uuid, context_id=json_context_id(context_uuid))) diff --git a/src/compute/service/rest_server/nbi_plugins/etsi_mec_015/__init__.py b/src/compute/service/rest_server/nbi_plugins/etsi_mec_015/__init__.py new file mode 100644 index 000000000..04e67525f --- /dev/null +++ b/src/compute/service/rest_server/nbi_plugins/etsi_mec_015/__init__.py @@ -0,0 +1,29 @@ +# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from compute.service.rest_server.RestServer import RestServer +from .Resources import (BwInfo, BwInfoId) + +URL_PREFIX = '/bwm/v1' + +# Use 'path' type since some identifiers might contain char '/' and Flask is unable to recognize them in 'string' type. +RESOURCES = [ + # (endpoint_name, resource_class, resource_url) + ('api.bw_info', BwInfo, '/bw_allocations'), + ('api.bw_info_id', BwInfoId, '/bw_allocations/'), +] + +def register_mec_015_api(rest_server : RestServer): + for endpoint_name, resource_class, resource_url in RESOURCES: + rest_server.add_resource(resource_class, URL_PREFIX + resource_url, endpoint=endpoint_name) -- GitLab From f3c6973465c7525ebdbc630938ae68d95e4e32d5 Mon Sep 17 00:00:00 2001 From: Carlos Manso Date: Fri, 29 Sep 2023 15:51:01 +0200 Subject: [PATCH 2/4] rename etsi_mec_015 to ets_bwm --- src/compute/service/__main__.py | 4 ++-- .../nbi_plugins/{etsi_mec_015 => etsi_bwm}/Resources.py | 0 .../nbi_plugins/{etsi_mec_015 => etsi_bwm}/Tools.py | 0 .../nbi_plugins/{etsi_mec_015 => etsi_bwm}/__init__.py | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename src/compute/service/rest_server/nbi_plugins/{etsi_mec_015 => etsi_bwm}/Resources.py (100%) rename src/compute/service/rest_server/nbi_plugins/{etsi_mec_015 => etsi_bwm}/Tools.py (100%) rename src/compute/service/rest_server/nbi_plugins/{etsi_mec_015 => etsi_bwm}/__init__.py (95%) diff --git a/src/compute/service/__main__.py b/src/compute/service/__main__.py index 888391b71..ccd888d4e 100644 --- a/src/compute/service/__main__.py +++ b/src/compute/service/__main__.py @@ -23,7 +23,7 @@ from .rest_server.RestServer import RestServer from .rest_server.nbi_plugins.debug_api import register_debug_api from .rest_server.nbi_plugins.ietf_l2vpn import register_ietf_l2vpn from .rest_server.nbi_plugins.ietf_network_slice import register_ietf_nss -from .rest_server.nbi_plugins.etsi_mec_015 import register_mec_015_api +from .rest_server.nbi_plugins.etsi_bwm import register_etsi_bwm_api terminate = threading.Event() @@ -64,7 +64,7 @@ def main(): register_ietf_l2vpn(rest_server) # Registering L2VPN entrypoint register_ietf_nss(rest_server) # Registering NSS entrypoint register_debug_api(rest_server) - register_mec_015_api(rest_server) + register_etsi_bwm_api(rest_server) rest_server.start() # Wait for Ctrl+C or termination signal diff --git a/src/compute/service/rest_server/nbi_plugins/etsi_mec_015/Resources.py b/src/compute/service/rest_server/nbi_plugins/etsi_bwm/Resources.py similarity index 100% rename from src/compute/service/rest_server/nbi_plugins/etsi_mec_015/Resources.py rename to src/compute/service/rest_server/nbi_plugins/etsi_bwm/Resources.py diff --git a/src/compute/service/rest_server/nbi_plugins/etsi_mec_015/Tools.py b/src/compute/service/rest_server/nbi_plugins/etsi_bwm/Tools.py similarity index 100% rename from src/compute/service/rest_server/nbi_plugins/etsi_mec_015/Tools.py rename to src/compute/service/rest_server/nbi_plugins/etsi_bwm/Tools.py diff --git a/src/compute/service/rest_server/nbi_plugins/etsi_mec_015/__init__.py b/src/compute/service/rest_server/nbi_plugins/etsi_bwm/__init__.py similarity index 95% rename from src/compute/service/rest_server/nbi_plugins/etsi_mec_015/__init__.py rename to src/compute/service/rest_server/nbi_plugins/etsi_bwm/__init__.py index 04e67525f..61b37b7b4 100644 --- a/src/compute/service/rest_server/nbi_plugins/etsi_mec_015/__init__.py +++ b/src/compute/service/rest_server/nbi_plugins/etsi_bwm/__init__.py @@ -24,6 +24,6 @@ RESOURCES = [ ('api.bw_info_id', BwInfoId, '/bw_allocations/'), ] -def register_mec_015_api(rest_server : RestServer): +def register_etsi_bwm_api(rest_server : RestServer): for endpoint_name, resource_class, resource_url in RESOURCES: rest_server.add_resource(resource_class, URL_PREFIX + resource_url, endpoint=endpoint_name) -- GitLab From e56acc1fa986f4cf96b4d310b8a69badd999fa4c Mon Sep 17 00:00:00 2001 From: Carlos Manso Date: Fri, 29 Sep 2023 16:12:03 +0200 Subject: [PATCH 3/4] tests added and cleanup --- src/compute/service/__main__.py | 7 +- .../rest_server/nbi_plugins/etsi_bwm/Tools.py | 6 +- .../nbi_plugins/etsi_bwm/tests_etsi_bwm.txt | 81 +++++++++++++++++++ 3 files changed, 87 insertions(+), 7 deletions(-) create mode 100644 src/compute/service/rest_server/nbi_plugins/etsi_bwm/tests_etsi_bwm.txt diff --git a/src/compute/service/__main__.py b/src/compute/service/__main__.py index ccd888d4e..a9f224e15 100644 --- a/src/compute/service/__main__.py +++ b/src/compute/service/__main__.py @@ -21,10 +21,9 @@ from common.Settings import ( from .ComputeService import ComputeService from .rest_server.RestServer import RestServer from .rest_server.nbi_plugins.debug_api import register_debug_api +from .rest_server.nbi_plugins.etsi_bwm import register_etsi_bwm_api from .rest_server.nbi_plugins.ietf_l2vpn import register_ietf_l2vpn from .rest_server.nbi_plugins.ietf_network_slice import register_ietf_nss -from .rest_server.nbi_plugins.etsi_bwm import register_etsi_bwm_api - terminate = threading.Event() LOGGER = None @@ -61,10 +60,10 @@ def main(): grpc_service.start() rest_server = RestServer() - register_ietf_l2vpn(rest_server) # Registering L2VPN entrypoint - register_ietf_nss(rest_server) # Registering NSS entrypoint register_debug_api(rest_server) register_etsi_bwm_api(rest_server) + register_ietf_l2vpn(rest_server) # Registering L2VPN entrypoint + register_ietf_nss(rest_server) # Registering NSS entrypoint rest_server.start() # Wait for Ctrl+C or termination signal diff --git a/src/compute/service/rest_server/nbi_plugins/etsi_bwm/Tools.py b/src/compute/service/rest_server/nbi_plugins/etsi_bwm/Tools.py index 444bac606..023d1006c 100644 --- a/src/compute/service/rest_server/nbi_plugins/etsi_bwm/Tools.py +++ b/src/compute/service/rest_server/nbi_plugins/etsi_bwm/Tools.py @@ -12,14 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json +import logging +import time 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.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 -import time -import json -import logging LOGGER = logging.getLogger(__name__) diff --git a/src/compute/service/rest_server/nbi_plugins/etsi_bwm/tests_etsi_bwm.txt b/src/compute/service/rest_server/nbi_plugins/etsi_bwm/tests_etsi_bwm.txt new file mode 100644 index 000000000..9cfbe5625 --- /dev/null +++ b/src/compute/service/rest_server/nbi_plugins/etsi_bwm/tests_etsi_bwm.txt @@ -0,0 +1,81 @@ +-----------------------GET----------------------- + +curl --request GET \ + --url http://10.1.7.203:80/restconf/bwm/v1/bw_allocations + + +-----------------------POST----------------------- +curl --request POST \ + --url http://10.1.7.203:80/restconf/bwm/v1/bw_allocations \ + --header 'Content-Type: application/json' \ + --data '{ + "allocationDirection": "string", + "appInsId": "service_uuid", + "fixedAllocation": "123", + "fixedBWPriority": "SEE_DESCRIPTION", + "requestType": 0, + "sessionFilter": [ + { + "dstAddress": "192.168.3.2", + "dstPort": [ + "b" + ], + "protocol": "string", + "sourceIp": "192.168.1.2", + "sourcePort": [ + "a" + ] + } + ], + "timeStamp": { + "nanoSeconds": 1, + "seconds": 1 + } +}' + + +-----------------------GET2----------------------- +curl --request GET \ + --url http://10.1.7.203:80/restconf/bwm/v1/bw_allocations/service_uuid + +-----------------------PUT----------------------- + curl --request PUT \ + --url http://10.1.7.203:80/restconf/bwm/v1/bw_allocations/service_uuid \ + --header 'Content-Type: application/json' \ + --data '{ + "allocationDirection": "string", + "appInsId": "service_uuid", + "fixedAllocation": "123", + "fixedBWPriority": "efefe", + "requestType": 0, + "sessionFilter": [ + { + "dstAddress": "192.168.3.2", + "dstPort": [ + "b" + ], + "protocol": "string", + "sourceIp": "192.168.1.2", + "sourcePort": [ + "a" + ] + } + ], + "timeStamp": { + "nanoSeconds": 1, + "seconds": 1 + } +}' + +-----------------------PATCH----------------------- +curl --request PATCH \ + --url http://10.1.7.203:80/restconf/bwm/v1/bw_allocations/service_uuid \ + --header 'Content-Type: application/json' \ + --data '{ + "fixedBWPriority": "uuuuuuuuuuuuuu" +}' + + +-----------------------DELETE----------------------- +curl --request DELETE \ + --url http://10.1.7.203:80/restconf/bwm/v1/bw_allocations/service_uuid \ No newline at end of file -- GitLab From c9ef5706dafd43c997838d84865c0d91897416ac Mon Sep 17 00:00:00 2001 From: Carlos Manso Date: Fri, 29 Sep 2023 16:14:08 +0200 Subject: [PATCH 4/4] cleanup --- .../service/rest_server/nbi_plugins/etsi_bwm/Resources.py | 5 +++-- .../service/rest_server/nbi_plugins/etsi_bwm/__init__.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/compute/service/rest_server/nbi_plugins/etsi_bwm/Resources.py b/src/compute/service/rest_server/nbi_plugins/etsi_bwm/Resources.py index 09819ba10..38534b754 100644 --- a/src/compute/service/rest_server/nbi_plugins/etsi_bwm/Resources.py +++ b/src/compute/service/rest_server/nbi_plugins/etsi_bwm/Resources.py @@ -12,13 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. +import copy +from common.Constants import DEFAULT_CONTEXT_NAME from flask_restful import Resource, request from context.client.ContextClient import ContextClient from service.client.ServiceClient import ServiceClient from .Tools import ( format_grpc_to_json, grpc_context_id, grpc_service_id, bwInfo_2_service, service_2_bwInfo) -import copy -from common.Constants import DEFAULT_CONTEXT_NAME + class _Resource(Resource): diff --git a/src/compute/service/rest_server/nbi_plugins/etsi_bwm/__init__.py b/src/compute/service/rest_server/nbi_plugins/etsi_bwm/__init__.py index 61b37b7b4..8b81a4057 100644 --- a/src/compute/service/rest_server/nbi_plugins/etsi_bwm/__init__.py +++ b/src/compute/service/rest_server/nbi_plugins/etsi_bwm/__init__.py @@ -13,7 +13,7 @@ # limitations under the License. from compute.service.rest_server.RestServer import RestServer -from .Resources import (BwInfo, BwInfoId) +from .Resources import BwInfo, BwInfoId URL_PREFIX = '/bwm/v1' -- GitLab