1 # -------------------------------------------------------------------------
\r
2 # Copyright (c) 2015-2017 AT&T Intellectual Property
\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
8 # http://www.apache.org/licenses/LICENSE-2.0
\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
16 # -------------------------------------------------------------------------
\r
20 This application generates conductor API calls using the information received from SO and Policy platform.
\r
26 from jinja2 import Template
\r
27 from requests import RequestException
\r
29 from osdf.logging.osdf_logging import debug_log
\r
30 from osdf.optimizers.placementopt.conductor.api_builder import conductor_api_builder
\r
31 from osdf.utils.interfaces import RestClient
\r
32 from osdf.operation.exceptions import BusinessException
\r
35 def request(req_object, osdf_config, flat_policies):
\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
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
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
60 max_retries = config.get('conductorMaxRetries', 30)
\r
61 ping_wait_time = config.get('conductorPingWaitTime', 60)
\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
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
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
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
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
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
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
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
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
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
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
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
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
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
151 for key, value in c.items():
\r
152 if key in ["location_id", "location_type", "is_rehome", "host_id"]:
\r
154 solution['assignmentInfo'].append({"key": name_map.get(key, key), "value": value})
\r
156 debug_log.debug("The key[{}] is not mapped and will not be returned in assignment info".format(key))
\r
158 for key, value in reco[resource]['attributes'].items():
\r
160 solution['assignmentInfo'].append({"key": name_map.get(key, key), "value": value})
\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
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
171 if composite_solutions:
\r
172 solution_info.setdefault('placementSolutions', [])
\r
173 solution_info['placementSolutions'].append(composite_solutions)
\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
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
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
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