-# -------------------------------------------------------------------------\r
-# Copyright (c) 2015-2017 AT&T Intellectual Property\r
-#\r
-# Licensed under the Apache License, Version 2.0 (the "License");\r
-# you may not use this file except in compliance with the License.\r
-# You may obtain a copy of the License at\r
-#\r
-# http://www.apache.org/licenses/LICENSE-2.0\r
-#\r
-# Unless required by applicable law or agreed to in writing, software\r
-# distributed under the License is distributed on an "AS IS" BASIS,\r
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
-# See the License for the specific language governing permissions and\r
-# limitations under the License.\r
-#\r
-# -------------------------------------------------------------------------\r
-#\r
-\r
-"""\r
-This application generates conductor API calls using the information received from SO and Policy platform.\r
-"""\r
-\r
-import json\r
-import time\r
-\r
-from jinja2 import Template\r
-from requests import RequestException\r
-\r
-from apps.placement.optimizers.conductor.api_builder import conductor_api_builder\r
-from osdf.logging.osdf_logging import debug_log\r
-from osdf.utils.interfaces import RestClient\r
-from osdf.operation.exceptions import BusinessException\r
-\r
-\r
-def request(req_object, osdf_config, flat_policies):\r
- """\r
- Process a placement request from a Client (build Conductor API call, make the call, return result)\r
- :param req_object: Request parameters from the client\r
- :param osdf_config: Configuration specific to SNIRO application (core + deployment)\r
- :param flat_policies: policies related to placement (fetched based on request)\r
- :param prov_status: provStatus retrieved from Subscriber policy\r
- :return: response from Conductor (accounting for redirects from Conductor service\r
- """\r
- config = osdf_config.deployment\r
- local_config = osdf_config.core\r
- uid, passwd = config['conductorUsername'], config['conductorPassword']\r
- conductor_url = config['conductorUrl']\r
- req_id = req_object['requestInfo']['requestId']\r
- transaction_id = req_object['requestInfo']['transactionId']\r
- headers = dict(transaction_id=transaction_id)\r
- placement_ver_enabled = config.get('placementVersioningEnabled', False)\r
- \r
- if placement_ver_enabled:\r
- cond_minor_version = config.get('conductorMinorVersion', None) \r
- if cond_minor_version is not None:\r
- x_minor_version = str(cond_minor_version)\r
- headers.update({'X-MinorVersion': x_minor_version})\r
- debug_log.debug("Versions set in HTTP header to conductor: X-MinorVersion: {} ".format(x_minor_version))\r
-\r
- max_retries = config.get('conductorMaxRetries', 30)\r
- ping_wait_time = config.get('conductorPingWaitTime', 60)\r
-\r
- rc = RestClient(userid=uid, passwd=passwd, method="GET", log_func=debug_log.debug, headers=headers)\r
- conductor_req_json_str = conductor_api_builder(req_object, flat_policies, local_config)\r
- conductor_req_json = json.loads(conductor_req_json_str)\r
-\r
- debug_log.debug("Sending first Conductor request for request_id {}".format(req_id))\r
- resp, raw_resp = initial_request_to_conductor(rc, conductor_url, conductor_req_json)\r
- # Very crude way of keeping track of time.\r
- # We are not counting initial request time, first call back, or time for HTTP request\r
- total_time, ctr = 0, 2\r
- client_timeout = req_object['requestInfo']['timeout']\r
- configured_timeout = max_retries * ping_wait_time\r
- max_timeout = min(client_timeout, configured_timeout)\r
-\r
- while True: # keep requesting conductor till we get a result or we run out of time\r
- if resp is not None:\r
- if resp["plans"][0].get("status") in ["error"]:\r
- raise RequestException(response=raw_resp, request=raw_resp.request)\r
-\r
- if resp["plans"][0].get("status") in ["done", "not found"]:\r
- if resp["plans"][0].get("recommendations"):\r
- return conductor_response_processor(resp, raw_resp, req_id)\r
- else: # "solved" but no solutions found\r
- return conductor_no_solution_processor(resp, raw_resp, req_id)\r
- new_url = resp['plans'][0]['links'][0][0]['href'] # TODO: check why a list of lists\r
-\r
- if total_time >= max_timeout:\r
- raise BusinessException("Conductor could not provide a solution within {} seconds,"\r
- "this transaction is timing out".format(max_timeout))\r
- time.sleep(ping_wait_time)\r
- ctr += 1\r
- debug_log.debug("Attempt number {} url {}; prior status={}".format(ctr, new_url, resp['plans'][0]['status']))\r
- total_time += ping_wait_time\r
-\r
- try:\r
- raw_resp = rc.request(new_url, raw_response=True)\r
- resp = raw_resp.json()\r
- except RequestException as e:\r
- debug_log.debug("Conductor attempt {} for request_id {} has failed because {}".format(ctr, req_id, str(e)))\r
-\r
-\r
-def initial_request_to_conductor(rc, conductor_url, conductor_req_json):\r
- """First steps in the request-redirect chain in making a call to Conductor\r
- :param rc: REST client object for calling conductor\r
- :param conductor_url: conductor's base URL to submit a placement request\r
- :param conductor_req_json: request json object to send to Conductor\r
- :return: URL to check for follow up (similar to redirects); we keep checking these till we get a result/error\r
- """\r
- debug_log.debug("Payload to Conductor: {}".format(json.dumps(conductor_req_json)))\r
- raw_resp = rc.request(url=conductor_url, raw_response=True, method="POST", json=conductor_req_json)\r
- resp = raw_resp.json()\r
- if resp["status"] != "template":\r
- raise RequestException(response=raw_resp, request=raw_resp.request)\r
- time.sleep(10) # 10 seconds wait time to avoid being too quick!\r
- plan_url = resp["links"][0][0]["href"]\r
- debug_log.debug("Attempting to read the plan from the conductor provided url {}".format(plan_url))\r
- raw_resp = rc.request(raw_response=True, url=plan_url) # TODO: check why a list of lists for links\r
- resp = raw_resp.json()\r
-\r
- if resp["plans"][0]["status"] in ["error"]:\r
- raise RequestException(response=raw_resp, request=raw_resp.request)\r
- return resp, raw_resp # now the caller of this will handle further follow-ups\r
-\r
-\r
-def conductor_response_processor(conductor_response, raw_response, req_id):\r
- """Build a response object to be sent to client's callback URL from Conductor's response\r
- This includes Conductor's placement optimization response, and required ASDC license artifacts\r
-\r
- :param conductor_response: JSON response from Conductor\r
- :param raw_response: Raw HTTP response corresponding to above\r
- :param req_id: Id of a request\r
- :return: JSON object that can be sent to the client's callback URL\r
- """\r
- composite_solutions = []\r
- name_map = {"physical-location-id": "cloudClli", "host_id": "vnfHostName",\r
- "cloud_version": "cloudVersion", "cloud_owner": "cloudOwner",\r
- "cloud": "cloudRegionId", "service": "serviceInstanceId", "is_rehome": "isRehome",\r
- "location_id": "locationId", "location_type": "locationType", "directives": "oof_directives"}\r
- for reco in conductor_response['plans'][0]['recommendations']:\r
- for resource in reco.keys():\r
- c = reco[resource]['candidate']\r
- solution = {\r
- 'resourceModuleName': resource,\r
- 'serviceResourceId': reco[resource].get('service_resource_id', ""),\r
- 'solution': {"identifierType": name_map.get(c['inventory_type'], c['inventory_type']),\r
- 'identifiers': [c['candidate_id']],\r
- 'cloudOwner': c.get('cloud_owner', "")},\r
- 'assignmentInfo': []\r
- }\r
- for key, value in c.items():\r
- if key in ["location_id", "location_type", "is_rehome", "host_id"]:\r
- try:\r
- solution['assignmentInfo'].append({"key": name_map.get(key, key), "value": value})\r
- except KeyError:\r
- debug_log.debug("The key[{}] is not mapped and will not be returned in assignment info".format(key))\r
-\r
- for key, value in reco[resource]['attributes'].items():\r
- try:\r
- solution['assignmentInfo'].append({"key": name_map.get(key, key), "value": value})\r
- except KeyError:\r
- debug_log.debug("The key[{}] is not mapped and will not be returned in assignment info".format(key))\r
- composite_solutions.append(solution)\r
-\r
- request_status = "completed" if conductor_response['plans'][0]['status'] == "done" \\r
- else conductor_response['plans'][0]['status']\r
- transaction_id = raw_response.headers.get('transaction_id', "")\r
- status_message = conductor_response.get('plans')[0].get('message', "")\r
-\r
- solution_info = {}\r
- if composite_solutions:\r
- solution_info.setdefault('placementSolutions', [])\r
- solution_info['placementSolutions'].append(composite_solutions)\r
-\r
- resp = {\r
- "transactionId": transaction_id,\r
- "requestId": req_id,\r
- "requestStatus": request_status,\r
- "statusMessage": status_message,\r
- "solutions": solution_info\r
- }\r
- return resp\r
-\r
-\r
-def conductor_no_solution_processor(conductor_response, raw_response, request_id,\r
- template_placement_response="templates/plc_opt_response.jsont"):\r
- """Build a response object to be sent to client's callback URL from Conductor's response\r
- This is for case where no solution is found\r
-\r
- :param conductor_response: JSON response from Conductor\r
- :param raw_response: Raw HTTP response corresponding to above\r
- :param request_id: request Id associated with the client request (same as conductor response's "name")\r
- :param template_placement_response: the template for generating response to client (plc_opt_response.jsont)\r
- :return: JSON object that can be sent to the client's callback URL\r
- """\r
- status_message = conductor_response["plans"][0].get("message")\r
- templ = Template(open(template_placement_response).read())\r
- return json.loads(templ.render(composite_solutions=[], requestId=request_id, license_solutions=[],\r
- transactionId=raw_response.headers.get('transaction_id', ""),\r
- requestStatus="completed", statusMessage=status_message, json=json))\r
-\r
-\r