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