CVS changes for osdf placment api 35/67535/7
authorChayal, Avteet (ac229e) <ac229e@att.com>
Wed, 19 Sep 2018 00:35:17 +0000 (00:35 +0000)
committerChayal, Avteet (ac229e) <ac229e@att.com>
Wed, 19 Sep 2018 20:10:36 +0000 (20:10 +0000)
Implemented ONAP Common Versioning Strategy

Issue-ID: OPTFRA-285
Change-Id: I31df699afddbeb8962b2ca0fa501eff45f70ed5d
Signed-off-by: Chayal, Avteet (ac229e) <ac229e@att.com>
config/osdf_config.yaml
osdf/operation/responses.py [changed mode: 0644->0755]
osdf/optimizers/placementopt/conductor/conductor.py [changed mode: 0644->0755]
osdf/utils/api_data_utils.py [new file with mode: 0755]
osdfapp.py
test/test_api_data_utils.py [new file with mode: 0755]

index 5f206f5..636b6ad 100755 (executable)
@@ -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
old mode 100644 (file)
new mode 100755 (executable)
index 84bb2cc..a3623ba
@@ -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
+# -------------------------------------------------------------------------\r
+#   Copyright (c) 2015-2017 AT&T Intellectual Property\r
+#\r
+#   Licensed under the Apache License, Version 2.0 (the "License");\r
+#   you may not use this file except in compliance with the License.\r
+#   You may obtain a copy of the License at\r
+#\r
+#       http://www.apache.org/licenses/LICENSE-2.0\r
+#\r
+#   Unless required by applicable law or agreed to in writing, software\r
+#   distributed under the License is distributed on an "AS IS" BASIS,\r
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+#   See the License for the specific language governing permissions and\r
+#   limitations under the License.\r
+#\r
+# -------------------------------------------------------------------------\r
+#\r
+\r
+from flask import Response\r
+\r
+from osdf import ACCEPTED_MESSAGE_TEMPLATE\r
+from osdf.logging.osdf_logging import debug_log\r
+\r
+def osdf_response_for_request_accept(request_id="", transaction_id="", request_status="", status_message="",\r
+                                     version_info = {\r
+                                          'placementVersioningEnabled': False,\r
+                                          'placementMajorVersion': '1',\r
+                                          'placementMinorVersion': '0',\r
+                                          'placementPatchVersion': '0'\r
+                                      },\r
+                                     response_code=202, as_http=True):\r
+    """Helper method to create a response object for request acceptance, so that the object can be sent to a client\r
+    :param request_id: request ID provided by the caller\r
+    :param transaction_id: transaction ID provided by the caller\r
+    :param request_status: the status of a request\r
+    :param status_message: details on the status of a request\r
+    :param response_code: the HTTP status code to send -- default is 202 (accepted)\r
+    :param as_http: whether to send response as HTTP response object or as a string\r
+    :return: if as_http is True, return a HTTP Response object. Otherwise, return json-encoded-message\r
+    """\r
+    response_message = ACCEPTED_MESSAGE_TEMPLATE.render(request_id=request_id, transaction_id=transaction_id,\r
+                                                        request_status=request_status, status_message=status_message)\r
+    if not as_http:\r
+        return response_message\r
+\r
+    response = Response(response_message, content_type='application/json; charset=utf-8')\r
+    response.headers.add('content-length', len(response_message))\r
+    \r
+    placement_ver_enabled = version_info['placementVersioningEnabled']\r
+    \r
+    if placement_ver_enabled:\r
+        placement_minor_version = version_info['placementMinorVersion']\r
+        placement_patch_version = version_info['placementPatchVersion']\r
+        placement_major_version = version_info['placementMajorVersion']\r
+        x_latest_version = placement_major_version+'.'+placement_minor_version+'.'+placement_patch_version\r
+        response.headers.add('X-MinorVersion', placement_minor_version)\r
+        response.headers.add('X-PatchVersion', placement_patch_version)\r
+        response.headers.add('X-LatestVersion', x_latest_version)\r
+        \r
+        debug_log.debug("Versions set in HTTP header for synchronous response: X-MinorVersion: {}  X-PatchVersion: {}  X-LatestVersion: {}"\r
+                        .format(placement_minor_version, placement_patch_version, x_latest_version))\r
+    \r
+    response.status_code = response_code\r
+    return response\r
old mode 100644 (file)
new mode 100755 (executable)
index f40ee95..29f0bbc
-# -------------------------------------------------------------------------
-#   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))
-
-
+# -------------------------------------------------------------------------\r
+#   Copyright (c) 2015-2017 AT&T Intellectual Property\r
+#\r
+#   Licensed under the Apache License, Version 2.0 (the "License");\r
+#   you may not use this file except in compliance with the License.\r
+#   You may obtain a copy of the License at\r
+#\r
+#       http://www.apache.org/licenses/LICENSE-2.0\r
+#\r
+#   Unless required by applicable law or agreed to in writing, software\r
+#   distributed under the License is distributed on an "AS IS" BASIS,\r
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+#   See the License for the specific language governing permissions and\r
+#   limitations under the License.\r
+#\r
+# -------------------------------------------------------------------------\r
+#\r
+\r
+"""\r
+This application generates conductor API calls using the information received from SO and Policy platform.\r
+"""\r
+\r
+import json\r
+import time\r
+\r
+from jinja2 import Template\r
+from requests import RequestException\r
+\r
+from osdf.logging.osdf_logging import debug_log\r
+from osdf.optimizers.placementopt.conductor.api_builder import conductor_api_builder\r
+from osdf.utils.interfaces import RestClient\r
+from osdf.operation.exceptions import BusinessException\r
+\r
+\r
+def request(req_object, osdf_config, flat_policies):\r
+    """\r
+    Process a placement request from a Client (build Conductor API call, make the call, return result)\r
+    :param req_object: Request parameters from the client\r
+    :param osdf_config: Configuration specific to SNIRO application (core + deployment)\r
+    :param flat_policies: policies related to placement (fetched based on request)\r
+    :param prov_status: provStatus retrieved from Subscriber policy\r
+    :return: response from Conductor (accounting for redirects from Conductor service\r
+    """\r
+    config = osdf_config.deployment\r
+    local_config = osdf_config.core\r
+    uid, passwd = config['conductorUsername'], config['conductorPassword']\r
+    conductor_url = config['conductorUrl']\r
+    req_id = req_object['requestInfo']['requestId']\r
+    transaction_id = req_object['requestInfo']['transactionId']\r
+    headers = dict(transaction_id=transaction_id)\r
+    placement_ver_enabled = config.get('placementVersioningEnabled', False)\r
+    \r
+    if placement_ver_enabled:\r
+        cond_minor_version = config.get('conductorMinorVersion', None) \r
+        if cond_minor_version is not None:\r
+            x_minor_version = str(cond_minor_version)\r
+            headers.update({'X-MinorVersion': x_minor_version})\r
+        debug_log.debug("Versions set in HTTP header to conductor: X-MinorVersion: {} ".format(x_minor_version))\r
+\r
+    max_retries = config.get('conductorMaxRetries', 30)\r
+    ping_wait_time = config.get('conductorPingWaitTime', 60)\r
+\r
+    rc = RestClient(userid=uid, passwd=passwd, method="GET", log_func=debug_log.debug, headers=headers)\r
+    conductor_req_json_str = conductor_api_builder(req_object, flat_policies, local_config)\r
+    conductor_req_json = json.loads(conductor_req_json_str)\r
+\r
+    debug_log.debug("Sending first Conductor request for request_id {}".format(req_id))\r
+    resp, raw_resp = initial_request_to_conductor(rc, conductor_url, conductor_req_json)\r
+    # Very crude way of keeping track of time.\r
+    # We are not counting initial request time, first call back, or time for HTTP request\r
+    total_time, ctr = 0, 2\r
+    client_timeout = req_object['requestInfo']['timeout']\r
+    configured_timeout = max_retries * ping_wait_time\r
+    max_timeout = min(client_timeout, configured_timeout)\r
+\r
+    while True:  # keep requesting conductor till we get a result or we run out of time\r
+        if resp is not None:\r
+            if resp["plans"][0].get("status") in ["error"]:\r
+                raise RequestException(response=raw_resp, request=raw_resp.request)\r
+\r
+            if resp["plans"][0].get("status") in ["done", "not found"]:\r
+                if resp["plans"][0].get("recommendations"):\r
+                    return conductor_response_processor(resp, raw_resp, req_id)\r
+                else:  # "solved" but no solutions found\r
+                    return conductor_no_solution_processor(resp, raw_resp, req_id)\r
+            new_url = resp['plans'][0]['links'][0][0]['href']  # TODO: check why a list of lists\r
+\r
+        if total_time >= max_timeout:\r
+            raise BusinessException("Conductor could not provide a solution within {} seconds,"\r
+                                    "this transaction is timing out".format(max_timeout))\r
+        time.sleep(ping_wait_time)\r
+        ctr += 1\r
+        debug_log.debug("Attempt number {} url {}; prior status={}".format(ctr, new_url, resp['plans'][0]['status']))\r
+        total_time += ping_wait_time\r
+\r
+        try:\r
+            raw_resp = rc.request(new_url, raw_response=True)\r
+            resp = raw_resp.json()\r
+        except RequestException as e:\r
+            debug_log.debug("Conductor attempt {} for request_id {} has failed because {}".format(ctr, req_id, str(e)))\r
+\r
+\r
+def initial_request_to_conductor(rc, conductor_url, conductor_req_json):\r
+    """First steps in the request-redirect chain in making a call to Conductor\r
+    :param rc: REST client object for calling conductor\r
+    :param conductor_url: conductor's base URL to submit a placement request\r
+    :param conductor_req_json: request json object to send to Conductor\r
+    :return: URL to check for follow up (similar to redirects); we keep checking these till we get a result/error\r
+    """\r
+    debug_log.debug("Payload to Conductor: {}".format(json.dumps(conductor_req_json)))\r
+    raw_resp = rc.request(url=conductor_url, raw_response=True, method="POST", json=conductor_req_json)\r
+    resp = raw_resp.json()\r
+    if resp["status"] != "template":\r
+        raise RequestException(response=raw_resp, request=raw_resp.request)\r
+    time.sleep(10)  # 10 seconds wait time to avoid being too quick!\r
+    plan_url = resp["links"][0][0]["href"]\r
+    debug_log.debug("Attempting to read the plan from the conductor provided url {}".format(plan_url))\r
+    raw_resp = rc.request(raw_response=True, url=plan_url)  # TODO: check why a list of lists for links\r
+    resp = raw_resp.json()\r
+\r
+    if resp["plans"][0]["status"] in ["error"]:\r
+        raise RequestException(response=raw_resp, request=raw_resp.request)\r
+    return resp, raw_resp  # now the caller of this will handle further follow-ups\r
+\r
+\r
+def conductor_response_processor(conductor_response, raw_response, req_id):\r
+    """Build a response object to be sent to client's callback URL from Conductor's response\r
+    This includes Conductor's placement optimization response, and required ASDC license artifacts\r
+\r
+    :param conductor_response: JSON response from Conductor\r
+    :param raw_response: Raw HTTP response corresponding to above\r
+    :param req_id: Id of a request\r
+    :return: JSON object that can be sent to the client's callback URL\r
+    """\r
+    composite_solutions = []\r
+    name_map = {"physical-location-id": "cloudClli", "host_id": "vnfHostName",\r
+                "cloud_version": "cloudVersion", "cloud_owner": "cloudOwner",\r
+                "cloud": "cloudRegionId", "service": "serviceInstanceId", "is_rehome": "isRehome",\r
+                "location_id": "locationId", "location_type": "locationType"}\r
+    for reco in conductor_response['plans'][0]['recommendations']:\r
+        for resource in reco.keys():\r
+            c = reco[resource]['candidate']\r
+            solution = {\r
+                'resourceModuleName': resource,\r
+                'serviceResourceId': reco[resource].get('service_resource_id', ""),\r
+                'solution': {"identifierType": name_map.get(c['inventory_type'], c['inventory_type']),\r
+                             'identifiers': [c['candidate_id']],\r
+                             'cloudOwner': c.get('cloud_owner', "")},\r
+                'assignmentInfo': []\r
+            }\r
+            for key, value in c.items():\r
+                if key in ["location_id", "location_type", "is_rehome", "host_id"]:\r
+                    try:\r
+                        solution['assignmentInfo'].append({"key": name_map.get(key, key), "value": value})\r
+                    except KeyError:\r
+                        debug_log.debug("The key[{}] is not mapped and will not be returned in assignment info".format(key))\r
+\r
+            for key, value in reco[resource]['attributes'].items():\r
+                try:\r
+                    solution['assignmentInfo'].append({"key": name_map.get(key, key), "value": value})\r
+                except KeyError:\r
+                    debug_log.debug("The key[{}] is not mapped and will not be returned in assignment info".format(key))\r
+            composite_solutions.append(solution)\r
+\r
+    request_status = "completed" if conductor_response['plans'][0]['status'] == "done" \\r
+        else conductor_response['plans'][0]['status']\r
+    transaction_id = raw_response.headers.get('transaction_id', "")\r
+    status_message = conductor_response.get('plans')[0].get('message', "")\r
+\r
+    solution_info = {}\r
+    if composite_solutions:\r
+        solution_info.setdefault('placementSolutions', [])\r
+        solution_info['placementSolutions'].append(composite_solutions)\r
+\r
+    resp = {\r
+        "transactionId": transaction_id,\r
+        "requestId": req_id,\r
+        "requestStatus": request_status,\r
+        "statusMessage": status_message,\r
+        "solutions": solution_info\r
+    }\r
+    return resp\r
+\r
+\r
+def conductor_no_solution_processor(conductor_response, raw_response, request_id,\r
+                                    template_placement_response="templates/plc_opt_response.jsont"):\r
+    """Build a response object to be sent to client's callback URL from Conductor's response\r
+    This is for case where no solution is found\r
+\r
+    :param conductor_response: JSON response from Conductor\r
+    :param raw_response: Raw HTTP response corresponding to above\r
+    :param request_id: request Id associated with the client request (same as conductor response's "name")\r
+    :param template_placement_response: the template for generating response to client (plc_opt_response.jsont)\r
+    :return: JSON object that can be sent to the client's callback URL\r
+    """\r
+    status_message = conductor_response["plans"][0].get("message")\r
+    templ = Template(open(template_placement_response).read())\r
+    return json.loads(templ.render(composite_solutions=[], requestId=request_id, license_solutions=[],\r
+                                   transactionId=raw_response.headers.get('transaction_id', ""),\r
+                                   requestStatus="completed", statusMessage=status_message, json=json))\r
+\r
+\r
diff --git a/osdf/utils/api_data_utils.py b/osdf/utils/api_data_utils.py
new file mode 100755 (executable)
index 0000000..a7a4c12
--- /dev/null
@@ -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
index 5f13108..1e076f1 100755 (executable)
@@ -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 (executable)
index 0000000..99d7a2e
--- /dev/null
@@ -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