diff options
Diffstat (limited to 'apps/slice_selection')
10 files changed, 473 insertions, 0 deletions
diff --git a/apps/slice_selection/__init__.py b/apps/slice_selection/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/apps/slice_selection/__init__.py diff --git a/apps/slice_selection/models/api/__init__.py b/apps/slice_selection/models/api/__init__.py new file mode 100644 index 0000000..b45f74d --- /dev/null +++ b/apps/slice_selection/models/api/__init__.py @@ -0,0 +1,17 @@ +# ------------------------------------------------------------------------- +# Copyright (C) 2020 Wipro Limited. +# +# 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. +# +# ------------------------------------------------------------------------- +# diff --git a/apps/slice_selection/models/api/nsi_selection_request.py b/apps/slice_selection/models/api/nsi_selection_request.py new file mode 100644 index 0000000..b395012 --- /dev/null +++ b/apps/slice_selection/models/api/nsi_selection_request.py @@ -0,0 +1,62 @@ +# ------------------------------------------------------------------------- +# Copyright (C) 2020 Wipro Limited. +# +# 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 osdf.models.api.common import OSDFModel +from schematics.types import BaseType +from schematics.types import BooleanType +from schematics.types.compound import DictType +from schematics.types.compound import ListType +from schematics.types.compound import ModelType +from schematics.types import IntType +from schematics.types import StringType +from schematics.types import URLType + + +class RequestInfo(OSDFModel): + """Info for northbound request from client such as SO""" + transactionId = StringType(required=True) + requestId = StringType(required=True) + callbackUrl = URLType(required=True) + sourceId = StringType(required=True) + callbackHeader = DictType(BaseType) + timeout = IntType() + numSolutions = IntType() + addtnlArgs = DictType(BaseType) + + +class NxTInfo(OSDFModel): + """Information about NST/NSST model""" + invariantUUID = StringType(required=True) + UUID = StringType(required=True) + name = StringType(required=True) + + +class SubnetCapability(OSDFModel): + """Subnet capability of every subnet""" + domainType = StringType(required=True) + capabilityDetails = DictType(BaseType, required=True) + + +class NSISelectionAPI(OSDFModel): + """Request for nsi selection (specific to optimization and additional metadata""" + requestInfo = ModelType(RequestInfo, required=True) + NSTInfo = ModelType(NxTInfo, required=True) + NSSTInfo = ListType(ModelType(NxTInfo), required=False) + serviceProfile = DictType(BaseType, required=True) + subnetCapabilities = ListType(ModelType(SubnetCapability), required=True) + preferReuse = BooleanType() diff --git a/apps/slice_selection/models/api/nsi_selection_response.py b/apps/slice_selection/models/api/nsi_selection_response.py new file mode 100644 index 0000000..3c6d35b --- /dev/null +++ b/apps/slice_selection/models/api/nsi_selection_response.py @@ -0,0 +1,54 @@ +# ------------------------------------------------------------------------- +# Copyright (C) 2020 Wipro Limited. +# +# 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 osdf.models.api.common import OSDFModel +from schematics.types import BaseType, StringType, BooleanType +from schematics.types.compound import ModelType, ListType, DictType + + +# TODO: update osdf.models +class SharedNSISolution(OSDFModel): + """Represents the shared NSI Solution object""" + invariantUUID = StringType(required=True) + UUID = StringType(required=True) + NSIName = StringType(required=True) + NSIId = StringType(required=True) + matchLevel = StringType(required=True) + + +class NewNSISolution(OSDFModel): + """Represents the New NSI Solution object containing tuple of slice profiles""" + sliceProfiles = ListType(DictType(BaseType), required=True) + matchLevel = StringType(required=True) + + +class NSISolution(OSDFModel): + """Represents the NSI Solution object""" + """This solution object contains either sharedNSISolution or newNSISolution""" + existingNSI = BooleanType(required=True) + sharedNSISolution = ModelType(SharedNSISolution) + newNSISolution = ModelType(NewNSISolution) + + +class NSISelectionResponse(OSDFModel): + """Response sent to NSMF(SO)""" + transactionId = StringType(required=True) + requestId = StringType(required=True) + requestStatus = StringType(required=True) + solutions = ListType(ModelType(NSISolution), required=True) + statusMessage = StringType() diff --git a/apps/slice_selection/models/api/nssi_selection_request.py b/apps/slice_selection/models/api/nssi_selection_request.py new file mode 100644 index 0000000..c670abe --- /dev/null +++ b/apps/slice_selection/models/api/nssi_selection_request.py @@ -0,0 +1,41 @@ +# ------------------------------------------------------------------------- +# Copyright (C) 2020 Wipro Limited. +# +# 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 osdf.models.api.common import OSDFModel +from schematics.types import BaseType, StringType, URLType, IntType +from schematics.types.compound import ModelType, DictType + +from apps.slice_selection.models.api.nsi_selection_request import NxTInfo + + +class RequestInfo(OSDFModel): + """Info for northbound request from client such as SO""" + transactionId = StringType(required=True) + requestId = StringType(required=True) + callbackUrl = URLType(required=True) + sourceId = StringType(required=True) + callbackHeader = DictType(BaseType) + timeout = IntType() + numSolutions = IntType() + addtnlArgs = DictType(BaseType) + + +class NSSISelectionAPI(OSDFModel): + """Request for NSSI selection (specific to optimization and additional metadata""" + requestInfo = ModelType(RequestInfo, required=True) + NSSTInfo = ModelType(NxTInfo, required=True) + sliceProfile = DictType(BaseType, required=True) diff --git a/apps/slice_selection/models/api/nssi_selection_response.py b/apps/slice_selection/models/api/nssi_selection_response.py new file mode 100644 index 0000000..af67f65 --- /dev/null +++ b/apps/slice_selection/models/api/nssi_selection_response.py @@ -0,0 +1,40 @@ +# ------------------------------------------------------------------------- +# Copyright (C) 2020 Wipro Limited. +# +# 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 osdf.models.api.common import OSDFModel +from schematics.types import StringType +from schematics.types.compound import ModelType, ListType + + +# TODO: update osdf.models +class SharedNSSISolution(OSDFModel): + """Represents the shared NSSI Solution object""" + invariantUUID = StringType(required=True) + UUID = StringType(required=True) + NSSIName = StringType(required=True) + NSSIId = StringType(required=True) + matchLevel = StringType(required=True) + + +class NSSISelectionResponse(OSDFModel): + """Response sent to NSSMF(SO)""" + transactionId = StringType(required=True) + requestId = StringType(required=True) + requestStatus = StringType(required=True) + solutions = ListType(ModelType(SharedNSSISolution), required=True) + statusMessage = StringType() diff --git a/apps/slice_selection/optimizers/__init__.py b/apps/slice_selection/optimizers/__init__.py new file mode 100644 index 0000000..b45f74d --- /dev/null +++ b/apps/slice_selection/optimizers/__init__.py @@ -0,0 +1,17 @@ +# ------------------------------------------------------------------------- +# Copyright (C) 2020 Wipro Limited. +# +# 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. +# +# ------------------------------------------------------------------------- +# diff --git a/apps/slice_selection/optimizers/conductor/__init__.py b/apps/slice_selection/optimizers/conductor/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/apps/slice_selection/optimizers/conductor/__init__.py diff --git a/apps/slice_selection/optimizers/conductor/remote_opt_processor.py b/apps/slice_selection/optimizers/conductor/remote_opt_processor.py new file mode 100644 index 0000000..68c9409 --- /dev/null +++ b/apps/slice_selection/optimizers/conductor/remote_opt_processor.py @@ -0,0 +1,134 @@ +# ------------------------------------------------------------------------- +# Copyright (C) 2020 Wipro Limited. +# +# 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. +# +# ------------------------------------------------------------------------- +# + +""" +Module for processing slice selection request +""" + +from requests import RequestException +from threading import Thread +import traceback + +from apps.slice_selection.optimizers.conductor.response_processor import ResponseProcessor +from osdf.adapters.conductor import conductor +from osdf.adapters.policy.interface import get_policies +from osdf.logging.osdf_logging import debug_log +from osdf.logging.osdf_logging import error_log +from osdf.utils.interfaces import get_rest_client +from osdf.utils.mdc_utils import mdc_from_json + + +class SliceSelectionOptimizer(Thread): + def __init__(self, osdf_config, slice_config, request_json, model_type): + super().__init__() + self.osdf_config = osdf_config + self.slice_config = slice_config + self.request_json = request_json + self.model_type = model_type + self.response_processor = ResponseProcessor(request_json['requestInfo'], slice_config) + + def run(self): + self.process_slice_selection_opt() + + def process_slice_selection_opt(self): + """Process the slice selection request from the API layer""" + req_info = self.request_json['requestInfo'] + rc = get_rest_client(self.request_json, service='so') + + try: + if self.model_type == 'NSSI' \ + and self.request_json['sliceProfile'].get('resourceSharingLevel', "") \ + in ['not-shared', 'non-shared']: + final_response = self.response_processor.get_slice_selection_response([]) + + else: + final_response = self.do_slice_selection() + + except Exception as ex: + error_log.error("Error for {} {}".format(req_info.get('requestId'), + traceback.format_exc())) + error_message = str(ex) + final_response = self.response_processor.process_error_response(error_message) + + try: + rc.request(json=final_response, noresponse=True) + except RequestException: + error_log.error("Error sending asynchronous notification for {} {}".format(req_info['request_id'], + traceback.format_exc())) + + def do_slice_selection(self): + req_info = self.request_json['requestInfo'] + app_info = self.slice_config['app_info'][self.model_type] + mdc_from_json(self.request_json) + requirements = self.request_json.get(app_info['requirements_field'], {}) + model_info = self.request_json.get(app_info['model_info']) + model_name = model_info['name'] + policies = self.get_app_policies(model_name, app_info['app_name']) + request_parameters = self.get_request_parameters(requirements, model_info) + + demands = [ + { + "resourceModuleName": model_name, + "resourceModelInfo": {} + } + ] + + try: + template_fields = { + 'location_enabled': False, + 'version': '2020-08-13' + } + resp = conductor.request(req_info, demands, request_parameters, {}, template_fields, + self.osdf_config, policies) + except RequestException as e: + resp = e.response.json() + error = resp['plans'][0]['message'] + if isinstance(error, list) and "Unable to find any" in error[0]: + return self.response_processor.get_slice_selection_response([]) + error_log.error('Error from conductor {}'.format(error)) + return self.response_processor.process_error_response(error) + + debug_log.debug("Response from conductor {}".format(str(resp))) + recommendations = resp["plans"][0].get("recommendations") + subnets = [subnet['domainType'] for subnet in self.request_json['subnetCapabilities']] \ + if self.request_json.get('subnetCapabilities') else [] + return self.response_processor.process_response(recommendations, model_info, subnets, self.model_type) + + def get_request_parameters(self, requirements, model_info): + camel_to_snake = self.slice_config['attribute_mapping']['camel_to_snake'] + request_params = {camel_to_snake[key]: value for key, value in requirements.items()} + subnet_capabilities = self.request_json.get('subnetCapabilities') + if subnet_capabilities: + for subnet_capability in subnet_capabilities: + domain_type = f"{subnet_capability['domainType']}_" + capability_details = subnet_capability['capabilityDetails'] + for key, value in capability_details.items(): + request_params[f"{domain_type}{camel_to_snake[key]}"] = value + request_params.update(model_info) + return request_params + + def get_app_policies(self, model_name, app_name): + policy_request_json = self.request_json.copy() + policy_request_json['serviceInfo'] = {'serviceName': model_name} + if 'serviceProfile' in self.request_json: + slice_scope = self.request_json['serviceProfile']['resourceSharingLevel'] + if 'preferReuse' in self.request_json and slice_scope == "shared": + slice_scope = slice_scope + "," + ("reuse" if self.request_json['preferReuse'] else "create_new") + policy_request_json['slice_scope'] = slice_scope + debug_log.debug("policy_request_json {}".format(str(policy_request_json))) + return get_policies(policy_request_json, app_name) diff --git a/apps/slice_selection/optimizers/conductor/response_processor.py b/apps/slice_selection/optimizers/conductor/response_processor.py new file mode 100644 index 0000000..2357ab9 --- /dev/null +++ b/apps/slice_selection/optimizers/conductor/response_processor.py @@ -0,0 +1,108 @@ +# ------------------------------------------------------------------------- +# Copyright (C) 2020 Wipro Limited. +# +# 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. +# +# ------------------------------------------------------------------------- +# + +""" +Module for processing response from conductor for slice selection +""" + +import re + + +class ResponseProcessor(object): + def __init__(self, request_info, slice_config): + self.request_info = request_info + self.slice_config = slice_config + + def process_response(self, recommendations, model_info, subnets, model_type): + """Process conductor response to form the response for the API request + + :param recommendations: recommendations from conductor + :param model_info: model info from the request + :param subnets: list of subnets + :param model_type: NSI or NSSI + :return: response json as a dictionary + """ + if not recommendations: + return self.get_slice_selection_response([]) + model_name = model_info['name'] + solutions = [self.get_solution_from_candidate(rec[model_name]['candidate'], model_info, subnets, model_type) + for rec in recommendations] + return self.get_slice_selection_response(solutions) + + def get_solution_from_candidate(self, candidate, model_info, subnets, model_type): + if candidate['inventory_type'] == 'slice_profiles': + return { + 'existingNSI': False, + 'newNSISolution': { + 'sliceProfiles': self.get_slice_profiles_from_candidate(candidate, subnets) + } + } + elif model_type == 'NSSI': + return { + 'UUID': model_info['UUID'], + 'invariantUUID': model_info['invariantUUID'], + 'NSSIName': candidate['instance_name'], + 'NSSIId': candidate['instance_id'] + } + + elif model_type == 'NSI': + return { + 'existingNSI': True, + 'sharedNSISolution': { + 'UUID': model_info['UUID'], + 'invariantUUID': model_info['invariantUUID'], + 'NSIName': candidate['instance_name'], + 'NSIId': candidate['instance_id'] + } + } + + def get_slice_profiles_from_candidate(self, candidate, subnets): + slice_profiles = [] + for subnet in subnets: + slice_profile = {self.get_profile_attribute(k, subnet): v for k, v in candidate.items() + if k.startswith(subnet)} + slice_profile['domainType'] = subnet + slice_profiles.append(slice_profile) + return slice_profiles + + def get_profile_attribute(self, attribute, subnet): + snake_to_camel = self.slice_config['attribute_mapping']['snake_to_camel'] + return snake_to_camel[re.sub(f'^{subnet}_', '', attribute)] + + def process_error_response(self, error_message): + """Form response message from the error message + + :param error_message: error message while processing the request + :return: response json as dictionary + """ + return {'requestId': self.request_info['requestId'], + 'transactionId': self.request_info['transactionId'], + 'requestStatus': 'error', + 'statusMessage': error_message} + + def get_slice_selection_response(self, solutions): + """Get NSI selection response from final solution + + :param solutions: final solutions + :return: NSI selection response to send back as dictionary + """ + return {'requestId': self.request_info['requestId'], + 'transactionId': self.request_info['transactionId'], + 'requestStatus': 'completed', + 'statusMessage': '', + 'solutions': solutions} |