Re-org folders, onboard test folder, test config
[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, prov_status):
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, prov_status)
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, this transaction is timing out".format(max_timeout))
82         time.sleep(ping_wait_time)
83         ctr += 1
84         debug_log.debug("Attempt number {} url {}; prior status={}".format(ctr, new_url, resp['plans'][0]['status']))
85         total_time += ping_wait_time
86
87         try:
88             raw_resp = rc.request(new_url, raw_response=True)
89             resp = raw_resp.json()
90         except RequestException as e:
91             debug_log.debug("Conductor attempt {} for request_id {} has failed because {}".format(ctr, req_id, str(e)))
92
93
94 def initial_request_to_conductor(rc, conductor_url, conductor_req_json):
95     """First steps in the request-redirect chain in making a call to Conductor
96     :param rc: REST client object for calling conductor
97     :param conductor_url: conductor's base URL to submit a placement request
98     :param conductor_req_json: request json object to send to Conductor
99     :return: URL to check for follow up (similar to redirects); we keep checking these till we get a result/error
100     """
101     debug_log.debug("Payload to Conductor: {}".format(json.dumps(conductor_req_json)))
102     raw_resp = rc.request(url=conductor_url, raw_response=True, method="POST", json=conductor_req_json)
103     resp = raw_resp.json()
104     if resp["status"] != "template":
105         raise RequestException(response=raw_resp, request=raw_resp.request)
106     time.sleep(10)  # 10 seconds wait time to avoid being too quick!
107     plan_url = resp["links"][0][0]["href"]
108     debug_log.debug("Attemping to read the plan from the conductor provided url {}".format(plan_url))
109     raw_resp = rc.request(raw_response=True, url=plan_url)  # TODO: check why a list of lists for links
110     resp = raw_resp.json()
111
112     if resp["plans"][0]["status"] in ["error"]:
113         raise RequestException(response=raw_resp, request=raw_resp.request)
114     return resp, raw_resp  # now the caller of this will handle further follow-ups
115
116
117 def conductor_response_processor(conductor_response, raw_response, req_id):
118     """Build a response object to be sent to client's callback URL from Conductor's response
119     This includes Conductor's placement optimization response, and required ASDC license artifacts
120
121     :param conductor_response: JSON response from Conductor
122     :param raw_response: Raw HTTP response corresponding to above
123     :param req_id: Id of a request
124     :return: JSON object that can be sent to the client's callback URL
125     """
126     composite_solutions = []
127     name_map = {"physical-location-id": "cloudClli", "host_id": "vnfHostName",
128                 "cloud_version": "cloudVersion", "cloud_owner": "cloudOwner"}
129     for reco in conductor_response['plans'][0]['recommendations']:
130         for resource in reco.keys():
131             c = reco[resource]['candidate']
132             solution = {
133                 'resourceModuleName': resource,
134                 'serviceResourceId': reco[resource]['service_resource_id'],
135                 'inventoryType': c['inventory_type'],
136                 'serviceInstanceId': c['candidate_id'] if c['inventory_type'] == "service" else "",
137                 'cloudRegionId': c['location_id'],
138                 'assignmentInfo': []
139             }
140
141             for key, value in reco[resource]['attributes'].items():
142                 try:
143                     solution['assignmentInfo'].append({"variableName": name_map[key], "variableValue": value})
144                 except KeyError:
145                     debug_log.debug("The key[{}] is not mapped and will not be returned in assignment info".format(key))
146
147             if c.get('host_id'):
148                 solution['assignmentInfo'].append({'variableName': name_map['host_id'], 'variableValue': c['host_id']})
149             composite_solutions.append(solution)
150
151     request_state = conductor_response['plans'][0]['status']
152     transaction_id = raw_response.headers.get('transaction_id', "")
153     status_message = conductor_response.get('plans')[0].get('message', "")
154
155     solution_info = {}
156     if composite_solutions:
157         solution_info['placementInfo'] = composite_solutions
158
159     resp = {
160         "transactionId": transaction_id,
161         "requestId": req_id,
162         "requestState": request_state,
163         "statusMessage": status_message,
164         "solutionInfo": solution_info
165     }
166     return resp
167
168
169 def conductor_no_solution_processor(conductor_response, raw_response, request_id,
170                                     template_placement_response="templates/plc_opt_response.jsont"):
171     """Build a response object to be sent to client's callback URL from Conductor's response
172     This is for case where no solution is found
173
174     :param conductor_response: JSON response from Conductor
175     :param raw_response: Raw HTTP response corresponding to above
176     :param request_id: request Id associated with the client request (same as conductor response's "name")
177     :param template_placement_response: the template for generating response to client (plc_opt_response.jsont)
178     :return: JSON object that can be sent to the client's callback URL
179     """
180     status_message = conductor_response["plans"][0].get("message")
181     templ = Template(open(template_placement_response).read())
182     return json.loads(templ.render(composite_solutions=[], requestId=request_id,
183                                    transactionId=raw_response.headers.get('transaction_id', ""),
184                                    statusMessage=status_message, json=json))
185
186