diff options
Diffstat (limited to 'apps/placement')
-rw-r--r-- | apps/placement/__init__.py | 0 | ||||
-rw-r--r-- | apps/placement/models/__init__.py | 0 | ||||
-rw-r--r-- | apps/placement/models/api/__init__.py | 0 | ||||
-rw-r--r-- | apps/placement/models/api/placementRequest.py | 105 | ||||
-rw-r--r-- | apps/placement/models/api/placementResponse.py | 64 | ||||
-rw-r--r-- | apps/placement/optimizers/__init__.py | 0 | ||||
-rw-r--r-- | apps/placement/optimizers/conductor/__init__.py | 17 | ||||
-rw-r--r-- | apps/placement/optimizers/conductor/remote_opt_processor.py | 178 | ||||
-rwxr-xr-x | apps/placement/templates/plc_opt_request.jsont | 142 | ||||
-rwxr-xr-x | apps/placement/templates/plc_opt_response.jsont | 10 | ||||
-rwxr-xr-x | apps/placement/templates/policy_request.jsont | 3 |
11 files changed, 519 insertions, 0 deletions
diff --git a/apps/placement/__init__.py b/apps/placement/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/apps/placement/__init__.py diff --git a/apps/placement/models/__init__.py b/apps/placement/models/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/apps/placement/models/__init__.py diff --git a/apps/placement/models/api/__init__.py b/apps/placement/models/api/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/apps/placement/models/api/__init__.py diff --git a/apps/placement/models/api/placementRequest.py b/apps/placement/models/api/placementRequest.py new file mode 100644 index 0000000..e04c2af --- /dev/null +++ b/apps/placement/models/api/placementRequest.py @@ -0,0 +1,105 @@ +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# +# 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, BooleanType +from schematics.types.compound import ModelType, ListType, DictType + + +class RequestInfo(OSDFModel): + """Info for northbound request from client such as SO""" + transactionId = StringType(required=True) + requestId = StringType(required=True) + callbackUrl = URLType(required=True) + callbackHeader = DictType(BaseType) + sourceId = StringType(required=True) + requestType = StringType(required=True) + numSolutions = IntType() + optimizers = ListType(StringType(required=True)) + timeout = IntType() + + +class Candidates(OSDFModel): + """Preferred candidate for a resource (sent as part of a request from client)""" + identifierType = StringType(required=True) + identifiers = ListType(StringType(required=True)) + cloudOwner = StringType() + + +class ModelMetaData(OSDFModel): + """Model information for a specific resource""" + modelInvariantId = StringType(required=True) + modelVersionId = StringType(required=True) + modelName = StringType() + modelType = StringType() + modelVersion = StringType() + modelCustomizationName = StringType() + + +class LicenseModel(OSDFModel): + entitlementPoolUUID = ListType(StringType(required=True)) + licenseKeyGroupUUID = ListType(StringType(required=True)) + + +class LicenseDemands(OSDFModel): + resourceModuleName = StringType(required=True) + serviceResourceId = StringType(required=True) + resourceModelInfo = ModelType(ModelMetaData, required=True) + existingLicenses = ModelType(LicenseModel) + + +class LicenseInfo(OSDFModel): + licenseDemands = ListType(ModelType(LicenseDemands)) + + +class PlacementDemand(OSDFModel): + resourceModuleName = StringType(required=True) + serviceResourceId = StringType(required=True) + tenantId = StringType() + resourceModelInfo = ModelType(ModelMetaData, required=True) + existingCandidates = ListType(ModelType(Candidates)) + excludedCandidates = ListType(ModelType(Candidates)) + requiredCandidates = ListType(ModelType(Candidates)) + + +class ServiceInfo(OSDFModel): + serviceInstanceId = StringType(required=True) + modelInfo = ModelType(ModelMetaData, required=True) + serviceName = StringType(required=True) + + +class SubscriberInfo(OSDFModel): + """Details on the customer that subscribes to the VNFs""" + globalSubscriberId = StringType(required=True) + subscriberName = StringType() + subscriberCommonSiteId = StringType() + + +class PlacementInfo(OSDFModel): + """Information specific to placement optimization""" + requestParameters = DictType(BaseType) + placementDemands = ListType(ModelType(PlacementDemand), min_size=1) + subscriberInfo = ModelType(SubscriberInfo) + + +class PlacementAPI(OSDFModel): + """Request for placement optimization (specific to optimization and additional metadata""" + requestInfo = ModelType(RequestInfo, required=True) + placementInfo = ModelType(PlacementInfo, required=True) + licenseInfo = ModelType(LicenseInfo) + serviceInfo = ModelType(ServiceInfo, required=True)
\ No newline at end of file diff --git a/apps/placement/models/api/placementResponse.py b/apps/placement/models/api/placementResponse.py new file mode 100644 index 0000000..13b8d7a --- /dev/null +++ b/apps/placement/models/api/placementResponse.py @@ -0,0 +1,64 @@ +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# +# 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 +from schematics.types.compound import ModelType, ListType, DictType + + +# TODO: update osdf.models + +class LicenseSolution(OSDFModel): + serviceResourceId = StringType(required=True) + resourceModuleName = StringType(required=True) + entitlementPoolUUID = ListType(StringType(required=True)) + licenseKeyGroupUUID = ListType(StringType(required=True)) + entitlementPoolInvariantUUID = ListType(StringType(required=True)) + licenseKeyGroupInvariantUUID = ListType(StringType(required=True)) + + +class Candidates(OSDFModel): + """Preferred candidate for a resource (sent as part of a request from client)""" + identifierType = StringType(required=True) + identifiers = ListType(StringType(required=True)) + cloudOwner = StringType() + + +class AssignmentInfo(OSDFModel): + key = StringType(required=True) + value = BaseType(required=True) + + +class PlacementSolution(OSDFModel): + serviceResourceId = StringType(required=True) + resourceModuleName = StringType(required=True) + solution = ModelType(Candidates, required=True) + assignmentInfo = ListType(ModelType(AssignmentInfo)) + + +class Solution(OSDFModel): + placementSolutions = ListType(ListType(ModelType(PlacementSolution), min_size=1)) + licenseSolutions = ListType(ModelType(LicenseSolution), min_size=1) + + +class PlacementResponse(OSDFModel): + transactionId = StringType(required=True) + requestId = StringType(required=True) + requestStatus = StringType(required=True) + statusMessage = StringType() + solutions = ModelType(Solution, required=True) diff --git a/apps/placement/optimizers/__init__.py b/apps/placement/optimizers/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/apps/placement/optimizers/__init__.py diff --git a/apps/placement/optimizers/conductor/__init__.py b/apps/placement/optimizers/conductor/__init__.py new file mode 100644 index 0000000..4b25e5b --- /dev/null +++ b/apps/placement/optimizers/conductor/__init__.py @@ -0,0 +1,17 @@ +# ------------------------------------------------------------------------- +# Copyright (c) 2017-2018 AT&T Intellectual Property +# +# 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/placement/optimizers/conductor/remote_opt_processor.py b/apps/placement/optimizers/conductor/remote_opt_processor.py new file mode 100644 index 0000000..2e681be --- /dev/null +++ b/apps/placement/optimizers/conductor/remote_opt_processor.py @@ -0,0 +1,178 @@ +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# 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 jinja2 import Template +import json +from requests import RequestException +import traceback + +from apps.license.optimizers.simple_license_allocation import license_optim +from osdf.adapters.conductor import conductor +from osdf.logging.osdf_logging import debug_log +from osdf.logging.osdf_logging import error_log +from osdf.logging.osdf_logging import metrics_log +from osdf.logging.osdf_logging import MH +from osdf.operation.error_handling import build_json_error_body +from osdf.utils.interfaces import get_rest_client +from osdf.utils.mdc_utils import mdc_from_json + + +def conductor_response_processor(conductor_response, req_id, transaction_id): + """Build a response object to be sent to client's callback URL from Conductor's response + + This includes Conductor's placement optimization response, and required ASDC license artifacts + :param conductor_response: JSON response from Conductor + :param raw_response: Raw HTTP response corresponding to above + :param req_id: Id of a request + :return: JSON object that can be sent to the client's callback URL + """ + composite_solutions = [] + name_map = {"physical-location-id": "cloudClli", "host_id": "vnfHostName", + "cloud_version": "cloudVersion", "cloud_owner": "cloudOwner", + "cloud": "cloudRegionId", "service": "serviceInstanceId", "is_rehome": "isRehome", + "location_id": "locationId", "location_type": "locationType", "directives": "oof_directives"} + for reco in conductor_response['plans'][0]['recommendations']: + for resource in reco.keys(): + c = reco[resource]['candidate'] + solution = { + 'resourceModuleName': resource, + 'serviceResourceId': reco[resource].get('service_resource_id', ""), + 'solution': {"identifierType": name_map.get(c['inventory_type'], c['inventory_type']), + 'identifiers': [c['candidate_id']], + 'cloudOwner': c.get('cloud_owner', "")}, + 'assignmentInfo': [] + } + for key, value in c.items(): + if key in ["location_id", "location_type", "is_rehome", "host_id"]: + try: + solution['assignmentInfo'].append({"key": name_map.get(key, key), "value": value}) + except KeyError: + debug_log.debug("The key[{}] is not mapped and will not be returned in assignment info" + .format(key)) + + for key, value in reco[resource]['attributes'].items(): + try: + solution['assignmentInfo'].append({"key": name_map.get(key, key), "value": value}) + except KeyError: + debug_log.debug("The key[{}] is not mapped and will not be returned in assignment info" + .format(key)) + composite_solutions.append(solution) + + request_status = "completed" if conductor_response['plans'][0]['status'] == "done" \ + else conductor_response['plans'][0]['status'] + status_message = conductor_response.get('plans')[0].get('message', "") + + solution_info = {} + if composite_solutions: + solution_info.setdefault('placementSolutions', []) + solution_info['placementSolutions'].append(composite_solutions) + + resp = { + "transactionId": transaction_id, + "requestId": req_id, + "requestStatus": request_status, + "statusMessage": status_message, + "solutions": solution_info + } + return resp + + +def conductor_no_solution_processor(conductor_response, request_id, transaction_id, + template_placement_response="templates/plc_opt_response.jsont"): + """Build a response object to be sent to client's callback URL from Conductor's response + + This is for case where no solution is found + :param conductor_response: JSON response from Conductor + :param raw_response: Raw HTTP response corresponding to above + :param request_id: request Id associated with the client request (same as conductor response's "name") + :param template_placement_response: the template for generating response to client (plc_opt_response.jsont) + :return: JSON object that can be sent to the client's callback URL + """ + status_message = conductor_response["plans"][0].get("message") + templ = Template(open(template_placement_response).read()) + return json.loads(templ.render(composite_solutions=[], requestId=request_id, license_solutions=[], + transactionId=transaction_id, + requestStatus="completed", statusMessage=status_message, json=json)) + + +def process_placement_opt(request_json, policies, osdf_config): + """Perform the work for placement optimization (e.g. call SDC artifact and make conductor request) + + NOTE: there is scope to make the requests to policy asynchronous to speed up overall performance + :param request_json: json content from original request + :param policies: flattened policies corresponding to this request + :param osdf_config: configuration specific to OSDF app + :param prov_status: provStatus retrieved from Subscriber policy + :return: None, but make a POST to callback URL + """ + + try: + mdc_from_json(request_json) + rc = get_rest_client(request_json, service="so") + req_id = request_json["requestInfo"]["requestId"] + transaction_id = request_json['requestInfo']['transactionId'] + + metrics_log.info(MH.inside_worker_thread(req_id)) + license_info = None + if request_json.get('licenseInfo', {}).get('licenseDemands'): + license_info = license_optim(request_json) + + # Conductor only handles placement, only call Conductor if placementDemands exist + if request_json.get('placementInfo', {}).get('placementDemands'): + metrics_log.info(MH.requesting("placement/conductor", req_id)) + req_info = request_json['requestInfo'] + demands = request_json['placementInfo']['placementDemands'] + request_parameters = request_json['placementInfo']['requestParameters'] + service_info = request_json['serviceInfo'] + template_fields = { + 'location_enabled': True, + 'version': '2017-10-10' + } + resp = conductor.request(req_info, demands, request_parameters, service_info, template_fields, + osdf_config, policies) + if resp["plans"][0].get("recommendations"): + placement_response = conductor_response_processor(resp, req_id, transaction_id) + else: # "solved" but no solutions found + placement_response = conductor_no_solution_processor(resp, req_id, transaction_id) + if license_info: # Attach license solution if it exists + placement_response['solutionInfo']['licenseInfo'] = license_info + else: # License selection only scenario + placement_response = { + "transactionId": transaction_id, + "requestId": req_id, + "requestStatus": "completed", + "statusMessage": "License selection completed successfully", + "solutionInfo": {"licenseInfo": license_info} + } + except Exception as err: + error_log.error("Error for {} {}".format(req_id, traceback.format_exc())) + + try: + body = build_json_error_body(err) + metrics_log.info(MH.sending_response(req_id, "ERROR")) + rc.request(json=body, noresponse=True) + except RequestException: + error_log.error("Error sending asynchronous notification for {} {}".format(req_id, traceback.format_exc())) + return + + try: + metrics_log.info(MH.calling_back_with_body(req_id, rc.url, placement_response)) + rc.request(json=placement_response, noresponse=True) + except RequestException: # can't do much here but log it and move on + error_log.error("Error sending asynchronous notification for {} {}".format(req_id, traceback.format_exc())) diff --git a/apps/placement/templates/plc_opt_request.jsont b/apps/placement/templates/plc_opt_request.jsont new file mode 100755 index 0000000..a218b8a --- /dev/null +++ b/apps/placement/templates/plc_opt_request.jsont @@ -0,0 +1,142 @@ +{
+ "name": "{{ name }}",
+ "files": "{{ files }}",
+ "timeout": "{{ timeout }}",
+ "num_solution": "{{ limit }}",
+ "template": {
+ "CUST_ID": "{{ cust_id }}",
+ "E2EVPNKEY": "{{ e2evpnkey }}",
+ "UCPEHOST": "{{ ucpehost }}",
+ "WAN_PORT1_UP": "{{ wan_port1_up }}",
+ "WAN_PORT1_DOWN": "{{ wan_port1_down }}",
+ "EFFECTIVE_BANDWIDTH": "{{ effective_bandwidth }}",
+ "SERVICE_INST": "{{ service_inst }}",
+ "locations": {
+ "customer_loc": {
+ "host_name": "{{ ucpehost }}"
+ }
+ },
+ "demands": [
+ {% set comma=joiner(",") %}
+ {% for demand in demand_list %} {{ comma() }}
+ {
+ "{{ demand.vnf_name }}": [
+ {% set comma2=joiner(",") %}
+ {% for property in demand.property %}
+ "inventory_provider": {{ property.inventory_provider }},
+ "inventory_type": {{ property.inventory_type }},
+ "service_type": {{ property.service_type }},
+ "customer_id": {{ property.customer_id }},
+ "candidate_id": {{ property.candidate_id }}
+ {% endfor %}
+ ]
+ }
+ {% endfor %}
+ ],
+ "constraints": {
+ {% set comma_main=joiner(",") %}
+
+ {% if attribute_policy_list %} {{ comma_main() }} {% endif %}
+ {% set comma=joiner(",") %}
+ {% for attribute in attribute_policy_list %} {{ comma() }}
+ attribute['identity'] : {
+ "type": {{ attribute['type'] }},
+ "demands": {{ attribute['demands'] }},
+ "properties": {
+ "evaluate": {
+ "hypervisor": {{ attribute['property']['hypervisor'] }},
+ "aic_version": {{ attribute['property']['aicVersion'] }},
+ "aic_type": {{ attribute['property']['aicType'] }},
+ "dataplane": {{ attribute['property']['datatype'] }},
+ "network_roles": {{ attribute['property']['networkRoles'] }},
+ "complex": {{ attribute['property']['complex'] }}
+ }
+ }
+ }
+ {% endfor %}
+
+ {% if distance_to_location_policy_list %} {{ comma_main() }} {% endif %}
+ {% set comma=joiner(",") %}
+ {% for distance_location in distance_to_location_policy_list %} {{ comma() }}
+ distance_location['identity'] : {
+ "type": {{ distance_location['type'] }},
+ "demands": {{ distance_location['demands'] }},
+ "properties": {
+ "distance": {{ distance_location['property']['distance'] }},
+ "location": {{ distance_location['property']['location'] }}
+ }
+ }
+ {% endfor %}
+
+ {% if inventory_policy_list %} {{ comma_main() }} {% endif %}
+ {% set comma=joiner(",") %}
+ {% for inventory in inventory_policy_list %} {{ comma() }}
+ inventory['identity'] : {
+ "type": {{ inventory['type'] }},
+ "demands": {{ inventory['demands'] }}
+ }
+ {% endfor %}
+
+ {% if resource_instance_policy_list %} {{ comma_main() }} {% endif %}
+ {% set comma=joiner(",") %}
+ {% for resource_instance in resource_instance_policy_list %} {{ comma() }}
+ resource_instance['identity'] : {
+ "type": {{ resource_instance['type'] }},
+ "demands": {{ resource_instance['demands'] }},
+ "properties": {
+ "controller": {{ resource_instance['property']['controller'] }},
+ "request": {{ resource_instance['property']['request'] }}
+ }
+ }
+ {% endfor %}
+
+ {% if resource_region_policy_list %} {{ comma_main() }} {% endif %}
+ {% set comma=joiner(",") %}
+ {% for resource_region in resource_region_policy_list %} {{ comma() }}
+ resource_region['identity'] : {
+ "type": {{ resource_region['type'] }},
+ "demands": {{ resource_region['demands'] }},
+ "properties": {
+ "controller": {{ resource_region['property']['controller'] }},
+ "request": {{ resource_region['property']['request'] }}
+ }
+ }
+ {% endfor %}
+
+ {% if zone_policy_list %} {{ comma_main() }} {% endif %}
+ {% set comma=joiner(",") %}
+ {% for zone in zone_policy_list %} {{ comma() }}
+ zone['identity'] : {
+ "type": {{ zone['type'] }},
+ "demands": {{ zone['demands'] }},
+ "properties": {
+ "qualifier": {{ resource_region['property']['qualifier'] }},
+ "category": {{ resource_region['property']['category'] }}
+ }
+ }
+ {% endfor %}
+
+ {% if optmization_policy_list %} {{ comma_main() }} {% endif %}
+ {% set comma=joiner(",") %}
+ {% for optimization in optimization_policy_list %} {{ comma() }}
+ "optimization" : {
+ {{ optimization['objective'] }}: {
+ "sum": [
+ {% set comma2=joiner(",") %}
+ {% for parameter in optimization['parameter'] %} {{ comma() }}
+ {
+ "product": [
+ {{ parameter['weight'] }},
+ {
+ "distance_between": [{{ parameter['customerLocation'] }},{{ parameter['demand'] }}]
+ }
+ ]
+ }
+ {% endfor %}
+ ]
+ }
+ }
+ {% endfor %}
+ }
+ }
+}
diff --git a/apps/placement/templates/plc_opt_response.jsont b/apps/placement/templates/plc_opt_response.jsont new file mode 100755 index 0000000..e5709e7 --- /dev/null +++ b/apps/placement/templates/plc_opt_response.jsont @@ -0,0 +1,10 @@ +{ + "requestId": "{{requestId}}", + "transactionId": "{{transacationId}}", + "requestStatus": "{{requestStatus}}", + "statusMessage": "{{statusMessage}}" + "solutions": { + "placementSolutions": {{ json.dumps(composite_solutions) }}, + "licenseSolutions":{{ json.dumps(license_solutions) }} + } +} diff --git a/apps/placement/templates/policy_request.jsont b/apps/placement/templates/policy_request.jsont new file mode 100755 index 0000000..3a9e201 --- /dev/null +++ b/apps/placement/templates/policy_request.jsont @@ -0,0 +1,3 @@ +{ + "policyName": "{{policy_name}}" {# we currently only support query by policy name only -- policyName #} +} |