Add support to process NSI selection request
[optf/osdf.git] / apps / placement / optimizers / conductor / remote_opt_processor.py
1 # -------------------------------------------------------------------------
2 #   Copyright (c) 2015-2017 AT&T Intellectual Property
3 #   Copyright (C) 2020 Wipro Limited.
4 #
5 #   Licensed under the Apache License, Version 2.0 (the "License");
6 #   you may not use this file except in compliance with the License.
7 #   You may obtain a copy of the License at
8 #
9 #       http://www.apache.org/licenses/LICENSE-2.0
10 #
11 #   Unless required by applicable law or agreed to in writing, software
12 #   distributed under the License is distributed on an "AS IS" BASIS,
13 #   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 #   See the License for the specific language governing permissions and
15 #   limitations under the License.
16 #
17 # -------------------------------------------------------------------------
18 #
19
20 from jinja2 import Template
21 import json
22 from requests import RequestException
23 import traceback
24
25 from apps.license.optimizers.simple_license_allocation import license_optim
26 from osdf.adapters.conductor import conductor
27 from osdf.logging.osdf_logging import debug_log
28 from osdf.logging.osdf_logging import error_log
29 from osdf.logging.osdf_logging import metrics_log
30 from osdf.logging.osdf_logging import MH
31 from osdf.operation.error_handling import build_json_error_body
32 from osdf.utils.interfaces import get_rest_client
33 from osdf.utils.mdc_utils import mdc_from_json
34
35
36 def conductor_response_processor(conductor_response, req_id, transaction_id):
37     """Build a response object to be sent to client's callback URL from Conductor's response
38
39     This includes Conductor's placement optimization response, and required ASDC license artifacts
40     :param conductor_response: JSON response from Conductor
41     :param raw_response: Raw HTTP response corresponding to above
42     :param req_id: Id of a request
43     :return: JSON object that can be sent to the client's callback URL
44     """
45     composite_solutions = []
46     name_map = {"physical-location-id": "cloudClli", "host_id": "vnfHostName",
47                 "cloud_version": "cloudVersion", "cloud_owner": "cloudOwner",
48                 "cloud": "cloudRegionId", "service": "serviceInstanceId", "is_rehome": "isRehome",
49                 "location_id": "locationId", "location_type": "locationType", "directives": "oof_directives"}
50     for reco in conductor_response['plans'][0]['recommendations']:
51         for resource in reco.keys():
52             c = reco[resource]['candidate']
53             solution = {
54                 'resourceModuleName': resource,
55                 'serviceResourceId': reco[resource].get('service_resource_id', ""),
56                 'solution': {"identifierType": name_map.get(c['inventory_type'], c['inventory_type']),
57                              'identifiers': [c['candidate_id']],
58                              'cloudOwner': c.get('cloud_owner', "")},
59                 'assignmentInfo': []
60             }
61             for key, value in c.items():
62                 if key in ["location_id", "location_type", "is_rehome", "host_id"]:
63                     try:
64                         solution['assignmentInfo'].append({"key": name_map.get(key, key), "value": value})
65                     except KeyError:
66                         debug_log.debug("The key[{}] is not mapped and will not be returned in assignment info"
67                                         .format(key))
68
69             for key, value in reco[resource]['attributes'].items():
70                 try:
71                     solution['assignmentInfo'].append({"key": name_map.get(key, key), "value": value})
72                 except KeyError:
73                     debug_log.debug("The key[{}] is not mapped and will not be returned in assignment info"
74                                     .format(key))
75             composite_solutions.append(solution)
76
77     request_status = "completed" if conductor_response['plans'][0]['status'] == "done" \
78         else conductor_response['plans'][0]['status']
79     status_message = conductor_response.get('plans')[0].get('message', "")
80
81     solution_info = {}
82     if composite_solutions:
83         solution_info.setdefault('placementSolutions', [])
84         solution_info['placementSolutions'].append(composite_solutions)
85
86     resp = {
87         "transactionId": transaction_id,
88         "requestId": req_id,
89         "requestStatus": request_status,
90         "statusMessage": status_message,
91         "solutions": solution_info
92     }
93     return resp
94
95
96 def conductor_no_solution_processor(conductor_response, request_id, transaction_id,
97                                     template_placement_response="templates/plc_opt_response.jsont"):
98     """Build a response object to be sent to client's callback URL from Conductor's response
99
100     This is for case where no solution is found
101     :param conductor_response: JSON response from Conductor
102     :param raw_response: Raw HTTP response corresponding to above
103     :param request_id: request Id associated with the client request (same as conductor response's "name")
104     :param template_placement_response: the template for generating response to client (plc_opt_response.jsont)
105     :return: JSON object that can be sent to the client's callback URL
106     """
107     status_message = conductor_response["plans"][0].get("message")
108     templ = Template(open(template_placement_response).read())
109     return json.loads(templ.render(composite_solutions=[], requestId=request_id, license_solutions=[],
110                                    transactionId=transaction_id,
111                                    requestStatus="completed", statusMessage=status_message, json=json))
112
113
114 def process_placement_opt(request_json, policies, osdf_config):
115     """Perform the work for placement optimization (e.g. call SDC artifact and make conductor request)
116
117     NOTE: there is scope to make the requests to policy asynchronous to speed up overall performance
118     :param request_json: json content from original request
119     :param policies: flattened policies corresponding to this request
120     :param osdf_config: configuration specific to OSDF app
121     :param prov_status: provStatus retrieved from Subscriber policy
122     :return: None, but make a POST to callback URL
123     """
124
125     try:
126         mdc_from_json(request_json)
127         rc = get_rest_client(request_json, service="so")
128         req_id = request_json["requestInfo"]["requestId"]
129         transaction_id = request_json['requestInfo']['transactionId']
130
131         metrics_log.info(MH.inside_worker_thread(req_id))
132         license_info = None
133         if request_json.get('licenseInfo', {}).get('licenseDemands'):
134             license_info = license_optim(request_json)
135
136         # Conductor only handles placement, only call Conductor if placementDemands exist
137         if request_json.get('placementInfo', {}).get('placementDemands'):
138             metrics_log.info(MH.requesting("placement/conductor", req_id))
139             req_info = request_json['requestInfo']
140             demands = request_json['placementInfo']['placementDemands']
141             request_parameters = request_json['placementInfo']['requestParameters']
142             service_info = request_json['serviceInfo']
143             template_fields = {
144                 'location_enabled': True,
145                 'version': '2017-10-10'
146             }
147             resp = conductor.request(req_info, demands, request_parameters, service_info, template_fields,
148                                      osdf_config, policies)
149             if resp["plans"][0].get("recommendations"):
150                 placement_response = conductor_response_processor(resp, req_id, transaction_id)
151             else:  # "solved" but no solutions found
152                 placement_response = conductor_no_solution_processor(resp, req_id, transaction_id)
153             if license_info:  # Attach license solution if it exists
154                 placement_response['solutionInfo']['licenseInfo'] = license_info
155         else:  # License selection only scenario
156             placement_response = {
157                 "transactionId": transaction_id,
158                 "requestId": req_id,
159                 "requestStatus": "completed",
160                 "statusMessage": "License selection completed successfully",
161                 "solutionInfo": {"licenseInfo": license_info}
162             }
163     except Exception as err:
164         error_log.error("Error for {} {}".format(req_id, traceback.format_exc()))
165
166         try:
167             body = build_json_error_body(err)
168             metrics_log.info(MH.sending_response(req_id, "ERROR"))
169             rc.request(json=body, noresponse=True)
170         except RequestException:
171             error_log.error("Error sending asynchronous notification for {} {}".format(req_id, traceback.format_exc()))
172         return
173
174     try:
175         metrics_log.info(MH.calling_back_with_body(req_id, rc.url, placement_response))
176         rc.request(json=placement_response, noresponse=True)
177     except RequestException:  # can't do much here but log it and move on
178         error_log.error("Error sending asynchronous notification for {} {}".format(req_id, traceback.format_exc()))