aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--config/__init__.py32
-rw-r--r--config/base.py36
-rw-r--r--config/credentials.py60
-rw-r--r--config/loader.py51
-rw-r--r--operation/__init__.py0
-rw-r--r--operation/error_handling.py93
-rw-r--r--operation/exceptions.py40
-rw-r--r--operation/responses.py39
-rwxr-xr-xosdfapp.py168
-rwxr-xr-xosdfapp.sh53
-rw-r--r--utils/__init__.py0
-rw-r--r--utils/data_conversion.py62
-rw-r--r--utils/data_types.py30
-rw-r--r--utils/interfaces.py90
-rw-r--r--utils/local_processing.py43
-rw-r--r--utils/programming_utils.py105
-rw-r--r--webapp/__init__.py0
-rw-r--r--webapp/appcontroller.py47
18 files changed, 949 insertions, 0 deletions
diff --git a/config/__init__.py b/config/__init__.py
new file mode 100644
index 0000000..303a8ce
--- /dev/null
+++ b/config/__init__.py
@@ -0,0 +1,32 @@
+# -------------------------------------------------------------------------
+# 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.
+#
+# -------------------------------------------------------------------------
+
+
+import yaml
+import json
+
+from osdf.utils.programming_utils import MetaSingleton
+
+
+class CoreConfig(metaclass=MetaSingleton):
+ core_config = None
+
+ def get_core_config(self, config_file=None):
+ if self.core_config is None:
+ self.core_config = yaml.load(open(config_file))
+ return self.core_config
+
diff --git a/config/base.py b/config/base.py
new file mode 100644
index 0000000..b8aacff
--- /dev/null
+++ b/config/base.py
@@ -0,0 +1,36 @@
+# -------------------------------------------------------------------------
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+
+import os
+
+import osdf.config.loader as config_loader
+import osdf.config.credentials as creds
+from osdf.utils.programming_utils import DotDict
+
+config_spec = {
+ "deployment": os.environ.get("OSDF_MANAGER_CONFIG_FILE", "config/osdf_config.yaml"),
+ "core": "config/common_config.yaml"
+ }
+
+osdf_config = DotDict(config_loader.all_configs(**config_spec))
+
+http_basic_auth_credentials = creds.load_credentials(osdf_config)
+
+dmaap_creds = creds.dmaap_creds()
+
+creds_prefixes = {"so": "so", "cm": "cmPortal"}
diff --git a/config/credentials.py b/config/credentials.py
new file mode 100644
index 0000000..e5a6399
--- /dev/null
+++ b/config/credentials.py
@@ -0,0 +1,60 @@
+# -------------------------------------------------------------------------
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+
+import json
+
+from osdf import auth_groups, userid_suffix, passwd_suffix
+
+
+def dmaap_creds(dmaap_file="/etc/dcae/dmaap.conf"):
+ """Get DMaaP credentials from DCAE for publish and subscribe"""
+ try:
+ dmaap_creds = _get_dmaap_creds(dmaap_file)
+ except:
+ dmaap_creds = {}
+ return dmaap_creds
+
+
+def _get_dmaap_creds(dmaap_file):
+ """Get DMaaP credentials from DCAE for publish and subscribe"""
+ streams = json.load(open(dmaap_file, 'r'))
+ pubs = [x for x in streams
+ if x['dmaapStreamId'] == 'requests' and x['dmaapAction'] == 'publish']
+ subs = [x for x in streams
+ if x['dmaapStreamId'] == 'responses' and x['dmaapAction'] == 'subscribe']
+
+ def get_dmaap_info(x):
+ """Get DMaaP credentials from dmaap_object 'x'"""
+ return dict(url=x.get('dmaapUrl'), userid=x.get('dmaapUserName'), passwd=x.get('dmaapPassword'))
+
+ return {'pub': get_dmaap_info(pubs[0]), 'sub': get_dmaap_info(subs[0])}
+
+
+def load_credentials(osdf_config):
+ """Get credentials as dictionaries grouped by auth_group (e.g. creds["Placement"]["user1"] = "pass1")"""
+ creds = dict((x, dict()) for x in auth_groups) # each auth group has userid, passwd dict
+ suffix_start = len(userid_suffix)
+
+ config = osdf_config.deployment
+
+ for element, username in config.items():
+ for x in auth_groups:
+ if element.startswith("osdf" + x) and element.endswith(userid_suffix):
+ passwd = config[element[:-suffix_start] + passwd_suffix]
+ creds[x][username] = passwd
+ return creds
diff --git a/config/loader.py b/config/loader.py
new file mode 100644
index 0000000..7cb363a
--- /dev/null
+++ b/config/loader.py
@@ -0,0 +1,51 @@
+# -------------------------------------------------------------------------
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+
+import json
+
+import yaml
+
+
+def load_config_file(config_file: str, child_name="dockerConfiguration") -> dict:
+ """
+ Load OSDF configuration from a file -- currently only yaml/json are supported
+ :param config_file: path to config file (.yaml or .json).
+ :param child_name: if present, return only that child node
+ :return: config (all or specific child node)
+ """
+ with open(config_file, 'r') as fid:
+ res = {}
+ if config_file.endswith(".yaml"):
+ res = yaml.load(fid)
+ elif config_file.endswith(".json") or config_file.endswith("json"):
+ res = json.load(fid)
+ return res.get(child_name, res) if child_name else res
+
+
+def dcae_config(config_file: str) -> dict:
+ return load_config_file(config_file, child_name="dockerConfiguration")
+
+
+def all_configs(**kwargs: dict) -> dict:
+ """
+ Load all specified configurations
+ :param config_file_spec: key-value pairs
+ (e.g. { "core": "common_config.yaml", "deployment": "/tmp/1452523532json" })
+ :return: merged config as a nested dictionary
+ """
+ return {k: load_config_file(fname) for k, fname in kwargs.items()}
diff --git a/operation/__init__.py b/operation/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/operation/__init__.py
diff --git a/operation/error_handling.py b/operation/error_handling.py
new file mode 100644
index 0000000..dfb0848
--- /dev/null
+++ b/operation/error_handling.py
@@ -0,0 +1,93 @@
+# -------------------------------------------------------------------------
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+
+import json
+
+from schematics.exceptions import DataError
+
+from requests import RequestException
+from requests import ConnectionError, HTTPError, Timeout
+from osdf.operation.exceptions import BusinessException
+
+import osdf
+
+ERROR_TEMPLATE = osdf.ERROR_TEMPLATE
+
+MESSAGE_BASE = "A solution couldn't be determined because an external application"
+HTTP_ERROR_MESSAGE = MESSAGE_BASE + " returned a HTTP error"
+TIMEOUT_ERROR_MESSAGE = MESSAGE_BASE + " could not respond in time, please check the external application"
+CONNECTION_ERROR_MESSAGE = MESSAGE_BASE + " could not be reached"
+
+internal_error_body = {
+ "serviceException": {
+ "text": "Unhandled internal exception, request could not be processed"
+ }
+}
+
+internal_error_message = json.dumps(internal_error_body)
+
+
+def build_json_error_body(error):
+ if isinstance(error,RequestException):
+ return request_exception_to_json_body(error)
+ elif isinstance(error, DataError):
+ return data_error_to_json_body(error)
+ elif type(error) is BusinessException: # return the error message, because it is well formatted
+ return ERROR_TEMPLATE.render(description=str(error))
+ else:
+ return internal_error_message
+
+
+def data_error_to_json_body(error):
+ description = str(error).replace('"', '\\"')
+ error_message = ERROR_TEMPLATE.render(description=description)
+ return error_message
+
+
+def request_exception_to_json_body(error):
+ friendly_message = "A request exception has occurred when contacting an external system"
+ if type(error) is HTTPError:
+ friendly_message = HTTP_ERROR_MESSAGE
+ if type(error) is ConnectionError:
+ friendly_message = CONNECTION_ERROR_MESSAGE
+ if type(error) is Timeout:
+ friendly_message = TIMEOUT_ERROR_MESSAGE
+
+ eie_body = {
+ "serviceException": {
+ "text": friendly_message,
+ "errorType": "InterfaceError"
+ },
+ "externalApplicationDetails": {
+ "httpMethod": error.request.method,
+ "url": error.request.url
+ }
+ }
+
+ response = error.response
+
+ if response is not None:
+ eie_body['externalApplicationDetails']['httpStatusCode'] = response.status_code
+ content_type = response.headers.get('content-type')
+ if content_type is not None:
+ if 'application/json' in content_type:
+ eie_body['externalApplicationDetails']['responseMessage'] = response.json()
+ elif 'text/html' in content_type:
+ eie_body['externalApplicationDetails']['responseMessage'] = response.text
+ error_message = json.dumps(eie_body)
+ return error_message
diff --git a/operation/exceptions.py b/operation/exceptions.py
new file mode 100644
index 0000000..5277b01
--- /dev/null
+++ b/operation/exceptions.py
@@ -0,0 +1,40 @@
+# -------------------------------------------------------------------------
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+
+class BusinessException(Exception):
+ pass
+
+
+class MessageBusConfigurationException(Exception):
+ pass
+
+
+class CMDataError(Exception):
+ pass
+
+
+class CMSOExecutionError(Exception):
+ pass
+
+
+class CMSOCallBackError(Exception):
+ pass
+
+
+class CMSOInvalidRequestException(Exception):
+ pass
diff --git a/operation/responses.py b/operation/responses.py
new file mode 100644
index 0000000..22a94f7
--- /dev/null
+++ b/operation/responses.py
@@ -0,0 +1,39 @@
+# -------------------------------------------------------------------------
+# 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(req_id="", text="", 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 req_id: request ID provided by the caller
+ :param text: extra text description about accepting the request (e.g. "Request accepted")
+ :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(description=text, request_id=req_id)
+ 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
diff --git a/osdfapp.py b/osdfapp.py
new file mode 100755
index 0000000..f854dca
--- /dev/null
+++ b/osdfapp.py
@@ -0,0 +1,168 @@
+# -------------------------------------------------------------------------
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+
+"""
+OSDF Manager Main Flask Application
+"""
+
+import sys
+from threading import Thread # for scaling up, may need celery with RabbitMQ or redis
+
+from flask import Flask, request, Response, g
+
+import osdf
+import pydevd
+import json
+import osdf.adapters.policy.interface
+import osdf.config.credentials
+import osdf.config.loader
+import osdf.datasources.aai.aai_local_cached_data
+import osdf.operation.error_handling
+import osdf.operation.responses
+import traceback
+from osdf.adapters.policy.interface import get_policies
+from osdf.adapters.response_parsing.aots_ueb_cm_data import aots_ds_ueb_listener
+from osdf.config.base import osdf_config, DOCKER_CM_OPTIMIZER, AOTS_CM_MESSAGE_BUS
+from osdf.optimizers.cmopt.rcscheduler.local_opt_processor import process_local_cm_scheduler_opt
+from osdf.optimizers.placementopt.conductor.remote_opt_processor import process_placement_opt
+from osdf.webapp.appcontroller import auth_basic
+from optparse import OptionParser
+from osdf.operation.exceptions import BusinessException
+from osdf.operation.error_handling import request_exception_to_json_body, internal_error_message
+from requests import RequestException
+from schematics.exceptions import DataError
+from osdf.logging.osdf_logging import MH, audit_log, error_log
+from osdf.models.placementRequest import PlacementAPI
+from osdf.models.schedulerRequest import SchedulerAPI
+
+ERROR_TEMPLATE = osdf.ERROR_TEMPLATE
+
+app = Flask(__name__)
+
+
+
+BAD_CLIENT_REQUEST_MESSAGE = 'Client sent an invalid request'
+
+# An exception explicitly raised due to some business rule
+@app.errorhandler(BusinessException)
+def handle_business_exception(e):
+ error_log.error("Synchronous error for request id {} {}".format(g.request_id, traceback.format_exc()))
+ err_msg = ERROR_TEMPLATE.render(description=str(e))
+ response = Response(err_msg, content_type='application/json; charset=utf-8')
+ response.status_code = 400
+ return response
+
+# Returns a detailed synchronous message to the calling client when osdf fails due to a remote call to another system
+@app.errorhandler(RequestException)
+def handle_request_exception(e):
+ error_log.error("Synchronous error for request id {} {}".format(g.request_id, traceback.format_exc()))
+ err_msg = request_exception_to_json_body(e)
+ response = Response(err_msg, content_type='application/json; charset=utf-8')
+ response.status_code = 400
+ return response
+
+# Returns a detailed message to the calling client when the initial synchronous message is invalid
+@app.errorhandler(DataError)
+def handle_data_error(e):
+ error_log.error("Synchronous error for request id {} {}".format(g.request_id, traceback.format_exc()))
+
+ body_dictionary = {
+ "serviceException": {
+ "text": BAD_CLIENT_REQUEST_MESSAGE,
+ "exceptionMessage": str(e.messages),
+ "errorType": "InvalidClientRequest"
+ }
+ }
+
+ body_as_json = json.dumps(body_dictionary)
+ response = Response(body_as_json, content_type='application/json; charset=utf-8')
+ response.status_code = 400
+ return response
+
+
+@app.route("/osdf/api/v2/placement", methods=["POST"])
+@auth_basic.login_required
+def do_placement_opt():
+ """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
+ """
+ request_json = request.get_json()
+ 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)))
+
+ PlacementAPI(request_json).validate()
+
+ # Currently policies are being used only during placement, so only fetch them if placement demands is not empty
+ policies = {}
+
+ if 'placementDemand' in request_json['placementInfo']['demandInfo']:
+ policies, prov_status = get_policies(request_json, "placement")
+
+ audit_log.info(MH.new_worker_thread(req_id, "[for placement]"))
+ t = Thread(target=process_placement_opt, args=(request_json, policies, osdf_config, prov_status))
+ t.start()
+ audit_log.info(MH.accepted_valid_request(req_id, request))
+ return osdf.operation.responses.osdf_response_for_request_accept(
+ req_id=req_id, text="Accepted placement request. Response will be posted to callback URL")
+
+
+# Returned when unexpected coding errors occur during initial synchronous processing
+@app.errorhandler(500)
+def interal_failure(error):
+ error_log.error("Synchronous error for request id {} {}".format(g.request_id, traceback.format_exc()))
+ response = Response(internal_error_message, content_type='application/json; charset=utf-8')
+ response.status_code = 500
+ return response
+
+
+def getOptions(argv):
+ program_version_string = '%%prog %s' % ("v1.0")
+ #program_usage = '''usage: spam two eggs''' # optional - will be autogenerated by optparse
+ program_longdesc = ""
+ program_license = ""
+
+ # setup option parser
+ parser = OptionParser(version=program_version_string, epilog=program_longdesc, description=program_license)
+ parser.add_option("-l", "--local", dest="local", help="run locally", action="store_true", default=False)
+ parser.add_option("-t", "--devtest", dest="devtest", help="run in dev/test environment", action="store_true", default=False)
+ parser.add_option("-d", "--debughost", dest="debughost", help="IP Address of host running debug server", default='')
+ parser.add_option("-p", "--debugport", dest="debugport", help="Port number of debug server", type=int, default=5678)
+ (opts, args) = parser.parse_args(argv)
+ if (opts.debughost != ''):
+ print('pydevd.settrace(%s, port=%s)' % (opts.debughost, opts.debugport))
+ pydevd.settrace(opts.debughost, port=opts.debugport)
+ return opts
+
+
+if __name__ == "__main__":
+
+ sys_conf = osdf_config['core']['osdf_system']
+ ports = sys_conf['osdf_ports']
+ internal_port, external_port = ports['internal'], ports['external']
+ ssl_context = tuple(sys_conf['ssl_context'])
+
+ common_app_opts = dict(host='0.0.0.0', threaded=True, use_reloader=False)
+
+ opts = getOptions(sys.argv)
+ if (opts.local == False and opts.devtest == False): # normal deployment
+ app.run(port=internal_port, ssl_context=ssl_context, debug=False, **common_app_opts)
+ else:
+ port = internal_port if opts.local == True else external_port
+ app.run(port=port, debug=True, **common_app_opts)
diff --git a/osdfapp.sh b/osdfapp.sh
new file mode 100755
index 0000000..c54d59c
--- /dev/null
+++ b/osdfapp.sh
@@ -0,0 +1,53 @@
+#!/bin/bash
+
+# -------------------------------------------------------------------------
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+
+# Call osdf app after setting LD_LIBRARY_PATH for oracle client, postgres client, etc.
+
+cd $(dirname $0)
+
+# Environment variables below are for ORACLE_HOME and such things, and not needed for 1707 onwards
+# . ../dependencies/env.sh
+
+bash ../etc/make-certs.sh # create the https certificates if they are not present
+
+set -e
+
+mkdir -p logs
+
+if [ ! -e "osdf-optim" ]; then
+(
+ mkdir tmp
+ cd tmp
+ tar xzf ../../dependencies/SNIROOptimizationPack.tgz
+ mv osdf ../osdf-optim
+ cd ../osdf-optim/pywheels
+ pip install docopt* jsonschema*
+)
+cp etc/run-case-local.sh osdf-optim/run/
+fi
+
+if [ $# -ge 1 ]; then
+ export SNIRO_MANAGER_CONFIG_FILE="$1" # this file is passed by the DCAE controller
+fi
+
+# export FLASK_APP=osdfapp.py
+
+# flask run
+python osdfapp.py # running the app
diff --git a/utils/__init__.py b/utils/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/utils/__init__.py
diff --git a/utils/data_conversion.py b/utils/data_conversion.py
new file mode 100644
index 0000000..2f678fa
--- /dev/null
+++ b/utils/data_conversion.py
@@ -0,0 +1,62 @@
+# -------------------------------------------------------------------------
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+
+import itertools
+from collections import defaultdict
+
+from dateutil import tz
+from dateutil.parser import parse
+
+
+def tuples_to_multi_val_dict(kvw_tuples, colnums=(0, 1)):
+ """Given a list of k,v tuples, get a dictionary of the form k -> [v1,v2,...,vn]
+ :param kvw_tuples: list of k,v,w tuples (e.g. [(k1,v1,a1), (k2,v2,a2), (k1,v3,a3), (k1,v4,a4)]
+ :param colnums: column numbers
+ :return: a dict of str:set, something like {k1: {v1, v3, v4}, k2: {v2}} or {k1: {a1, a3, a4}, k2: {a2}}
+ """
+ res = defaultdict(set)
+ for x in kvw_tuples:
+ key, val = x[colnums[0]], x[colnums[1]]
+ res[key].add(val)
+ return dict((k, set(v)) for k, v in res.items())
+
+
+def tuples_to_dict(kvw_tuples, colnums=(0, 1)):
+ """Given a list of k,v tuples, get a dictionary of the form k -> v
+ :param kvw_tuples: list of k,v,w tuples (e.g. [(k1,v1,a1), (k2,v2,a2), (k3,v3,a3), (k1,v4,a4)]
+ :param colnums: column numbers
+ :return: a dict; something like {k1: v4, k2: v2, k3: v3} (note, k1 is repeated, so last val is retained)
+ """
+ return dict((x[colnums[0]], x[colnums[1]]) for x in kvw_tuples)
+
+
+def utc_time_from_ts(timestamp):
+ """Return corresponding UTC timestamp for a given ISO timestamp (or anything that parse accepts)"""
+ return parse(timestamp).astimezone(tz.tzutc()).strftime('%Y-%m-%d %H:%M:%S')
+
+
+def list_flatten(l):
+ """Flatten a complex nested list of nested lists into a flat list"""
+ return itertools.chain(*[list_flatten(j) if isinstance(j, list) else [j] for j in l])
+
+
+text_to_symbol = {
+ 'greater': ">",
+ 'less': "<",
+ 'equal': "="
+}
diff --git a/utils/data_types.py b/utils/data_types.py
new file mode 100644
index 0000000..877d4a1
--- /dev/null
+++ b/utils/data_types.py
@@ -0,0 +1,30 @@
+# -------------------------------------------------------------------------
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+
+import collections
+
+
+def list_like(obj):
+ """Check if an object is a list-like object, but not a string or dict"""
+ return isinstance(obj, collections.Sequence) and not isinstance(obj, (str, bytes))
+
+
+def dict_like(obj):
+ """Check if an object is a list-like object, but not a string or dict"""
+ return isinstance(obj, collections.Mapping)
+
diff --git a/utils/interfaces.py b/utils/interfaces.py
new file mode 100644
index 0000000..7a0e3a9
--- /dev/null
+++ b/utils/interfaces.py
@@ -0,0 +1,90 @@
+# -------------------------------------------------------------------------
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+
+import requests
+
+from osdf.config.base import osdf_config, creds_prefixes
+from osdf.logging.osdf_logging import MH, debug_log
+
+
+def get_rest_client(request_json, service):
+ """Get a RestClient based on request_json's callback URL and osdf_config's credentials based on service name
+ :param request_json:
+ :param service: so or cm
+ :return: rc -- RestClient
+ """
+ callback_url = request_json["requestInfo"]["callbackUrl"]
+ prefix = creds_prefixes[service]
+ config = osdf_config.deployment
+ c_userid, c_passwd = config[prefix + "Username"], config[prefix + "Password"]
+ return RestClient(url=callback_url, userid=c_userid, passwd=c_passwd)
+
+
+class RestClient(object):
+ """Simple REST Client that supports get/post and basic auth"""
+
+ def __init__(self, userid=None, passwd=None, log_func=None, url=None, timeout=None, headers=None,
+ method="POST", req_id=None):
+ self.auth = (userid, passwd) if userid and passwd else None
+ self.headers = headers if headers else {}
+ self.method = method
+ self.url = url
+ self.log_func = log_func
+ self.timeout = (30, 90) if timeout is None else timeout
+ self.req_id = req_id
+
+ def add_headers(self, headers):
+ self.headers.update(headers)
+
+ def request(self, url=None, method=None, asjson=True, ok_codes=(2, ),
+ raw_response=False, noresponse=False, timeout=None, **kwargs):
+ """
+ :param url: REST end point to query
+ :param method: GET or POST (default is None => self.method)
+ :param asjson: whether the expected response is in json format
+ :param ok_codes: expected codes (prefix matching -- e.g. can be (20, 21, 32) or (2, 3))
+ :param noresponse: If no response is expected (as long as response codes are OK)
+ :param raw_response: If we need just the raw response (e.g. conductor sends transaction IDs in headers)
+ :param timeout: Connection and read timeouts
+ :param kwargs: Other parameters
+ :return:
+ """
+ if not self.req_id:
+ debug_log.debug("Requesting URL: {}".format(url or self.url))
+ else:
+ debug_log.debug("Requesting URL: {} for request ID: {}".format(url or self.url, self.req_id))
+
+ res = requests.request(url=url or self.url, method=method or self.method,
+ auth=self.auth, headers=self.headers,
+ timeout=timeout or self.timeout, **kwargs)
+
+ if self.log_func:
+ self.log_func(MH.received_http_response(res))
+
+ res_code = str(res.status_code)
+ if not any(res_code.startswith(x) for x in map(str, ok_codes)):
+ raise res.raise_for_status()
+
+ if raw_response:
+ return res
+ elif noresponse:
+ return None
+ elif asjson:
+ return res.json()
+ else:
+ return res.content
diff --git a/utils/local_processing.py b/utils/local_processing.py
new file mode 100644
index 0000000..6768839
--- /dev/null
+++ b/utils/local_processing.py
@@ -0,0 +1,43 @@
+# -------------------------------------------------------------------------
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+
+import os
+
+from osdf.logging.osdf_logging import metrics_log, MH, warn_audit_error
+
+
+def local_create_job_file(req_id, json_req, fname='osdf-req-data.json'):
+ """Creates a "work" folder for local processing and place relevant
+ job task file in there"""
+
+ work_dir = 'osdf-optim/work/' + req_id
+ work_file = '{}/{}'.format(work_dir, fname)
+ try:
+ cur_task = "Making a local directory in the OSDF manager for req-id: {}".format(req_id)
+ metrics_log.info(MH.creating_local_env(cur_task))
+ os.makedirs(work_dir, exist_ok=True)
+ except Exception as err:
+ warn_audit_error(MH.error_local_env(req_id, "Can't create directory {}".format(work_dir), err))
+ return None
+ try:
+ with open(work_file, 'w') as fid:
+ fid.write(json_req['payload'])
+ return work_dir
+ except Exception as err:
+ warn_audit_error(MH.error_local_env(req_id, "can't create file {}".format(work_file), err))
+ return None
diff --git a/utils/programming_utils.py b/utils/programming_utils.py
new file mode 100644
index 0000000..a0a8fde
--- /dev/null
+++ b/utils/programming_utils.py
@@ -0,0 +1,105 @@
+# -------------------------------------------------------------------------
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+
+import collections
+import itertools
+
+
+class DotDict(dict):
+ """A dot-dict mixin to be able to access a dictionary via dot notation
+ source: https://stackoverflow.com/questions/2352181/how-to-use-a-dot-to-access-members-of-dictionary
+ """
+ __getattr__ = dict.get
+ __setattr__ = dict.__setitem__
+ __delattr__ = dict.__delitem__
+
+
+class MetaSingleton(type):
+ """Singleton class (2nd Chapter) from Learning Python Design Patterns - 2nd ed.
+ Chetan Giridhar, Packt Publ. 2016"""
+ _instances = {}
+
+ def __call__(cls, *args, **kwargs):
+ if cls not in cls._instances:
+ cls._instances[cls] = super(MetaSingleton, cls).__call__(*args, **kwargs)
+ return cls._instances[cls]
+
+
+def namedtuple_with_defaults(typename, field_names, default_values=()):
+ """A namedtuple with default values -- Stack overflow recipe from Mark Lodato
+ http://stackoverflow.com/questions/11351032/named-tuple-and-optional-keyword-arguments
+ :param typename: Name for the class (same as for namedtuple)
+ :param field_names: Field names (same as for namedtuple)
+ :param default_values: Can be specified as a dictionary or as a list
+ :return: New namedtuple object
+ """
+ T = collections.namedtuple(typename, field_names)
+ T.__new__.__defaults__ = (None,) * len(T._fields)
+ if isinstance(default_values, collections.Mapping):
+ prototype = T(**default_values)
+ else:
+ prototype = T(*default_values)
+ T.__new__.__defaults__ = tuple(prototype)
+ return T
+
+
+def dot_notation(dict_like, dot_spec):
+ """Return the value corresponding to the dot_spec from a dict_like object
+ :param dict_like: dictionary, JSON, etc.
+ :param dot_spec: a dot notation (e.g. a1.b1.c1.d1 => a1["b1"]["c1"]["d1"])
+ :return: the value referenced by the dot_spec
+ """
+ attrs = dot_spec.split(".") # we split the path
+ parent = dict_like.get(attrs[0])
+ children = ".".join(attrs[1:])
+ if not (parent and children): # if no children or no parent, bail out
+ return parent
+ if isinstance(parent, list): # here, we apply remaining path spec to all children
+ return [dot_notation(j, children) for j in parent]
+ elif isinstance(parent, dict):
+ return dot_notation(parent, children)
+ else:
+ return None
+
+
+def list_flatten(l):
+ """
+ Flatten a complex nested list of nested lists into a flat list (DFS).
+ For example, [ [1, 2], [[[2,3,4], [2,3,4]], [3,4,5, 'hello']]]
+ will produce [1, 2, 2, 3, 4, 2, 3, 4, 3, 4, 5, 'hello']
+ """
+ return list(itertools.chain(*[list_flatten(j) if isinstance(j, list) else [j] for j in l]))
+
+
+def inverted_dict(keys: list, key_val_dict: dict) -> dict:
+ """
+ Get val -> [keys] mapping for the given keys using key_val_dict
+ :param keys: the keys we are interested in (a list)
+ :param key_val_dict: the key -> val mapping
+ :return: inverted dictionary of val -> [keys] (for the subset dict of given keys)
+ """
+ res = {}
+ all_tuples = ((k, key_val_dict[k] if k in key_val_dict else 'no-parent-' + k) for k in keys)
+ for k, v in all_tuples:
+ if v in res:
+ res[v].append(k)
+ else:
+ res[v] = [k]
+ # making sure to remove duplicate keys
+ res = dict((v, list(set(k_list))) for v, k_list in res.items())
+ return res
diff --git a/webapp/__init__.py b/webapp/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/webapp/__init__.py
diff --git a/webapp/appcontroller.py b/webapp/appcontroller.py
new file mode 100644
index 0000000..49f84ff
--- /dev/null
+++ b/webapp/appcontroller.py
@@ -0,0 +1,47 @@
+# -------------------------------------------------------------------------
+# 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 request
+from flask_httpauth import HTTPBasicAuth
+from flask import Response
+import json
+import osdf
+from osdf.config.base import http_basic_auth_credentials
+
+auth_basic = HTTPBasicAuth()
+
+error_body = {
+ "serviceException": {
+ "text": "Unauthorized, check username and password"
+ }
+}
+
+unauthorized_message = json.dumps(error_body)
+
+@auth_basic.get_password
+def get_pw(username):
+ end_point = request.url.split('/')[-1]
+ auth_group = osdf.end_point_auth_mapping.get(end_point)
+ return http_basic_auth_credentials[auth_group].get(username) if auth_group else None
+
+@auth_basic.error_handler
+def auth_error():
+ response = Response(unauthorized_message, content_type='application/json; charset=utf-8')
+ response.headers.add('content-length', len(unauthorized_message))
+ response.status_code = 401
+ return response