41b901c6b76030c02a1c270e5994c8afe3baceb8
[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                 "cloud": "cloudRegionId", "service": "serviceInstanceId", "is_rehome": "isRehome",
131                 "location_id": "locationId", "location_type": "locationType"}
132     for reco in conductor_response['plans'][0]['recommendations']:
133         for resource in reco.keys():
134             c = reco[resource]['candidate']
135             solution = {
136                 'resourceModuleName': resource,
137                 'serviceResourceId': reco[resource].get('service_resource_id', ""),
138                 'solution': {"identifier": c['inventory_type'],
139                              'identifiers': [c['candidate_id']],
140                              'cloudOwner': c.get('cloud_owner', "")},
141                 'assignmentInfo': []
142             }
143             for key, value in c.items():
144                 if key in ["location_id", "location_type", "is_rehome", "host_id"]:
145                     try:
146                         solution['assignmentInfo'].append({"key": name_map.get(key, key), "value": value})
147                     except KeyError:
148                         debug_log.debug("The key[{}] is not mapped and will not be returned in assignment info".format(key))
149
150             for key, value in reco[resource]['attributes'].items():
151                 try:
152                     solution['assignmentInfo'].append({"key": name_map.get(key, key), "value": value})
153                 except KeyError:
154                     debug_log.debug("The key[{}] is not mapped and will not be returned in assignment info".format(key))
155             composite_solutions.append(solution)
156
157     request_state = conductor_response['plans'][0]['status']
158     transaction_id = raw_response.headers.get('transaction_id', "")
159     status_message = conductor_response.get('plans')[0].get('message', "")
160
161     solution_info = {}
162     if composite_solutions:
163         solution_info.setdefault('placementSolutions', [])
164         solution_info['placementSolutions'].append(composite_solutions)
165
166     resp = {
167         "transactionId": transaction_id,
168         "requestId": req_id,
169         "requestState": request_state,
170         "statusMessage": status_message,
171         "solutions": solution_info
172     }
173     return resp
174
175
176 def conductor_no_solution_processor(conductor_response, raw_response, request_id,
177                                     template_placement_response="templates/plc_opt_response.jsont"):
178     """Build a response object to be sent to client's callback URL from Conductor's response
179     This is for case where no solution is found
180
181     :param conductor_response: JSON response from Conductor
182     :param raw_response: Raw HTTP response corresponding to above
183     :param request_id: request Id associated with the client request (same as conductor response's "name")
184     :param template_placement_response: the template for generating response to client (plc_opt_response.jsont)
185     :return: JSON object that can be sent to the client's callback URL
186     """
187     status_message = conductor_response["plans"][0].get("message")
188     templ = Template(open(template_placement_response).read())
189     return json.loads(templ.render(composite_solutions=[], requestId=request_id, license_solutions=[],
190                                    transactionId=raw_response.headers.get('transaction_id', ""),
191                                    requestState="completed", statusMessage=status_message, json=json))
192
193