osdf rearchitecture into apps and libs
[optf/osdf.git] / apps / placement / optimizers / conductor / conductor.py
1 # -------------------------------------------------------------------------\r
2 #   Copyright (c) 2015-2017 AT&T Intellectual Property\r
3 #\r
4 #   Licensed under the Apache License, Version 2.0 (the "License");\r
5 #   you may not use this file except in compliance with the License.\r
6 #   You may obtain a copy of the License at\r
7 #\r
8 #       http://www.apache.org/licenses/LICENSE-2.0\r
9 #\r
10 #   Unless required by applicable law or agreed to in writing, software\r
11 #   distributed under the License is distributed on an "AS IS" BASIS,\r
12 #   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
13 #   See the License for the specific language governing permissions and\r
14 #   limitations under the License.\r
15 #\r
16 # -------------------------------------------------------------------------\r
17 #\r
18 \r
19 """\r
20 This application generates conductor API calls using the information received from SO and Policy platform.\r
21 """\r
22 \r
23 import json\r
24 import time\r
25 \r
26 from jinja2 import Template\r
27 from requests import RequestException\r
28 \r
29 from apps.placement.optimizers.conductor.api_builder import conductor_api_builder\r
30 from osdf.logging.osdf_logging import debug_log\r
31 from osdf.utils.interfaces import RestClient\r
32 from osdf.operation.exceptions import BusinessException\r
33 \r
34 \r
35 def request(req_object, osdf_config, flat_policies):\r
36     """\r
37     Process a placement request from a Client (build Conductor API call, make the call, return result)\r
38     :param req_object: Request parameters from the client\r
39     :param osdf_config: Configuration specific to SNIRO application (core + deployment)\r
40     :param flat_policies: policies related to placement (fetched based on request)\r
41     :param prov_status: provStatus retrieved from Subscriber policy\r
42     :return: response from Conductor (accounting for redirects from Conductor service\r
43     """\r
44     config = osdf_config.deployment\r
45     local_config = osdf_config.core\r
46     uid, passwd = config['conductorUsername'], config['conductorPassword']\r
47     conductor_url = config['conductorUrl']\r
48     req_id = req_object['requestInfo']['requestId']\r
49     transaction_id = req_object['requestInfo']['transactionId']\r
50     headers = dict(transaction_id=transaction_id)\r
51     placement_ver_enabled = config.get('placementVersioningEnabled', False)\r
52     \r
53     if placement_ver_enabled:\r
54         cond_minor_version = config.get('conductorMinorVersion', None) \r
55         if cond_minor_version is not None:\r
56             x_minor_version = str(cond_minor_version)\r
57             headers.update({'X-MinorVersion': x_minor_version})\r
58         debug_log.debug("Versions set in HTTP header to conductor: X-MinorVersion: {} ".format(x_minor_version))\r
59 \r
60     max_retries = config.get('conductorMaxRetries', 30)\r
61     ping_wait_time = config.get('conductorPingWaitTime', 60)\r
62 \r
63     rc = RestClient(userid=uid, passwd=passwd, method="GET", log_func=debug_log.debug, headers=headers)\r
64     conductor_req_json_str = conductor_api_builder(req_object, flat_policies, local_config)\r
65     conductor_req_json = json.loads(conductor_req_json_str)\r
66 \r
67     debug_log.debug("Sending first Conductor request for request_id {}".format(req_id))\r
68     resp, raw_resp = initial_request_to_conductor(rc, conductor_url, conductor_req_json)\r
69     # Very crude way of keeping track of time.\r
70     # We are not counting initial request time, first call back, or time for HTTP request\r
71     total_time, ctr = 0, 2\r
72     client_timeout = req_object['requestInfo']['timeout']\r
73     configured_timeout = max_retries * ping_wait_time\r
74     max_timeout = min(client_timeout, configured_timeout)\r
75 \r
76     while True:  # keep requesting conductor till we get a result or we run out of time\r
77         if resp is not None:\r
78             if resp["plans"][0].get("status") in ["error"]:\r
79                 raise RequestException(response=raw_resp, request=raw_resp.request)\r
80 \r
81             if resp["plans"][0].get("status") in ["done", "not found"]:\r
82                 if resp["plans"][0].get("recommendations"):\r
83                     return conductor_response_processor(resp, raw_resp, req_id)\r
84                 else:  # "solved" but no solutions found\r
85                     return conductor_no_solution_processor(resp, raw_resp, req_id)\r
86             new_url = resp['plans'][0]['links'][0][0]['href']  # TODO: check why a list of lists\r
87 \r
88         if total_time >= max_timeout:\r
89             raise BusinessException("Conductor could not provide a solution within {} seconds,"\r
90                                     "this transaction is timing out".format(max_timeout))\r
91         time.sleep(ping_wait_time)\r
92         ctr += 1\r
93         debug_log.debug("Attempt number {} url {}; prior status={}".format(ctr, new_url, resp['plans'][0]['status']))\r
94         total_time += ping_wait_time\r
95 \r
96         try:\r
97             raw_resp = rc.request(new_url, raw_response=True)\r
98             resp = raw_resp.json()\r
99         except RequestException as e:\r
100             debug_log.debug("Conductor attempt {} for request_id {} has failed because {}".format(ctr, req_id, str(e)))\r
101 \r
102 \r
103 def initial_request_to_conductor(rc, conductor_url, conductor_req_json):\r
104     """First steps in the request-redirect chain in making a call to Conductor\r
105     :param rc: REST client object for calling conductor\r
106     :param conductor_url: conductor's base URL to submit a placement request\r
107     :param conductor_req_json: request json object to send to Conductor\r
108     :return: URL to check for follow up (similar to redirects); we keep checking these till we get a result/error\r
109     """\r
110     debug_log.debug("Payload to Conductor: {}".format(json.dumps(conductor_req_json)))\r
111     raw_resp = rc.request(url=conductor_url, raw_response=True, method="POST", json=conductor_req_json)\r
112     resp = raw_resp.json()\r
113     if resp["status"] != "template":\r
114         raise RequestException(response=raw_resp, request=raw_resp.request)\r
115     time.sleep(10)  # 10 seconds wait time to avoid being too quick!\r
116     plan_url = resp["links"][0][0]["href"]\r
117     debug_log.debug("Attempting to read the plan from the conductor provided url {}".format(plan_url))\r
118     raw_resp = rc.request(raw_response=True, url=plan_url)  # TODO: check why a list of lists for links\r
119     resp = raw_resp.json()\r
120 \r
121     if resp["plans"][0]["status"] in ["error"]:\r
122         raise RequestException(response=raw_resp, request=raw_resp.request)\r
123     return resp, raw_resp  # now the caller of this will handle further follow-ups\r
124 \r
125 \r
126 def conductor_response_processor(conductor_response, raw_response, req_id):\r
127     """Build a response object to be sent to client's callback URL from Conductor's response\r
128     This includes Conductor's placement optimization response, and required ASDC license artifacts\r
129 \r
130     :param conductor_response: JSON response from Conductor\r
131     :param raw_response: Raw HTTP response corresponding to above\r
132     :param req_id: Id of a request\r
133     :return: JSON object that can be sent to the client's callback URL\r
134     """\r
135     composite_solutions = []\r
136     name_map = {"physical-location-id": "cloudClli", "host_id": "vnfHostName",\r
137                 "cloud_version": "cloudVersion", "cloud_owner": "cloudOwner",\r
138                 "cloud": "cloudRegionId", "service": "serviceInstanceId", "is_rehome": "isRehome",\r
139                 "location_id": "locationId", "location_type": "locationType", "directives": "oof_directives"}\r
140     for reco in conductor_response['plans'][0]['recommendations']:\r
141         for resource in reco.keys():\r
142             c = reco[resource]['candidate']\r
143             solution = {\r
144                 'resourceModuleName': resource,\r
145                 'serviceResourceId': reco[resource].get('service_resource_id', ""),\r
146                 'solution': {"identifierType": name_map.get(c['inventory_type'], c['inventory_type']),\r
147                              'identifiers': [c['candidate_id']],\r
148                              'cloudOwner': c.get('cloud_owner', "")},\r
149                 'assignmentInfo': []\r
150             }\r
151             for key, value in c.items():\r
152                 if key in ["location_id", "location_type", "is_rehome", "host_id"]:\r
153                     try:\r
154                         solution['assignmentInfo'].append({"key": name_map.get(key, key), "value": value})\r
155                     except KeyError:\r
156                         debug_log.debug("The key[{}] is not mapped and will not be returned in assignment info".format(key))\r
157 \r
158             for key, value in reco[resource]['attributes'].items():\r
159                 try:\r
160                     solution['assignmentInfo'].append({"key": name_map.get(key, key), "value": value})\r
161                 except KeyError:\r
162                     debug_log.debug("The key[{}] is not mapped and will not be returned in assignment info".format(key))\r
163             composite_solutions.append(solution)\r
164 \r
165     request_status = "completed" if conductor_response['plans'][0]['status'] == "done" \\r
166         else conductor_response['plans'][0]['status']\r
167     transaction_id = raw_response.headers.get('transaction_id', "")\r
168     status_message = conductor_response.get('plans')[0].get('message', "")\r
169 \r
170     solution_info = {}\r
171     if composite_solutions:\r
172         solution_info.setdefault('placementSolutions', [])\r
173         solution_info['placementSolutions'].append(composite_solutions)\r
174 \r
175     resp = {\r
176         "transactionId": transaction_id,\r
177         "requestId": req_id,\r
178         "requestStatus": request_status,\r
179         "statusMessage": status_message,\r
180         "solutions": solution_info\r
181     }\r
182     return resp\r
183 \r
184 \r
185 def conductor_no_solution_processor(conductor_response, raw_response, request_id,\r
186                                     template_placement_response="templates/plc_opt_response.jsont"):\r
187     """Build a response object to be sent to client's callback URL from Conductor's response\r
188     This is for case where no solution is found\r
189 \r
190     :param conductor_response: JSON response from Conductor\r
191     :param raw_response: Raw HTTP response corresponding to above\r
192     :param request_id: request Id associated with the client request (same as conductor response's "name")\r
193     :param template_placement_response: the template for generating response to client (plc_opt_response.jsont)\r
194     :return: JSON object that can be sent to the client's callback URL\r
195     """\r
196     status_message = conductor_response["plans"][0].get("message")\r
197     templ = Template(open(template_placement_response).read())\r
198     return json.loads(templ.render(composite_solutions=[], requestId=request_id, license_solutions=[],\r
199                                    transactionId=raw_response.headers.get('transaction_id', ""),\r
200                                    requestStatus="completed", statusMessage=status_message, json=json))\r
201 \r
202 \r