From d7eb44a65b37000d5d30245e6ac26bd68827804d Mon Sep 17 00:00:00 2001 From: "Chayal, Avteet (ac229e)" Date: Wed, 19 Sep 2018 00:35:17 +0000 Subject: CVS changes for osdf placment api Implemented ONAP Common Versioning Strategy Issue-ID: OPTFRA-285 Change-Id: I31df699afddbeb8962b2ca0fa501eff45f70ed5d Signed-off-by: Chayal, Avteet (ac229e) --- config/osdf_config.yaml | 14 + osdf/operation/responses.py | 107 +++--- .../optimizers/placementopt/conductor/conductor.py | 396 +++++++++++---------- osdf/utils/api_data_utils.py | 59 +++ osdfapp.py | 14 +- test/test_api_data_utils.py | 21 ++ 6 files changed, 373 insertions(+), 238 deletions(-) mode change 100644 => 100755 osdf/operation/responses.py mode change 100644 => 100755 osdf/optimizers/placementopt/conductor/conductor.py create mode 100755 osdf/utils/api_data_utils.py create mode 100755 test/test_api_data_utils.py diff --git a/config/osdf_config.yaml b/config/osdf_config.yaml index 5f206f5..636b6ad 100755 --- a/config/osdf_config.yaml +++ b/config/osdf_config.yaml @@ -1,3 +1,15 @@ +placementVersioningEnabled: False + +# Placement API latest version numbers to be set in HTTP header +placementMajorVersion: "1" +placementMinorVersion: "0" +placementPatchVersion: "0" + +# Placement API default version numbers to be set in HTTP header +placementDefaultMajorVersion: "1" +placementDefaultMinorVersion: "0" +placementDefaultPatchVersion: "0" + # Credentials for SO soUsername: "" # SO username for call back. soPassword: "" # SO password for call back. @@ -8,6 +20,8 @@ conductorUsername: admin1 conductorPassword: plan.15 conductorPingWaitTime: 60 # seconds to wait before calling the conductor retry URL conductorMaxRetries: 30 # if we don't get something in 30 minutes, give up +# versions to be set in HTTP header +conductorMinorVersion: 0 # Policy Platform -- requires ClientAuth, Authorization, and Environment policyPlatformUrl: http://policy.api.simpledemo.onap.org:8081/pdp/api/getConfig # Policy Dev platform URL diff --git a/osdf/operation/responses.py b/osdf/operation/responses.py old mode 100644 new mode 100755 index 84bb2cc..a3623ba --- a/osdf/operation/responses.py +++ b/osdf/operation/responses.py @@ -1,43 +1,64 @@ -# ------------------------------------------------------------------------- -# Copyright (c) 2015-2017 AT&T Intellectual Property -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------- -# - -from flask import Response - -from osdf import ACCEPTED_MESSAGE_TEMPLATE - - -def osdf_response_for_request_accept(request_id="", transaction_id="", request_status="", status_message="", - response_code=202, as_http=True): - """Helper method to create a response object for request acceptance, so that the object can be sent to a client - :param request_id: request ID provided by the caller - :param transaction_id: transaction ID provided by the caller - :param request_status: the status of a request - :param status_message: details on the status of a request - :param response_code: the HTTP status code to send -- default is 202 (accepted) - :param as_http: whether to send response as HTTP response object or as a string - :return: if as_http is True, return a HTTP Response object. Otherwise, return json-encoded-message - """ - response_message = ACCEPTED_MESSAGE_TEMPLATE.render(request_id=request_id, transaction_id=transaction_id, - request_status=request_status, status_message=status_message) - if not as_http: - return response_message - - response = Response(response_message, content_type='application/json; charset=utf-8') - response.headers.add('content-length', len(response_message)) - response.status_code = response_code - return response +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------- +# + +from flask import Response + +from osdf import ACCEPTED_MESSAGE_TEMPLATE +from osdf.logging.osdf_logging import debug_log + +def osdf_response_for_request_accept(request_id="", transaction_id="", request_status="", status_message="", + version_info = { + 'placementVersioningEnabled': False, + 'placementMajorVersion': '1', + 'placementMinorVersion': '0', + 'placementPatchVersion': '0' + }, + response_code=202, as_http=True): + """Helper method to create a response object for request acceptance, so that the object can be sent to a client + :param request_id: request ID provided by the caller + :param transaction_id: transaction ID provided by the caller + :param request_status: the status of a request + :param status_message: details on the status of a request + :param response_code: the HTTP status code to send -- default is 202 (accepted) + :param as_http: whether to send response as HTTP response object or as a string + :return: if as_http is True, return a HTTP Response object. Otherwise, return json-encoded-message + """ + response_message = ACCEPTED_MESSAGE_TEMPLATE.render(request_id=request_id, transaction_id=transaction_id, + request_status=request_status, status_message=status_message) + if not as_http: + return response_message + + response = Response(response_message, content_type='application/json; charset=utf-8') + response.headers.add('content-length', len(response_message)) + + placement_ver_enabled = version_info['placementVersioningEnabled'] + + if placement_ver_enabled: + placement_minor_version = version_info['placementMinorVersion'] + placement_patch_version = version_info['placementPatchVersion'] + placement_major_version = version_info['placementMajorVersion'] + x_latest_version = placement_major_version+'.'+placement_minor_version+'.'+placement_patch_version + response.headers.add('X-MinorVersion', placement_minor_version) + response.headers.add('X-PatchVersion', placement_patch_version) + response.headers.add('X-LatestVersion', x_latest_version) + + debug_log.debug("Versions set in HTTP header for synchronous response: X-MinorVersion: {} X-PatchVersion: {} X-LatestVersion: {}" + .format(placement_minor_version, placement_patch_version, x_latest_version)) + + response.status_code = response_code + return response diff --git a/osdf/optimizers/placementopt/conductor/conductor.py b/osdf/optimizers/placementopt/conductor/conductor.py old mode 100644 new mode 100755 index f40ee95..29f0bbc --- a/osdf/optimizers/placementopt/conductor/conductor.py +++ b/osdf/optimizers/placementopt/conductor/conductor.py @@ -1,194 +1,202 @@ -# ------------------------------------------------------------------------- -# Copyright (c) 2015-2017 AT&T Intellectual Property -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------- -# - -""" -This application generates conductor API calls using the information received from SO and Policy platform. -""" - -import json -import time - -from jinja2 import Template -from requests import RequestException - -from osdf.logging.osdf_logging import debug_log -from osdf.optimizers.placementopt.conductor.api_builder import conductor_api_builder -from osdf.utils.interfaces import RestClient -from osdf.operation.exceptions import BusinessException - - -def request(req_object, osdf_config, flat_policies): - """ - Process a placement request from a Client (build Conductor API call, make the call, return result) - :param req_object: Request parameters from the client - :param osdf_config: Configuration specific to SNIRO application (core + deployment) - :param flat_policies: policies related to placement (fetched based on request) - :param prov_status: provStatus retrieved from Subscriber policy - :return: response from Conductor (accounting for redirects from Conductor service - """ - config = osdf_config.deployment - local_config = osdf_config.core - uid, passwd = config['conductorUsername'], config['conductorPassword'] - conductor_url = config['conductorUrl'] - req_id = req_object['requestInfo']['requestId'] - transaction_id = req_object['requestInfo']['transactionId'] - headers = dict(transaction_id=transaction_id) - - max_retries = config.get('conductorMaxRetries', 30) - ping_wait_time = config.get('conductorPingWaitTime', 60) - - rc = RestClient(userid=uid, passwd=passwd, method="GET", log_func=debug_log.debug, headers=headers) - conductor_req_json_str = conductor_api_builder(req_object, flat_policies, local_config) - conductor_req_json = json.loads(conductor_req_json_str) - - debug_log.debug("Sending first Conductor request for request_id {}".format(req_id)) - resp, raw_resp = initial_request_to_conductor(rc, conductor_url, conductor_req_json) - # Very crude way of keeping track of time. - # We are not counting initial request time, first call back, or time for HTTP request - total_time, ctr = 0, 2 - client_timeout = req_object['requestInfo']['timeout'] - configured_timeout = max_retries * ping_wait_time - max_timeout = min(client_timeout, configured_timeout) - - while True: # keep requesting conductor till we get a result or we run out of time - if resp is not None: - if resp["plans"][0].get("status") in ["error"]: - raise RequestException(response=raw_resp, request=raw_resp.request) - - if resp["plans"][0].get("status") in ["done", "not found"]: - if resp["plans"][0].get("recommendations"): - return conductor_response_processor(resp, raw_resp, req_id) - else: # "solved" but no solutions found - return conductor_no_solution_processor(resp, raw_resp, req_id) - new_url = resp['plans'][0]['links'][0][0]['href'] # TODO: check why a list of lists - - if total_time >= max_timeout: - raise BusinessException("Conductor could not provide a solution within {} seconds," - "this transaction is timing out".format(max_timeout)) - time.sleep(ping_wait_time) - ctr += 1 - debug_log.debug("Attempt number {} url {}; prior status={}".format(ctr, new_url, resp['plans'][0]['status'])) - total_time += ping_wait_time - - try: - raw_resp = rc.request(new_url, raw_response=True) - resp = raw_resp.json() - except RequestException as e: - debug_log.debug("Conductor attempt {} for request_id {} has failed because {}".format(ctr, req_id, str(e))) - - -def initial_request_to_conductor(rc, conductor_url, conductor_req_json): - """First steps in the request-redirect chain in making a call to Conductor - :param rc: REST client object for calling conductor - :param conductor_url: conductor's base URL to submit a placement request - :param conductor_req_json: request json object to send to Conductor - :return: URL to check for follow up (similar to redirects); we keep checking these till we get a result/error - """ - debug_log.debug("Payload to Conductor: {}".format(json.dumps(conductor_req_json))) - raw_resp = rc.request(url=conductor_url, raw_response=True, method="POST", json=conductor_req_json) - resp = raw_resp.json() - if resp["status"] != "template": - raise RequestException(response=raw_resp, request=raw_resp.request) - time.sleep(10) # 10 seconds wait time to avoid being too quick! - plan_url = resp["links"][0][0]["href"] - debug_log.debug("Attempting to read the plan from the conductor provided url {}".format(plan_url)) - raw_resp = rc.request(raw_response=True, url=plan_url) # TODO: check why a list of lists for links - resp = raw_resp.json() - - if resp["plans"][0]["status"] in ["error"]: - raise RequestException(response=raw_resp, request=raw_resp.request) - return resp, raw_resp # now the caller of this will handle further follow-ups - - -def conductor_response_processor(conductor_response, raw_response, req_id): - """Build a response object to be sent to client's callback URL from Conductor's response - This includes Conductor's placement optimization response, and required ASDC license artifacts - - :param conductor_response: JSON response from Conductor - :param raw_response: Raw HTTP response corresponding to above - :param req_id: Id of a request - :return: JSON object that can be sent to the client's callback URL - """ - composite_solutions = [] - name_map = {"physical-location-id": "cloudClli", "host_id": "vnfHostName", - "cloud_version": "cloudVersion", "cloud_owner": "cloudOwner", - "cloud": "cloudRegionId", "service": "serviceInstanceId", "is_rehome": "isRehome", - "location_id": "locationId", "location_type": "locationType"} - for reco in conductor_response['plans'][0]['recommendations']: - for resource in reco.keys(): - c = reco[resource]['candidate'] - solution = { - 'resourceModuleName': resource, - 'serviceResourceId': reco[resource].get('service_resource_id', ""), - 'solution': {"identifierType": name_map.get(c['inventory_type'], c['inventory_type']), - 'identifiers': [c['candidate_id']], - 'cloudOwner': c.get('cloud_owner', "")}, - 'assignmentInfo': [] - } - for key, value in c.items(): - if key in ["location_id", "location_type", "is_rehome", "host_id"]: - try: - solution['assignmentInfo'].append({"key": name_map.get(key, key), "value": value}) - except KeyError: - debug_log.debug("The key[{}] is not mapped and will not be returned in assignment info".format(key)) - - for key, value in reco[resource]['attributes'].items(): - try: - solution['assignmentInfo'].append({"key": name_map.get(key, key), "value": value}) - except KeyError: - debug_log.debug("The key[{}] is not mapped and will not be returned in assignment info".format(key)) - composite_solutions.append(solution) - - request_status = "completed" if conductor_response['plans'][0]['status'] == "done" \ - else conductor_response['plans'][0]['status'] - transaction_id = raw_response.headers.get('transaction_id', "") - status_message = conductor_response.get('plans')[0].get('message', "") - - solution_info = {} - if composite_solutions: - solution_info.setdefault('placementSolutions', []) - solution_info['placementSolutions'].append(composite_solutions) - - resp = { - "transactionId": transaction_id, - "requestId": req_id, - "requestStatus": request_status, - "statusMessage": status_message, - "solutions": solution_info - } - return resp - - -def conductor_no_solution_processor(conductor_response, raw_response, request_id, - template_placement_response="templates/plc_opt_response.jsont"): - """Build a response object to be sent to client's callback URL from Conductor's response - This is for case where no solution is found - - :param conductor_response: JSON response from Conductor - :param raw_response: Raw HTTP response corresponding to above - :param request_id: request Id associated with the client request (same as conductor response's "name") - :param template_placement_response: the template for generating response to client (plc_opt_response.jsont) - :return: JSON object that can be sent to the client's callback URL - """ - status_message = conductor_response["plans"][0].get("message") - templ = Template(open(template_placement_response).read()) - return json.loads(templ.render(composite_solutions=[], requestId=request_id, license_solutions=[], - transactionId=raw_response.headers.get('transaction_id', ""), - requestStatus="completed", statusMessage=status_message, json=json)) - - +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------- +# + +""" +This application generates conductor API calls using the information received from SO and Policy platform. +""" + +import json +import time + +from jinja2 import Template +from requests import RequestException + +from osdf.logging.osdf_logging import debug_log +from osdf.optimizers.placementopt.conductor.api_builder import conductor_api_builder +from osdf.utils.interfaces import RestClient +from osdf.operation.exceptions import BusinessException + + +def request(req_object, osdf_config, flat_policies): + """ + Process a placement request from a Client (build Conductor API call, make the call, return result) + :param req_object: Request parameters from the client + :param osdf_config: Configuration specific to SNIRO application (core + deployment) + :param flat_policies: policies related to placement (fetched based on request) + :param prov_status: provStatus retrieved from Subscriber policy + :return: response from Conductor (accounting for redirects from Conductor service + """ + config = osdf_config.deployment + local_config = osdf_config.core + uid, passwd = config['conductorUsername'], config['conductorPassword'] + conductor_url = config['conductorUrl'] + req_id = req_object['requestInfo']['requestId'] + transaction_id = req_object['requestInfo']['transactionId'] + headers = dict(transaction_id=transaction_id) + placement_ver_enabled = config.get('placementVersioningEnabled', False) + + if placement_ver_enabled: + cond_minor_version = config.get('conductorMinorVersion', None) + if cond_minor_version is not None: + x_minor_version = str(cond_minor_version) + headers.update({'X-MinorVersion': x_minor_version}) + debug_log.debug("Versions set in HTTP header to conductor: X-MinorVersion: {} ".format(x_minor_version)) + + max_retries = config.get('conductorMaxRetries', 30) + ping_wait_time = config.get('conductorPingWaitTime', 60) + + rc = RestClient(userid=uid, passwd=passwd, method="GET", log_func=debug_log.debug, headers=headers) + conductor_req_json_str = conductor_api_builder(req_object, flat_policies, local_config) + conductor_req_json = json.loads(conductor_req_json_str) + + debug_log.debug("Sending first Conductor request for request_id {}".format(req_id)) + resp, raw_resp = initial_request_to_conductor(rc, conductor_url, conductor_req_json) + # Very crude way of keeping track of time. + # We are not counting initial request time, first call back, or time for HTTP request + total_time, ctr = 0, 2 + client_timeout = req_object['requestInfo']['timeout'] + configured_timeout = max_retries * ping_wait_time + max_timeout = min(client_timeout, configured_timeout) + + while True: # keep requesting conductor till we get a result or we run out of time + if resp is not None: + if resp["plans"][0].get("status") in ["error"]: + raise RequestException(response=raw_resp, request=raw_resp.request) + + if resp["plans"][0].get("status") in ["done", "not found"]: + if resp["plans"][0].get("recommendations"): + return conductor_response_processor(resp, raw_resp, req_id) + else: # "solved" but no solutions found + return conductor_no_solution_processor(resp, raw_resp, req_id) + new_url = resp['plans'][0]['links'][0][0]['href'] # TODO: check why a list of lists + + if total_time >= max_timeout: + raise BusinessException("Conductor could not provide a solution within {} seconds," + "this transaction is timing out".format(max_timeout)) + time.sleep(ping_wait_time) + ctr += 1 + debug_log.debug("Attempt number {} url {}; prior status={}".format(ctr, new_url, resp['plans'][0]['status'])) + total_time += ping_wait_time + + try: + raw_resp = rc.request(new_url, raw_response=True) + resp = raw_resp.json() + except RequestException as e: + debug_log.debug("Conductor attempt {} for request_id {} has failed because {}".format(ctr, req_id, str(e))) + + +def initial_request_to_conductor(rc, conductor_url, conductor_req_json): + """First steps in the request-redirect chain in making a call to Conductor + :param rc: REST client object for calling conductor + :param conductor_url: conductor's base URL to submit a placement request + :param conductor_req_json: request json object to send to Conductor + :return: URL to check for follow up (similar to redirects); we keep checking these till we get a result/error + """ + debug_log.debug("Payload to Conductor: {}".format(json.dumps(conductor_req_json))) + raw_resp = rc.request(url=conductor_url, raw_response=True, method="POST", json=conductor_req_json) + resp = raw_resp.json() + if resp["status"] != "template": + raise RequestException(response=raw_resp, request=raw_resp.request) + time.sleep(10) # 10 seconds wait time to avoid being too quick! + plan_url = resp["links"][0][0]["href"] + debug_log.debug("Attempting to read the plan from the conductor provided url {}".format(plan_url)) + raw_resp = rc.request(raw_response=True, url=plan_url) # TODO: check why a list of lists for links + resp = raw_resp.json() + + if resp["plans"][0]["status"] in ["error"]: + raise RequestException(response=raw_resp, request=raw_resp.request) + return resp, raw_resp # now the caller of this will handle further follow-ups + + +def conductor_response_processor(conductor_response, raw_response, req_id): + """Build a response object to be sent to client's callback URL from Conductor's response + This includes Conductor's placement optimization response, and required ASDC license artifacts + + :param conductor_response: JSON response from Conductor + :param raw_response: Raw HTTP response corresponding to above + :param req_id: Id of a request + :return: JSON object that can be sent to the client's callback URL + """ + composite_solutions = [] + name_map = {"physical-location-id": "cloudClli", "host_id": "vnfHostName", + "cloud_version": "cloudVersion", "cloud_owner": "cloudOwner", + "cloud": "cloudRegionId", "service": "serviceInstanceId", "is_rehome": "isRehome", + "location_id": "locationId", "location_type": "locationType"} + for reco in conductor_response['plans'][0]['recommendations']: + for resource in reco.keys(): + c = reco[resource]['candidate'] + solution = { + 'resourceModuleName': resource, + 'serviceResourceId': reco[resource].get('service_resource_id', ""), + 'solution': {"identifierType": name_map.get(c['inventory_type'], c['inventory_type']), + 'identifiers': [c['candidate_id']], + 'cloudOwner': c.get('cloud_owner', "")}, + 'assignmentInfo': [] + } + for key, value in c.items(): + if key in ["location_id", "location_type", "is_rehome", "host_id"]: + try: + solution['assignmentInfo'].append({"key": name_map.get(key, key), "value": value}) + except KeyError: + debug_log.debug("The key[{}] is not mapped and will not be returned in assignment info".format(key)) + + for key, value in reco[resource]['attributes'].items(): + try: + solution['assignmentInfo'].append({"key": name_map.get(key, key), "value": value}) + except KeyError: + debug_log.debug("The key[{}] is not mapped and will not be returned in assignment info".format(key)) + composite_solutions.append(solution) + + request_status = "completed" if conductor_response['plans'][0]['status'] == "done" \ + else conductor_response['plans'][0]['status'] + transaction_id = raw_response.headers.get('transaction_id', "") + status_message = conductor_response.get('plans')[0].get('message', "") + + solution_info = {} + if composite_solutions: + solution_info.setdefault('placementSolutions', []) + solution_info['placementSolutions'].append(composite_solutions) + + resp = { + "transactionId": transaction_id, + "requestId": req_id, + "requestStatus": request_status, + "statusMessage": status_message, + "solutions": solution_info + } + return resp + + +def conductor_no_solution_processor(conductor_response, raw_response, request_id, + template_placement_response="templates/plc_opt_response.jsont"): + """Build a response object to be sent to client's callback URL from Conductor's response + This is for case where no solution is found + + :param conductor_response: JSON response from Conductor + :param raw_response: Raw HTTP response corresponding to above + :param request_id: request Id associated with the client request (same as conductor response's "name") + :param template_placement_response: the template for generating response to client (plc_opt_response.jsont) + :return: JSON object that can be sent to the client's callback URL + """ + status_message = conductor_response["plans"][0].get("message") + templ = Template(open(template_placement_response).read()) + return json.loads(templ.render(composite_solutions=[], requestId=request_id, license_solutions=[], + transactionId=raw_response.headers.get('transaction_id', ""), + requestStatus="completed", statusMessage=status_message, json=json)) + + diff --git a/osdf/utils/api_data_utils.py b/osdf/utils/api_data_utils.py new file mode 100755 index 0000000..a7a4c12 --- /dev/null +++ b/osdf/utils/api_data_utils.py @@ -0,0 +1,59 @@ +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2017 AT&T Intellectual Property +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------- +# + +from collections import defaultdict +from osdf.logging.osdf_logging import debug_log +from osdf.config.base import osdf_config + + +def retrieve_version_info(request, request_id): + version_info_dict = defaultdict(dict) + config = osdf_config.deployment + placement_ver_enabled = config.get('placementVersioningEnabled', False) + + if placement_ver_enabled: + placement_major_version = config.get('placementMajorVersion', None) + placement_minor_version = config.get('placementMinorVersion', None) + placement_patch_version = config.get('placementPatchVersion', None) + + http_header = request.headers.environ + http_x_minorversion = http_header.get("HTTP_X_MINORVERSION") + http_x_patchversion = http_header.get("HTTP_X_PATCHVERSION") + http_x_latestversion = http_header.get("HTTP_X_LATESTVERSION") + + debug_log.debug("Versions sent in HTTP header for request ID {} are: X-MinorVersion: {} X-PatchVersion: {} X-LatestVersion: {}" + .format(request_id, http_x_minorversion, http_x_patchversion, http_x_latestversion)) + debug_log.debug("latest versions specified in osdf config file are: placementMajorVersion: {} placementMinorVersion: {} placementPatchVersion: {}" + .format(placement_major_version, placement_minor_version, placement_patch_version)) + else: + placement_major_version = config.get('placementDefaultMajorVersion', "1") + placement_minor_version = config.get('placementDefaultMinorVersion', "0") + placement_patch_version = config.get('placementDefaultPatchVersion', "0") + + debug_log.debug("Default versions specified in osdf config file are: placementDefaultMajorVersion: {} placementDefaultMinorVersion: {} placementDefaultPatchVersion: {}" + .format(placement_major_version, placement_minor_version, placement_patch_version)) + + version_info_dict.update({ + 'placementVersioningEnabled': placement_ver_enabled, + 'placementMajorVersion': str(placement_major_version), + 'placementMinorVersion': str(placement_minor_version), + 'placementPatchVersion': str( placement_patch_version) + }) + + return version_info_dict + \ No newline at end of file diff --git a/osdfapp.py b/osdfapp.py index 5f13108..1e076f1 100755 --- a/osdfapp.py +++ b/osdfapp.py @@ -49,6 +49,7 @@ from osdf.models.api.pciOptimizationRequest import PCIOptimizationAPI from osdf.operation.responses import osdf_response_for_request_accept as req_accept from osdf.optimizers.routeopt.simple_route_opt import RouteOpt from osdf.optimizers.pciopt.pci_opt_processor import process_pci_optimation +from osdf.utils import api_data_utils ERROR_TEMPLATE = osdf.ERROR_TEMPLATE @@ -107,6 +108,16 @@ def do_osdf_health_check(): @app.route("/api/oof/v1/placement", methods=["POST"]) @auth_basic.login_required def do_placement_opt(): + return placement_rest_api() + + +@app.route("/api/oof/placement/v1", methods=["POST"]) +@auth_basic.login_required +def do_placement_opt_common_versioning(): + return placement_rest_api() + + +def placement_rest_api(): """Perform placement optimization after validating the request and fetching policies Make a call to the call-back URL with the output of the placement request. Note: Call to Conductor for placement optimization may have redirects, so account for them @@ -115,6 +126,7 @@ def do_placement_opt(): req_id = request_json['requestInfo']['requestId'] g.request_id = req_id audit_log.info(MH.received_request(request.url, request.remote_addr, json.dumps(request_json))) + api_version_info = api_data_utils.retrieve_version_info(request, req_id) PlacementAPI(request_json).validate() policies = get_policies(request_json, "placement") audit_log.info(MH.new_worker_thread(req_id, "[for placement]")) @@ -123,7 +135,7 @@ def do_placement_opt(): audit_log.info(MH.accepted_valid_request(req_id, request)) return req_accept(request_id=req_id, transaction_id=request_json['requestInfo']['transactionId'], - request_status="accepted", status_message="") + version_info=api_version_info, request_status="accepted", status_message="") @app.route("/api/oof/v1/route", methods=["POST"]) diff --git a/test/test_api_data_utils.py b/test/test_api_data_utils.py new file mode 100755 index 0000000..99d7a2e --- /dev/null +++ b/test/test_api_data_utils.py @@ -0,0 +1,21 @@ +import json +import os +from osdf.utils import api_data_utils +from collections import defaultdict + + +BASE_DIR = os.path.dirname(__file__) + +with open(os.path.join(BASE_DIR, "placement-tests/request.json")) as json_data: + req_json = json.load(json_data) + +class TestVersioninfo(): +# +# Tests for api_data_utils.py +# + def test_retrieve_version_info(self): + request_id = 'test12345' + test_dict = {'placementVersioningEnabled': False, 'placementMajorVersion': '1', 'placementPatchVersion': '0', 'placementMinorVersion': '0'} + test_verison_info_dict = defaultdict(dict ,test_dict ) + verison_info_dict = api_data_utils.retrieve_version_info(req_json, request_id) + assert verison_info_dict == test_verison_info_dict \ No newline at end of file -- cgit 1.2.3-korg